├── .github ├── setup │ ├── copr-build-setup │ └── copr-custom-script └── workflows │ ├── push-copr-build.yml │ ├── python-diff-lint.yml │ └── tox.yml ├── .gitignore ├── .mypy.ini ├── .travis.yml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NEWS ├── README.md ├── argparse-manpage ├── argparse_manpage ├── __init__.py ├── cli.py ├── compat.py ├── manpage.py └── tooling.py ├── build_manpages ├── __init__.py ├── build_manpage.py ├── build_manpages.py ├── compat.py └── manpage.py ├── check ├── examples ├── argument_groups │ ├── bin │ │ └── test │ ├── expected │ │ └── test.1 │ ├── setup.cfg │ └── setup.py ├── copr │ ├── .gitignore │ ├── copr_cli │ │ ├── __init__.py │ │ ├── build_config.py │ │ ├── main.py │ │ └── util.py │ ├── expected-output.1 │ ├── expected-output.1.python3.13 │ ├── fake-deps │ │ ├── HACK │ │ ├── copr │ │ │ ├── README │ │ │ ├── __init__.py │ │ │ ├── client │ │ │ │ ├── __init__.py │ │ │ │ └── responses.py │ │ │ └── exceptions.py │ │ ├── jinja2.py │ │ └── simplejson.py │ ├── setup.cfg │ └── setup.py ├── old_format │ ├── README.md │ ├── example.py │ ├── expected-output.1 │ ├── setup.cfg │ └── setup.py ├── old_format_file_name │ ├── README.md │ ├── example.py │ ├── expected-output.1 │ ├── setup.cfg │ └── setup.py ├── osc │ ├── expected-output.1 │ ├── osc │ │ ├── __init__.py │ │ └── main.py │ ├── setup.cfg │ └── setup.py ├── pre-written-man-page │ ├── psutils.1 │ ├── setup.cfg │ └── setup.py ├── raw-description │ ├── bin │ │ └── dg │ ├── expected-output.1 │ ├── setup.cfg │ └── setup.py └── resalloc │ ├── bin │ ├── resalloc │ └── resalloc-maint │ ├── expected │ └── man │ │ ├── resalloc-maint.1 │ │ └── resalloc.1 │ ├── requirements.txt │ ├── resalloc │ ├── __init__.py │ ├── client.py │ └── version.py │ ├── resallocserver │ ├── __init__.py │ └── maint.py │ ├── setup.cfg │ └── setup.py ├── pylintrc ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── rpm ├── .gitignore ├── Makefile ├── argparse-manpage.spec.tpl └── build-helper ├── setup.cfg ├── setup.py ├── tests ├── argparse_testlib.py ├── extra.man ├── test_basic.py ├── test_examples.py └── test_script.py └── tox.ini /.github/setup/copr-build-setup: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Helper script (used by praiskup atm) to update CI scripting on Copr side. 4 | 5 | script=$(readlink -f "$(dirname "$0")")/copr-custom-script 6 | script_resultdir=argparse-manpage/rpm 7 | pkgname=argparse-manpage 8 | 9 | PROJECT_PUSH=praiskup/argparse-manpage-ci 10 | PROJECT_PR=praiskup/argparse-manpage-pull-requests 11 | 12 | build_deps=( 13 | git 14 | make 15 | python3-setuptools 16 | python-unversioned-command 17 | ) 18 | 19 | for PROJECT in $PROJECT_PR $PROJECT_PUSH; do 20 | copr_cmd=( 21 | copr edit-package-custom "$PROJECT" \ 22 | --webhook-rebuild on \ 23 | --script "$script" \ 24 | --script-chroot "fedora-latest-x86_64" \ 25 | --script-builddeps "${build_deps[*]}" \ 26 | --name "$pkgname" \ 27 | --script-resultdir "$script_resultdir" 28 | ) 29 | test "$PROJECT" = "$PROJECT_PR" && copr_cmd+=( --max-builds 20 ) 30 | "${copr_cmd[@]}" 31 | done 32 | -------------------------------------------------------------------------------- /.github/setup/copr-custom-script: -------------------------------------------------------------------------------- 1 | #! /bin/bash -x 2 | 3 | set -e 4 | 5 | clone_url_parent=https://github.com/praiskup/argparse-manpage 6 | 7 | workdir=$(basename "$clone_url_parent") 8 | workdir=${workdir%%.git} 9 | 10 | hook_payload=$(readlink -f "${HOOK_PAYLOAD-hook_payload}") 11 | 12 | # clone the helper scripts when needed, and add to PATH 13 | test -d copr-ci-tooling \ 14 | || git clone --depth 1 https://github.com/praiskup/copr-ci-tooling.git 15 | export PATH="$PWD/copr-ci-tooling:$PATH" 16 | 17 | # clone the tested project 18 | git clone \ 19 | --recursive \ 20 | --no-single-branch \ 21 | "$clone_url_parent" 22 | 23 | # checkout requested revision 24 | cd "$workdir" 25 | 26 | webhook-checkout "$hook_payload" 27 | 28 | cd rpm || exit 1 29 | make srpm 30 | -------------------------------------------------------------------------------- /.github/workflows/push-copr-build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: RPM build in Fedora Copr 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | # This always runs against the default branch. Two runs per month. 10 | - cron: '0 0 1,16 * *' 11 | 12 | jobs: 13 | build: 14 | name: Submit a Copr build 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Check out proper version of sources 19 | uses: actions/checkout@v1 20 | 21 | - name: Submit the build 22 | env: 23 | COPR_PR_WEBHOOK: https://copr.fedorainfracloud.org/webhooks/custom/33631/780e2120-1749-4623-8222-b8705b3414c7/argparse-manpage/ 24 | COPR_PUSH_WEBHOOK: ${{ secrets.COPR_PUSH_WEBHOOK }} 25 | run: | 26 | curl https://raw.githubusercontent.com/praiskup/copr-ci-tooling/main/copr-gh-actions-submit > submit 27 | bash submit ${{ github.event.pull_request.number }} 28 | -------------------------------------------------------------------------------- /.github/workflows/python-diff-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Python issues 3 | 4 | on: 5 | push: 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | python-lint-job: 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | # required for all workflows 18 | security-events: write 19 | steps: 20 | - name: Repository checkout 21 | uses: actions/checkout@v3 22 | 23 | - id: VCS_Diff_Lint 24 | name: VCS Diff Lint 25 | uses: fedora-copr/vcs-diff-lint-action@v1 26 | 27 | - name: Upload artifact with detected defects in SARIF format 28 | uses: actions/upload-artifact@v3 29 | with: 30 | name: VCS Diff Lint SARIF 31 | path: ${{ steps.VCS_Diff_Lint.outputs.sarif }} 32 | if: ${{ always() }} 33 | 34 | - name: Upload SARIF to GitHub using github/codeql-action/upload-sarif 35 | uses: github/codeql-action/upload-sarif@v2 36 | with: 37 | sarif_file: ${{ steps.VCS_Diff_Lint.outputs.sarif }} 38 | if: ${{ always() }} 39 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | name: Run Tox tests 11 | 12 | jobs: 13 | tox_test: 14 | name: Tox test 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Run Tox tests 18 | id: test 19 | uses: fedora-python/tox-github-action@main 20 | with: 21 | tox_env: ${{ matrix.tox_env }} 22 | strategy: 23 | matrix: 24 | # sync with tox.ini! 25 | tox_env: [py36, py37, py38, py39, py310, py311] 26 | 27 | # Use GitHub's Linux Docker host 28 | runs-on: ubuntu-latest 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.1 3 | *_install_dir/ 4 | build 5 | *.egg-info 6 | .cache 7 | MANIFEST 8 | dist/ 9 | man/ 10 | .mypy_cache/ 11 | tags 12 | TAGS 13 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | arch: 3 | - amd64 4 | - ppc64le 5 | python: 6 | - 2.7 7 | - 3.5 8 | - 3.6 9 | - 3.7 10 | - 3.8 11 | - 3.9-dev 12 | 13 | install: 14 | - "pip install -r requirements.txt" 15 | 16 | script: "PYTHON=python$TRAVIS_PYTHON_VERSION ./check" 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Andi Albrecht 2 | Pavel Raiskup 3 | René 'Necoro' Neumann (seealso option) 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include argparse-manpage 2 | include AUTHORS 3 | include LICENSE 4 | include NEWS 5 | recursive-include examples *.py *.cfg *.md expected-output.1* expected/**.1 requirements.txt 6 | recursive-include tests *.py 7 | recursive-include unittests *.py 8 | include examples/argument_groups/bin/test 9 | include examples/resalloc/expected/man/resalloc-maint.1 10 | include examples/resalloc/expected/man/resalloc.1 11 | include examples/copr/fake-deps/HACK 12 | include examples/copr/fake-deps/copr/README 13 | include examples/pre-written-man-page/psutils.1 14 | include examples/raw-description/bin/dg 15 | include examples/resalloc/bin/resalloc 16 | include examples/resalloc/bin/resalloc-maint 17 | include tests/extra.man 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check lint unittests 2 | 3 | check: 4 | $(MAKE) unittests 5 | $(MAKE) lint 6 | 7 | unittests: 8 | PYTHON=python3 ./check 9 | 10 | lint: 11 | vcs-diff-lint 12 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | 3 | WARNING: The 'build_manpage' setup.py command will be removed v5 4 | WARNING: We'll drop the Python 2.7 support in v5 5 | 6 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | 8 | News in v4.5 9 | 10 | * We newly provide `build_manpages.build_py` and `build_manpages.install` 11 | command classes that are re-usable from `pyproject.toml`. No need to 12 | provide `setup.py` because of `argparse-manpage`. Solved issue#85. 13 | 14 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | News in v4.4 17 | 18 | * The `prog=` specifier (in setup.py/setup.cfg/pyproject.toml) is now 19 | better handled so it doesn't conflict with ArgumentParser(prog=..). 20 | Fixes https://github.com/praiskup/argparse-manpage/issues/79 21 | 22 | 23 | News in v4.3 24 | 25 | * The pyproject.toml parsing feature now depends on the python3-tomli library 26 | instead of python-toml for "python_environment >3, <=3.10". 27 | 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | News in v4.2 31 | 32 | * Support for pyproject.toml specification of manpages added. 33 | 34 | * Support for pre-written man pages (the --manfile option) 35 | 36 | Bugfixes in version 4.2 37 | 38 | * Incorrect dict access for --include support fixed. 39 | 40 | * Provide useful AUTHORS section with e-mail from 41 | Distribution.get_author_email() even if Distribution.get_author() returns 42 | None. 43 | 44 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | 46 | News in v4.1 47 | 48 | * A new `--include` feature, inspired by `help2man --include`. 49 | 50 | * Allow overriding build date with SOURCE_DATE_EPOCH environment variable 51 | in order to make builds reproducible. See this link for more info: 52 | https://reproducible-builds.org/specs/source-date-epoch/ 53 | 54 | * The AUTHORS section was changed to more standard AUTHOR. 55 | 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | News in v4 59 | 60 | * The manual page generator logic is now separated from the 'build_manpages' 61 | module (which provides setup.py integration helpers). Therefore the 62 | argparse-manpage doesn't necessarily have to depend on setuptools. 63 | Projects that want to integrate using 'setup.py' should though place a new 64 | "extra" named 'argparse-manpage[setuptools]' into their set of build 65 | requirements in project.toml file. 66 | 67 | * The old 'build_manpage' command (!= 'build_manpages') is now more 68 | isolated from the rest of the code, likely never loaded. 69 | 70 | * the Manpage class API was changed in v3 so it required an additional 71 | constructor 'data' argument. This change was reverted, and the only 72 | argument is again the ArgumentParser object. 73 | 74 | * The 'version' and 'description' options were fixed. 75 | 76 | * New options 'manual_section' and 'manual_title' were added. 77 | 78 | * The manual page now automatically generates a current date in headers. 79 | 80 | * Several groff escaping issues were fixed. 81 | 82 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 83 | 84 | New in version 3 85 | 86 | * New man page format: single-commands-section 87 | * Add CLI and conf options for setting the output format 88 | * Add CLI and conf options to explicitly specify %prog value 89 | * Skip showing commands with help == SUPPRESS 90 | * Avoid rendering docs for command aliases 91 | * Print program name in upper case in header and footer 92 | * The --author CLI option has changed, and takes arbitrary string 93 | (typically 'Author Name '), and newly can be specified 94 | multiple times. Therefore, it now replaces the '--author-email' option. 95 | The --author-email option is kept, but is just an alias to the --author 96 | option. 97 | * All CLI options can be specified also in setup.cfg 98 | * Don't render AUTHORS and DISTRIBUTION if they would contain undefined values 99 | * Remove '... was written by' from AUTHORS 100 | 101 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 102 | 103 | Bugfixes in version 2.2 104 | 105 | * Fixed the testsuite against the setuptools v60+ 106 | 107 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 108 | 109 | New in version 2.1 110 | 111 | * Output manual pages should be built reproducibly, no matter the current 112 | size of the terminal. 113 | 114 | * Methods build_manpages, get_build_py_cmd, get_install_cmd are now 115 | provided in top-level module. 116 | 117 | * More portable opening and parsing given by python file name. 118 | 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | 121 | New in version 2 122 | 123 | * Split out options by generic argument groups, not only predefined 124 | "positional arguments", "options", and Subparsers. 125 | 126 | * RPM spec file updated to comply with the recent (Fedora 35+) guidelines. 127 | 128 | * Drop python3-six requirement. 129 | 130 | * Properly highligh all option argument METAVARs. 131 | 132 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 133 | 134 | New in version 1.5 135 | 136 | * The "epilog" from argparse object is dumped to "COMMENTS" sections. 137 | 138 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 139 | 140 | New in version 1.4 141 | 142 | * fixed testsuite for Python 3.9 143 | 144 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 145 | 146 | New in version 1.3 147 | 148 | * drop additional .br tag from paragraphs so the multiline text is nicer 149 | 150 | * provide argparse-manpage via entry_point 151 | 152 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 153 | 154 | New in version 1.2 155 | 156 | * Fixed shebang in /bin/argparse-manpage script so it works on 157 | Debian/Ubuntu as well as on Fedora. 158 | 159 | * LICENSE file included in release tarball. 160 | 161 | * Command-line executable now takes an optional '--output' argument. 162 | 163 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArgumentParser instance → manual page 2 | 3 | Avoid documenting your Python script arguments on two places! This is typically 4 | done in an [argparse.ArgumentParser][ap-docs] help configuration (`help=`, 5 | `description=`, etc.), and also in a manually crafted manual page. 6 | 7 | The good thing about an `ArgumentParser` objects is that it actually provides 8 | a traversable "tree-like" structure, with all the necessary info needed to 9 | **automatically generate** documentation, for example in a *groff* typesetting 10 | system (manual pages). And this is where this project can help. 11 | 12 | There are two supported ways to generate the manual, either script it using the 13 | installed command `argparse-manpage`, or via `setup.py build` automation (with a 14 | slight bonus of automatic manual page installation with `setup.py install`). 15 | 16 | 17 | ## What is need? 18 | 19 | Most of the (meta)data is stored in the `ArgumentParser` object, therefore 20 | `argparse-manpage` needs to know its location—it can be either the object 21 | itself, or a method to call to get the object [^1]. 22 | 23 | On top of this, several manual page fields (like *author* or *project* name) 24 | need to be specified, either on command-line or via `setup.py` metadata. 25 | 26 | 27 | ## Command-line usage 28 | 29 | See the following example: 30 | 31 | ``` 32 | $ argparse-manpage --pyfile ./pythonfile.py --function get_parser \ 33 | --author "John --author-email doe@example.com" \ 34 | --project-name myproject --url https://pagure.io/myproject \ 35 | > cool-manpage.1 36 | ``` 37 | 38 | This (a) processes the `./pythonfile.py`, (b) calls the `get_parser` inside to 39 | obtain the `ArgumentParser` instance, (c) transforms it into a manual page and 40 | (d) stores it into the `cool-manpage.1` file. 41 | 42 | Alternatively those options above can be combined with 43 | 44 | - option `--module mymodule.main`, to load a Python module `mymodule.main` 45 | from `PYTHONPATH`, or 46 | - `--object parser_object_name` if the `parser_object_name` is a global 47 | variable. 48 | 49 | 50 | ## Use with pyproject.toml 51 | 52 | First, you need to declare in `pyproject.toml` that argparse-manpage is needed 53 | at build-time and use the setuptools.builds_meta` backend: 54 | 55 | ```toml 56 | [build-system] 57 | requires = ["argparse-manpage[setuptools]"] 58 | build-backend = "setuptools.build_meta" 59 | ``` 60 | 61 | Alternatively, you can place the `build_manpages` (sub)directory from this 62 | project somewhere onto `PYTHONPATH` so you can use it at build time. For 63 | example: 64 | 65 | ```bash 66 | git submodule add --name build_manpages https://github.com/praiskup/build_manpages 67 | git submodule update --init 68 | ``` 69 | 70 | Then in `pyproject.toml` (re)define `cmdclass` commands: 71 | 72 | ```toml 73 | [tool.setuptools.cmdclass] 74 | build_py = "build_manpages.build_py" 75 | install = "build_manpages.install" 76 | build_manpages = "build_manpages.build_manpages" 77 | ``` 78 | 79 | And specify the list of built manual pages: 80 | 81 | ```toml 82 | [tool.build_manpages] 83 | manpages = [ 84 | "man/foo.1:object=parser:pyfile=bin/foo.py", 85 | "man/bar.1:function=get_parser:pyfile=bin/bar", 86 | "man/baz.1:function=get_parser:pyfile=bin/bar:prog=baz", 87 | ] 88 | ``` 89 | 90 | ## Use with setup.py 91 | 92 | In your `setup.py` use pattern like: 93 | 94 | ```python 95 | [...] 96 | from build_manpages import build_manpages, get_build_py_cmd, get_install_cmd 97 | 98 | setup( 99 | [...] 100 | cmdclass={ 101 | 'build_manpages': build_manpages, 102 | # Re-define build_py and install commands so the manual pages 103 | # are automatically re-generated and installed 104 | 'build_py': get_build_py_cmd(), 105 | 'install': get_install_cmd(), 106 | } 107 | ) 108 | ``` 109 | 110 | And in `setup.cfg` configure the manual pages you want to automatically 111 | generate and install: 112 | 113 | ``` 114 | [build_manpages] 115 | manpages = 116 | man/foo.1:object=parser:pyfile=bin/foo.py 117 | man/bar.1:function=get_parser:pyfile=bin/bar 118 | man/baz.1:function=get_parser:pyfile=bin/bar:prog=baz 119 | ``` 120 | 121 | ## List of manual pages 122 | 123 | The format of those lines is a colon separated list of arguments/options. The 124 | first argument determines the filename of the generated manual page. Then 125 | follows a list of options of format `option=value`. Supported values are: 126 | 127 | - pyfile - what python file the argparse object resides in 128 | - object - the name of arparse object in "pyfile" to import 129 | - function - the name of function in pyfile to call to get the argparse object 130 | - format - format of the generated man page: `pretty` (default), `single-commands-section` 131 | - author - author of the program; can be specified multiple times 132 | - description - description of the program, used in the NAME section, after the 133 | leading 'name - ' part, see man (7) man-pages for more info 134 | - project_name - name of the project the program is part of 135 | - version - version of the project, visible in manual page footer 136 | - prog - value that substitutes %prog in ArgumentParser's usage 137 | - url - link to project download page 138 | - manual_section - section of the manual, by default 1, see man (7) man-pages 139 | for more info about existing sections 140 | - manual_title - the title of the manual, by default "Generated Python Manual", 141 | see man (7) man-pages for more instructions 142 | - include - a file of extra material to include; see below for the format 143 | - manfile - a file containing a complete man page that just needs to be installed 144 | (such files must also be listed in `MANIFEST.in`) 145 | 146 | The values from setup.cfg override values from setup.py's setup(). Note that 147 | when `manfile` is set for a particular page, no other option is allowed. 148 | 149 | Then run `setup.py build_manpages` to build a manpages for your project. Also, 150 | if you used `get_build_py` helper, `setup.py build` then transitively builds the 151 | manual pages. 152 | 153 | ## Include file format 154 | 155 | The include file format is based on GNU `help2man`'s `--include` format. 156 | 157 | The format is simple: 158 | 159 | ``` 160 | [section] 161 | text 162 | 163 | /pattern/ 164 | text 165 | ``` 166 | 167 | Blocks of verbatim *roff text are inserted into the output either at 168 | the start of the given `section` (case insensitive), or after a 169 | paragraph matching `pattern`, a Python regular expression. 170 | 171 | Lines before the first section are silently ignored and may be used for 172 | comments and the like. 173 | 174 | Other sections are prepended to the automatically produced output for the 175 | standard sections given above, or included near the bottom of the man page, 176 | before the `AUTHOR` section, in the order they occur in the include file. 177 | 178 | Placement of the text within the section may be explicitly requested by 179 | using the syntax `[section]` to place the 180 | additional text before, in place of, or after the default output 181 | respectively. 182 | 183 | ## Installation 184 | 185 | This package is distributed [in PyPI][pypi-page], can be installed by: 186 | 187 | $ pip install argparse-manpage 188 | 189 | It can simply downloaded, or distributed as a git submodule (see above). 190 | 191 | 192 | ## Packaging status 193 | 194 | The Git snapshot RPMs–pre-release version automatically built from the `main` 195 | branch–are available in Fedora Copr build system 196 | 197 | [![build status](https://copr.fedorainfracloud.org/coprs/praiskup/argparse-manpage-ci/package/argparse-manpage/status_image/last_build.png)](https://copr.fedorainfracloud.org/coprs/praiskup/argparse-manpage-ci/) 198 | 199 | The `argparse-manpage` project is provided natively on many distributions: 200 | 201 | ![build status](https://repology.org/badge/vertical-allrepos/python:argparse-manpage.svg?exclude_unsupported=1&header=argparse-manpage) 202 | 203 | Try your package manager directly (e.g. on Fedora `dnf install -y 204 | argparse-manpage`). 205 | 206 | 207 | ## History 208 | 209 | The initial code was developed for CrunchyFrog, a database query tool for Gnome. 210 | The [frog] is now retired and [RunSQLRun] is it's successor. Then, the 211 | `build_manpage` command was developed in [andialbrecht] and edited slightly 212 | in [gabrielegiammatteo]. There's even an [old blog post] about this command. 213 | 214 | Since some useful work has been done in [python pull request], the code from the 215 | PR has been used here too. 216 | 217 | Later more options and flexibility has been implemented in this fork, with the 218 | help of many contributors. Thank you! 219 | 220 | Historically, `build_manpage` setup.py command was provided (mostly for 221 | `OptionParser`). Later we migrated to more versatile `build_manpages` command. 222 | But the old variant is still [supported](examples/old\_format/README.md). 223 | 224 | ## License 225 | 226 | This work is released under the terms of the Apache License v2.0. 227 | See LICENSE for details. 228 | 229 | 230 | [^1]: `argparse-manpage` needs to process the location (file/module) via Python 231 | interpreter, and thus please avoid side-effects (typically, the `main.py` 232 | files need to use the `if __name__ == "__main__"` condition, and similar). 233 | 234 | [gabrielegiammatteo]: https://github.com/andialbrecht/build\_manpage 235 | [andialbrecht]: https://github.com/andialbrecht/build\_manpage 236 | [frog]: http://crunchyfrog.googlecode.com/svn/ 237 | [RunSQLRun]: https://github.com/andialbrecht/runsqlrun 238 | [old blog post]: https://andialbrecht.wordpress.com/2009/03/17/creating-a-man-page-with-distutils-and-optparse/ 239 | [python pull request]: https://github.com/python/cpython/pull/1169 240 | [ap-docs]: https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser 241 | [pypi-page]: https://pypi.org/project/argparse-manpage/ 242 | -------------------------------------------------------------------------------- /argparse-manpage: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # Copyright (C) 2022 Red Hat, Inc. 4 | 5 | srcdir=$(readlink -f "$(dirname "$0")") 6 | 7 | run_python=${PYTHON-python3} 8 | 9 | echo >&2 10 | echo >&2 " !! running argparse-manpage from git, this is not supported PYTHON=$run_python!!" 11 | echo >&2 12 | 13 | export PYTHONPATH=$srcdir/${PYTHONPATH+:$PYTHONPATH} 14 | exec $run_python -c 'from argparse_manpage.cli import main; main()' "$@" 15 | -------------------------------------------------------------------------------- /argparse_manpage/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | argparse_manpage project 3 | """ 4 | 5 | __version__ = '4.6' 6 | -------------------------------------------------------------------------------- /argparse_manpage/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Red Hat, Inc. 2 | 3 | # Python 2.7 hack. Without this, 'from build_manpages.build_manpage' attempts 4 | # to import 'build_manpages.build_manpages.build_manpage' because of our 5 | # unfortunate file naming. 6 | from __future__ import absolute_import 7 | 8 | import argparse 9 | 10 | from argparse_manpage.tooling import get_parser, write_to_filename 11 | from argparse_manpage.manpage import MANPAGE_DATA_ATTRS, Manpage 12 | 13 | 14 | description = """ 15 | Build manual page from Python's argparse.ArgumentParser object. 16 | """.strip() 17 | 18 | ap = argparse.ArgumentParser( 19 | prog='argparse-manpage', 20 | description=description, 21 | ) 22 | 23 | src_group = ap.add_mutually_exclusive_group(required=True) 24 | src_group.add_argument( 25 | "--module", 26 | help="search the OBJECT/FUNCTION in MODULE" 27 | ) 28 | 29 | src_group.add_argument( 30 | "--pyfile", 31 | help="search the OBJECT/FUNCTION in FILE" 32 | ) 33 | 34 | obj_group = ap.add_mutually_exclusive_group(required=True) 35 | obj_group.add_argument( 36 | "--function", 37 | help="call FUNCTION from MODULE/FILE to obtain ArgumentParser object", 38 | ) 39 | obj_group.add_argument( 40 | "--object", 41 | help="obtain ArgumentParser OBJECT from FUNCTION (to get argparse object) from MODULE or FILE", 42 | ) 43 | 44 | 45 | ap.add_argument("--project-name", help="Name of the project the documented program is part of.") 46 | ap.add_argument("--prog", help="Substitutes %%prog in ArgumentParser's usage.") 47 | ap.add_argument("--version", help=( 48 | "Version of the program, will be visible in the " 49 | "manual page footer.")) 50 | ap.add_argument("--description", metavar="TEXT", help=( 51 | "description of the program, used in the NAME section, after the " 52 | "leading 'name - ' part, see man (7) man-pages for more info")) 53 | ap.add_argument("--long-description", metavar="TEXT", help=argparse.SUPPRESS) 54 | ap.add_argument("--author", action="append", dest="authors", metavar="[AUTHOR]", 55 | help="Author of the program. Can be specified multiple times.") 56 | ap.add_argument("--author-email", action="append", dest="authors", 57 | help=argparse.SUPPRESS) 58 | ap.add_argument("--url", help="Link to project's homepage") 59 | ap.add_argument("--format", default="pretty", choices=("pretty", "single-commands-section"), 60 | help="Format of the generated man page. Defaults to 'pretty'.") 61 | ap.add_argument("--output", dest='outfile', default='-', 62 | help="Output file. Defaults to stdout.") 63 | ap.add_argument("--manual-section", help=( 64 | "Section of the manual, by default 1. See man (7) man-pages for more " 65 | "info about existing sections.")) 66 | ap.add_argument("--manual-title", help=( 67 | "The title of the manual, by default \"Generated Python Manual\". " 68 | "See man (7) man-pages for more instructions.")) 69 | ap.add_argument("--include", metavar="FILE", help=( 70 | "File that contains extra material for the man page.")) 71 | ap.add_argument("--manfile", metavar="FILE", help=( 72 | "File containing a complete man page.")) 73 | 74 | 75 | def args_to_manpage_data(args): 76 | data = {} 77 | for attr in MANPAGE_DATA_ATTRS: 78 | value = getattr(args, attr) 79 | data[attr] = value 80 | return data 81 | 82 | 83 | def main(): 84 | args = ap.parse_args() 85 | 86 | import_type = 'pyfile' 87 | import_from = args.pyfile 88 | if args.module: 89 | import_type = 'module' 90 | import_from = args.module 91 | 92 | obj_type = 'object' 93 | obj_name = args.object 94 | if args.function: 95 | obj_type = 'function' 96 | obj_name = args.function 97 | 98 | parser = get_parser(import_type, import_from, obj_name, obj_type, prog=args.prog) 99 | data = args_to_manpage_data(args) 100 | manpage = Manpage(parser, format=args.format, _data=data) 101 | write_to_filename(str(manpage), args.outfile) 102 | -------------------------------------------------------------------------------- /argparse_manpage/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compatibility hacks for the argparse-manpage project. 3 | """ 4 | 5 | import sys 6 | 7 | # Drop once Python 2.7 is dropped 8 | # pylint: disable=unused-import 9 | try: 10 | from configparser import ConfigParser, NoSectionError 11 | except ImportError: 12 | from ConfigParser import SafeConfigParser as ConfigParser, NoSectionError # type: ignore 13 | 14 | if sys.version_info < (3, 0): 15 | import imp # pylint: disable=deprecated-module 16 | def load_py_file(filename): 17 | """ Small wrapper having the same call arg list as runpy.run_path() """ 18 | return imp.load_source("argparse_manpage_loaded_file", filename) 19 | else: 20 | from runpy import run_path as load_py_file 21 | 22 | def get_module_object(module_or_dict, objname, objtype): 23 | """ 24 | Get OBJNAME from a given MODULE (or dict, if loaded using runpy.run_path(), 25 | but call the object first if OBJTYPE is 'function'. 26 | """ 27 | obj = None 28 | if isinstance(module_or_dict, dict): 29 | obj = module_or_dict[objname] 30 | else: 31 | obj = getattr(module_or_dict, objname) 32 | if objtype != 'object': 33 | obj = obj() 34 | return obj 35 | 36 | 37 | def load_file_as_module(filename): 38 | """ 39 | Load a given python filename as a dict (runpy on Python 3) or as a module 40 | (imp module, Python 2). Note that 'runpy.run_path()' doesn't work correctly 41 | with Python 2.7 (the imported object doesn't see it's own globals/imported 42 | modules), and 'imp' module is deprecated for modern Python 3. 43 | """ 44 | 45 | # We used to call 'runpy.run_path()' here, but that did not work correctly 46 | # with Python 2.7 where the imported object did not see it's own 47 | # globals/imported modules (including the 'argparse' module). 48 | return load_py_file(filename) 49 | -------------------------------------------------------------------------------- /argparse_manpage/manpage.py: -------------------------------------------------------------------------------- 1 | from argparse import SUPPRESS, HelpFormatter, _SubParsersAction, _HelpAction 2 | from collections import OrderedDict 3 | import datetime 4 | import os 5 | import time 6 | import re 7 | 8 | 9 | DEFAULT_GROUP_NAMES = { 10 | # We replace ArgumentGroup title (value) with alias (key). 11 | None: [ 12 | 'positional arguments', 13 | ], 14 | 'OPTIONS': [ 15 | 'optional arguments', 16 | 'options', 17 | ] 18 | } 19 | 20 | 21 | DEFAULT_GROUP_NAMES_SUBCOMMANDS = { 22 | # We replace ArgumentGroup title (value) with alias (key). 23 | "arguments:": [ 24 | 'positional arguments', 25 | ], 26 | 'options:': [ 27 | 'optional arguments', 28 | 'options', 29 | ] 30 | } 31 | 32 | 33 | # all manpage attributes that can be set via CLI or setup.cfg 34 | MANPAGE_DATA_ATTRS = ( 35 | "authors", # just "author" in setup.cfg, can be specified multiple times 36 | "description", 37 | "long_description", # not in the manpage.py, we use parser.description 38 | "project_name", # maps to distribution.get_name() 39 | "prog", 40 | "url", 41 | "version", 42 | "format", 43 | "manual_section", 44 | "manual_title", 45 | "include", 46 | "manfile", 47 | ) 48 | 49 | # manpage sections that are handled specially, so need special treatment 50 | # when --include'ing extra material; see Manpage.add_section. 51 | SPECIAL_MANPAGE_SECTIONS = ( 52 | "author", 53 | "comments", 54 | "description", 55 | "distribution", 56 | "synopsis", 57 | ) 58 | 59 | 60 | def _markup(text): 61 | """ 62 | Escape the text for the markdown format. 63 | """ 64 | if isinstance(text, str): 65 | return text.replace('\\', '\\\\').replace('-', r'\-') 66 | return text 67 | 68 | 69 | def get_manpage_data_from_distribution(distribution, data): 70 | """ 71 | Update `data` with values from `distribution`. 72 | """ 73 | # authors 74 | if not "authors" in data: 75 | author = None 76 | if distribution.get_author(): 77 | author = distribution.get_author() 78 | if distribution.get_author_email(): 79 | author += " <{}>".format(distribution.get_author_email()) 80 | elif distribution.get_author_email(): 81 | author = distribution.get_author_email() 82 | if author: 83 | data["authors"] = [author] 84 | 85 | attrs = list(MANPAGE_DATA_ATTRS) 86 | attrs.remove("authors") 87 | attrs.remove("prog") # not available, copied from 'project_name' later 88 | attrs.remove("format") # not available, must be set in setup.cfg 89 | # we want the utility description, not the project description 90 | attrs.remove("description") 91 | for attr in attrs: 92 | if data.get(attr, None): 93 | continue 94 | 95 | # map data["project_name"] to distribution.get_name() 96 | get_attr = "name" if attr == "project_name" else attr 97 | 98 | getter = getattr(distribution, "get_" + get_attr, None) 99 | if not getter: 100 | continue 101 | value = getter() 102 | 103 | data[attr] = value 104 | 105 | if "prog" not in data: 106 | data["prog"] = data["project_name"] 107 | 108 | 109 | def _get_footer_lines(data): 110 | ret = [] 111 | project_name = data.get("project_name", "") 112 | authors = data.get("authors") 113 | url = data.get("url") 114 | 115 | needs_separator = False 116 | if authors: 117 | ret.append('.SH AUTHOR') 118 | for author in authors: 119 | ret.append(".nf") 120 | ret.append(author) 121 | ret.append(".fi") 122 | needs_separator = True 123 | 124 | if url: 125 | if needs_separator: 126 | ret.append("") 127 | ret.append(".SH DISTRIBUTION") 128 | ret.append("The latest version of {0} may " 129 | "be downloaded from".format(_markup(project_name))) 130 | ret.append(".UR {0}".format(_markup(url))) 131 | ret.append(".UE") 132 | return ret 133 | 134 | 135 | def get_footer(data): 136 | """ 137 | Return a manual page footer based on the data returned from 138 | get_manpage_data_from_distribution(). Used only by the old build_manpage 139 | module. 140 | """ 141 | return "\n".join(_get_footer_lines(data)) + "\n" 142 | 143 | 144 | # This is already considered an API, and seems like a valid scenario: 145 | # https://github.com/pypa/pipx/blob/fd6650bcaeca3088/scripts/generate_man.py 146 | 147 | class Manpage(object): 148 | # pylint: disable=too-many-instance-attributes 149 | def __init__(self, parser, _data=None, format='pretty'): 150 | """ 151 | Manual page abstraction. Generates, with the help of formater, a manual 152 | page by __str__() method. Please avoid using the private _data 153 | argument (see https://github.com/praiskup/argparse-manpage/issues/7), 154 | instead override the `self.` when needed. 155 | """ 156 | self.prog = parser.prog 157 | self.parser = parser 158 | self.format = format 159 | self._data = _data or {} 160 | self._match_texts = [] 161 | if not getattr(parser, '_manpage', None): 162 | self.parser._manpage = [] 163 | 164 | self.manfile = self._data.get("manfile") 165 | if self.manfile: 166 | if len(self._data) > 1: 167 | raise ValueError("manfile set, so no other key is allowed") 168 | return 169 | 170 | self.formatter = self.parser._get_formatter() 171 | self.mf = _ManpageFormatter(self.prog, self.formatter, format=self.format) 172 | self.synopsis = self.parser.format_usage().split(':', 1)[-1].split() 173 | 174 | self.date = self._data.get("date") 175 | if not self.date: 176 | builddate = datetime.datetime.utcfromtimestamp( 177 | int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 178 | ) 179 | self.date = builddate.strftime('%Y-%m-%d') 180 | 181 | self.source = self._data.get("project_name") 182 | if not self.source: 183 | self.source = self.prog 184 | 185 | version = self._data.get("version") 186 | if version: 187 | self.source += " " + str(version) 188 | 189 | self.manual = self._data.get("manual_title") 190 | if not self.manual: 191 | self.manual = "Generated Python Manual" 192 | 193 | self.section = self._data.get("manual_section") 194 | if not self.section: 195 | self.section = 1 196 | 197 | self.description = self._data.get("description") 198 | 199 | include = self._data.get("include") 200 | if include is not None: 201 | self.parse_include(include) 202 | 203 | def format_text(self, text): 204 | # Wrap by parser formatter and convert to manpage format 205 | return self.mf.format_text(self.formatter._format_text(text)).strip('\n') 206 | 207 | def __str__(self): 208 | if self.manfile: 209 | with open(self.manfile) as fd: 210 | return fd.read() 211 | 212 | lines = [] 213 | 214 | # Header 215 | # per man (7) man-pages: .TH title section date source manual 216 | header = '.TH {title} "{section}" "{date}" "{source}" "{manual}"' 217 | lines.append(header.format( 218 | title=_markup(self.prog.upper()), 219 | section=self.section, 220 | date=_markup(self.date), 221 | source=_markup(self.source), 222 | manual=_markup(self.manual), 223 | )) 224 | 225 | # Name 226 | lines.append('.SH NAME') 227 | line = self.prog 228 | 229 | description = None 230 | if getattr(self.parser, 'man_short_description', None): 231 | # Let's keep this undocumented. There's a way to specify this in 232 | # setup.cfg: 'description' 233 | description = self.parser.man_short_description 234 | if self.description: 235 | description = self.description 236 | if description: 237 | line += " - " + description 238 | lines.append(_markup(line)) 239 | 240 | # Synopsis 241 | synopsis_section = self.get_extra_section("synopsis") 242 | if self.synopsis or synopsis_section: 243 | lines.append('.SH SYNOPSIS') 244 | if synopsis_section: 245 | lines.append(synopsis_section["content"]) 246 | else: 247 | lines.append('.B {}'.format(_markup(self.synopsis[0]))) 248 | lines.append(' '.join(self.synopsis[1:])) 249 | 250 | extra_description = None 251 | description_section = self.get_extra_section("description") 252 | if description_section: 253 | extra_description = description_section["content"] 254 | lines.extend(self.mf.format_parser(self.parser, extra_description=extra_description)) 255 | 256 | comments_section = self.get_extra_section("comments") 257 | if self.parser.epilog or comments_section: 258 | lines.append("") 259 | lines.append('.SH COMMENTS') 260 | if comments_section: 261 | lines.append(comments_section["content"]) 262 | else: 263 | lines.append(self.format_text(self.parser.epilog)) 264 | 265 | # Additional sections 266 | for section in self.parser._manpage: # pylint: disable=protected-access 267 | if section["heading"] not in SPECIAL_MANPAGE_SECTIONS: 268 | lines.append('.SH {}'.format(section['heading'].upper())) 269 | lines.append(section['content']) 270 | 271 | lines.append("") 272 | lines.extend(self.mf.format_footer(self._data)) 273 | 274 | # Finally add --include sections that match text in the page 275 | final_lines = [] 276 | for line in lines: 277 | final_lines.append(line) 278 | for match in self._match_texts: 279 | if re.search(match['match_text'], line): 280 | final_lines.append(match['content']) 281 | 282 | return "\n".join(final_lines).strip("\n") + "\n" 283 | 284 | def get_extra_section(self, heading): 285 | """ 286 | Return supplementary section for the `Manpage` (created with 287 | `--include`), or `None` 288 | """ 289 | for section in self.parser._manpage: # pylint: disable=protected-access 290 | if section["heading"] == heading: 291 | return section 292 | return None 293 | 294 | def add_section(self, heading, position, content): 295 | """ 296 | Add a supplementary section to a `Manpage` 297 | """ 298 | # Sections that need special treatment 299 | heading = heading.lower() 300 | if heading in ("author", "distribution"): 301 | if heading == "author": 302 | self._data['authors'] = [content] 303 | elif heading == "distribution": 304 | self._data['url'] = content 305 | section = self.get_extra_section(heading) 306 | if section is None: 307 | section = {"heading": heading, "content": ""} 308 | self.parser._manpage.append(section) # pylint: disable=protected-access 309 | if position == '<': 310 | section["content"] = content + section["content"] 311 | elif position == '=': 312 | section["content"] = content 313 | elif position == '>': 314 | section["content"] += content 315 | else: 316 | raise ValueError("invalid position " + position) 317 | 318 | def parse_include(self, file): 319 | """ 320 | Parse include file and add its contents to the man page 321 | """ 322 | def get_section(lines, n): 323 | for i, line in enumerate(lines[n:]): 324 | if re.match(r'[\[/]', line): 325 | return n + i, lines[n:n + i] 326 | return len(lines), lines[n:] 327 | 328 | with open(file) as f: 329 | lines = f.readlines() 330 | i = 0 331 | while i < len(lines): 332 | # Parse a header line 333 | m = re.match(r"/([^/]+)/$", lines[i]) 334 | if m: 335 | match_text = m.group(1) 336 | i, section_lines = get_section(lines, i + 1) 337 | self._match_texts.append({"match_text": match_text, "content": "".join(section_lines).strip()}) 338 | else: 339 | m = re.match(r"\[([<=>])?([^\]]+)\]$", lines[i]) 340 | if m: 341 | position = m.group(1) or '<' 342 | heading = m.group(2).upper() 343 | if heading == "NAME": 344 | raise ValueError("Invalid include section " + heading) 345 | i, section_lines = get_section(lines, i + 1) 346 | self.add_section(heading, position, "".join(section_lines).strip()) 347 | else: 348 | raise ValueError("Invalid or missing section header in include file %s:\n%s" % (file, lines[i])) 349 | 350 | 351 | def underline(text): 352 | """ 353 | Wrap text with \fI for underlined text 354 | """ 355 | return r'\fI\,{0}\/\fR'.format(_markup(text)) 356 | 357 | 358 | def bold(text): 359 | """ Wrap text by "bold" groff tags """ 360 | return r"\fB{0}\fR".format(_markup(text)) 361 | 362 | 363 | def quoted(text): 364 | """ Wrap by single-quotes """ 365 | return "'{0}'".format(text) 366 | 367 | 368 | class _ManpageFormatter(HelpFormatter): 369 | def __init__(self, prog, old_formatter, format): 370 | super(HelpFormatter, self).__init__() 371 | self._prog = prog 372 | self.of = old_formatter 373 | assert format in ("pretty", "single-commands-section") 374 | self.format = format 375 | 376 | @staticmethod 377 | def _get_aliases_str(aliases): 378 | if not aliases: 379 | return "" 380 | return " (" + ", ".join(aliases) + ")" 381 | 382 | def _format_action_invocation(self, action): 383 | if not action.option_strings: 384 | metavar, = self._metavar_formatter(action, action.dest)(1) 385 | return bold(metavar) 386 | 387 | parts = [] 388 | 389 | # if the Optional doesn't take a value, format is: 390 | # -s, --long 391 | if action.nargs == 0: 392 | parts.extend(map(bold, action.option_strings)) 393 | 394 | # if the Optional takes a value, format is: 395 | # -s ARGS, --long ARGS 396 | else: 397 | default = action.dest.upper() 398 | args_string = self._format_args(action, default) 399 | for option_string in action.option_strings: 400 | parts.append('{} {}'.format(bold(option_string), 401 | underline(args_string))) 402 | return ', '.join(parts) 403 | 404 | def _format_parser(self, parser, subcommand=None, aliases=None, help=None, extra_description=None): 405 | # The parser "tree" looks like 406 | # ---------------------------- 407 | # Parser -> [ActionGroup, ActionGroup, ..] 408 | # Group -> [Action, Action, ..] 409 | # Action -> Option 410 | # Action -> Subparsers 411 | # Subparser -> [Parser, Parser, ..] So called "choices". 412 | 413 | lines = [] 414 | if subcommand: 415 | if self.format == "pretty": 416 | lines.append("") 417 | # start a new section for each command 418 | first_line = ".SH COMMAND" 419 | first_line += " " + underline(quoted(subcommand)) 420 | elif self.format == "single-commands-section": 421 | # do not start a new section, start subsection of COMMANDS instead 422 | first_line = ".SS" 423 | first_line += " " + bold(subcommand + self._get_aliases_str(aliases)) 424 | lines.append(first_line) 425 | 426 | if help: 427 | if self.format == "pretty": 428 | # help is printed on top in the list of commands already 429 | pass 430 | elif self.format == "single-commands-section": 431 | # print help 432 | lines.append(help) 433 | lines.append("") 434 | 435 | lines.append(self.format_text(parser.format_usage())) 436 | 437 | if parser.description or extra_description: 438 | if subcommand: 439 | lines.append("") 440 | else: 441 | lines.append(".SH DESCRIPTION") 442 | 443 | if extra_description: 444 | lines.append(extra_description) 445 | if parser.description: 446 | lines.append(self.format_text(parser.description)) 447 | 448 | is_subsequent_ag = True 449 | for group in parser._action_groups: 450 | ag_lines = self._format_action_group(group, subcommand) 451 | if not ag_lines: 452 | continue 453 | if is_subsequent_ag: 454 | lines.append("") 455 | lines.extend(ag_lines) 456 | is_subsequent_ag = True 457 | 458 | return lines 459 | 460 | def format_parser(self, parser, extra_description=None): 461 | """ 462 | Return lines Groff formatted text for given parser 463 | """ 464 | return self._format_parser(parser, extra_description=extra_description) 465 | 466 | def _format_action(self, action): 467 | parts = [] 468 | parts.append('.TP') 469 | 470 | action_header = self._format_action_invocation(action) 471 | parts.append(action_header) 472 | 473 | # if there was help for the action, add lines of help text 474 | if action.help: 475 | help_text = self.of._format_text(self._expand_help(action)).strip('\n') 476 | parts.append(self.format_text(help_text)) 477 | 478 | return parts 479 | 480 | def _format_ag_subcommands(self, actions, prog): 481 | lines = [] 482 | 483 | for action in actions: 484 | if getattr(action, 'help', None) == SUPPRESS: 485 | continue 486 | lines.append('.TP') 487 | lines.append(bold(prog) + ' ' + underline(action.dest)) 488 | if hasattr(action, 'help'): 489 | lines.append(self.format_text(action.help)) 490 | 491 | return '\n'.join(lines) 492 | 493 | def _format_subparsers(self, action_group, action, subcommand=None): 494 | lines = [] 495 | 496 | if subcommand: 497 | if self.format == "pretty": 498 | # start a new section for each command 499 | lines.append('.SH') 500 | title = action_group.title.upper() 501 | title += " " + underline(quoted(subcommand)) 502 | lines.append(title) 503 | elif self.format == "single-commands-section": 504 | # do not start a new section, append subsections to the COMMANDS section 505 | pass 506 | else: 507 | # start a new section on top-level 508 | lines.append('.SH') 509 | title = action_group.title.upper() 510 | lines.append(title) 511 | 512 | if self.format == "pretty": 513 | # print list of subcommands 514 | lines.append(self._format_ag_subcommands(action._choices_actions, 515 | subcommand or self._prog)) 516 | elif self.format == "single-commands-section": 517 | # skip printing list of subcommands 518 | pass 519 | 520 | # gather (sub-)command aliases 521 | command_aliases = {} 522 | command_aliases_names = set() 523 | for name, command in action._name_parser_map.items(): 524 | if command not in command_aliases: 525 | command_aliases[command] = [] 526 | else: 527 | command_aliases[command].append(name) 528 | command_aliases_names.add(name) 529 | 530 | command_help = {} 531 | for i in action._choices_actions: 532 | command_help[i.dest] = i.help 533 | 534 | for name, choice in action.choices.items(): 535 | if name in command_aliases_names: 536 | # don't print aliased commands multiple times 537 | continue 538 | new_subcommand = "{} {}".format(subcommand or self._prog, name) 539 | aliases = command_aliases[choice] 540 | help = command_help.get(name, None) 541 | if help == SUPPRESS: 542 | # don't print hidden commands 543 | continue 544 | lines.extend(self._format_parser(choice, new_subcommand, aliases, help)) 545 | 546 | return lines 547 | 548 | def _format_action_group(self, action_group, subcommand=None): 549 | # Parser consists of these action_groups: 550 | # - positional arguments (no group_actions) 551 | # - ungrouped options 552 | # - group 1 options 553 | # - group 2 options 554 | # - ... 555 | # - subparsers 556 | 557 | content = [] 558 | some_action = False 559 | for action in action_group._group_actions: 560 | if action.help == SUPPRESS: 561 | continue 562 | 563 | if isinstance(action, _SubParsersAction): 564 | return self._format_subparsers(action_group, action, 565 | subcommand) 566 | 567 | if '--help' in action.option_strings: 568 | # TODO: put out some man page comment .. 569 | continue 570 | 571 | if some_action: 572 | # Separate actions 573 | content.append("") 574 | 575 | some_action = True 576 | content.extend(self._format_action(action)) 577 | 578 | # We don't print empty argument groups. 579 | if not some_action: 580 | return [] 581 | 582 | title = action_group.title 583 | 584 | group_names = DEFAULT_GROUP_NAMES 585 | if subcommand: 586 | if self.format == "pretty": 587 | pass 588 | elif self.format == "single-commands-section": 589 | group_names = DEFAULT_GROUP_NAMES_SUBCOMMANDS 590 | 591 | for replace_with, defaults in group_names.items(): 592 | if title in defaults: 593 | title = replace_with 594 | 595 | if subcommand: 596 | if self.format == "pretty": 597 | title = title.upper() if title else "" 598 | if title: 599 | title += " " + underline(quoted(subcommand)) 600 | title = [] if not title else [".SH " + title] 601 | elif self.format == "single-commands-section": 602 | title = [] if not title else [title] 603 | else: 604 | title = title.upper() if title else "" 605 | title = [] if not title else [".SH " + title] 606 | 607 | description = [] 608 | if action_group.description: 609 | description.append(self.format_text(action_group.description)) 610 | description.append("") 611 | 612 | if subcommand: 613 | if self.format == "pretty": 614 | # don't indent the whole content of a subcommand 615 | pass 616 | elif self.format == "single-commands-section": 617 | # indent the whole content of a subcommand 618 | content = [".RS 7"] + content + [".RE"] + [""] 619 | 620 | return title + description + content 621 | 622 | @staticmethod 623 | def format_text(text): 624 | """ 625 | Format a block of text as it was a single line in set of other lines 626 | (e.g. no trailing newline). 627 | """ 628 | return _markup(text.strip('\n')) 629 | 630 | @staticmethod 631 | def format_footer(data): 632 | """ 633 | Get lines for footer. 634 | """ 635 | return _get_footer_lines(data) 636 | -------------------------------------------------------------------------------- /argparse_manpage/tooling.py: -------------------------------------------------------------------------------- 1 | """ 2 | A tooling helpers for the argparse-manpage project. 3 | """ 4 | 5 | import importlib 6 | import os 7 | import sys 8 | 9 | from .compat import load_file_as_module, get_module_object 10 | 11 | 12 | def _environ_hack(): 13 | os.environ['BUILD_MANPAGES_RUNNING'] = 'TRUE' 14 | 15 | 16 | def get_parser_from_module(module, objname, objtype='object', prog=None): 17 | """ 18 | Read the given module and return the requested object from there. 19 | """ 20 | _environ_hack() 21 | # We need to set argv[0] to properly so argparse returns appropriate "usage" 22 | # strings. Like "usage: argparse-manpage [-h] ...", instead of 23 | # "usage: setup.py ...". 24 | backup_argv = sys.argv 25 | if prog: 26 | sys.argv = [prog] 27 | 28 | mod = importlib.import_module(module) 29 | obj = get_module_object(mod, objname, objtype) 30 | 31 | # Restore callee's argv 32 | sys.argv = backup_argv 33 | return obj 34 | 35 | 36 | def get_parser_from_file(filename, objname, objtype='object', prog=None): 37 | """ 38 | Load the given filename as a module and return the requested object from 39 | there. 40 | """ 41 | _environ_hack() 42 | # We need to set argv[0] to properly so argparse returns appropriate "usage" 43 | # strings. Like "usage: argparse-manpage [-h] ...", instead of 44 | # "usage: setup.py ...". 45 | backup_argv = sys.argv 46 | if prog: 47 | sys.argv = [prog] 48 | else: 49 | sys.argv = [os.path.basename(filename)] 50 | 51 | # Get the ArgumentParser object 52 | module_loaded = load_file_as_module(filename) 53 | obj = get_module_object(module_loaded, objname, objtype) 54 | 55 | # Restore callee's argv 56 | sys.argv = backup_argv 57 | return obj 58 | 59 | 60 | def get_parser(import_type, import_from, objname, objtype, prog=None): 61 | """ 62 | Load a function or object from a given file or module. 63 | """ 64 | if import_type == 'pyfile': 65 | return get_parser_from_file(import_from, objname, objtype, prog=prog) 66 | return get_parser_from_module(import_from, objname, objtype, prog=prog) 67 | 68 | 69 | def write_to_filename(text, filename): 70 | """ 71 | Write given text into a filename at once. Pre-create the parent directory 72 | if it doesn't exist yet. Print to stdout if filename == '-'. 73 | """ 74 | if filename == '-': 75 | sys.stdout.write(text) 76 | else: 77 | dirname = os.path.dirname(filename) 78 | if dirname and not os.path.exists(dirname): 79 | os.makedirs(dirname) 80 | with open(filename, 'w') as stream: 81 | stream.write(text) 82 | -------------------------------------------------------------------------------- /build_manpages/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Export some useful methods in top-level. 3 | """ 4 | 5 | from argparse_manpage import __version__ 6 | from .build_manpages import build_manpages, get_build_py_cmd, get_install_cmd 7 | 8 | install = get_install_cmd() 9 | build_py = get_build_py_cmd() 10 | -------------------------------------------------------------------------------- /build_manpages/build_manpage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """build_manpage command -- Generate man page from setup()""" 4 | 5 | import datetime 6 | import optparse 7 | import argparse 8 | import os 9 | import time 10 | import warnings 11 | 12 | from distutils.core import Command 13 | from distutils.errors import DistutilsOptionError 14 | 15 | from argparse_manpage.manpage import ( 16 | get_manpage_data_from_distribution, 17 | get_footer, 18 | ) 19 | 20 | from argparse_manpage.tooling import ( 21 | get_parser_from_file, 22 | get_parser_from_module, 23 | write_to_filename, 24 | ) 25 | 26 | warnings.warn( 27 | "The 'build_manpage' module will be removed in the next argparse-manpage " 28 | "version v5. Please migrate to 'build_manpages'.", DeprecationWarning, 29 | ) 30 | 31 | class ManPageWriter(object): 32 | _parser = None 33 | _command = None 34 | _type = None 35 | 36 | def __init__(self, parser, values): 37 | self._parser = parser 38 | self.values = values 39 | self._today = datetime.datetime.utcfromtimestamp( 40 | int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 41 | ) 42 | 43 | if isinstance(parser, argparse.ArgumentParser): 44 | self._type = 'argparse' 45 | if parser.formatter_class == argparse.HelpFormatter: 46 | # Hack for issue #36, to have reproducible manual page content 47 | # regardless the terminal window size. Long term we should avoid 48 | # using the built-in usage formatter, and generate our own. 49 | parser.formatter_class = \ 50 | lambda prog: argparse.HelpFormatter(prog, width=78) 51 | 52 | else: 53 | self._parser.formatter = ManPageFormatter() 54 | self._parser.formatter.set_parser(self._parser) 55 | 56 | def _markup(self, txt): 57 | return txt.replace('-', '\\-') 58 | 59 | def _write_header(self): 60 | version = self.values["version"] 61 | prog = self.values["prog"] 62 | ret = [] 63 | 64 | ret.append('.TH %s 1 %s "%s v.%s"\n' % (self._markup(prog), 65 | self._today.strftime('%Y\\-%m\\-%d'), prog, version)) 66 | 67 | description = self.values.get("description") 68 | if description: 69 | name = self._markup('%s - %s' % (prog, description.splitlines()[0])) 70 | else: 71 | name = self._markup(prog) 72 | 73 | ret.append('.SH NAME\n%s\n' % name) 74 | if getattr(self._parser, 'format_usage', None): 75 | synopsis = self._parser.format_usage() 76 | else: 77 | synopsis = self._parser.get_usage() 78 | 79 | if synopsis: 80 | synopsis = synopsis.replace('%s ' % prog, '') 81 | ret.append('.SH SYNOPSIS\n.B %s\n%s\n' % (self._markup(prog), 82 | synopsis)) 83 | long_desc = self.values.get("long_description", "") 84 | if long_desc: 85 | ret.append('.SH DESCRIPTION\n%s\n' % self._markup(long_desc)) 86 | return ''.join(ret) 87 | 88 | def _write_options(self, action_name=None, parser=None): 89 | if not parser: 90 | parser = self._parser 91 | 92 | if not action_name: 93 | ret = ['.SH OPTIONS\n'] 94 | else: 95 | ret = ['.SH OPTIONS ' + action_name.upper() + '\n'] 96 | 97 | ret.append(parser.format_option_help()) 98 | if self._type != 'argparse': 99 | return ''.join(ret) 100 | 101 | subparsers_actions = [ 102 | action for action in parser._actions 103 | if isinstance(action, argparse._SubParsersAction)] 104 | 105 | for subparser_action in subparsers_actions: 106 | for name, obj in subparser_action.choices.items(): 107 | if action_name: 108 | an = action_name + " " + name 109 | else: 110 | an = name 111 | ret.append(self._write_options(an, obj)) 112 | 113 | return ''.join(ret) 114 | 115 | def _write_seealso(self, text): 116 | ret = [] 117 | ret.append('.SH "SEE ALSO"\n') 118 | 119 | for i in text: 120 | name, sect = i.split(":") 121 | 122 | if len(ret) > 1: 123 | ret.append(',\n') 124 | 125 | ret.append('.BR %s (%s)' % (name, sect)) 126 | 127 | return ''.join(ret) 128 | 129 | def write(self, filename, seealso=None): 130 | manpage = [] 131 | manpage.append(self._write_header()) 132 | manpage.append(self._write_options()) 133 | manpage.append(get_footer(self.values)) 134 | if seealso: 135 | manpage.append(self._write_seealso(seealso)) 136 | write_to_filename(''.join(manpage), filename) 137 | 138 | 139 | class build_manpage(Command): 140 | 141 | description = 'Generate man page from setup().' 142 | 143 | user_options = [ 144 | ('output=', 'O', 'output file'), 145 | ('parser=', None, 'module path to optparser (e.g. mymod:func'), 146 | ('parser-file=', None, 'file to the parser module'), 147 | ('file-and-object=', None, 'import parser object from file, e.g. "bin/blah.py:fooparser"'), 148 | ('seealso=', None, 'list of manpages to put into the SEE ALSO section (e.g. bash:1)') 149 | ] 150 | 151 | def initialize_options(self): 152 | self.output = None 153 | self.parser = None 154 | self.seealso = None 155 | self.parser_file = None 156 | self.file_and_object = None 157 | 158 | def _get_parser_from_module(self): 159 | mod_name, func_name = self.parser.split(':') 160 | if self.parser_file: 161 | # The 'modname' is entirely ignored in this case. This is a design 162 | # issue from 516ca12512979ab8e1a45f24e502a9cd1331f284. But we keep 163 | # the 'build_manpage' for compatibility reasons. 164 | return get_parser_from_file(self.parser_file, func_name, 165 | 'function') 166 | return get_parser_from_module(mod_name, func_name, 'function') 167 | 168 | def finalize_options(self): 169 | if self.output is None: 170 | raise DistutilsOptionError('\'output\' option is required') 171 | if self.parser is None and self.file_and_object is None: 172 | raise DistutilsOptionError('\'parser\' or \'file-and-object\' option is required') 173 | 174 | self.ensure_string_list('seealso') 175 | 176 | if self.file_and_object: 177 | filename, objname = self.file_and_object.split(':') 178 | self._parser = get_parser_from_file(filename, objname) 179 | else: 180 | self._parser = self._get_parser_from_module() 181 | 182 | 183 | def run(self): 184 | self.announce('Writing man page %s' % self.output) 185 | 186 | data = {} 187 | get_manpage_data_from_distribution(self.distribution, data) 188 | 189 | # the format is actually unused 190 | mpw = ManPageWriter(self._parser, data) 191 | mpw.write(self.output, seealso=self.seealso) 192 | 193 | 194 | class ManPageFormatter(optparse.HelpFormatter): 195 | 196 | def __init__(self, 197 | indent_increment=2, 198 | max_help_position=24, 199 | width=None, 200 | short_first=1): 201 | optparse.HelpFormatter.__init__(self, indent_increment, 202 | max_help_position, width, short_first) 203 | 204 | def _markup(self, txt): 205 | return txt.replace('-', '\\-') 206 | 207 | def format_usage(self, usage): 208 | return self._markup(usage) 209 | 210 | def format_heading(self, heading): 211 | if self.level == 0: 212 | return '' 213 | return '.TP\n%s\n' % self._markup(heading.upper()) 214 | 215 | def format_option(self, option): 216 | result = [] 217 | opts = self.option_strings[option] 218 | result.append('.TP\n.B %s\n' % self._markup(opts)) 219 | if option.help: 220 | help_text = '%s\n' % self._markup(self.expand_default(option)) 221 | result.append(help_text) 222 | return ''.join(result) 223 | -------------------------------------------------------------------------------- /build_manpages/build_manpages.py: -------------------------------------------------------------------------------- 1 | """ 2 | build_manpages command -- generate set of manual pages by the setup() 3 | command. 4 | """ 5 | 6 | import os 7 | import shutil 8 | 9 | try: 10 | import tomllib 11 | from tomllib import TOMLDecodeError 12 | except ImportError: 13 | try: 14 | import tomli as tomllib 15 | from tomli import TOMLDecodeError 16 | except ImportError: 17 | import toml as tomllib 18 | from toml import TomlDecodeError as TOMLDecodeError 19 | 20 | from argparse_manpage.compat import ConfigParser, NoSectionError 21 | from argparse_manpage.tooling import get_parser, write_to_filename 22 | from argparse_manpage.manpage import ( 23 | Manpage, 24 | MANPAGE_DATA_ATTRS, 25 | get_manpage_data_from_distribution, 26 | ) 27 | 28 | from .compat import ( 29 | build_py, 30 | Command, 31 | DistutilsOptionError, 32 | install, 33 | ) 34 | 35 | DEFAULT_CMD_NAME = 'build_manpages' 36 | 37 | def parse_manpages_spec(string): 38 | manpages_data = {} 39 | for spec in string.strip().split('\n'): 40 | manpagedata = {} 41 | output = True 42 | 43 | basename = None 44 | for option in spec.split(':'): 45 | if output: 46 | outputfile = option 47 | output = False 48 | continue 49 | 50 | oname, ovalue = option.split('=') 51 | 52 | if oname == 'function' or oname == 'object': 53 | assert(not 'objtype' in manpagedata) 54 | manpagedata['objtype'] = oname 55 | manpagedata['objname'] = ovalue 56 | 57 | elif oname == 'pyfile' or oname == 'module': 58 | assert(not 'import_type' in manpagedata) 59 | manpagedata['import_type'] = oname 60 | manpagedata['import_from'] = ovalue 61 | if oname == 'pyfile': 62 | basename = os.path.basename(ovalue) 63 | 64 | elif oname == 'format': 65 | assert(not 'format' in manpagedata) 66 | manpagedata[oname] = ovalue 67 | 68 | elif oname == 'author': 69 | manpagedata.setdefault("authors", []).append(ovalue) 70 | 71 | elif oname in MANPAGE_DATA_ATTRS and oname != "authors": 72 | assert(not oname in manpagedata) 73 | manpagedata[oname] = ovalue 74 | 75 | else: 76 | raise ValueError("Unknown manpage configuration option: {}".format(oname)) 77 | 78 | if "prog" not in manpagedata and basename: 79 | manpagedata["prog"] = basename 80 | 81 | manpages_data[outputfile] = manpagedata 82 | 83 | return manpages_data 84 | 85 | def get_pyproject_settings(): 86 | """Parse and handle errors of a toml configuration file.""" 87 | try: 88 | with open("pyproject.toml", mode="r") as fp: 89 | content = tomllib.loads(fp.read()) 90 | except TOMLDecodeError: 91 | return None 92 | 93 | try: 94 | value = content["tool"][DEFAULT_CMD_NAME]["manpages"] 95 | if isinstance(value, list): 96 | value = "\n".join(value) 97 | return str(value) 98 | except KeyError: 99 | return None 100 | 101 | class build_manpages(Command): 102 | description = 'Generate set of man pages from setup().' 103 | user_options = [ 104 | ('manpages=', 'O', 'list man pages specifications'), 105 | ] 106 | 107 | def initialize_options(self): 108 | self.manpages = None 109 | 110 | 111 | def finalize_options(self): 112 | manpages = self.manpages or get_pyproject_settings() 113 | if not manpages: 114 | raise DistutilsOptionError('\'manpages\' option is required') 115 | self.manpages_data = parse_manpages_spec(manpages) 116 | 117 | # if a value wasn't set in setup.cfg, use the value from setup.py 118 | for page, data in self.manpages_data.items(): 119 | get_manpage_data_from_distribution(self.distribution, data) 120 | 121 | def run(self): 122 | for page, data in self.manpages_data.items(): 123 | if data.get('manfile'): 124 | print ("using pre-written " + page) 125 | return 126 | print ("generating " + page) 127 | parser = get_parser(data['import_type'], data['import_from'], data['objname'], data['objtype'], data.get('prog', None)) 128 | format = data.get('format', 'pretty') 129 | if format in ('pretty', 'single-commands-section'): 130 | manpage = Manpage(parser, format=format, _data=data) 131 | write_to_filename(str(manpage), page) 132 | elif format == 'old': 133 | # TODO: drop the "old" format support, and stop depending on ManPageWriter 134 | # pylint: disable=import-outside-toplevel 135 | from .build_manpage import ManPageWriter 136 | mw = ManPageWriter(parser, data) 137 | mw.write(page) 138 | else: 139 | raise ValueError("Unknown format: {}".format(format)) 140 | 141 | 142 | def get_build_py_cmd(command=build_py): 143 | """ 144 | Override the default 'setup.py build_py' command with one that automatically 145 | generates manual pages. By default we use an overridden 146 | 'setuptools.command.build_py' (class). If your project already uses an 147 | overridden class, specify the optional 'command=YourCommandClass`. 148 | """ 149 | class _build_manpages_build_py(command): 150 | def run(self): 151 | self.run_command(DEFAULT_CMD_NAME) 152 | command.run(self) 153 | 154 | return _build_manpages_build_py 155 | 156 | 157 | def get_install_cmd(command=install): 158 | """ 159 | Override the default 'setup.py install' command with one that automatically 160 | installs the manual pages generated by `build_manpages`, see 161 | 'build_manpages.get_build_py_cmd()'. By default we use the 162 | 'setuptools.command.install' class as the base. If you already use an 163 | such an overridden class, set the optional 'command=YourCommandClass'. 164 | """ 165 | class _build_manpages_install(command): 166 | def install_manual_pages(self): 167 | """ 168 | Additional logic for installing the generated manual pages 169 | """ 170 | config = ConfigParser() 171 | config.read('setup.cfg') 172 | try: 173 | spec = config.get(DEFAULT_CMD_NAME, 'manpages') 174 | except NoSectionError: 175 | spec = get_pyproject_settings() 176 | if spec is None: 177 | raise ValueError("'manpage' configuration not found in setup.cfg or pyproject.toml") 178 | 179 | data = parse_manpages_spec(spec) 180 | 181 | mandir = os.path.join(self.install_data, 'share/man/man1') 182 | if not os.path.exists(mandir): 183 | os.makedirs(mandir) 184 | for key, _ in data.items(): 185 | print ('installing {0}'.format(key)) 186 | shutil.copy(key, mandir) 187 | 188 | def run(self): 189 | command.run(self) 190 | self.install_manual_pages() 191 | 192 | return _build_manpages_install 193 | -------------------------------------------------------------------------------- /build_manpages/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compat hacks for argparse-manpage's build_manpages module 3 | """ 4 | 5 | # pylint: disable=unused-import,deprecated-module,raise-missing-from 6 | 7 | try: 8 | from setuptools import Command 9 | from setuptools.command.build_py import build_py 10 | from setuptools.command.install import install 11 | except ImportError: 12 | try: 13 | from distutils.core import Command 14 | from distutils.command.build_py import build_py 15 | from distutils.command.install import install 16 | except ImportError as orig_err: 17 | raise ImportError( 18 | "To use the 'build_manpages' tool on Python 3.12+, " 19 | "you need to install 'setuptools'." 20 | ) 21 | 22 | # A separate try-except block from the one above. This is more likely to fail 23 | # than the above setuptools part and we would start using distutils just 24 | # beacause of the exception. 25 | try: 26 | from setuptools.errors import OptionError as DistutilsOptionError 27 | except ImportError: 28 | from distutils.errors import DistutilsOptionError 29 | -------------------------------------------------------------------------------- /build_manpages/manpage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compat only module. Users should use argparse_manpage.manpage:Manpage 3 | """ 4 | 5 | # pylint: disable=unused-import 6 | from argparse_manpage.manpage import Manpage 7 | -------------------------------------------------------------------------------- /check: -------------------------------------------------------------------------------- 1 | #! /bin/bash -x 2 | 3 | for python in ${PYTHON-python3}; do 4 | PYTHONPATH=$(pwd) ${python/-dev/} -m pytest -vv "$@" || exit 1 5 | done 6 | -------------------------------------------------------------------------------- /examples/argument_groups/bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | from argparse import ArgumentParser 4 | 5 | parser = ArgumentParser() 6 | 7 | parser.man_short_description = "templating system/generator for distributions" 8 | 9 | group1 = parser.add_argument_group("group1 title", "description for group 1") 10 | group2 = parser.add_argument_group() 11 | 12 | group1.add_argument( 13 | '--group-1-option', 14 | metavar='PROJECTDIR', 15 | type=str, 16 | help='Directory with project (defaults to CWD)', 17 | default="." 18 | ) 19 | 20 | group2.add_argument( 21 | '--group-2-option', 22 | metavar='DIST', 23 | type=str, 24 | help='Use distribution metadata specified by DIST yaml file', 25 | default="fedora-21-x86_64.yaml", 26 | ) 27 | 28 | group2.add_argument( 29 | "g2arg", 30 | help=( 31 | "Some longer multiline description should go here, " 32 | "and here and here. If you want, even here." 33 | ), 34 | ) 35 | 36 | parser.add_argument( 37 | '--top-option', 38 | metavar='MULTISPEC', 39 | type=str, 40 | help='Use MULTISPEC yaml file to fill the TEMPLATE file', 41 | ) 42 | 43 | subparsers = parser.add_subparsers( 44 | title="subparsers", 45 | description="subparsers description", 46 | help="subparsers help") 47 | 48 | parser_a = subparsers.add_parser('subparserA', help='a help') 49 | 50 | group = parser_a.add_argument_group( 51 | "subgroup", 52 | description=( 53 | "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris " 54 | "tincidunt sem sed arcu. Etiam dictum tincidunt diam. Duis sapien " 55 | "nunc, commodo et, interdum suscipit, sollicitudin et, dolor. " 56 | )) 57 | 58 | group.add_argument("--subgroup-option") 59 | 60 | parser_a.add_argument("--sub-parser-option") 61 | 62 | subsubparsers = parser_a.add_subparsers( 63 | title="sub-subparsers title", 64 | help="Some help text for sub-subparser", 65 | ) 66 | 67 | subsubparser_a = subsubparsers.add_parser('sub-subparserA', help='a help') 68 | subsubparser_a.add_argument("--doh") 69 | 70 | 71 | tpl_or_combinations = parser.add_mutually_exclusive_group(required=True) 72 | 73 | tpl_or_combinations.add_argument( 74 | '--template', 75 | metavar='TEMPLATE', 76 | type=str, 77 | help='Use TEMPLATE file, e.g. docker.tpl or a template string, ' 78 | 'e.g. "{{ config.docker.from }}"' 79 | ) 80 | 81 | tpl_or_combinations.add_argument( 82 | '--multispec-combinations', 83 | action='store_true', 84 | help='Print available multispec combinations', 85 | ) 86 | 87 | 88 | def _main(): 89 | parser.parse_args() 90 | 91 | if __name__ == "__main__": 92 | _main() 93 | -------------------------------------------------------------------------------- /examples/argument_groups/expected/test.1: -------------------------------------------------------------------------------- 1 | .TH TEST "2" "2023\-04\-09" "example 0.1.0" "Test Manual" 2 | .SH NAME 3 | test \- templating system/generator for distributions 4 | .SH SYNOPSIS 5 | .B test 6 | [-h] [--group-1-option PROJECTDIR] [--group-2-option DIST] [--top-option MULTISPEC] (--template TEMPLATE | --multispec-combinations) g2arg {subparserA} ... 7 | 8 | .SH OPTIONS 9 | .TP 10 | \fB\-\-top\-option\fR \fI\,MULTISPEC\/\fR 11 | Use MULTISPEC yaml file to fill the TEMPLATE file 12 | 13 | .TP 14 | \fB\-\-template\fR \fI\,TEMPLATE\/\fR 15 | Use TEMPLATE file, e.g. docker.tpl or a template string, e.g. "{{ 16 | config.docker.from }}" 17 | 18 | .TP 19 | \fB\-\-multispec\-combinations\fR 20 | Print available multispec combinations 21 | 22 | .SH GROUP1 TITLE 23 | description for group 1 24 | 25 | .TP 26 | \fB\-\-group\-1\-option\fR \fI\,PROJECTDIR\/\fR 27 | Directory with project (defaults to CWD) 28 | 29 | .TP 30 | \fB\-\-group\-2\-option\fR \fI\,DIST\/\fR 31 | Use distribution metadata specified by DIST yaml file 32 | 33 | .TP 34 | \fBg2arg\fR 35 | Some longer multiline description should go here, and here and here. If you 36 | want, even here. 37 | 38 | .SH 39 | SUBPARSERS 40 | .TP 41 | \fBtest\fR \fI\,subparserA\/\fR 42 | a help 43 | 44 | .SH COMMAND \fI\,'test subparserA'\/\fR 45 | usage: test g2arg subparserA [\-h] [\-\-subgroup\-option SUBGROUP_OPTION] 46 | [\-\-sub\-parser\-option SUB_PARSER_OPTION] 47 | {sub\-subparserA} ... 48 | 49 | .SH OPTIONS \fI\,'test subparserA'\/\fR 50 | .TP 51 | \fB\-\-sub\-parser\-option\fR \fI\,SUB_PARSER_OPTION\/\fR 52 | 53 | .SH SUBGROUP \fI\,'test subparserA'\/\fR 54 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Mauris tincidunt sem sed arcu. Etiam dictum tincidunt diam. Duis sapien nunc, commodo et, interdum suscipit, sollicitudin et, dolor. 55 | 56 | .TP 57 | \fB\-\-subgroup\-option\fR \fI\,SUBGROUP_OPTION\/\fR 58 | 59 | .SH 60 | SUB-SUBPARSERS TITLE \fI\,'test subparserA'\/\fR 61 | .TP 62 | \fBtest subparserA\fR \fI\,sub\-subparserA\/\fR 63 | a help 64 | 65 | .SH COMMAND \fI\,'test subparserA sub\-subparserA'\/\fR 66 | usage: test g2arg subparserA sub\-subparserA [\-h] [\-\-doh DOH] 67 | 68 | .SH OPTIONS \fI\,'test subparserA sub\-subparserA'\/\fR 69 | .TP 70 | \fB\-\-doh\fR \fI\,DOH\/\fR 71 | 72 | .SH AUTHOR 73 | .nf 74 | John Doe 75 | .fi 76 | 77 | .SH DISTRIBUTION 78 | The latest version of example may be downloaded from 79 | .UR http://example.com 80 | .UE 81 | -------------------------------------------------------------------------------- /examples/argument_groups/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpages] 2 | manpages = 3 | man/test.1:object=parser:pyfile=bin/test:manual_section=2:manual_title=Test Manual 4 | -------------------------------------------------------------------------------- /examples/argument_groups/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Example of argparse taken from: 4 | # https://pagure.io/copr/copr/blob/a4feb01bc35b8554f503d41795e7a184ff929dd4/f/cli/copr_cli 5 | 6 | import os 7 | import sys 8 | 9 | from setuptools import setup, find_packages 10 | 11 | # Just to make sure that build_manpage can be found. 12 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) 13 | 14 | from build_manpages.build_manpages \ 15 | import build_manpages, get_build_py_cmd, get_install_cmd 16 | 17 | from setuptools.command.install import install 18 | from distutils.command.build import build 19 | 20 | setup( 21 | name='example', 22 | description='This project does nothing.', 23 | long_description=('Long description of the project.'), 24 | author='John Doe', 25 | author_email='jd@example.com', 26 | version='0.1.0', 27 | url='http://example.com', 28 | packages=find_packages(), 29 | scripts=['bin/test'], 30 | cmdclass={ 31 | 'build_manpages': build_manpages, 32 | 'build': get_build_py_cmd(build), 33 | 'install': get_install_cmd(install), 34 | }, 35 | ) 36 | -------------------------------------------------------------------------------- /examples/copr/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /examples/copr/copr_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/copr/copr_cli/__init__.py -------------------------------------------------------------------------------- /examples/copr/copr_cli/build_config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from jinja2 import Environment 4 | 5 | template_string = """\ 6 | # This is development/testing only mock profile, not exactly the same as 7 | # is used on copr builders; but it is basically similar. If you need an 8 | # exact mock configuration (because you e.g. try to reproduce failed 9 | # build), such configuration is put alongside the built RPMs. 10 | 11 | include('/etc/mock/{{chroot}}.cfg') 12 | 13 | config_opts['root'] = '{{project_id}}_{{chroot}}' 14 | config_opts['chroot_additional_packages'] = ' 15 | {%- for pkg in additional_packages -%} 16 | {%- if loop.last -%} 17 | {{ pkg }} 18 | {%- else -%} 19 | {{ pkg }} {% endif -%} 20 | {%- endfor -%}' 21 | 22 | {% if repos %} 23 | config_opts['yum.conf'] += \"\"\" 24 | {% for repo in repos %} 25 | [{{ repo.id }}] 26 | name="{{ repo.name }}" 27 | baseurl={{ repo.url }} 28 | gpgcheck=0 29 | enabled=1 30 | skip_if_unavailable=1 31 | metadata_expire=0 32 | cost=1 33 | best=1 34 | {% endfor %} 35 | \"\"\" 36 | {% endif %} 37 | """ 38 | 39 | class MockProfile(object): 40 | def __init__(self, data): 41 | self.data = data 42 | 43 | def __str__(self): 44 | template = Environment().from_string(template_string) 45 | return template.render(self.data) 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/copr/copr_cli/util.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | try: 4 | from progress.bar import Bar 5 | except ImportError: 6 | progress = False 7 | else: 8 | progress = True 9 | 10 | 11 | def format_size(bytes_in): 12 | if bytes_in > 1000 * 1000: 13 | return '%.1fMB' % (bytes_in / 1000.0 / 1000) 14 | elif bytes_in > 10 * 1000: 15 | return '%ikB' % (bytes_in / 1000) 16 | elif bytes_in > 1000: 17 | return '%.1fkB' % (bytes_in / 1000.0) 18 | else: 19 | return '%ibytes' % bytes_in 20 | 21 | 22 | class ProgressMixin(object): 23 | 24 | @property 25 | def download_speed(self): 26 | if self.avg == 0.0: 27 | return "..." 28 | return format_size(1 / self.avg) + "/s" 29 | 30 | @property 31 | def downloaded(self): 32 | return format_size(self.index) 33 | 34 | 35 | class DummyBar(object): 36 | # pylint: disable=redefined-builtin 37 | def __init__(self, max=None): 38 | pass 39 | 40 | def next(self, n=None): 41 | pass 42 | 43 | def finish(self): 44 | pass 45 | 46 | 47 | if progress: 48 | class ProgressBar(Bar, ProgressMixin): 49 | message = "%(percent)d%%" 50 | suffix = "%(downloaded)s %(download_speed)s eta %(eta_td)s" 51 | else: 52 | ProgressBar = DummyBar 53 | -------------------------------------------------------------------------------- /examples/copr/expected-output.1: -------------------------------------------------------------------------------- 1 | .TH COPR "1" "2023\-04\-09" "example setup\-py\-overriden" "Generated Python Manual" 2 | .SH NAME 3 | copr 4 | .SH SYNOPSIS 5 | .B copr 6 | [-h] [--debug] [--config CONFIG] [--version] {hidden,whoami,list,mock-config,create,modify,delete,fork,build,buildpypi,buildgem,buildfedpkg,buildtito,buildmock,status,download-build,cancel,watch-build,delete-build,edit-chroot,get-chroot,add-package-tito,edit-package-tito,add-package-pypi,edit-package-pypi,add-package-mockscm,edit-package-mockscm,add-package-rubygems,edit-package-rubygems,list-packages,list-package-names,get-package,delete-package,reset-package,build-package,build-module} ... 7 | 8 | .SH OPTIONS 9 | .TP 10 | \fB\-\-debug\fR 11 | Enable debug output 12 | 13 | .TP 14 | \fB\-\-config\fR \fI\,CONFIG\/\fR 15 | Path to an alternative configuration file 16 | 17 | .TP 18 | \fB\-\-version\fR 19 | show program's version number and exit 20 | 21 | .SH 22 | ACTIONS 23 | .TP 24 | \fBcopr\fR \fI\,whoami\/\fR 25 | Print username that the client authenticates with against copr\-frontend 26 | .TP 27 | \fBcopr\fR \fI\,list\/\fR 28 | List all the copr of the provided 29 | .TP 30 | \fBcopr\fR \fI\,mock\-config\/\fR 31 | Get the mock profile (similar to koji mock\-config) 32 | .TP 33 | \fBcopr\fR \fI\,create\/\fR 34 | Create a new copr 35 | .TP 36 | \fBcopr\fR \fI\,modify\/\fR 37 | Modify existing copr 38 | .TP 39 | \fBcopr\fR \fI\,delete\/\fR 40 | Deletes the entire project 41 | .TP 42 | \fBcopr\fR \fI\,fork\/\fR 43 | Fork the project and builds in it 44 | .TP 45 | \fBcopr\fR \fI\,build\/\fR 46 | Build packages to a specified copr 47 | .TP 48 | \fBcopr\fR \fI\,buildpypi\/\fR 49 | Build PyPI package to a specified copr 50 | .TP 51 | \fBcopr\fR \fI\,buildgem\/\fR 52 | Build gem from rubygems.org to a specified copr 53 | .TP 54 | \fBcopr\fR \fI\,buildfedpkg\/\fR 55 | Build package from pkgs.fedoraproject.org 56 | .TP 57 | \fBcopr\fR \fI\,buildtito\/\fR 58 | submit a build from Git repository via Tito to a specified copr 59 | .TP 60 | \fBcopr\fR \fI\,buildmock\/\fR 61 | submit a build from SCM repository via Mock to a specified copr 62 | .TP 63 | \fBcopr\fR \fI\,status\/\fR 64 | Get build status of build specified by its ID 65 | .TP 66 | \fBcopr\fR \fI\,download\-build\/\fR 67 | Fetches built packages 68 | .TP 69 | \fBcopr\fR \fI\,cancel\/\fR 70 | Cancel build specified by its ID 71 | .TP 72 | \fBcopr\fR \fI\,watch\-build\/\fR 73 | Watch status and progress of build(s) specified by their ID 74 | .TP 75 | \fBcopr\fR \fI\,delete\-build\/\fR 76 | Delete build specified by its ID 77 | .TP 78 | \fBcopr\fR \fI\,edit\-chroot\/\fR 79 | Edit chroot of a project 80 | .TP 81 | \fBcopr\fR \fI\,get\-chroot\/\fR 82 | Get chroot of a project 83 | .TP 84 | \fBcopr\fR \fI\,add\-package\-tito\/\fR 85 | Creates a new Tito package 86 | .TP 87 | \fBcopr\fR \fI\,edit\-package\-tito\/\fR 88 | Edits an existing Tito package 89 | .TP 90 | \fBcopr\fR \fI\,add\-package\-pypi\/\fR 91 | Creates a new PyPI package 92 | .TP 93 | \fBcopr\fR \fI\,edit\-package\-pypi\/\fR 94 | Edits an existing PyPI package 95 | .TP 96 | \fBcopr\fR \fI\,add\-package\-mockscm\/\fR 97 | Creates a new Mock\-SCM package 98 | .TP 99 | \fBcopr\fR \fI\,edit\-package\-mockscm\/\fR 100 | Edits an existing Mock\-SCM package 101 | .TP 102 | \fBcopr\fR \fI\,add\-package\-rubygems\/\fR 103 | Creates a new RubyGems package 104 | .TP 105 | \fBcopr\fR \fI\,edit\-package\-rubygems\/\fR 106 | Edits a new RubyGems package 107 | .TP 108 | \fBcopr\fR \fI\,list\-packages\/\fR 109 | Returns list of packages in the given copr 110 | .TP 111 | \fBcopr\fR \fI\,list\-package\-names\/\fR 112 | Returns list of package names in the given copr 113 | .TP 114 | \fBcopr\fR \fI\,get\-package\/\fR 115 | Returns package of the given name in the given copr 116 | .TP 117 | \fBcopr\fR \fI\,delete\-package\/\fR 118 | Deletes the specified package 119 | .TP 120 | \fBcopr\fR \fI\,reset\-package\/\fR 121 | Resets (clears) default source of the specified package 122 | .TP 123 | \fBcopr\fR \fI\,build\-package\/\fR 124 | Builds the package from its default source 125 | .TP 126 | \fBcopr\fR \fI\,build\-module\/\fR 127 | Builds a given module in Copr 128 | 129 | .SH COMMAND \fI\,'copr whoami'\/\fR 130 | usage: copr whoami [\-h] 131 | 132 | .SH COMMAND \fI\,'copr list'\/\fR 133 | usage: copr list [\-h] [username|@groupname] 134 | 135 | .TP 136 | \fBusername|@groupname\fR 137 | The username or @groupname that you would like to list the coprs of (defaults 138 | to current user) 139 | 140 | .SH COMMAND \fI\,'copr mock\-config'\/\fR 141 | usage: copr mock\-config [\-h] project chroot 142 | 143 | .TP 144 | \fBproject\fR 145 | Expected format is /, / (including '@') or 146 | (name of project you own). 147 | 148 | .TP 149 | \fBchroot\fR 150 | chroot id, e.g. 'fedora\-rawhide\-x86_64' 151 | 152 | .SH COMMAND \fI\,'copr create'\/\fR 153 | usage: copr create [\-h] [\-\-chroot CHROOTS] [\-\-repo REPOS] 154 | [\-\-initial\-pkgs INITIAL_PKGS] [\-\-description DESCRIPTION] 155 | [\-\-instructions INSTRUCTIONS] 156 | [\-\-disable_createrepo DISABLE_CREATEREPO] 157 | [\-\-enable\-net {on,off}] [\-\-unlisted\-on\-hp {on,off}] 158 | [\-\-persistent] [\-\-auto\-prune {on,off}] 159 | name 160 | 161 | .TP 162 | \fBname\fR 163 | The name of the copr to create 164 | 165 | .SH OPTIONS \fI\,'copr create'\/\fR 166 | .TP 167 | \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 168 | Chroot to use for this copr 169 | 170 | .TP 171 | \fB\-\-repo\fR \fI\,REPOS\/\fR 172 | Repository to add to this copr 173 | 174 | .TP 175 | \fB\-\-initial\-pkgs\fR \fI\,INITIAL_PKGS\/\fR 176 | List of packages URL to build in this new copr 177 | 178 | .TP 179 | \fB\-\-description\fR \fI\,DESCRIPTION\/\fR 180 | Description of the copr 181 | 182 | .TP 183 | \fB\-\-instructions\fR \fI\,INSTRUCTIONS\/\fR 184 | Instructions for the copr 185 | 186 | .TP 187 | \fB\-\-disable_createrepo\fR \fI\,DISABLE_CREATEREPO\/\fR 188 | Disable metadata auto generation 189 | 190 | .TP 191 | \fB\-\-enable\-net\fR \fI\,{on,off}\/\fR 192 | If net should be enabled for builds in this project (default is off) 193 | 194 | .TP 195 | \fB\-\-unlisted\-on\-hp\fR \fI\,{on,off}\/\fR 196 | The project will not be shown on COPR home page 197 | 198 | .TP 199 | \fB\-\-persistent\fR 200 | Project and its builds will be undeletable. This option can only be specified 201 | by a COPR admin. 202 | 203 | .TP 204 | \fB\-\-auto\-prune\fR \fI\,{on,off}\/\fR 205 | If auto\-deletion of project's obsoleted builds should be enabled (default is 206 | on). This option can only be specified by a COPR admin. 207 | 208 | .SH COMMAND \fI\,'copr modify'\/\fR 209 | usage: copr modify [\-h] [\-\-chroot CHROOTS] [\-\-description DESCRIPTION] 210 | [\-\-instructions INSTRUCTIONS] [\-\-repo REPOS] 211 | [\-\-disable_createrepo DISABLE_CREATEREPO] 212 | [\-\-enable\-net {on,off}] [\-\-unlisted\-on\-hp {on,off}] 213 | [\-\-auto\-prune {on,off}] 214 | name 215 | 216 | .TP 217 | \fBname\fR 218 | The name of the copr to modify 219 | 220 | .SH OPTIONS \fI\,'copr modify'\/\fR 221 | .TP 222 | \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 223 | Chroot to use for this copr 224 | 225 | .TP 226 | \fB\-\-description\fR \fI\,DESCRIPTION\/\fR 227 | Description of the copr 228 | 229 | .TP 230 | \fB\-\-instructions\fR \fI\,INSTRUCTIONS\/\fR 231 | Instructions for the copr 232 | 233 | .TP 234 | \fB\-\-repo\fR \fI\,REPOS\/\fR 235 | Repository to add to this copr 236 | 237 | .TP 238 | \fB\-\-disable_createrepo\fR \fI\,DISABLE_CREATEREPO\/\fR 239 | Disable metadata auto generation 240 | 241 | .TP 242 | \fB\-\-enable\-net\fR \fI\,{on,off}\/\fR 243 | If net should be enabled for builds in this project (default is "don't 244 | change") 245 | 246 | .TP 247 | \fB\-\-unlisted\-on\-hp\fR \fI\,{on,off}\/\fR 248 | The project will not be shown on COPR home page 249 | 250 | .TP 251 | \fB\-\-auto\-prune\fR \fI\,{on,off}\/\fR 252 | If auto\-deletion of project's obsoleted builds should be enabled. This option 253 | can only be specified by a COPR admin. 254 | 255 | .SH COMMAND \fI\,'copr delete'\/\fR 256 | usage: copr delete [\-h] copr 257 | 258 | .TP 259 | \fBcopr\fR 260 | Name of your project to be deleted. 261 | 262 | .SH COMMAND \fI\,'copr fork'\/\fR 263 | usage: copr fork [\-h] [\-\-confirm] src dst 264 | 265 | .TP 266 | \fBsrc\fR 267 | Which project should be forked 268 | 269 | .TP 270 | \fBdst\fR 271 | Name of the new project 272 | 273 | .SH OPTIONS \fI\,'copr fork'\/\fR 274 | .TP 275 | \fB\-\-confirm\fR 276 | Confirm forking into existing project 277 | 278 | .SH COMMAND \fI\,'copr build'\/\fR 279 | usage: copr build [\-h] [\-\-memory MEMORY] [\-\-timeout TIMEOUT] [\-\-nowait] 280 | [\-r CHROOTS] [\-\-background] 281 | copr pkgs [pkgs ...] 282 | 283 | .TP 284 | \fBcopr\fR 285 | The copr repo to build the package in. Can be just name of project or even in 286 | format username/project or @groupname/project. 287 | 288 | .TP 289 | \fBpkgs\fR 290 | filename of SRPM or URL of packages to build 291 | 292 | .SH OPTIONS \fI\,'copr build'\/\fR 293 | .TP 294 | \fB\-\-memory\fR \fI\,MEMORY\/\fR 295 | 296 | .TP 297 | \fB\-\-timeout\fR \fI\,TIMEOUT\/\fR 298 | 299 | .TP 300 | \fB\-\-nowait\fR 301 | Don't wait for build 302 | 303 | .TP 304 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 305 | If you don't need this build for all the project's chroots. You can use it 306 | several times for each chroot you need. 307 | 308 | .TP 309 | \fB\-\-background\fR 310 | Mark the build as a background job. It will have lesser priority than regular 311 | builds. 312 | 313 | .SH COMMAND \fI\,'copr buildpypi'\/\fR 314 | usage: copr buildpypi [\-h] [\-\-pythonversions [VERSION ...]] 315 | [\-\-packageversion PYPIVERSION] \-\-packagename PYPINAME 316 | [\-\-memory MEMORY] [\-\-timeout TIMEOUT] [\-\-nowait] 317 | [\-r CHROOTS] [\-\-background] 318 | copr 319 | 320 | .TP 321 | \fBcopr\fR 322 | The copr repo to build the package in. Can be just name of project or even in 323 | format username/project or @groupname/project. 324 | 325 | .SH OPTIONS \fI\,'copr buildpypi'\/\fR 326 | .TP 327 | \fB\-\-pythonversions\fR \fI\,[VERSION ...]\/\fR 328 | For what Python versions to build (by default: 3 2) 329 | 330 | .TP 331 | \fB\-\-packageversion\fR \fI\,PYPIVERSION\/\fR 332 | Version of the PyPI package to be built (by default latest) 333 | 334 | .TP 335 | \fB\-\-packagename\fR \fI\,PYPINAME\/\fR 336 | Name of the PyPI package to be built, required. 337 | 338 | .TP 339 | \fB\-\-memory\fR \fI\,MEMORY\/\fR 340 | 341 | .TP 342 | \fB\-\-timeout\fR \fI\,TIMEOUT\/\fR 343 | 344 | .TP 345 | \fB\-\-nowait\fR 346 | Don't wait for build 347 | 348 | .TP 349 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 350 | If you don't need this build for all the project's chroots. You can use it 351 | several times for each chroot you need. 352 | 353 | .TP 354 | \fB\-\-background\fR 355 | Mark the build as a background job. It will have lesser priority than regular 356 | builds. 357 | 358 | .SH COMMAND \fI\,'copr buildgem'\/\fR 359 | usage: copr buildgem [\-h] [\-\-gem GEM] [\-\-memory MEMORY] [\-\-timeout TIMEOUT] 360 | [\-\-nowait] [\-r CHROOTS] [\-\-background] 361 | copr 362 | 363 | .TP 364 | \fBcopr\fR 365 | The copr repo to build the package in. Can be just name of project or even in 366 | format username/project or @groupname/project. 367 | 368 | .SH OPTIONS \fI\,'copr buildgem'\/\fR 369 | .TP 370 | \fB\-\-gem\fR \fI\,GEM\/\fR 371 | Specify gem name 372 | 373 | .TP 374 | \fB\-\-memory\fR \fI\,MEMORY\/\fR 375 | 376 | .TP 377 | \fB\-\-timeout\fR \fI\,TIMEOUT\/\fR 378 | 379 | .TP 380 | \fB\-\-nowait\fR 381 | Don't wait for build 382 | 383 | .TP 384 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 385 | If you don't need this build for all the project's chroots. You can use it 386 | several times for each chroot you need. 387 | 388 | .TP 389 | \fB\-\-background\fR 390 | Mark the build as a background job. It will have lesser priority than regular 391 | builds. 392 | 393 | .SH COMMAND \fI\,'copr buildfedpkg'\/\fR 394 | usage: copr buildfedpkg [\-h] \-\-clone\-url URL [\-\-branch BRANCH] 395 | [\-\-memory MEMORY] [\-\-timeout TIMEOUT] [\-\-nowait] 396 | [\-r CHROOTS] [\-\-background] 397 | copr 398 | 399 | .TP 400 | \fBcopr\fR 401 | The copr repo to build the package in. Can be just name of project or even in 402 | format username/project or @groupname/project. 403 | 404 | .SH OPTIONS \fI\,'copr buildfedpkg'\/\fR 405 | .TP 406 | \fB\-\-clone\-url\fR \fI\,URL\/\fR 407 | Specify clone url for the distgit repository 408 | 409 | .TP 410 | \fB\-\-branch\fR \fI\,BRANCH\/\fR 411 | Specify branch to be used 412 | 413 | .TP 414 | \fB\-\-memory\fR \fI\,MEMORY\/\fR 415 | 416 | .TP 417 | \fB\-\-timeout\fR \fI\,TIMEOUT\/\fR 418 | 419 | .TP 420 | \fB\-\-nowait\fR 421 | Don't wait for build 422 | 423 | .TP 424 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 425 | If you don't need this build for all the project's chroots. You can use it 426 | several times for each chroot you need. 427 | 428 | .TP 429 | \fB\-\-background\fR 430 | Mark the build as a background job. It will have lesser priority than regular 431 | builds. 432 | 433 | .SH COMMAND \fI\,'copr buildtito'\/\fR 434 | usage: copr buildtito [\-h] \-\-git\-url URL [\-\-git\-dir DIRECTORY] 435 | [\-\-git\-branch BRANCH] [\-\-test {on,off}] 436 | [\-\-memory MEMORY] [\-\-timeout TIMEOUT] [\-\-nowait] 437 | [\-r CHROOTS] [\-\-background] 438 | copr 439 | 440 | .TP 441 | \fBcopr\fR 442 | The copr repo to build the package in. Can be just name of project or even in 443 | format username/project or @groupname/project. 444 | 445 | .SH OPTIONS \fI\,'copr buildtito'\/\fR 446 | .TP 447 | \fB\-\-git\-url\fR \fI\,URL\/\fR 448 | URL to a project managed by Tito 449 | 450 | .TP 451 | \fB\-\-git\-dir\fR \fI\,DIRECTORY\/\fR 452 | Relative path from Git root to directory containing .spec file 453 | 454 | .TP 455 | \fB\-\-git\-branch\fR \fI\,BRANCH\/\fR 456 | Git branch that you want to build from 457 | 458 | .TP 459 | \fB\-\-test\fR \fI\,{on,off}\/\fR 460 | Build the last commit instead of the last release tag 461 | 462 | .TP 463 | \fB\-\-memory\fR \fI\,MEMORY\/\fR 464 | 465 | .TP 466 | \fB\-\-timeout\fR \fI\,TIMEOUT\/\fR 467 | 468 | .TP 469 | \fB\-\-nowait\fR 470 | Don't wait for build 471 | 472 | .TP 473 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 474 | If you don't need this build for all the project's chroots. You can use it 475 | several times for each chroot you need. 476 | 477 | .TP 478 | \fB\-\-background\fR 479 | Mark the build as a background job. It will have lesser priority than regular 480 | builds. 481 | 482 | .SH COMMAND \fI\,'copr buildmock'\/\fR 483 | usage: copr buildmock [\-h] [\-\-scm\-type TYPE] [\-\-scm\-url URL] 484 | [\-\-scm\-branch BRANCH] [\-\-spec FILE] [\-\-memory MEMORY] 485 | [\-\-timeout TIMEOUT] [\-\-nowait] [\-r CHROOTS] 486 | [\-\-background] 487 | copr 488 | 489 | .TP 490 | \fBcopr\fR 491 | The copr repo to build the package in. Can be just name of project or even in 492 | format username/project or @groupname/project. 493 | 494 | .SH OPTIONS \fI\,'copr buildmock'\/\fR 495 | .TP 496 | \fB\-\-scm\-type\fR \fI\,TYPE\/\fR 497 | specify versioning tool, default is 'git' 498 | 499 | .TP 500 | \fB\-\-scm\-url\fR \fI\,URL\/\fR 501 | url to a project versioned by Git or SVN, required 502 | 503 | .TP 504 | \fB\-\-scm\-branch\fR \fI\,BRANCH\/\fR 505 | 506 | .TP 507 | \fB\-\-spec\fR \fI\,FILE\/\fR 508 | relative path from SCM root to .spec file, required 509 | 510 | .TP 511 | \fB\-\-memory\fR \fI\,MEMORY\/\fR 512 | 513 | .TP 514 | \fB\-\-timeout\fR \fI\,TIMEOUT\/\fR 515 | 516 | .TP 517 | \fB\-\-nowait\fR 518 | Don't wait for build 519 | 520 | .TP 521 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 522 | If you don't need this build for all the project's chroots. You can use it 523 | several times for each chroot you need. 524 | 525 | .TP 526 | \fB\-\-background\fR 527 | Mark the build as a background job. It will have lesser priority than regular 528 | builds. 529 | 530 | .SH COMMAND \fI\,'copr status'\/\fR 531 | usage: copr status [\-h] build_id 532 | 533 | .TP 534 | \fBbuild_id\fR 535 | Build ID 536 | 537 | .SH COMMAND \fI\,'copr download\-build'\/\fR 538 | usage: copr download\-build [\-h] [\-r CHROOTS] [\-\-dest DEST] build_id 539 | 540 | .TP 541 | \fBbuild_id\fR 542 | Build ID 543 | 544 | .SH OPTIONS \fI\,'copr download\-build'\/\fR 545 | .TP 546 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 547 | Select chroots to fetch 548 | 549 | .TP 550 | \fB\-\-dest\fR \fI\,DEST\/\fR, \fB\-d\fR \fI\,DEST\/\fR 551 | Base directory to store packages 552 | 553 | .SH COMMAND \fI\,'copr cancel'\/\fR 554 | usage: copr cancel [\-h] build_id 555 | 556 | .TP 557 | \fBbuild_id\fR 558 | Build ID 559 | 560 | .SH COMMAND \fI\,'copr watch\-build'\/\fR 561 | usage: copr watch\-build [\-h] build_id [build_id ...] 562 | 563 | .TP 564 | \fBbuild_id\fR 565 | Build ID 566 | 567 | .SH COMMAND \fI\,'copr delete\-build'\/\fR 568 | usage: copr delete\-build [\-h] build_id 569 | 570 | .TP 571 | \fBbuild_id\fR 572 | Build ID 573 | 574 | .SH COMMAND \fI\,'copr edit\-chroot'\/\fR 575 | usage: copr edit\-chroot [\-h] [\-\-upload\-comps FILEPATH | \-\-delete\-comps] 576 | [\-\-packages PACKAGES] [\-\-repos REPOS] 577 | coprchroot 578 | 579 | .TP 580 | \fBcoprchroot\fR 581 | Path to a project chroot as owner/project/chroot or project/chroot 582 | 583 | .SH OPTIONS \fI\,'copr edit\-chroot'\/\fR 584 | .TP 585 | \fB\-\-upload\-comps\fR \fI\,FILEPATH\/\fR 586 | filepath to the comps.xml file to be uploaded 587 | 588 | .TP 589 | \fB\-\-delete\-comps\fR 590 | deletes already existing comps.xml for the chroot 591 | 592 | .TP 593 | \fB\-\-packages\fR \fI\,PACKAGES\/\fR 594 | space separated string of package names to be added to buildroot 595 | 596 | .TP 597 | \fB\-\-repos\fR \fI\,REPOS\/\fR 598 | space separated string of additional repo urls for chroot 599 | 600 | .SH COMMAND \fI\,'copr get\-chroot'\/\fR 601 | usage: copr get\-chroot [\-h] coprchroot 602 | 603 | .TP 604 | \fBcoprchroot\fR 605 | Path to a project chroot as owner/project/chroot or project/chroot 606 | 607 | .SH COMMAND \fI\,'copr add\-package\-tito'\/\fR 608 | usage: copr add\-package\-tito [\-h] \-\-git\-url URL [\-\-git\-dir DIRECTORY] 609 | [\-\-git\-branch BRANCH] [\-\-test {on,off}] \-\-name 610 | PKGNAME [\-\-webhook\-rebuild {on,off}] 611 | copr 612 | 613 | .TP 614 | \fBcopr\fR 615 | The copr repo for the package. Can be just name of project or even in format 616 | username/project or @groupname/project. 617 | 618 | .SH OPTIONS \fI\,'copr add\-package\-tito'\/\fR 619 | .TP 620 | \fB\-\-git\-url\fR \fI\,URL\/\fR 621 | URL to a project managed by Tito 622 | 623 | .TP 624 | \fB\-\-git\-dir\fR \fI\,DIRECTORY\/\fR 625 | Relative path from Git root to directory containing .spec file 626 | 627 | .TP 628 | \fB\-\-git\-branch\fR \fI\,BRANCH\/\fR 629 | Git branch that you want to build from 630 | 631 | .TP 632 | \fB\-\-test\fR \fI\,{on,off}\/\fR 633 | Build the last commit instead of the last release tag 634 | 635 | .TP 636 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 637 | Name of the package to be edited or created 638 | 639 | .TP 640 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 641 | Enable auto\-rebuilding. 642 | 643 | .SH COMMAND \fI\,'copr edit\-package\-tito'\/\fR 644 | usage: copr edit\-package\-tito [\-h] \-\-git\-url URL [\-\-git\-dir DIRECTORY] 645 | [\-\-git\-branch BRANCH] [\-\-test {on,off}] \-\-name 646 | PKGNAME [\-\-webhook\-rebuild {on,off}] 647 | copr 648 | 649 | .TP 650 | \fBcopr\fR 651 | The copr repo for the package. Can be just name of project or even in format 652 | username/project or @groupname/project. 653 | 654 | .SH OPTIONS \fI\,'copr edit\-package\-tito'\/\fR 655 | .TP 656 | \fB\-\-git\-url\fR \fI\,URL\/\fR 657 | URL to a project managed by Tito 658 | 659 | .TP 660 | \fB\-\-git\-dir\fR \fI\,DIRECTORY\/\fR 661 | Relative path from Git root to directory containing .spec file 662 | 663 | .TP 664 | \fB\-\-git\-branch\fR \fI\,BRANCH\/\fR 665 | Git branch that you want to build from 666 | 667 | .TP 668 | \fB\-\-test\fR \fI\,{on,off}\/\fR 669 | Build the last commit instead of the last release tag 670 | 671 | .TP 672 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 673 | Name of the package to be edited or created 674 | 675 | .TP 676 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 677 | Enable auto\-rebuilding. 678 | 679 | .SH COMMAND \fI\,'copr add\-package\-pypi'\/\fR 680 | usage: copr add\-package\-pypi [\-h] [\-\-pythonversions [VERSION ...]] 681 | [\-\-packageversion PYPIVERSION] \-\-packagename 682 | PYPINAME \-\-name PKGNAME 683 | [\-\-webhook\-rebuild {on,off}] 684 | copr 685 | 686 | .TP 687 | \fBcopr\fR 688 | The copr repo for the package. Can be just name of project or even in format 689 | username/project or @groupname/project. 690 | 691 | .SH OPTIONS \fI\,'copr add\-package\-pypi'\/\fR 692 | .TP 693 | \fB\-\-pythonversions\fR \fI\,[VERSION ...]\/\fR 694 | For what Python versions to build (by default: 3 2) 695 | 696 | .TP 697 | \fB\-\-packageversion\fR \fI\,PYPIVERSION\/\fR 698 | Version of the PyPI package to be built (by default latest) 699 | 700 | .TP 701 | \fB\-\-packagename\fR \fI\,PYPINAME\/\fR 702 | Name of the PyPI package to be built, required. 703 | 704 | .TP 705 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 706 | Name of the package to be edited or created 707 | 708 | .TP 709 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 710 | Enable auto\-rebuilding. 711 | 712 | .SH COMMAND \fI\,'copr edit\-package\-pypi'\/\fR 713 | usage: copr edit\-package\-pypi [\-h] [\-\-pythonversions [VERSION ...]] 714 | [\-\-packageversion PYPIVERSION] \-\-packagename 715 | PYPINAME \-\-name PKGNAME 716 | [\-\-webhook\-rebuild {on,off}] 717 | copr 718 | 719 | .TP 720 | \fBcopr\fR 721 | The copr repo for the package. Can be just name of project or even in format 722 | username/project or @groupname/project. 723 | 724 | .SH OPTIONS \fI\,'copr edit\-package\-pypi'\/\fR 725 | .TP 726 | \fB\-\-pythonversions\fR \fI\,[VERSION ...]\/\fR 727 | For what Python versions to build (by default: 3 2) 728 | 729 | .TP 730 | \fB\-\-packageversion\fR \fI\,PYPIVERSION\/\fR 731 | Version of the PyPI package to be built (by default latest) 732 | 733 | .TP 734 | \fB\-\-packagename\fR \fI\,PYPINAME\/\fR 735 | Name of the PyPI package to be built, required. 736 | 737 | .TP 738 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 739 | Name of the package to be edited or created 740 | 741 | .TP 742 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 743 | Enable auto\-rebuilding. 744 | 745 | .SH COMMAND \fI\,'copr add\-package\-mockscm'\/\fR 746 | usage: copr add\-package\-mockscm [\-h] [\-\-scm\-type TYPE] [\-\-scm\-url URL] 747 | [\-\-scm\-branch BRANCH] [\-\-spec FILE] \-\-name 748 | PKGNAME [\-\-webhook\-rebuild {on,off}] 749 | copr 750 | 751 | .TP 752 | \fBcopr\fR 753 | The copr repo for the package. Can be just name of project or even in format 754 | username/project or @groupname/project. 755 | 756 | .SH OPTIONS \fI\,'copr add\-package\-mockscm'\/\fR 757 | .TP 758 | \fB\-\-scm\-type\fR \fI\,TYPE\/\fR 759 | specify versioning tool, default is 'git' 760 | 761 | .TP 762 | \fB\-\-scm\-url\fR \fI\,URL\/\fR 763 | url to a project versioned by Git or SVN, required 764 | 765 | .TP 766 | \fB\-\-scm\-branch\fR \fI\,BRANCH\/\fR 767 | 768 | .TP 769 | \fB\-\-spec\fR \fI\,FILE\/\fR 770 | relative path from SCM root to .spec file, required 771 | 772 | .TP 773 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 774 | Name of the package to be edited or created 775 | 776 | .TP 777 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 778 | Enable auto\-rebuilding. 779 | 780 | .SH COMMAND \fI\,'copr edit\-package\-mockscm'\/\fR 781 | usage: copr edit\-package\-mockscm [\-h] [\-\-scm\-type TYPE] [\-\-scm\-url URL] 782 | [\-\-scm\-branch BRANCH] [\-\-spec FILE] \-\-name 783 | PKGNAME [\-\-webhook\-rebuild {on,off}] 784 | copr 785 | 786 | .TP 787 | \fBcopr\fR 788 | The copr repo for the package. Can be just name of project or even in format 789 | username/project or @groupname/project. 790 | 791 | .SH OPTIONS \fI\,'copr edit\-package\-mockscm'\/\fR 792 | .TP 793 | \fB\-\-scm\-type\fR \fI\,TYPE\/\fR 794 | specify versioning tool, default is 'git' 795 | 796 | .TP 797 | \fB\-\-scm\-url\fR \fI\,URL\/\fR 798 | url to a project versioned by Git or SVN, required 799 | 800 | .TP 801 | \fB\-\-scm\-branch\fR \fI\,BRANCH\/\fR 802 | 803 | .TP 804 | \fB\-\-spec\fR \fI\,FILE\/\fR 805 | relative path from SCM root to .spec file, required 806 | 807 | .TP 808 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 809 | Name of the package to be edited or created 810 | 811 | .TP 812 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 813 | Enable auto\-rebuilding. 814 | 815 | .SH COMMAND \fI\,'copr add\-package\-rubygems'\/\fR 816 | usage: copr add\-package\-rubygems [\-h] [\-\-gem GEM] \-\-name PKGNAME 817 | [\-\-webhook\-rebuild {on,off}] 818 | copr 819 | 820 | .TP 821 | \fBcopr\fR 822 | The copr repo for the package. Can be just name of project or even in format 823 | username/project or @groupname/project. 824 | 825 | .SH OPTIONS \fI\,'copr add\-package\-rubygems'\/\fR 826 | .TP 827 | \fB\-\-gem\fR \fI\,GEM\/\fR 828 | Specify gem name 829 | 830 | .TP 831 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 832 | Name of the package to be edited or created 833 | 834 | .TP 835 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 836 | Enable auto\-rebuilding. 837 | 838 | .SH COMMAND \fI\,'copr edit\-package\-rubygems'\/\fR 839 | usage: copr edit\-package\-rubygems [\-h] [\-\-gem GEM] \-\-name PKGNAME 840 | [\-\-webhook\-rebuild {on,off}] 841 | copr 842 | 843 | .TP 844 | \fBcopr\fR 845 | The copr repo for the package. Can be just name of project or even in format 846 | username/project or @groupname/project. 847 | 848 | .SH OPTIONS \fI\,'copr edit\-package\-rubygems'\/\fR 849 | .TP 850 | \fB\-\-gem\fR \fI\,GEM\/\fR 851 | Specify gem name 852 | 853 | .TP 854 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 855 | Name of the package to be edited or created 856 | 857 | .TP 858 | \fB\-\-webhook\-rebuild\fR \fI\,{on,off}\/\fR 859 | Enable auto\-rebuilding. 860 | 861 | .SH COMMAND \fI\,'copr list\-packages'\/\fR 862 | usage: copr list\-packages [\-h] [\-\-with\-latest\-build] 863 | [\-\-with\-latest\-succeeded\-build] [\-\-with\-all\-builds] 864 | copr 865 | 866 | .TP 867 | \fBcopr\fR 868 | The copr repo to list the packages of. Can be just name of project or even in 869 | format owner/project. 870 | 871 | .SH OPTIONS \fI\,'copr list\-packages'\/\fR 872 | .TP 873 | \fB\-\-with\-latest\-build\fR 874 | Also display data related to the latest build for the package. 875 | 876 | .TP 877 | \fB\-\-with\-latest\-succeeded\-build\fR 878 | Also display data related to the latest succeeded build for the package. 879 | 880 | .TP 881 | \fB\-\-with\-all\-builds\fR 882 | Also display data related to the builds for the package. 883 | 884 | .SH COMMAND \fI\,'copr list\-package\-names'\/\fR 885 | usage: copr list\-package\-names [\-h] copr 886 | 887 | .TP 888 | \fBcopr\fR 889 | The copr repo to list the packages of. Can be just name of project or even in 890 | format owner/project. 891 | 892 | .SH COMMAND \fI\,'copr get\-package'\/\fR 893 | usage: copr get\-package [\-h] \-\-name PKGNAME [\-\-with\-latest\-build] 894 | [\-\-with\-latest\-succeeded\-build] [\-\-with\-all\-builds] 895 | copr 896 | 897 | .TP 898 | \fBcopr\fR 899 | The copr repo to list the packages of. Can be just name of project or even in 900 | format owner/project. 901 | 902 | .SH OPTIONS \fI\,'copr get\-package'\/\fR 903 | .TP 904 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 905 | Name of a single package to be displayed 906 | 907 | .TP 908 | \fB\-\-with\-latest\-build\fR 909 | Also display data related to the latest build for each package. 910 | 911 | .TP 912 | \fB\-\-with\-latest\-succeeded\-build\fR 913 | Also display data related to the latest succeeded build for each package. 914 | 915 | .TP 916 | \fB\-\-with\-all\-builds\fR 917 | Also display data related to the builds for each package. 918 | 919 | .SH COMMAND \fI\,'copr delete\-package'\/\fR 920 | usage: copr delete\-package [\-h] \-\-name PKGNAME copr 921 | 922 | .TP 923 | \fBcopr\fR 924 | The copr repo to list the packages of. Can be just name of project or even in 925 | format owner/project. 926 | 927 | .SH OPTIONS \fI\,'copr delete\-package'\/\fR 928 | .TP 929 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 930 | Name of a package to be deleted 931 | 932 | .SH COMMAND \fI\,'copr reset\-package'\/\fR 933 | usage: copr reset\-package [\-h] \-\-name PKGNAME copr 934 | 935 | .TP 936 | \fBcopr\fR 937 | The copr repo to list the packages of. Can be just name of project or even in 938 | format owner/project. 939 | 940 | .SH OPTIONS \fI\,'copr reset\-package'\/\fR 941 | .TP 942 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 943 | Name of a package to be reset 944 | 945 | .SH COMMAND \fI\,'copr build\-package'\/\fR 946 | usage: copr build\-package [\-h] [\-\-memory MEMORY] [\-\-timeout TIMEOUT] 947 | [\-\-nowait] [\-r CHROOTS] [\-\-background] \-\-name 948 | PKGNAME 949 | copr 950 | 951 | .TP 952 | \fBcopr\fR 953 | The copr repo to build the package in. Can be just name of project or even in 954 | format username/project or @groupname/project. 955 | 956 | .SH OPTIONS \fI\,'copr build\-package'\/\fR 957 | .TP 958 | \fB\-\-memory\fR \fI\,MEMORY\/\fR 959 | 960 | .TP 961 | \fB\-\-timeout\fR \fI\,TIMEOUT\/\fR 962 | 963 | .TP 964 | \fB\-\-nowait\fR 965 | Don't wait for build 966 | 967 | .TP 968 | \fB\-r\fR \fI\,CHROOTS\/\fR, \fB\-\-chroot\fR \fI\,CHROOTS\/\fR 969 | If you don't need this build for all the project's chroots. You can use it 970 | several times for each chroot you need. 971 | 972 | .TP 973 | \fB\-\-background\fR 974 | Mark the build as a background job. It will have lesser priority than regular 975 | builds. 976 | 977 | .TP 978 | \fB\-\-name\fR \fI\,PKGNAME\/\fR 979 | Name of a package to be built 980 | 981 | .SH COMMAND \fI\,'copr build\-module'\/\fR 982 | usage: copr build\-module [\-h] (\-\-url URL | \-\-yaml YAML) [copr] 983 | 984 | .TP 985 | \fBcopr\fR 986 | The copr repo to list the packages of. Can be just name of project or even in 987 | format owner/project. 988 | 989 | .SH OPTIONS \fI\,'copr build\-module'\/\fR 990 | .TP 991 | \fB\-\-url\fR \fI\,URL\/\fR 992 | SCM with modulemd file in yaml format 993 | 994 | .TP 995 | \fB\-\-yaml\fR \fI\,YAML\/\fR 996 | Path to modulemd file in yaml format 997 | 998 | .SH COMMENTS 999 | dummy text 1000 | 1001 | .SH AUTHOR 1002 | .nf 1003 | John Doe 1004 | .fi 1005 | 1006 | .SH DISTRIBUTION 1007 | The latest version of example may be downloaded from 1008 | .UR http://example.com 1009 | .UE 1010 | -------------------------------------------------------------------------------- /examples/copr/fake-deps/HACK: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/copr/fake-deps/HACK -------------------------------------------------------------------------------- /examples/copr/fake-deps/copr/README: -------------------------------------------------------------------------------- 1 | Hack to work-around missing stuff. 2 | -------------------------------------------------------------------------------- /examples/copr/fake-deps/copr/__init__.py: -------------------------------------------------------------------------------- 1 | # Hack 2 | 3 | class CoprClient(object): 4 | pass 5 | -------------------------------------------------------------------------------- /examples/copr/fake-deps/copr/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/copr/fake-deps/copr/client/__init__.py -------------------------------------------------------------------------------- /examples/copr/fake-deps/copr/client/responses.py: -------------------------------------------------------------------------------- 1 | class CoprResponse: 2 | pass 3 | -------------------------------------------------------------------------------- /examples/copr/fake-deps/copr/exceptions.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/copr/fake-deps/copr/exceptions.py -------------------------------------------------------------------------------- /examples/copr/fake-deps/jinja2.py: -------------------------------------------------------------------------------- 1 | class Environment: 2 | pass 3 | -------------------------------------------------------------------------------- /examples/copr/fake-deps/simplejson.py: -------------------------------------------------------------------------------- 1 | HACK -------------------------------------------------------------------------------- /examples/copr/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpages] 2 | manpages = 3 | copr-cli.1:module=copr_cli.main:function=setup_parser:version=setup-py-overriden 4 | -------------------------------------------------------------------------------- /examples/copr/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Example of argparse taken from: 4 | # https://pagure.io/copr/copr/blob/a4feb01bc35b8554f503d41795e7a184ff929dd4/f/cli/copr_cli 5 | 6 | import os 7 | import sys 8 | 9 | from setuptools import setup, find_packages 10 | 11 | # Just to make sure that build_manpage can be found. 12 | sys.path = [os.path.join(os.getcwd(), 'fake-deps')] + sys.path 13 | 14 | from build_manpages.build_manpages \ 15 | import build_manpages, get_build_py_cmd, get_install_cmd 16 | 17 | from setuptools.command.build_py import build_py 18 | from setuptools.command.install import install 19 | 20 | setup( 21 | name='example', 22 | description='This project does nothing.', 23 | long_description=('Long description of the project.'), 24 | author='John Doe', 25 | author_email='jd@example.com', 26 | version='0.1.0', 27 | url='http://example.com', 28 | cmdclass={ 29 | 'build_manpages': build_manpages, 30 | 'build_py': get_build_py_cmd(build_py), 31 | 'install': get_install_cmd(install), 32 | }, 33 | packages=find_packages(), 34 | ) 35 | -------------------------------------------------------------------------------- /examples/old_format/README.md: -------------------------------------------------------------------------------- 1 | # Usage (DEPRECATED, USE BUILD\_MANPAGES INSTEAD OF BUILD\_MANPAGE) 2 | 3 | Download `build_manpage.py` and place it somewhere where Python can 4 | find it. 5 | 6 | In your `setup.py` add: 7 | 8 | ```python 9 | [...] 10 | from build_manpage import build_manpage 11 | 12 | setup( 13 | [...] 14 | cmdclass={'build_manpage': build_manpage} 15 | ) 16 | ``` 17 | 18 | In your `setup.cfg` add: 19 | 20 | ``` 21 | [build_manpage] 22 | output=data/mymanpage.1 23 | parser=myapp.somemod:get_parser 24 | ``` 25 | 26 | It's also possible to specify filename to use: 27 | 28 | ``` 29 | [build_manpage] 30 | output=data/mymanpage.1 31 | parser=UNUSED:get_parser 32 | parser-file=example.py 33 | ``` 34 | 35 | The `output` is the destination path for the generated 36 | manpage and `parser` is an import path pointing to a optparser 37 | instance or a function returning such an instance. 38 | Note that this doesn't work with `argparse` module. Please use 39 | `build_manpages` (not `build_manpage`) to have argparse support. 40 | 41 | Then run `setup.py build_manpage` to build a manpage for 42 | your project. 43 | -------------------------------------------------------------------------------- /examples/old_format/example.py: -------------------------------------------------------------------------------- 1 | from optparse import OptionParser 2 | 3 | 4 | # This function returns a OptionParser 5 | def get_parser(): 6 | parser = OptionParser( 7 | usage="The usage.", 8 | description="This program does nothing.") 9 | parser.add_option("-f", "--file", dest="filename", 10 | help="write report to FILE", metavar="FILE") 11 | parser.add_option("-q", "--quiet", 12 | action="store_false", dest="verbose", default=True, 13 | help="don't print status messages to stdout") 14 | return parser 15 | -------------------------------------------------------------------------------- /examples/old_format/expected-output.1: -------------------------------------------------------------------------------- 1 | .TH example 1 2017\-09\-24 "example v.0.1.0.dev0" 2 | .SH NAME 3 | example 4 | .SH SYNOPSIS 5 | .B example 6 | The usage. 7 | .SH DESCRIPTION 8 | Description and long description are both used by build_manpage. 9 | .SH OPTIONS 10 | .TP 11 | .B \-h, \-\-help 12 | show this help message and exit 13 | .TP 14 | .B \-f FILE, \-\-file=FILE 15 | write report to FILE 16 | .TP 17 | .B \-q, \-\-quiet 18 | don't print status messages to stdout 19 | .SH AUTHOR 20 | .nf 21 | John Doe 22 | .fi 23 | 24 | .SH DISTRIBUTION 25 | The latest version of example may be downloaded from 26 | .UR http://example.com 27 | .UE 28 | -------------------------------------------------------------------------------- /examples/old_format/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpage] 2 | output=example.1 3 | parser=example:get_parser 4 | -------------------------------------------------------------------------------- /examples/old_format/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Example setup.py script 4 | # 5 | # The path tweaking at the top is just to make sure, that build_manpage 6 | # can be imported. 7 | # 8 | # 1. Run "setup.py build_manpage" 9 | # 2. Run "man ./example.1" to see the result. 10 | # 11 | 12 | import os 13 | import sys 14 | 15 | from distutils.core import setup 16 | 17 | 18 | # Just to make sure that build_manpage can be found. 19 | sys.path.insert(0, os.getcwd()) 20 | 21 | from build_manpages.build_manpage import build_manpage 22 | 23 | setup( 24 | name='example', 25 | description='This script does nothing.', 26 | long_description=( 27 | 'Description and long description are both used by build_manpage.'), 28 | author='John Doe', 29 | author_email='jd@example.com', 30 | version='0.1.0-dev', 31 | url='http://example.com', 32 | py_modules=['example'], 33 | cmdclass={'build_manpage': build_manpage} 34 | ) 35 | -------------------------------------------------------------------------------- /examples/old_format_file_name/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Download `build_manpage.py` and place it somewhere where Python can 4 | find it. 5 | 6 | In your `setup.py` add: 7 | 8 | ```python 9 | [...] 10 | from build_manpage import build_manpage 11 | 12 | setup( 13 | [...] 14 | cmdclass={'build_manpage': build_manpage} 15 | ) 16 | ``` 17 | 18 | In your `setup.cfg` add: 19 | 20 | ``` 21 | [build_manpage] 22 | output=data/mymanpage.1 23 | parser=myapp.somemod:get_parser 24 | ``` 25 | 26 | where `output` is the destination path for the generated 27 | manpage and `parser` is an import path pointing to a optparser 28 | instance or a function returning such an instance. 29 | 30 | Then run `setup.py build_manpage` to build a manpage for 31 | your project. 32 | -------------------------------------------------------------------------------- /examples/old_format_file_name/example.py: -------------------------------------------------------------------------------- 1 | from optparse import OptionParser 2 | 3 | 4 | # This function returns a OptionParser 5 | def get_parser(): 6 | parser = OptionParser( 7 | usage="The usage.", 8 | description="This program does nothing.") 9 | parser.add_option("-f", "--file", dest="filename", 10 | help="write report to FILE", metavar="FILE") 11 | parser.add_option("-q", "--quiet", 12 | action="store_false", dest="verbose", default=True, 13 | help="don't print status messages to stdout") 14 | return parser 15 | -------------------------------------------------------------------------------- /examples/old_format_file_name/expected-output.1: -------------------------------------------------------------------------------- 1 | .TH example 1 2017\-09\-24 "example v.0.1.0.dev0" 2 | .SH NAME 3 | example 4 | .SH SYNOPSIS 5 | .B example 6 | The usage. 7 | .SH DESCRIPTION 8 | Description and long description are both used by build_manpage. 9 | .SH OPTIONS 10 | .TP 11 | .B \-h, \-\-help 12 | show this help message and exit 13 | .TP 14 | .B \-f FILE, \-\-file=FILE 15 | write report to FILE 16 | .TP 17 | .B \-q, \-\-quiet 18 | don't print status messages to stdout 19 | .SH AUTHOR 20 | .nf 21 | John Doe 22 | .fi 23 | 24 | .SH DISTRIBUTION 25 | The latest version of example may be downloaded from 26 | .UR http://example.com 27 | .UE 28 | -------------------------------------------------------------------------------- /examples/old_format_file_name/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpage] 2 | output=example.1 3 | parser=IGNORED:get_parser 4 | parser-file=example.py 5 | -------------------------------------------------------------------------------- /examples/old_format_file_name/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Example setup.py script 4 | # 5 | # The path tweaking at the top is just to make sure, that build_manpage 6 | # can be imported. 7 | # 8 | # 1. Run "setup.py build_manpage" 9 | # 2. Run "man ./example.1" to see the result. 10 | # 11 | 12 | import os 13 | import sys 14 | 15 | from distutils.core import setup 16 | 17 | 18 | # Just to make sure that build_manpage can be found. 19 | sys.path.insert(0, os.getcwd()) 20 | 21 | from build_manpages.build_manpage import build_manpage 22 | 23 | setup( 24 | name='example', 25 | description='This script does nothing.', 26 | long_description=( 27 | 'Description and long description are both used by build_manpage.'), 28 | author='John Doe', 29 | author_email='jd@example.com', 30 | version='0.1.0-dev', 31 | url='http://example.com', 32 | py_modules=['example'], 33 | cmdclass={'build_manpage': build_manpage} 34 | ) 35 | -------------------------------------------------------------------------------- /examples/osc/expected-output.1: -------------------------------------------------------------------------------- 1 | .TH OSC "1" "2023\-04\-09" "osc 1.0" "Generated Python Manual" 2 | .SH NAME 3 | osc \- openSUSE commander command\-line 4 | .SH SYNOPSIS 5 | .B osc 6 | [global opts] [--help] [opts] [args] osc --help 7 | 8 | .SH 9 | COMMANDS 10 | .SS \fBosc build\fR 11 | Build a package on your local machine 12 | 13 | usage: 14 | osc [global opts] build (will try to guess a build environment) 15 | osc [global opts] build REPOSITORY ARCH BUILD_DESCR 16 | osc [global opts] build REPOSITORY ARCH 17 | osc [global opts] build REPOSITORY (ARCH = hostarch, BUILD_DESCR is detected automatically) 18 | osc [global opts] build ARCH (REPOSITORY = build_repository (config option), BUILD_DESCR is detected automatically) 19 | osc [global opts] build BUILD_DESCR (REPOSITORY = build_repository (config option), ARCH = hostarch) 20 | osc [global opts] build (REPOSITORY = build_repository (config option), ARCH = hostarch, BUILD_DESCR is detected automatically) 21 | 22 | Build command description. 23 | 24 | options: 25 | .RS 7 26 | .TP 27 | \fB\-\-clean\fR 28 | Delete old build root before initializing it 29 | .RE 30 | 31 | .SS \fBosc list\fR 32 | List sources or binaries on the server 33 | 34 | usage: 35 | osc [global opts] list [PROJECT [PACKAGE]] 36 | osc [global opts] list \-b [PROJECT [PACKAGE [REPO [ARCH]]]] 37 | 38 | List command description. 39 | 40 | options: 41 | .RS 7 42 | .TP 43 | \fB\-M\fR, \fB\-\-meta\fR 44 | list meta data files 45 | 46 | .TP 47 | \fB\-v\fR, \fB\-\-verbose\fR 48 | print extra information 49 | .RE 50 | 51 | .SS \fBosc foo\fR 52 | Test command with subcommands 53 | 54 | usage: 55 | osc [global opts] foo [\-h] {bar,baz} ... 56 | 57 | Some description. 58 | 59 | .SS \fBosc foo bar\fR 60 | help bar 61 | 62 | usage: osc [global opts] foo bar [\-h] [\-\-opt\-bar1 OPT_BAR1] 63 | [\-\-opt\-bar2 OPT_BAR2] 64 | 65 | options: 66 | .RS 7 67 | .TP 68 | \fB\-\-opt\-bar1\fR \fI\,OPT_BAR1\/\fR 69 | help opt\-bar1 70 | 71 | .TP 72 | \fB\-\-opt\-bar2\fR \fI\,OPT_BAR2\/\fR 73 | help opt\-bar2 74 | .RE 75 | 76 | .SS \fBosc foo baz\fR 77 | help baz 78 | 79 | usage: osc [global opts] foo baz [\-h] [\-\-opt\-baz1 OPT_BAZ1] 80 | [\-\-opt\-baz2 OPT_BAZ2] 81 | 82 | options: 83 | .RS 7 84 | .TP 85 | \fB\-\-opt\-baz1\fR \fI\,OPT_BAZ1\/\fR 86 | help opt\-baz1 87 | 88 | .TP 89 | \fB\-\-opt\-baz2\fR \fI\,OPT_BAZ2\/\fR 90 | help opt\-baz2 91 | .RE 92 | 93 | 94 | .SH AUTHOR 95 | .nf 96 | Contributors to the osc project. See the project's GIT history for the complete list. 97 | .fi 98 | 99 | .SH DISTRIBUTION 100 | The latest version of osc may be downloaded from 101 | .UR http://en.opensuse.org/openSUSE:OSC 102 | .UE 103 | -------------------------------------------------------------------------------- /examples/osc/osc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/osc/osc/__init__.py -------------------------------------------------------------------------------- /examples/osc/osc/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | 4 | import argparse 5 | 6 | 7 | class HelpFormatter(argparse.RawDescriptionHelpFormatter): 8 | def _format_action(self, action): 9 | if isinstance(action, argparse._SubParsersAction): 10 | parts = [] 11 | for i in action._get_subactions(): 12 | if i.help == argparse.SUPPRESS: 13 | # don't display commands with suppressed help 14 | continue 15 | if len(i.metavar) > 20: 16 | parts.append("%*s%-21s" % (self._current_indent, "", i.metavar)) 17 | parts.append("%*s %s" % (self._current_indent + 21, "", i.help)) 18 | else: 19 | parts.append("%*s%-21s %s" % (self._current_indent, "", i.metavar, i.help)) 20 | return "\n".join(parts) 21 | return super(HelpFormatter, self)._format_action(action) 22 | 23 | def _format_usage(self, usage, actions, groups, prefix): 24 | if usage: 25 | usage = usage.strip() 26 | usage = usage % dict(prog=self._prog) 27 | else: 28 | usage = super(HelpFormatter, self)._format_usage(usage, actions, groups, prefix) 29 | if usage.startswith("usage: "): 30 | usage = usage[7:] 31 | 32 | result = ["usage: "] 33 | for line in usage.strip().splitlines(): 34 | result.append(" " + line) 35 | result.append("") 36 | result.append("") 37 | return "\n".join(result) 38 | 39 | 40 | def get_parser(): 41 | parser = argparse.ArgumentParser( 42 | prog="osc", 43 | usage= 44 | "%(prog)s [global opts] [--help] [opts] [args]\n" 45 | "%(prog)s --help", 46 | formatter_class=HelpFormatter, 47 | ) 48 | 49 | commands = parser.add_subparsers( 50 | title="commands", 51 | dest="command", 52 | ) 53 | 54 | # IMPORTANT: 55 | # it is necessary to specify 'prog=' in all subcommands to get usage rendered correctly 56 | 57 | cmd_build = commands.add_parser( 58 | "build", 59 | help="Build a package on your local machine", 60 | description="Build command description.", 61 | prog="osc [global opts] build", 62 | usage= 63 | "%(prog)s (will try to guess a build environment)\n" 64 | "%(prog)s REPOSITORY ARCH BUILD_DESCR\n" 65 | "%(prog)s REPOSITORY ARCH\n" 66 | "%(prog)s REPOSITORY (ARCH = hostarch, BUILD_DESCR is detected automatically)\n" 67 | "%(prog)s ARCH (REPOSITORY = build_repository (config option), BUILD_DESCR is detected automatically)\n" 68 | "%(prog)s BUILD_DESCR (REPOSITORY = build_repository (config option), ARCH = hostarch)\n" 69 | "%(prog)s (REPOSITORY = build_repository (config option), ARCH = hostarch, BUILD_DESCR is detected automatically)", 70 | formatter_class=HelpFormatter, 71 | ) 72 | cmd_build.add_argument( 73 | "--clean", 74 | action="store_true", 75 | help="Delete old build root before initializing it", 76 | ) 77 | 78 | cmd_list = commands.add_parser( 79 | "list", 80 | help="List sources or binaries on the server", 81 | description="List command description.", 82 | prog="osc [global opts] list", 83 | usage= 84 | "%(prog)s [PROJECT [PACKAGE]]\n" 85 | "%(prog)s -b [PROJECT [PACKAGE [REPO [ARCH]]]]", 86 | formatter_class=HelpFormatter, 87 | ) 88 | cmd_list.add_argument( 89 | "-M", "--meta", 90 | action="store_true", 91 | help="list meta data files", 92 | ) 93 | cmd_list.add_argument( 94 | "-v", "--verbose", 95 | action="store_true", 96 | help="print extra information", 97 | ) 98 | 99 | cmd_foo = commands.add_parser( 100 | "foo", 101 | help="Test command with subcommands", 102 | description="Some description.", 103 | prog="osc [global opts] foo", 104 | formatter_class=HelpFormatter, 105 | ) 106 | 107 | cmd_foo_subcommands = cmd_foo.add_subparsers( 108 | title="commands", 109 | dest="commands", 110 | ) 111 | 112 | cmd_foo_bar = cmd_foo_subcommands.add_parser( 113 | "bar", 114 | help="help bar", 115 | prog="osc [global opts] foo bar", 116 | ) 117 | cmd_foo_bar.add_argument("--opt-bar1", help="help opt-bar1") 118 | cmd_foo_bar.add_argument("--opt-bar2", help="help opt-bar2") 119 | 120 | cmd_foo_baz = cmd_foo_subcommands.add_parser( 121 | "baz", 122 | help="help baz", 123 | prog="osc [global opts] foo baz", 124 | ) 125 | cmd_foo_baz.add_argument("--opt-baz1", help="help opt-baz1") 126 | cmd_foo_baz.add_argument("--opt-baz2", help="help opt-baz2") 127 | 128 | cmd_hidden = commands.add_parser( 129 | "hidden", 130 | help=argparse.SUPPRESS, 131 | description="A hidden command, not displayed in help", 132 | formatter_class=HelpFormatter, 133 | ) 134 | 135 | return parser 136 | 137 | 138 | def main(): 139 | parser = get_parser() 140 | parser.parse_args() 141 | 142 | 143 | if __name__ == "__main__": 144 | main() 145 | -------------------------------------------------------------------------------- /examples/osc/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpages] 2 | manpages = 3 | osc.1:module=osc.main:function=get_parser:format=single-commands-section:description=openSUSE commander command-line:prog=osc:author=Contributors to the osc project. See the project's GIT history for the complete list. 4 | -------------------------------------------------------------------------------- /examples/osc/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | from setuptools.command.build_py import build_py 5 | from setuptools.command.install import install 6 | 7 | # insert path to build_manpage 8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) 9 | 10 | from build_manpages.build_manpages \ 11 | import build_manpages, get_build_py_cmd, get_install_cmd 12 | 13 | 14 | setup( 15 | name='osc', 16 | version='1.0', 17 | description='openSUSE commander', 18 | author='openSUSE project', 19 | author_email='opensuse-buildservice@opensuse.org', 20 | url='http://en.opensuse.org/openSUSE:OSC', 21 | cmdclass={ 22 | 'build_manpages': build_manpages, 23 | 'build_py': get_build_py_cmd(build_py), 24 | 'install': get_install_cmd(install), 25 | }, 26 | packages=find_packages(), 27 | ) 28 | -------------------------------------------------------------------------------- /examples/pre-written-man-page/psutils.1: -------------------------------------------------------------------------------- 1 | .TH PSUTILS 1 "PSUtils" 2 | .SH NAME 3 | PSUtils \- PostScript utilities 4 | .SH DESCRIPTION 5 | PSUtils is a set of utilities for manipulating PostScript 6 | documents which follow the Adobe Document Structuring Conventions. 7 | .SH UNITS OF LENGTH 8 | PSUtils utilities accept lengths in various units: 9 | .IP \(bu 10 | .B pt 11 | (PostScript points, 72 points per inch) 12 | .IP \(bu 13 | .B mm 14 | (millimetres) 15 | .IP \(bu 16 | .B cm 17 | (centimetres) 18 | .IP \(bu 19 | .B in 20 | (inches, 1 inch is 25.4mm) 21 | .PP 22 | Write the length as a number directly followed by the unit, for example, 23 | .B 4.5mm 24 | or 25 | .BR 72pt . 26 | If no unit is given, PostScript points are assumed. 27 | .SH AUTHOR 28 | Written by Angus J. C. Duggan. 29 | .SH "SEE ALSO" 30 | .BR psbook (1), 31 | .BR psselect (1), 32 | .BR pstops (1), 33 | .BR epsffit (1), 34 | .BR psnup (1), 35 | .BR psresize (1), 36 | .BR psjoin (1), 37 | .BR extractres (1), 38 | .BR includeres (1) 39 | .SH TRADEMARKS 40 | .B PostScript 41 | is a trademark of Adobe Systems Incorporated. 42 | -------------------------------------------------------------------------------- /examples/pre-written-man-page/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpages] 2 | manpages = 3 | psutils.1:manfile=psutils.1 4 | -------------------------------------------------------------------------------- /examples/pre-written-man-page/setup.py: -------------------------------------------------------------------------------- 1 | from build_manpages.build_manpages import build_manpages, get_install_cmd, get_build_py_cmd 2 | from setuptools import setup 3 | 4 | setup( 5 | cmdclass={ 6 | 'build_manpages': build_manpages, 7 | 'build_py': get_build_py_cmd(), 8 | 'install': get_install_cmd(), 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /examples/raw-description/bin/dg: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | from __future__ import print_function 4 | 5 | import sys 6 | import tempfile 7 | import shutil 8 | 9 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 10 | 11 | def error(msg): 12 | print(msg, file=sys.stderr) 13 | 14 | def die(msg): 15 | error(msg) 16 | sys.exit(1) 17 | 18 | description = \ 19 | """ 20 | Generate script using predefined metadata about distribution and 21 | templates. 22 | 23 | As an example of 'dg' usage, to generate _Dockerfile_ for Fedora 24 | 21 64-bit system, you may use command(s): 25 | 26 | $ cd project/directory 27 | $ dg --spec docker-data.yaml \\ 28 | --template docker.tpl \\ 29 | --distro fedora-21-x86_64.yaml 30 | """ 31 | 32 | parser = ArgumentParser( 33 | description=description, 34 | formatter_class=RawDescriptionHelpFormatter, 35 | ) 36 | 37 | parser.man_short_description = "templating system/generator for distributions" 38 | 39 | parser.add_argument( 40 | '--projectdir', 41 | metavar='PROJECTDIR', 42 | type=str, 43 | help='Directory with project (defaults to CWD)', 44 | default="." 45 | ) 46 | 47 | parser.add_argument( 48 | '--distro', 49 | metavar='DIST', 50 | type=str, 51 | help='Use distribution metadata specified by DIST yaml file', 52 | default="fedora-21-x86_64.yaml", 53 | ) 54 | 55 | parser.add_argument( 56 | '--multispec', 57 | metavar='MULTISPEC', 58 | type=str, 59 | help='Use MULTISPEC yaml file to fill the TEMPLATE file', 60 | ) 61 | 62 | parser.add_argument( 63 | '--multispec-selector', 64 | metavar='MULTISPEC_SELECTOR', 65 | type=str, 66 | help='Selectors for the multispec file', 67 | action='append', 68 | default=[], 69 | ) 70 | 71 | parser.add_argument( 72 | '--spec', 73 | metavar='SPEC', 74 | type=str, 75 | help='Use SPEC yaml file to fill the TEMPLATE file', 76 | action='append', 77 | ) 78 | 79 | parser.add_argument( 80 | '--output', 81 | metavar='OUTPUT', 82 | type=str, 83 | help='Write result to OUTPUT file instead of stdout', 84 | ) 85 | 86 | parser.add_argument( 87 | '--macros-from', 88 | metavar='PROJECTDIR', 89 | type=str, 90 | action='append', 91 | help='Load variables from PROJECTDIR', 92 | ) 93 | 94 | parser.add_argument( 95 | '--container', 96 | metavar='CONTAINER_TYPE', 97 | type=str, 98 | help='Container type, e.g. \'docker\'', 99 | default=False, 100 | ) 101 | 102 | parser.add_argument( 103 | '--macro', 104 | metavar='MACRO', 105 | type=str, 106 | action='append', 107 | help='Define distgen\'s macro', 108 | ) 109 | 110 | parser.add_argument( 111 | '--max-passes', 112 | metavar='PASSES', 113 | type=int, 114 | default=1, 115 | help='Maximum number of rendering passes, defaults to 1 (== no re-rendering)', 116 | ) 117 | 118 | tpl_or_combinations = parser.add_mutually_exclusive_group(required=True) 119 | 120 | tpl_or_combinations.add_argument( 121 | '--template', 122 | metavar='TEMPLATE', 123 | type=str, 124 | help='Use TEMPLATE file, e.g. docker.tpl or a template string, ' 125 | 'e.g. "{{ config.docker.from }}"' 126 | ) 127 | 128 | tpl_or_combinations.add_argument( 129 | '--multispec-combinations', 130 | action='store_true', 131 | help='Print available multispec combinations', 132 | ) 133 | 134 | def print_multispec_combinations(args): 135 | ms = Multispec.from_path(args.projectdir, args.multispec) 136 | for c in ms.get_all_combinations(): 137 | to_print = ['--distro {0}'.format(c.pop('distro'))] 138 | [to_print.append('--multispec-selector {0}={1}'.format(k, v)) for k, v in c.items()] 139 | print(' '.join(to_print)) 140 | 141 | 142 | def render_template(args): 143 | temp_filename = False 144 | output = sys.stdout 145 | try: 146 | if args.output: 147 | _, temp_filename = tempfile.mkstemp(prefix="distgen-") 148 | output = open(temp_filename, 'w') 149 | except: 150 | die("can't create temporary file for '{0}'".format(args.output)) 151 | 152 | cmd_cfg = CommandsConfig() 153 | cmd_cfg.container = args.container 154 | 155 | explicit_macros = {} 156 | if args.macro: 157 | for i in args.macro: 158 | key, value = i.split(' ', 1) 159 | explicit_macros[key] = value 160 | 161 | if args.template == '-': 162 | args.template = "/proc/self/fd/0" 163 | 164 | generator = Generator() 165 | generator.load_project(args.projectdir) 166 | generator.render( 167 | args.spec, 168 | args.multispec, 169 | args.multispec_selector, 170 | args.template, 171 | args.distro, 172 | cmd_cfg, 173 | output, 174 | args.macros_from, 175 | explicit_macros, 176 | args.max_passes, 177 | ) 178 | 179 | if temp_filename: 180 | try: 181 | output.close() 182 | shutil.move(temp_filename, args.output) 183 | except: 184 | die("can't move '{0}' into '{1}'".format(temp_filename, args.output)) 185 | 186 | 187 | def main(): 188 | args = parser.parse_args() 189 | if args.multispec_combinations: 190 | print_multispec_combinations(args) 191 | else: 192 | render_template(args) 193 | 194 | 195 | if __name__ == "__main__": 196 | main() 197 | -------------------------------------------------------------------------------- /examples/raw-description/expected-output.1: -------------------------------------------------------------------------------- 1 | .TH DG "1" "2023\-04\-09" "example 0.1.0" "Generated Python Manual" 2 | .SH NAME 3 | dg \- templating system/generator for distributions 4 | .SH SYNOPSIS 5 | .B dg 6 | [-h] [--projectdir PROJECTDIR] [--distro DIST] [--multispec MULTISPEC] [--multispec-selector MULTISPEC_SELECTOR] [--spec SPEC] [--output OUTPUT] [--macros-from PROJECTDIR] [--container CONTAINER_TYPE] [--macro MACRO] [--max-passes PASSES] (--template TEMPLATE | --multispec-combinations) 7 | .SH DESCRIPTION 8 | Generate script using predefined metadata about distribution and 9 | templates. 10 | 11 | As an example of 'dg' usage, to generate _Dockerfile_ for Fedora 12 | 21 64\-bit system, you may use command(s): 13 | 14 | $ cd project/directory 15 | $ dg \-\-spec docker\-data.yaml \\ 16 | \-\-template docker.tpl \\ 17 | \-\-distro fedora\-21\-x86_64.yaml 18 | 19 | .SH OPTIONS 20 | .TP 21 | \fB\-\-projectdir\fR \fI\,PROJECTDIR\/\fR 22 | Directory with project (defaults to CWD) 23 | 24 | .TP 25 | \fB\-\-distro\fR \fI\,DIST\/\fR 26 | Use distribution metadata specified by DIST yaml file 27 | 28 | .TP 29 | \fB\-\-multispec\fR \fI\,MULTISPEC\/\fR 30 | Use MULTISPEC yaml file to fill the TEMPLATE file 31 | 32 | .TP 33 | \fB\-\-multispec\-selector\fR \fI\,MULTISPEC_SELECTOR\/\fR 34 | Selectors for the multispec file 35 | 36 | .TP 37 | \fB\-\-spec\fR \fI\,SPEC\/\fR 38 | Use SPEC yaml file to fill the TEMPLATE file 39 | 40 | .TP 41 | \fB\-\-output\fR \fI\,OUTPUT\/\fR 42 | Write result to OUTPUT file instead of stdout 43 | 44 | .TP 45 | \fB\-\-macros\-from\fR \fI\,PROJECTDIR\/\fR 46 | Load variables from PROJECTDIR 47 | 48 | .TP 49 | \fB\-\-container\fR \fI\,CONTAINER_TYPE\/\fR 50 | Container type, e.g. 'docker' 51 | 52 | .TP 53 | \fB\-\-macro\fR \fI\,MACRO\/\fR 54 | Define distgen's macro 55 | 56 | .TP 57 | \fB\-\-max\-passes\fR \fI\,PASSES\/\fR 58 | Maximum number of rendering passes, defaults to 1 (== no re\-rendering) 59 | 60 | .TP 61 | \fB\-\-template\fR \fI\,TEMPLATE\/\fR 62 | Use TEMPLATE file, e.g. docker.tpl or a template string, e.g. "{{ config.docker.from }}" 63 | 64 | .TP 65 | \fB\-\-multispec\-combinations\fR 66 | Print available multispec combinations 67 | 68 | .SH AUTHOR 69 | .nf 70 | John Doe 71 | .fi 72 | 73 | .SH DISTRIBUTION 74 | The latest version of example may be downloaded from 75 | .UR http://example.com 76 | .UE 77 | -------------------------------------------------------------------------------- /examples/raw-description/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpages] 2 | manpages = 3 | man/dg.1:object=parser:pyfile=bin/dg 4 | -------------------------------------------------------------------------------- /examples/raw-description/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Example of argparse taken from: 4 | # https://pagure.io/copr/copr/blob/a4feb01bc35b8554f503d41795e7a184ff929dd4/f/cli/copr_cli 5 | 6 | import os 7 | import sys 8 | 9 | from setuptools import setup, find_packages 10 | 11 | # Just to make sure that build_manpage can be found. 12 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) 13 | 14 | from build_manpages.build_manpages \ 15 | import build_manpages, get_build_py_cmd, get_install_cmd 16 | 17 | from setuptools.command.install import install 18 | from distutils.command.build import build 19 | 20 | setup( 21 | name='example', 22 | description='This project does nothing.', 23 | long_description=('Long description of the project.'), 24 | author='John Doe', 25 | author_email='jd@example.com', 26 | version='0.1.0', 27 | url='http://example.com', 28 | packages=find_packages(), 29 | scripts=['bin/dg'], 30 | cmdclass={ 31 | 'build_manpages': build_manpages, 32 | 'build': get_build_py_cmd(build), 33 | 'install': get_install_cmd(install), 34 | }, 35 | ) 36 | -------------------------------------------------------------------------------- /examples/resalloc/bin/resalloc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Resalloc client. 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | import sys 21 | import argparse 22 | 23 | from resalloc.client import Ticket, Connection 24 | from resalloc.version import resalloc_version 25 | 26 | 27 | parser = argparse.ArgumentParser() 28 | parser.add_argument( 29 | "--connection", dest="connection", default="http://localhost:49100") 30 | parser.add_argument( 31 | '--version', action='version', 32 | version='%(prog)s (client) {0}'.format(resalloc_version)) 33 | 34 | subparsers = parser.add_subparsers(title="actions", dest='subparser') 35 | subparsers.required = True 36 | parser_new_ticket = subparsers.add_parser( 37 | "ticket", 38 | help="Create ticket") 39 | parser_new_ticket.add_argument( 40 | "--tag", dest="tags", action="append", 41 | required=True, 42 | help="What tag the Resource should have") 43 | parser_get_ticket = subparsers.add_parser( 44 | "ticket-check", 45 | help="Obtain ticket") 46 | parser_get_ticket.add_argument( 47 | "ticket", 48 | help="Get the ticket") 49 | parser_wait_ticket = subparsers.add_parser( 50 | "ticket-wait", 51 | help="Wait till ticket is ready and write the output" 52 | ) 53 | parser_wait_ticket.add_argument( 54 | "ticket", 55 | help="ID of ticket to wait for") 56 | parser_close_ticket = subparsers.add_parser( 57 | "ticket-close", 58 | help="Close a ticket") 59 | parser_close_ticket.add_argument( 60 | "ticket", 61 | help="ID of ticket to be closed") 62 | 63 | def main(): 64 | try: 65 | arg = parser.parse_args() 66 | 67 | conn = Connection(arg.connection) 68 | if 'ticket' == arg.subparser: 69 | ticket = conn.newTicket(arg.tags) 70 | print(ticket.id) 71 | 72 | elif 'ticket-check' == arg.subparser: 73 | ticket = conn.getTicket(arg.ticket) 74 | if ticket.collect(): 75 | sys.stdout.write(str(ticket.output)) 76 | else: 77 | sys.stderr.write("ticket is still not processed\n") 78 | return 1 79 | 80 | elif 'ticket-wait' == arg.subparser: 81 | ticket = conn.getTicket(arg.ticket) 82 | output = ticket.wait() 83 | print(str(output)) 84 | 85 | elif 'ticket-close' == arg.subparser: 86 | ticket = conn.getTicket(arg.ticket) 87 | ticket.close() 88 | 89 | else: 90 | assert(0) 91 | 92 | except KeyboardInterrupt: 93 | sys.stderr.write("\nInterrupted by user.") 94 | sys.exit(1) 95 | 96 | if __name__ == "__main__": 97 | sys.exit(main()) 98 | -------------------------------------------------------------------------------- /examples/resalloc/bin/resalloc-maint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Resalloc administrating tool. 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | import sys 21 | import argparse 22 | 23 | from resallocserver.maint import Maintainer 24 | 25 | parser = argparse.ArgumentParser() 26 | subparsers = parser.add_subparsers(title="actions", dest='subparser') 27 | subparsers.required = True 28 | 29 | parser.add_argument("--duh", help="huhh") 30 | 31 | p_list = subparsers.add_parser( 32 | "resource-list", 33 | help="List available resources") 34 | p_list.add_argument( 35 | "--up", dest='up', action='store_true', 36 | help="List only ready-to-take resources") 37 | 38 | parser_resource_delete = subparsers.add_parser( 39 | "resource-delete", 40 | help="Delete resource") 41 | parser_resource_delete.add_argument( 42 | "resource", 43 | help="The resource ID") 44 | 45 | p_t_list = subparsers.add_parser( 46 | "ticket-list", 47 | help="List not-yet-closed tickets") 48 | 49 | 50 | def main(): 51 | try: 52 | args = parser.parse_args() 53 | maint = Maintainer() 54 | 55 | if 'resource-list' in args.subparser: 56 | maint.resource_list(up=args.up) 57 | 58 | elif 'resource-delete' in args.subparser: 59 | maint.resource_delete(args.resource) 60 | 61 | elif 'ticket-list' in args.subparser: 62 | maint.ticket_list() 63 | 64 | except KeyboardInterrupt: 65 | sys.stderr.write("\nInterrupted by user.") 66 | sys.exit(1) 67 | 68 | if __name__ == "__main__": 69 | sys.exit(main()) 70 | -------------------------------------------------------------------------------- /examples/resalloc/expected/man/resalloc-maint.1: -------------------------------------------------------------------------------- 1 | .TH RESALLOC\-MAINT "1" "2023\-04\-09" "resalloc 0" "Generated Python Manual" 2 | .SH NAME 3 | resalloc\-maint 4 | .SH SYNOPSIS 5 | .B resalloc\-maint 6 | [-h] [--duh DUH] {resource-list,resource-delete,ticket-list} ... 7 | 8 | .SH OPTIONS 9 | .TP 10 | \fB\-\-duh\fR \fI\,DUH\/\fR 11 | huhh 12 | 13 | .SH 14 | ACTIONS 15 | .TP 16 | \fBresalloc\-maint\fR \fI\,resource\-list\/\fR 17 | List available resources 18 | .TP 19 | \fBresalloc\-maint\fR \fI\,resource\-delete\/\fR 20 | Delete resource 21 | .TP 22 | \fBresalloc\-maint\fR \fI\,ticket\-list\/\fR 23 | List not\-yet\-closed tickets 24 | 25 | .SH COMMAND \fI\,'resalloc\-maint resource\-list'\/\fR 26 | usage: resalloc\-maint resource\-list [\-h] [\-\-up] 27 | 28 | .SH OPTIONS \fI\,'resalloc\-maint resource\-list'\/\fR 29 | .TP 30 | \fB\-\-up\fR 31 | List only ready\-to\-take resources 32 | 33 | .SH COMMAND \fI\,'resalloc\-maint resource\-delete'\/\fR 34 | usage: resalloc\-maint resource\-delete [\-h] resource 35 | 36 | .TP 37 | \fBresource\fR 38 | The resource ID 39 | 40 | .SH COMMAND \fI\,'resalloc\-maint ticket\-list'\/\fR 41 | usage: resalloc\-maint ticket\-list [\-h] 42 | 43 | .SH AUTHOR 44 | .nf 45 | Pavel Raiskup 46 | .fi 47 | 48 | .SH DISTRIBUTION 49 | The latest version of resalloc may be downloaded from 50 | .UR https://github.com/praiskup/resalloc 51 | .UE 52 | -------------------------------------------------------------------------------- /examples/resalloc/expected/man/resalloc.1: -------------------------------------------------------------------------------- 1 | .TH RESALLOC "1" "2023\-04\-09" "resalloc 0" "Generated Python Manual" 2 | .SH NAME 3 | resalloc 4 | .SH SYNOPSIS 5 | .B resalloc 6 | [-h] [--connection CONNECTION] [--version] {ticket,ticket-check,ticket-wait,ticket-close} ... 7 | 8 | .SH OPTIONS 9 | .TP 10 | \fB\-\-connection\fR \fI\,CONNECTION\/\fR 11 | 12 | .TP 13 | \fB\-\-version\fR 14 | show program's version number and exit 15 | 16 | .SH 17 | ACTIONS 18 | .TP 19 | \fBresalloc\fR \fI\,ticket\/\fR 20 | Create ticket 21 | .TP 22 | \fBresalloc\fR \fI\,ticket\-check\/\fR 23 | Obtain ticket 24 | .TP 25 | \fBresalloc\fR \fI\,ticket\-wait\/\fR 26 | Wait till ticket is ready and write the output 27 | .TP 28 | \fBresalloc\fR \fI\,ticket\-close\/\fR 29 | Close a ticket 30 | 31 | .SH COMMAND \fI\,'resalloc ticket'\/\fR 32 | usage: resalloc ticket [\-h] \-\-tag TAGS 33 | 34 | .SH OPTIONS \fI\,'resalloc ticket'\/\fR 35 | .TP 36 | \fB\-\-tag\fR \fI\,TAGS\/\fR 37 | What tag the Resource should have 38 | 39 | .SH COMMAND \fI\,'resalloc ticket\-check'\/\fR 40 | usage: resalloc ticket\-check [\-h] ticket 41 | 42 | .TP 43 | \fBticket\fR 44 | Get the ticket 45 | 46 | .SH COMMAND \fI\,'resalloc ticket\-wait'\/\fR 47 | usage: resalloc ticket\-wait [\-h] ticket 48 | 49 | .TP 50 | \fBticket\fR 51 | ID of ticket to wait for 52 | 53 | .SH COMMAND \fI\,'resalloc ticket\-close'\/\fR 54 | usage: resalloc ticket\-close [\-h] ticket 55 | 56 | .TP 57 | \fBticket\fR 58 | ID of ticket to be closed 59 | 60 | .SH AUTHOR 61 | .nf 62 | Pavel Raiskup 63 | .fi 64 | 65 | .SH DISTRIBUTION 66 | The latest version of resalloc may be downloaded from 67 | .UR https://github.com/praiskup/resalloc 68 | .UE 69 | -------------------------------------------------------------------------------- /examples/resalloc/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/resalloc/requirements.txt -------------------------------------------------------------------------------- /examples/resalloc/resalloc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/resalloc/resalloc/__init__.py -------------------------------------------------------------------------------- /examples/resalloc/resalloc/client.py: -------------------------------------------------------------------------------- 1 | class Ticket: 2 | pass 3 | 4 | class Connection: 5 | pass 6 | -------------------------------------------------------------------------------- /examples/resalloc/resalloc/version.py: -------------------------------------------------------------------------------- 1 | resalloc_version = '0' 2 | -------------------------------------------------------------------------------- /examples/resalloc/resallocserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/examples/resalloc/resallocserver/__init__.py -------------------------------------------------------------------------------- /examples/resalloc/resallocserver/maint.py: -------------------------------------------------------------------------------- 1 | class Maintainer: 2 | pass 3 | -------------------------------------------------------------------------------- /examples/resalloc/setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpages] 2 | manpages = 3 | man/resalloc.1:object=parser:pyfile=bin/resalloc 4 | man/resalloc-maint.1:object=parser:pyfile=bin/resalloc-maint 5 | -------------------------------------------------------------------------------- /examples/resalloc/setup.py: -------------------------------------------------------------------------------- 1 | # Resalloc setup script. 2 | # Copyright (C) 2017 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License along 15 | # with this program; if not, write to the Free Software Foundation, Inc., 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | import os, sys 19 | from setuptools import setup, find_packages 20 | from resalloc.version import resalloc_version 21 | from os import listdir, path 22 | 23 | # For the manual pages generator. 24 | from setuptools.command.build_py import build_py 25 | from setuptools.command.install import install 26 | try: 27 | sys.path = [os.path.join(os.getcwd(), 'build_manpages')] + sys.path 28 | from build_manpages.build_manpages \ 29 | import build_manpages, get_build_py_cmd, get_install_cmd 30 | except: 31 | print("=======================================") 32 | print("Use 'git submodule update --init' first") 33 | print("=======================================") 34 | raise 35 | 36 | project = "resalloc" 37 | datadir = "share" 38 | pkgdatadir = datadir + "/" + project 39 | 40 | def get_requirements(): 41 | with open('requirements.txt') as f: 42 | return f.read().splitlines() 43 | 44 | long_description=""" 45 | Resource allocator 46 | """.strip() 47 | 48 | setup( 49 | name=project, 50 | version=resalloc_version, 51 | description='Client/server application for maintaining (expensive) resources', 52 | long_description=long_description, 53 | author='Pavel Raiskup', 54 | author_email='praiskup@redhat.com', 55 | license='GPLv2+', 56 | url='https://github.com/praiskup/resalloc', 57 | platforms=['any'], 58 | packages=find_packages(), 59 | scripts=['bin/resalloc', 'bin/resalloc-maint'], 60 | install_requires=get_requirements(), 61 | cmdclass={ 62 | 'build_manpages': build_manpages, 63 | 'build_py': get_build_py_cmd(build_py), 64 | 'install': get_install_cmd(install), 65 | }, 66 | ) 67 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # Copr clients pylint configuration 2 | 3 | [MASTER] 4 | # Pickle collected data for later comparisons. 5 | persistent=no 6 | 7 | init-hook= 8 | import os 9 | import subprocess 10 | gitrootdir = subprocess.check_output(["git", "rev-parse", "--show-toplevel"]).decode("utf-8").strip() 11 | sys.path.insert(0, os.path.join(gitrootdir, '.pylintpath')) 12 | 13 | 14 | [MESSAGES CONTROL] 15 | # Reasoning for wide warning ignore 16 | # --------------------------------- 17 | # import-error 18 | # This is to fix our CI where we do not have all the build requirements for 19 | # all our sub-components. We can afford not listening to this error because 20 | # our packaging CI would discover the problems anyways. 21 | # useless-object-inheritance 22 | # We need to keep compatibility with Python 2 for EL7. 23 | # consider-using-f-string 24 | # We still support Python 2.7 (EL7) for clients. 25 | # unspecified-encoding 26 | # Python2.7: TypeError: 'encoding' is an invalid keyword argument for this function 27 | disable=import-error,useless-object-inheritance,super-with-arguments,consider-using-f-string,unspecified-encoding 28 | 29 | [VARIABLES] 30 | # A regular expression matching names used for dummy variables (i.e. not used). 31 | dummy-variables-rgx=_|dummy 32 | 33 | 34 | [BASIC] 35 | # Regular expression which should only match correct module names 36 | module-rgx=([a-zA-Z_][a-zA-Z0-9_]+)$ 37 | 38 | # Regular expression which should only match correct module level names 39 | const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$ 40 | 41 | # Regular expression which should only match correct class names 42 | class-rgx=[a-zA-Z_][a-zA-Z0-9_]+$ 43 | 44 | # Regular expression which should only match correct function names 45 | function-rgx=[a-z_][a-zA-Z0-9_]{,42}$ 46 | 47 | # Regular expression which should only match correct method names 48 | method-rgx=[a-z_][a-zA-Z0-9_]{,42}$ 49 | 50 | # Regular expression which should only match correct instance attribute names 51 | attr-rgx=[a-z_][a-zA-Z0-9_]{,30}$ 52 | 53 | # Regular expression which should only match correct argument names 54 | argument-rgx=[a-z_][a-zA-Z0-9_]{,30}$ 55 | 56 | # Regular expression which should only match correct variable names 57 | variable-rgx=[a-z_][a-zA-Z0-9_]{,30}$ 58 | 59 | # Regular expression which should only match correct list comprehension / 60 | # generator expression variable names 61 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 62 | 63 | # Regular expression which should only match correct class sttribute names 64 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,42}|(__.*__))$ 65 | 66 | # Good variable names which should always be accepted, separated by a comma 67 | good-names=i,j,k,ex,Run,_ 68 | 69 | # Bad variable names which should always be refused, separated by a comma 70 | bad-names=foo,bar,baz,toto,tutu,tata 71 | 72 | [DESIGN] 73 | 74 | # Maximum number of arguments for function / method 75 | max-args=10 76 | 77 | # Maximum number of locals for function / method body 78 | max-locals=20 79 | 80 | # Maximum number of return / yield for function / method body 81 | max-returns=6 82 | 83 | # Maximum number of branch for function / method body 84 | max-branches=20 85 | 86 | # Maximum number of statements in function / method body 87 | max-statements=50 88 | 89 | # Maximum number of parents for a class (see R0901). 90 | max-parents=7 91 | 92 | # Maximum number of attributes for a class (see R0902). 93 | max-attributes=7 94 | 95 | # Minimum number of public methods for a class (see R0903). 96 | min-public-methods=1 97 | 98 | # Maximum number of public methods for a class (see R0904). 99 | max-public-methods=20 100 | 101 | 102 | [FORMAT] 103 | # Maximum number of characters on a single line. 104 | max-line-length=120 105 | 106 | # Maximum number of lines in a module 107 | max-module-lines=1000 108 | 109 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 110 | # tab). 111 | indent-string=' ' 112 | 113 | 114 | [MISCELLANEOUS] 115 | # List of note tags to take in consideration, separated by a comma. 116 | notes= 117 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", 4 | "tomli; python_version >= '3' and python_version < '3.11'", 5 | "toml; python_version < '3'", 6 | "packaging", 7 | ] 8 | 9 | [tool.build_manpages] 10 | manpages = [ 11 | "some-file.1:pyfile=some-file:function=get_parser", 12 | ] 13 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = rpm 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praiskup/argparse-manpage/ade76cc8ef49ec804955c6b4027dbae52d482034/requirements.txt -------------------------------------------------------------------------------- /rpm/.gitignore: -------------------------------------------------------------------------------- 1 | *.spec 2 | *.tar.gz 3 | *.src.rpm 4 | noarch/ 5 | argparse-manpage*/ 6 | -------------------------------------------------------------------------------- /rpm/Makefile: -------------------------------------------------------------------------------- 1 | PYTHON := python3 2 | BUILD_HELPER := ./build-helper 3 | 4 | VERSION = $(shell $(BUILD_HELPER) --version) 5 | RELEASE = $(shell $(BUILD_HELPER) --release) 6 | DATE = $(shell LC_ALL=C date +'%a %b %d %Y') 7 | TARBALL = argparse-manpage-$(VERSION).tar.gz 8 | 9 | all: argparse-manpage.spec argparse-manpage-$(VERSION).tar.gz 10 | 11 | .PHONY: $(TARBALL) 12 | $(TARBALL): 13 | rm -f $(TARBALL) 14 | cd .. && python3 setup.py sdist && cp "dist/argparse-manpage-$(VERSION).tar.gz" rpm/ 15 | 16 | argparse-manpage.spec: argparse-manpage.spec.tpl ../setup.py 17 | @echo " GEN $@" ; \ 18 | rm -f "$@.output" ; \ 19 | sed -e "s|@VERSION@|$(VERSION)|g" \ 20 | -e "s|@RELEASE@|$(RELEASE)|g" \ 21 | -e "s|@DATE@|$(DATE)|g" \ 22 | $< > $@.output ; \ 23 | chmod 0444 "$@.output" ; \ 24 | mv -f "$@.output" "$@" # move when successful 25 | 26 | srpm: all 27 | dir=`pwd` ; \ 28 | rm *.src.rpm ; \ 29 | rpmbuild --define "_sourcedir $$dir" \ 30 | --define "_rpmdir $$dir" \ 31 | --define "_builddir $$dir" \ 32 | --define "_specdir $$dir" \ 33 | --define "_srcrpmdir $$dir" \ 34 | -bs *.spec 35 | 36 | clean: 37 | rm -rf *.src.rpm *.tar.gz *.spec noarch 38 | -------------------------------------------------------------------------------- /rpm/argparse-manpage.spec.tpl: -------------------------------------------------------------------------------- 1 | %if 0%{?fedora} || 0%{?rhel} >= 9 2 | %bcond_without pyproject 3 | %bcond_with python2 4 | %bcond_with python3 5 | %else 6 | %bcond_with pyproject 7 | %if 0%{?rhel} > 7 8 | %bcond_with python2 9 | %bcond_without python3 10 | %else 11 | %bcond_without python2 12 | %bcond_with python3 13 | %endif 14 | %endif 15 | 16 | %bcond_without check 17 | 18 | 19 | %global sum() Build manual page from %* ArgumentParser object 20 | %global desc \ 21 | Generate manual page an automatic way from ArgumentParser object, so the \ 22 | manpage 1:1 corresponds to the automatically generated --help output. The \ 23 | manpage generator needs to known the location of the object, user can \ 24 | specify that by (a) the module name or corresponding python filename and \ 25 | (b) the object name or the function name which returns the object. \ 26 | There is a limited support for (deprecated) optparse objects, too. 27 | 28 | 29 | Name: argparse-manpage 30 | Version: @VERSION@ 31 | Release: @RELEASE@%{?dist} 32 | Summary: %{sum Python} 33 | BuildArch: noarch 34 | 35 | License: Apache-2.0 36 | URL: https://github.com/praiskup/%{name} 37 | Source0: https://github.com/praiskup/%{name}/archive/v%{version}/%{name}-%{version}.tar.gz 38 | 39 | %if %{with python2} 40 | BuildRequires: python2-setuptools python2-devel 41 | BuildRequires: python2-packaging 42 | BuildRequires: python2-toml 43 | %if %{with check} 44 | %if 0%{?rhel} && 0%{?rhel} == 7 45 | BuildRequires: pytest 46 | %else 47 | BuildRequires: python2-pytest 48 | %endif 49 | %endif 50 | %endif 51 | 52 | %if %{with python3} 53 | BuildRequires: python3-setuptools python3-devel 54 | BuildRequires: python3-packaging 55 | BuildRequires: python3-tomli 56 | %if %{with check} 57 | BuildRequires: python3-pytest 58 | %endif 59 | %endif 60 | 61 | %if %{with pyproject} 62 | BuildRequires: python3-devel 63 | # EL9 needs this explicitly 64 | BuildRequires: pyproject-rpm-macros 65 | %if %{with check} 66 | BuildRequires: python3-pytest 67 | %endif 68 | %endif 69 | 70 | %if %{with python3} || %{with pyproject} 71 | Requires: python3-%name = %version-%release 72 | %else 73 | Requires: python2-%name = %version-%release 74 | %endif 75 | 76 | %description 77 | %desc 78 | 79 | 80 | %package -n python2-%name 81 | Summary: %{sum Python 2} 82 | Requires: python2-setuptools 83 | Requires: python2-toml 84 | 85 | %description -n python2-%name 86 | %{desc} 87 | 88 | 89 | %package -n python3-%name 90 | Summary: %{sum Python 3} 91 | %if %{without pyproject} 92 | Requires: python3-setuptools 93 | %endif 94 | 95 | %description -n python3-%name 96 | %{desc} 97 | 98 | 99 | %if %{with pyproject} 100 | %pyproject_extras_subpkg -n python3-%{name} setuptools 101 | %endif 102 | 103 | 104 | %prep 105 | %setup -q 106 | 107 | %if %{with pyproject} 108 | %generate_buildrequires 109 | %pyproject_buildrequires 110 | %endif 111 | 112 | 113 | %build 114 | %if %{with python2} 115 | %py2_build 116 | %endif 117 | %if %{with python3} 118 | %py3_build 119 | %endif 120 | %if %{with pyproject} 121 | %pyproject_wheel 122 | %endif 123 | 124 | 125 | %install 126 | %if %{with python2} 127 | %py2_install 128 | %endif 129 | %if %{with python3} 130 | %py3_install 131 | %endif 132 | %if %{with pyproject} 133 | %pyproject_install 134 | %endif 135 | 136 | 137 | 138 | %if %{with check} 139 | %check 140 | %if %{with python2} 141 | PYTHONPATH=%buildroot%python2_sitearch %__python2 -m pytest -vv 142 | %endif 143 | %if %{with python3} 144 | PYTHONPATH=%buildroot%python3_sitearch %__python3 -m pytest -vv 145 | %endif 146 | %if %{with pyproject} 147 | %pytest -vv 148 | %endif 149 | %endif 150 | 151 | 152 | %files 153 | %license LICENSE 154 | %{_bindir}/argparse-manpage 155 | %_mandir/man1/argparse-manpage.1.* 156 | %if %{with python3} || %{with pyproject} 157 | %python3_sitelib/argparse_manpage/cli.py 158 | %else 159 | %python2_sitelib/argparse_manpage/cli.py 160 | %endif 161 | 162 | 163 | %if %{with python2} 164 | %files -n python2-%name 165 | %license LICENSE 166 | %python2_sitelib/build_manpages 167 | %python2_sitelib/argparse_manpage 168 | %python2_sitelib/argparse_manpage-%{version}*.egg-info 169 | %exclude %python2_sitelib/argparse_manpages/cli.py 170 | %endif 171 | 172 | 173 | %if %{with python3} || %{with pyproject} 174 | %files -n python3-%name 175 | %license LICENSE 176 | %python3_sitelib/build_manpages 177 | %python3_sitelib/argparse_manpage 178 | %if %{with pyproject} 179 | %python3_sitelib/argparse_manpage-*dist-info 180 | %else 181 | %python3_sitelib/argparse_manpage-%{version}*.egg-info 182 | %endif 183 | %exclude %python3_sitelib/argparse_manpage/cli.py 184 | %endif 185 | 186 | 187 | %changelog 188 | * @DATE@ Pavel Raiskup - @VERSION@-@RELEASE@ 189 | - built from upstream, changelog ignored 190 | -------------------------------------------------------------------------------- /rpm/build-helper: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | case $1 in 4 | --version) 5 | cd .. ; ${PYTHON-python3} setup.py --version 6 | ;; 7 | --release) 8 | echo "git.$(date +"%Y%m%d_%H%M%S")" 9 | ;; 10 | esac 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_manpages] 2 | manpages = 3 | man/argparse-manpage.1:object=ap:pyfile=argparse_manpage/cli.py 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, find_packages 4 | 5 | # Simplify bootstrapping. We don't need to have an older argparse-manpage 6 | # installed to successfully build argparse-manpage. 7 | # pylint: disable=wrong-import-position 8 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | sys.path.append(ROOT_DIR) 10 | 11 | from build_manpages import __version__ 12 | from build_manpages import ( 13 | build_manpages, 14 | get_build_py_cmd, 15 | get_install_cmd, 16 | ) 17 | 18 | 19 | 20 | def get_readme(): 21 | with open(os.path.join(ROOT_DIR, 'README.md')) as fh: 22 | return ''.join(fh.readlines()) 23 | 24 | if sys.version_info >= (3,): 25 | install_requires = ['tomli;python_version<"3.11"'] 26 | else: 27 | install_requires = ['toml'] 28 | 29 | setup( 30 | name='argparse-manpage', 31 | version=__version__, 32 | url='https://github.com/praiskup/argparse-manpage', 33 | license='Apache-2.0', 34 | author='Gabriele Giammatteo', 35 | author_email='gabriele.giammatteo@eng.it', 36 | maintainer='Pavel Raiskup', 37 | maintainer_email='praiskup@redhat.com', 38 | packages=find_packages(), 39 | entry_points={ 40 | 'console_scripts': [ 41 | 'argparse-manpage=argparse_manpage.cli:main', 42 | ], 43 | }, 44 | description='Build manual page from python\'s ArgumentParser object.', 45 | long_description=get_readme(), 46 | long_description_content_type='text/markdown', 47 | cmdclass={ 48 | 'build_manpages': build_manpages, 49 | 'build_py': get_build_py_cmd(), 50 | 'install': get_install_cmd(), 51 | }, 52 | install_requires=install_requires, 53 | extras_require={ 54 | 'setuptools': ["setuptools"] 55 | }, 56 | ) 57 | -------------------------------------------------------------------------------- /tests/argparse_testlib.py: -------------------------------------------------------------------------------- 1 | """ 2 | unit-tests helpers 3 | """ 4 | 5 | import os 6 | from platform import python_version 7 | from pkg_resources import parse_version 8 | from contextlib import contextmanager 9 | 10 | import pytest 11 | 12 | def skip_on_python_older_than(minimal_version, message, condition=None): 13 | """ 14 | Raise the "skip" exception if python doesn't match the minimal required 15 | version for the calling test case 16 | """ 17 | 18 | if condition is not None and not condition: 19 | return 20 | 21 | if parse_version(python_version()) < parse_version(minimal_version): 22 | generic_msg = "Python {0} required, have {1}".format( 23 | minimal_version, 24 | python_version(), 25 | ) 26 | raise pytest.skip("{0} ({1})".format(message, generic_msg)) 27 | 28 | @contextmanager 29 | def pushd(path): 30 | old_dir = os.getcwd() 31 | os.chdir(path) 32 | try: 33 | yield 34 | finally: 35 | os.chdir(old_dir) 36 | -------------------------------------------------------------------------------- /tests/extra.man: -------------------------------------------------------------------------------- 1 | [EXTRA SECTION] 2 | This is an extra section. 3 | 4 | /John Doe/ 5 | Developer extraordinaire. 6 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os.path 3 | import sys 4 | import argparse 5 | 6 | sys.path = [os.path.join(os.path.dirname(os.path.realpath(__file__)),'..')]+sys.path 7 | 8 | from argparse_testlib import skip_on_python_older_than 9 | 10 | from build_manpages.manpage import Manpage 11 | 12 | 13 | 14 | class Tests(unittest.TestCase): 15 | def test_backslash_escape(self): 16 | parser = argparse.ArgumentParser('duh') 17 | parser.add_argument("--jej", help="c:\\something") 18 | man = Manpage(parser) 19 | assert 'c:\\\\something' in str(man).split('\n') 20 | assert '.SH OPTIONS' in str(man).split('\n') 21 | 22 | def test_argument_groups(self): 23 | parser1 = argparse.ArgumentParser('duh') 24 | parser = parser1.add_argument_group('g1') 25 | parser.add_argument("--jej", help="c:\\something") 26 | parser2 = parser1.add_argument_group('g2') 27 | parser2.add_argument("--jej2", help="c:\\something") 28 | parser2.add_argument("--else", help="c:\\something") 29 | man = Manpage(parser1) 30 | self.assertIn('.SH G1', str(man).split('\n')) 31 | self.assertIn('.SH G2', str(man).split('\n')) 32 | self.assertNotIn('.SH OPTIONS', str(man).split('\n')) 33 | 34 | def test_aliases(self): 35 | skip_on_python_older_than("3", "Python 2 doesn't support aliases=") 36 | parser = argparse.ArgumentParser('aliases_test') 37 | subparsers = parser.add_subparsers(title="actions") 38 | parser_list = subparsers.add_parser( 39 | "list", 40 | # TEST: add an alias that should not be rendered in the output 41 | aliases=["ls"], 42 | help="List all the copr of the " 43 | "provided " 44 | ) 45 | 46 | manpage_lines = str(Manpage(parser)).split("\n") 47 | exp_line = '\\fBaliases_test\\fR \\fI\\,list\\/\\fR' 48 | not_exp_line = '\\fBaliases_test\\fR \\fI\\,ls\\/\\fR' 49 | assert exp_line in manpage_lines 50 | assert not_exp_line not in manpage_lines 51 | assert 1 == sum([1 if "COMMAND" in line else 0 for line in manpage_lines]) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import unittest 3 | import os 4 | import re 5 | import shutil 6 | import subprocess 7 | import sys 8 | import sysconfig 9 | from contextlib import contextmanager 10 | 11 | import pytest 12 | 13 | sys.path = [os.path.join(os.path.dirname(os.path.realpath(__file__)),'..')]+sys.path 14 | 15 | from argparse_testlib import skip_on_python_older_than, pushd 16 | 17 | 18 | def _mandir(prefix, num=1): 19 | data = sysconfig.get_path('data', vars={'base': prefix}) 20 | return os.path.join(data, 'share/man/man' + str(num)) 21 | 22 | 23 | @contextmanager 24 | def change_argv(argv): 25 | old_argv = sys.argv 26 | sys.argv = argv 27 | try: 28 | yield 29 | finally: 30 | sys.argv = old_argv 31 | 32 | 33 | def _rmtree(directory): 34 | try: 35 | shutil.rmtree(directory) 36 | except OSError as err: 37 | if err.errno != errno.ENOENT: 38 | raise 39 | 40 | def run_pip(args): 41 | environ = os.environ.copy() 42 | environ['PYTHONPATH'] = ':'.join(sys.path) 43 | from pip import __version__ 44 | 45 | pip_version = tuple([int(x) for x in __version__.split('.')[:2]]) 46 | if pip_version < (21, 3): 47 | subprocess.call( 48 | [sys.executable, '-m', 'pip'] + args + ["--use-feature=in-tree-build", "."], 49 | env=environ) 50 | else: 51 | subprocess.call([sys.executable, '-m', 'pip'] + args + ["."], env=environ) 52 | 53 | 54 | def run_setup_py(args): 55 | environ = os.environ.copy() 56 | environ['PYTHONPATH'] = ':'.join(sys.path) 57 | with change_argv(['setup.py'] + args): 58 | return subprocess.call([sys.executable, 'setup.py'] + args, 59 | env=environ) 60 | 61 | 62 | def run_one_installer(installer, args): 63 | """ 64 | Run 'pip .' or 'python setup.py ' 65 | """ 66 | 67 | skip_on_python_older_than( 68 | "3.10", 69 | "Too old Python version for testing PIP installation", 70 | installer == "pip", 71 | ) 72 | method = run_pip if installer == "pip" else run_setup_py 73 | method(args) 74 | 75 | 76 | def _file_cmp_1(list1, list2, filter_string=None): 77 | first = True 78 | for left, right in zip(list1, list2): 79 | if first: 80 | left = re.sub('[0-9]{4}\\\\-[0-9]{2}\\\\-[0-9]{2}', '!!DATE!!', left) 81 | left = left.replace("-dev", ".dev0") # issue #50, setuptools < v60 82 | right = re.sub('[0-9]{4}\\\\-[0-9]{2}\\\\-[0-9]{2}', '!!DATE!!', right) 83 | first = False 84 | 85 | if filter_string is not None: 86 | left = filter_string(left) 87 | right = filter_string(right) 88 | 89 | if left != right: 90 | print("Expected: ", right) 91 | print("Got: ", left) 92 | return False 93 | return True 94 | 95 | 96 | def file_cmp(input_file, expected_outputs, filter_string=None): 97 | """ 98 | Compare the generated INPUT_FILE with EXPECTED_OUTPUTS. EXPECTED_OUTPUTS 99 | might be a filename, or list of filenames. Filter string is an optional 100 | string-filter method, if specified - applied on every line on both sides. 101 | """ 102 | with open(input_file, 'r') as f1: 103 | if not isinstance(expected_outputs, list): 104 | expected_outputs = [expected_outputs] 105 | for expected in expected_outputs: 106 | with open(expected, 'r') as f2: 107 | list1 = f1.readlines() 108 | list2 = f2.readlines() 109 | if _file_cmp_1(list1, list2, filter_string): 110 | return 111 | print("None of ", expected_outputs, " matches ", input_file) 112 | assert False 113 | 114 | 115 | class TestAllExamples: 116 | def test_old_example(self): 117 | with pushd('examples/old_format'): 118 | try: 119 | os.remove('example.1') 120 | except OSError: 121 | pass 122 | run_setup_py(['build_manpage']) 123 | file_cmp('example.1', 'expected-output.1') 124 | 125 | def test_old_example_file_name(self): 126 | with pushd('examples/old_format_file_name'): 127 | try: 128 | os.remove('example.1') 129 | except OSError: 130 | pass 131 | run_setup_py(['build_manpage']) 132 | file_cmp('example.1', 'expected-output.1') 133 | 134 | @pytest.mark.parametrize("installer", ["pip", "setuppy"]) 135 | def test_copr(self, installer): 136 | with pushd('examples/copr'): 137 | name = 'copr-cli.1' 138 | prefix = '/usr' 139 | idir = os.path.join(os.getcwd(), installer + "_install_dir") 140 | mandir = os.path.join(idir, _mandir("usr/")) 141 | _rmtree(idir) 142 | run_one_installer(installer, ['install', '--root', idir, '--prefix', prefix]) 143 | 144 | def version_version_filter(string): 145 | return string.replace('[VERSION [VERSION ...]]', 146 | '[VERSION ...]') 147 | 148 | file_cmp(os.path.join(mandir, os.path.basename(name)), 149 | ['expected-output.1', "expected-output.1.python3.13"], 150 | filter_string=version_version_filter) 151 | file_cmp(name, 152 | ['expected-output.1', "expected-output.1.python3.13"], 153 | filter_string=version_version_filter) 154 | 155 | 156 | @pytest.mark.parametrize("installer", ["pip", "setuppy"]) 157 | def test_distgen(self, installer): 158 | with pushd('examples/raw-description'): 159 | name = 'man/dg.1' 160 | prefix = "/usr" 161 | idir = os.path.join(os.getcwd(), installer + "_install_dir") 162 | _rmtree(idir) 163 | mandir = os.path.join(idir, _mandir("usr/")) 164 | run_one_installer(installer, ['install', '--root', idir, '--prefix', prefix]) 165 | file_cmp(os.path.join(mandir, os.path.basename(name)), 'expected-output.1') 166 | file_cmp(name, 'expected-output.1') 167 | 168 | 169 | @pytest.mark.parametrize("installer", ["pip", "setuppy"]) 170 | def test_resalloc(self, installer): 171 | with pushd('examples/resalloc'): 172 | prefix = "/usr" 173 | idir = os.path.join(os.getcwd(), installer + "_install_dir") 174 | _rmtree(idir) 175 | mandir = os.path.join(idir, _mandir("usr/")) 176 | run_one_installer(installer, ['install', '--root', idir, '--prefix', prefix]) 177 | for name in ['man/resalloc.1', 'man/resalloc-maint.1']: 178 | file_cmp(os.path.join(mandir, os.path.basename(name)), 179 | 'expected/' + name) 180 | file_cmp(name, 'expected/' + name) 181 | 182 | @pytest.mark.parametrize("installer", ["pip", "setuppy"]) 183 | def test_argument_groups_example(self, installer): 184 | with pushd('examples/argument_groups'): 185 | prefix = "/usr" 186 | idir = os.path.join(os.getcwd(), installer + "_install_dir") 187 | _rmtree(idir) 188 | mandir = os.path.join(idir, _mandir("usr/")) 189 | run_one_installer(installer, ['install', '--root', idir, '--prefix', prefix]) 190 | compiled = os.path.join('man', 'test.1') 191 | base = os.path.basename(compiled) 192 | expected = os.path.join('expected', base) 193 | installed = os.path.join(mandir, base) 194 | file_cmp(installed, expected) 195 | file_cmp(compiled, expected) 196 | 197 | @pytest.mark.parametrize("installer", ["pip", "setuppy"]) 198 | def test_osc(self, installer): 199 | with pushd('examples/osc'): 200 | name = 'osc.1' 201 | prefix = '/usr' 202 | idir = os.path.join(os.getcwd(), installer + "_install_dir") 203 | mandir = os.path.join(idir, _mandir("usr/")) 204 | _rmtree(idir) 205 | run_one_installer(installer, ['install', '--root', idir, '--prefix', prefix]) 206 | 207 | file_cmp(os.path.join(mandir, os.path.basename(name)), 'expected-output.1') 208 | file_cmp(name, 'expected-output.1') 209 | 210 | @pytest.mark.parametrize("installer", ["pip", "setuppy"]) 211 | def test_pre_written_man_page(self, installer): 212 | with pushd('examples/pre-written-man-page'): 213 | name = 'psutils.1' 214 | prefix = '/usr' 215 | idir = os.path.join(os.getcwd(), installer + "_install_dir") 216 | mandir = os.path.join(idir, _mandir("usr/")) 217 | _rmtree(idir) 218 | run_one_installer(installer, ['install', '--root', idir, '--prefix', prefix]) 219 | 220 | file_cmp(os.path.join(mandir, name), name) 221 | 222 | 223 | if __name__ == "__main__": 224 | unittest.main() 225 | -------------------------------------------------------------------------------- /tests/test_script.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the 'argparse-manpage' script. 3 | """ 4 | 5 | import datetime 6 | import os 7 | import shutil 8 | import sys 9 | import subprocess 10 | import tempfile 11 | import time 12 | import warnings 13 | 14 | from packaging import version 15 | 16 | import setuptools 17 | 18 | from test_examples import run_setup_py 19 | from argparse_testlib import pushd 20 | 21 | SIMPLE_FILE_CONTENTS = """\ 22 | import argparse 23 | 24 | def get_parser(): 25 | parser = argparse.ArgumentParser({ap_arguments}) 26 | parser.add_argument("test") 27 | return parser 28 | 29 | def main(): 30 | print("here") 31 | get_parser().parse_args() 32 | 33 | if __name__ == "__main__": 34 | main() 35 | """ 36 | 37 | SIMPLE_OUTPUT = """\ 38 | .TH {NAME} "1" "{DATE}" "{name}{version}" "Generated Python Manual" 39 | .SH NAME 40 | {name} 41 | .SH SYNOPSIS 42 | .B {name} 43 | [-h] test 44 | 45 | .TP 46 | \\fBtest\\fR 47 | """ 48 | 49 | FULL_OUTPUT = """\ 50 | .TH {NAME} "3" "{DATE}" "Proj\\-On\\-Cmdline 1.alpha" "Some\\-long Manual Name" 51 | .SH NAME 52 | {name} \\- some description 53 | .SH SYNOPSIS 54 | .B {name} 55 | [-h] test 56 | 57 | .TP 58 | \\fBtest\\fR 59 | .SH EXTRA SECTION 60 | This is an extra section. 61 | 62 | .SH AUTHOR 63 | .nf 64 | John Doe 65 | Developer extraordinaire. 66 | .fi 67 | .nf 68 | Mr. Foo and friends 69 | .fi 70 | """ 71 | 72 | SETUP_PY_FILE_CONTENTS = """\ 73 | from build_manpages import build_manpages, get_install_cmd, get_build_py_cmd 74 | from setuptools import setup 75 | 76 | setup( 77 | cmdclass={ 78 | 'build_manpages': build_manpages, 79 | 'build_py': get_build_py_cmd(), 80 | 'install': get_install_cmd(), 81 | } 82 | ) 83 | """ 84 | 85 | PYPROJECT_TOML_FILE_CONTENTS = """\ 86 | [tool.build_manpages] 87 | manpages = [ 88 | "some-file.1:project_name=some-file:module=somefile:function=get_parser", 89 | ] 90 | """ 91 | 92 | DATE = datetime.datetime.utcfromtimestamp( 93 | int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) 94 | ).strftime("%Y\\-%m\\-%d") 95 | 96 | class TestsArgparseManpageScript: 97 | def setup_method(self, _): 98 | self.workdir = tempfile.mkdtemp(prefix="argparse-manpage-tests-") 99 | os.environ["PYTHON"] = sys.executable 100 | os.environ["PYTHONPATH"] = os.getcwd() 101 | 102 | def teardown_method(self, _): 103 | shutil.rmtree(self.workdir) 104 | 105 | @staticmethod 106 | def _get_am_executable(): 107 | testdir = os.path.dirname(__file__) 108 | executable = os.path.join(testdir, '..', 'argparse-manpage') 109 | return executable 110 | 111 | def test_filepath_function(self): 112 | """ 113 | Test --pyfile && --function. 114 | """ 115 | name = "some-file" 116 | expname = r"some\-file" 117 | tested_executable = os.path.join(self.workdir, name) 118 | with open(tested_executable, "w+") as script_fd: 119 | script_fd.write(SIMPLE_FILE_CONTENTS.format(ap_arguments="")) 120 | 121 | cmd = [ 122 | self._get_am_executable(), 123 | "--pyfile", tested_executable, 124 | "--function", "get_parser", 125 | ] 126 | output = subprocess.check_output(cmd).decode("utf-8") 127 | assert output == SIMPLE_OUTPUT.format(name=expname, version="", 128 | NAME=expname.upper(), DATE=DATE) 129 | 130 | def test_filepath_prog(self): 131 | """ 132 | Test --pyfile --function with prog explicitly specified in 133 | ArgumentParser name. 134 | """ 135 | name = "some-file" 136 | expname = "progname" 137 | tested_executable = os.path.join(self.workdir, name) 138 | with open(tested_executable, "w+") as script_fd: 139 | script_fd.write(SIMPLE_FILE_CONTENTS.format(ap_arguments='"progname"')) 140 | 141 | cmd = [ 142 | self._get_am_executable(), 143 | "--pyfile", tested_executable, 144 | "--function", "get_parser", 145 | ] 146 | output = subprocess.check_output(cmd).decode("utf-8") 147 | name="progname" 148 | assert output == SIMPLE_OUTPUT.format(name=expname, version="", 149 | NAME=expname.upper(), DATE=DATE) 150 | 151 | def test_full_args(self): 152 | """ 153 | Submit as many commandline arguments as possible. 154 | """ 155 | name = "full_name" 156 | tested_executable = os.path.join(self.workdir, name) 157 | with open(tested_executable, "w+") as script_fd: 158 | script_fd.write(SIMPLE_FILE_CONTENTS.format(ap_arguments='"progname"')) 159 | 160 | cmd = [ 161 | self._get_am_executable(), 162 | "--pyfile", tested_executable, 163 | "--function", "get_parser", 164 | "--manual-section", "3", 165 | "--manual-title", "Some-long Manual Name", 166 | "--version", "1.alpha", 167 | "--author", "John Doe ", 168 | "--author", "Mr. Foo and friends", 169 | "--project-name", "Proj-On-Cmdline", 170 | "--description", "some description", 171 | "--long-description", "Some long description.", # unused 172 | "--include", "tests/extra.man", 173 | ] 174 | output = subprocess.check_output(cmd).decode("utf-8") 175 | name="progname" 176 | assert output == FULL_OUTPUT.format(name=name, NAME=name.upper(), 177 | DATE=DATE) 178 | 179 | def test_pyproject_toml(self): 180 | """ 181 | Test that we can read information from pyproject.toml. 182 | """ 183 | current_dir = os.getcwd() 184 | with pushd(self.workdir): 185 | with open("pyproject.toml", "w+") as script_fd: 186 | script_fd.write(PYPROJECT_TOML_FILE_CONTENTS.format(gitdir=current_dir)) 187 | with open("setup.py", "w+") as script_fd: 188 | script_fd.write(SETUP_PY_FILE_CONTENTS) 189 | 190 | modname = "somefile" 191 | name = r"some\-file" 192 | os.mkdir(modname) 193 | with open(os.path.join(modname, "__init__.py"), "w+") as script_fd: 194 | script_fd.write(SIMPLE_FILE_CONTENTS.format(ap_arguments="")) 195 | 196 | assert 0 == run_setup_py(["build"]) 197 | if version.parse(setuptools.__version__) >= version.parse("62.2.0"): 198 | with open("some-file.1") as script_fd: 199 | output = script_fd.read() 200 | assert output == SIMPLE_OUTPUT.format(name=name, version=" 0.0.0", 201 | NAME=name.upper(), DATE=DATE) 202 | else: 203 | warnings.warn("setuptools >= 62.2.0 required to generate man pages with pyprojects.toml") 204 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # sync with .github/workflows/tox.yml! 3 | envlist = py{36,37,38,39,310,311} 4 | skipsdist = True 5 | 6 | [testenv] 7 | deps = 8 | pytest 9 | setuptools 10 | py{36,37,38,39,310}: tomli 11 | commands = python -m pytest -v {posargs} 12 | --------------------------------------------------------------------------------