├── .github ├── code-of-conduct.md ├── dependabot.yaml ├── funding.yml └── workflows │ ├── autofix.yaml │ ├── autolock.yaml │ ├── changelog.yaml │ ├── docs.yaml │ ├── label-sponsors.yaml │ ├── labeller-content-based.yaml │ ├── labeller-file-based.yaml │ ├── labels.yaml │ ├── lint.yaml │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .mailmap ├── changelog.md ├── citation.cff ├── click_extra ├── __init__.py ├── colorize.py ├── commands.py ├── config.py ├── decorators.py ├── docs_update.py ├── envvar.py ├── logging.py ├── parameters.py ├── py.typed ├── pygments.py ├── pytest.py ├── sphinx.py ├── tabulate.py ├── telemetry.py ├── testing.py ├── timer.py └── version.py ├── docs ├── assets │ ├── click-extra-screen.png │ ├── click-help-screen.png │ ├── dependencies.mmd │ ├── logo-banner-transparent-background.png │ ├── logo-banner-white-background.png │ ├── logo-banner.svg │ ├── logo-square-transparent-background.png │ ├── logo-square-white-background.png │ └── logo-square.svg ├── changelog.md ├── click_extra.rst ├── code-of-conduct.md ├── colorize.md ├── commands.md ├── conf.py ├── config.md ├── index.md ├── install.md ├── issues.md ├── license.md ├── logging.md ├── parameters.md ├── pygments.md ├── pytest.md ├── sphinx.md ├── tabulate.md ├── testing.md ├── tests.rst ├── timer.md ├── todolist.md ├── tutorial.md └── version.md ├── license ├── pyproject.toml ├── readme.md ├── tests ├── __init__.py ├── conftest.py ├── test_colorize.py ├── test_commands.py ├── test_config.py ├── test_envvar.py ├── test_logging.py ├── test_parameters.py ├── test_pygments.py ├── test_tabulate.py ├── test_telemetry.py ├── test_testing.py ├── test_timer.py └── test_version.py └── uv.lock /.github/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor covenant code of conduct 2 | 3 | ## Our pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, 8 | body size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual 10 | identity and orientation. 11 | 12 | ## Our standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others’ private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an 52 | appointed representative at an online or offline event. Representation of a 53 | project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All complaints 59 | will be reviewed and investigated and will result in a response that is deemed 60 | necessary and appropriate to the circumstances. The project team is obligated 61 | to maintain confidentiality with regard to the reporter of an incident. Further 62 | details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project’s leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the 71 | [Contributor Covenant](https://contributor-covenant.org), version 1.4, 72 | available at 73 | [https://contributor-covenant.org/version/1/4](https://contributor-covenant.org/version/1/4/) 74 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates 2 | --- 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "pip" 7 | directory: "/" 8 | versioning-strategy: increase-if-necessary 9 | schedule: 10 | interval: "daily" 11 | labels: 12 | - "📦 dependencies" 13 | assignees: 14 | - "kdeldycke" 15 | 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | labels: 21 | - "📦 dependencies" 22 | assignees: 23 | - "kdeldycke" 24 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: kdeldycke 3 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Autofix 3 | "on": 4 | push: 5 | # Only targets main branch to avoid amplification effects of auto-fixing 6 | # the exact same stuff in multiple non-rebased branches. 7 | branches: 8 | - main 9 | 10 | jobs: 11 | 12 | autofix: 13 | uses: kdeldycke/workflows/.github/workflows/autofix.yaml@v4.17.1 14 | -------------------------------------------------------------------------------- /.github/workflows/autolock.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Autolock 3 | "on": 4 | schedule: 5 | # Run weekly, every Monday at 8:43. 6 | - cron: "43 8 * * 1" 7 | 8 | jobs: 9 | 10 | autolock: 11 | uses: kdeldycke/workflows/.github/workflows/autolock.yaml@v4.17.1 12 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Changelog & versions 3 | "on": 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - changelog.md 9 | - "**/pyproject.toml" 10 | - "*requirements.txt" 11 | - "requirements/*.txt" 12 | # Trigger on any workflow change to make sure version gets hard-coded everywhere. 13 | - .github/workflows/*.yaml 14 | 15 | jobs: 16 | 17 | changelog: 18 | uses: kdeldycke/workflows/.github/workflows/changelog.yaml@v4.17.1 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs 3 | "on": 4 | push: 5 | # Only targets main branch to avoid amplification effects of auto-fixing 6 | # the exact same stuff in multiple non-rebased branches. 7 | branches: 8 | - main 9 | 10 | jobs: 11 | 12 | docs: 13 | uses: kdeldycke/workflows/.github/workflows/docs.yaml@v4.17.1 14 | 15 | update-docs: 16 | needs: docs 17 | runs-on: ubuntu-24.04 18 | steps: 19 | - uses: actions/checkout@v4.2.2 20 | - name: Install uv 21 | run: | 22 | python -m pip install -r https://raw.githubusercontent.com/kdeldycke/workflows/v4.17.1/requirements/uv.txt 23 | - name: Generate dynamic doc 24 | run: | 25 | uv --no-progress run --frozen --extra pygments -m click_extra.docs_update 26 | - uses: peter-evans/create-pull-request@v7.0.8 27 | with: 28 | assignees: ${{ github.actor }} 29 | commit-message: "[autofix] Regenerate documentation" 30 | title: "[autofix] Regenerate documentation" 31 | body: > 32 |
Workflow metadata 33 | 34 | 35 | > [Auto-generated on run `#${{ github.run_id }}`](${{ github.event.repository.html_url }}/actions/runs/${{ 36 | github.run_id }}) by `${{ github.job }}` job from [`docs.yaml`](${{ github.event.repository.html_url 37 | }}/blob/${{ github.sha }}/.github/workflows/docs.yaml) workflow. 38 | 39 | 40 |
41 | labels: "📚 documentation" 42 | branch: update-docs 43 | -------------------------------------------------------------------------------- /.github/workflows/label-sponsors.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Label sponsors 3 | "on": 4 | pull_request: 5 | types: 6 | - opened 7 | issues: 8 | types: 9 | - opened 10 | 11 | jobs: 12 | 13 | label-sponsors: 14 | uses: kdeldycke/workflows/.github/workflows/label-sponsors.yaml@v4.17.1 -------------------------------------------------------------------------------- /.github/workflows/labeller-content-based.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Labeller (content-based) 3 | "on": 4 | issues: 5 | types: [opened] 6 | pull_request: 7 | types: [opened] 8 | 9 | jobs: 10 | 11 | labeller: 12 | uses: kdeldycke/workflows/.github/workflows/labeller-file-based.yaml@v4.17.1 -------------------------------------------------------------------------------- /.github/workflows/labeller-file-based.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Labeller (file-based) 3 | "on": 4 | pull_request: 5 | 6 | jobs: 7 | 8 | labeller: 9 | uses: kdeldycke/workflows/.github/workflows/labeller-file-based.yaml@v4.17.1 10 | -------------------------------------------------------------------------------- /.github/workflows/labels.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Labels 3 | "on": 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | 10 | labels: 11 | uses: kdeldycke/workflows/.github/workflows/labels.yaml@v4.17.1 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint 3 | "on": 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | lint: 12 | uses: kdeldycke/workflows/.github/workflows/lint.yaml@v4.17.1 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build & release 3 | "on": 4 | # Target are chosen so that all commits get a chance to have their build tested. 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | 12 | release: 13 | uses: kdeldycke/workflows/.github/workflows/release.yaml@v4.17.1 14 | secrets: 15 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | "on": 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | # Run tests every Monday at 9:17 to catch regressions. 10 | - cron: "17 9 * * 1" 11 | 12 | concurrency: 13 | # Group workflow jobs so new commits cancels in-progress execution triggered by previous commits. Source: 14 | # https://mail.python.org/archives/list/pypa-committers@python.org/thread/PCBCQMJF64JGRBOX7E2EE4YLKHT4DI55/ 15 | # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs 16 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | 21 | tests: 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Available OS: https://github.com/actions/runner-images#available-images 26 | # Only targets 2 variants per platforms to keep the matrix small. 27 | os: 28 | - ubuntu-24.04-arm # arm64 29 | - ubuntu-24.04 # x86 30 | - macos-15 # arm64 31 | - macos-13 # x86 32 | - windows-11-arm # arm64 33 | - windows-2025 # x86 34 | # Available Python: https://github.com/actions/python-versions/blob/main/versions-manifest.json 35 | python-version: 36 | - "3.11" 37 | - "3.12" 38 | - "3.13" 39 | - "3.14" 40 | click-version: 41 | - released 42 | - stable 43 | - main 44 | cloup-version: 45 | - released 46 | - master 47 | # Removes from the matrix some combinations for development versions of Click and Cloup. This reduce the size 48 | # of the matrix for tests on non-released versions. While keeping the full exhaustive tests on the released 49 | # versions of Click and Cloup. 50 | # XXX Ideally, the exclude section below could be reduced to: 51 | # exclude: 52 | # - click-version: stable 53 | # os: 54 | # - ubuntu-24.04 55 | # - macos-13 56 | # - windows-2025 57 | # python-version: 58 | # - "3.11" 59 | # - "3.12" 60 | # - "3.14" 61 | # - click-version: main 62 | # os: 63 | # - ubuntu-24.04 64 | # - macos-13 65 | # - windows-2025 66 | # python-version: 67 | # - "3.11" 68 | # - "3.12" 69 | # - "3.14" 70 | # - cloup-version: master 71 | # os: 72 | # - ubuntu-24.04 73 | # - macos-13 74 | # - windows-2025 75 | # python-version: 76 | # - "3.11" 77 | # - "3.12" 78 | # - "3.14" 79 | exclude: 80 | # Only targets the latest OS for each platform. 81 | - os: ubuntu-24.04 82 | click-version: stable 83 | - os: ubuntu-24.04 84 | click-version: main 85 | - os: ubuntu-24.04 86 | cloup-version: master 87 | - os: macos-13 88 | click-version: stable 89 | - os: macos-13 90 | click-version: main 91 | - os: macos-13 92 | cloup-version: master 93 | - os: windows-2025 94 | click-version: stable 95 | - os: windows-2025 96 | click-version: main 97 | - os: windows-2025 98 | cloup-version: master 99 | # Exclude old Python version. Only test on latest stable release. 100 | - python-version: "3.11" 101 | click-version: stable 102 | - python-version: "3.11" 103 | click-version: main 104 | - python-version: "3.11" 105 | cloup-version: master 106 | - python-version: "3.12" 107 | click-version: stable 108 | - python-version: "3.12" 109 | click-version: main 110 | - python-version: "3.12" 111 | cloup-version: master 112 | # Exclude Python's dev version. 113 | - python-version: "3.14" 114 | click-version: stable 115 | - python-version: "3.14" 116 | click-version: main 117 | - python-version: "3.14" 118 | cloup-version: master 119 | include: 120 | # Default all jobs as stable, unless marked otherwise below. 121 | - state: stable 122 | name: | 123 | ${{ matrix.state == 'stable' && '✅' || '⁉️' }} 124 | ${{ matrix.os }}, 125 | py${{ matrix.python-version }}, 126 | Click ${{ matrix.click-version }}, 127 | Cloup ${{ matrix.cloup-version }} 128 | runs-on: ${{ matrix.os }} 129 | # We keep going when a job flagged as not stable fails. 130 | continue-on-error: ${{ matrix.state == 'unstable' }} 131 | steps: 132 | - uses: actions/checkout@v4.2.2 133 | - name: Install uv 134 | run: | 135 | python -m pip install -r https://raw.githubusercontent.com/kdeldycke/workflows/v4.17.1/requirements/uv.txt 136 | - name: Install project 137 | run: | 138 | uv --no-progress venv --python ${{ matrix.python-version }} 139 | uv --no-progress sync --frozen --extra test --extra pygments --extra sphinx --extra pytest 140 | - name: Unittests 141 | run: > 142 | uv --no-progress run --frozen 143 | ${{ matrix.click-version != 'released' 144 | && format('--with "git+https://github.com/pallets/click.git@{0}"', matrix.click-version) || '' }} 145 | ${{ matrix.cloup-version != 'released' 146 | && format('--with "git+https://github.com/janluke/cloup.git@{0}"', matrix.cloup-version) || ''}} 147 | -- 148 | pytest 149 | - name: Codecov - coverage 150 | uses: codecov/codecov-action@v5.4.3 151 | with: 152 | token: ${{ secrets.CODECOV_TOKEN }} 153 | - name: Codecov - test results 154 | if: ${{ !cancelled() }} 155 | uses: codecov/test-results-action@v1.1.1 156 | with: 157 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/certificates,emacs,git,gpg,linux,macos,node,nohup,python,ssh,vim,virtualenv,visualstudiocode,windows 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=certificates,emacs,git,gpg,linux,macos,node,nohup,python,ssh,vim,virtualenv,visualstudiocode,windows 3 | 4 | ### certificates ### 5 | *.pem 6 | *.key 7 | *.crt 8 | *.cer 9 | *.der 10 | *.priv 11 | 12 | ### Emacs ### 13 | # -*- mode: gitignore; -*- 14 | *~ 15 | \#*\# 16 | /.emacs.desktop 17 | /.emacs.desktop.lock 18 | *.elc 19 | auto-save-list 20 | tramp 21 | .\#* 22 | 23 | # Org-mode 24 | .org-id-locations 25 | *_archive 26 | 27 | # flymake-mode 28 | *_flymake.* 29 | 30 | # eshell files 31 | /eshell/history 32 | /eshell/lastdir 33 | 34 | # elpa packages 35 | /elpa/ 36 | 37 | # reftex files 38 | *.rel 39 | 40 | # AUCTeX auto folder 41 | /auto/ 42 | 43 | # cask packages 44 | .cask/ 45 | dist/ 46 | 47 | # Flycheck 48 | flycheck_*.el 49 | 50 | # server auth directory 51 | /server/ 52 | 53 | # projectiles files 54 | .projectile 55 | 56 | # directory configuration 57 | .dir-locals.el 58 | 59 | # network security 60 | /network-security.data 61 | 62 | 63 | ### Git ### 64 | # Created by git for backups. To disable backups in Git: 65 | # $ git config --global mergetool.keepBackup false 66 | *.orig 67 | 68 | # Created by git when using merge tools for conflicts 69 | *.BACKUP.* 70 | *.BASE.* 71 | *.LOCAL.* 72 | *.REMOTE.* 73 | *_BACKUP_*.txt 74 | *_BASE_*.txt 75 | *_LOCAL_*.txt 76 | *_REMOTE_*.txt 77 | 78 | ### GPG ### 79 | secring.* 80 | 81 | 82 | ### Linux ### 83 | 84 | # temporary files which can be created if a process still has a handle open of a deleted file 85 | .fuse_hidden* 86 | 87 | # KDE directory preferences 88 | .directory 89 | 90 | # Linux trash folder which might appear on any partition or disk 91 | .Trash-* 92 | 93 | # .nfs files are created when an open file is removed but is still being accessed 94 | .nfs* 95 | 96 | ### macOS ### 97 | # General 98 | .DS_Store 99 | .AppleDouble 100 | .LSOverride 101 | 102 | # Icon must end with two \r 103 | Icon 104 | 105 | # Thumbnails 106 | ._* 107 | 108 | # Files that might appear in the root of a volume 109 | .DocumentRevisions-V100 110 | .fseventsd 111 | .Spotlight-V100 112 | .TemporaryItems 113 | .Trashes 114 | .VolumeIcon.icns 115 | .com.apple.timemachine.donotpresent 116 | 117 | # Directories potentially created on remote AFP share 118 | .AppleDB 119 | .AppleDesktop 120 | Network Trash Folder 121 | Temporary Items 122 | .apdisk 123 | 124 | ### macOS Patch ### 125 | # iCloud generated files 126 | *.icloud 127 | 128 | ### Node ### 129 | # Logs 130 | logs 131 | *.log 132 | npm-debug.log* 133 | yarn-debug.log* 134 | yarn-error.log* 135 | lerna-debug.log* 136 | .pnpm-debug.log* 137 | 138 | # Diagnostic reports (https://nodejs.org/api/report.html) 139 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 140 | 141 | # Runtime data 142 | pids 143 | *.pid 144 | *.seed 145 | *.pid.lock 146 | 147 | # Directory for instrumented libs generated by jscoverage/JSCover 148 | lib-cov 149 | 150 | # Coverage directory used by tools like istanbul 151 | coverage 152 | *.lcov 153 | 154 | # nyc test coverage 155 | .nyc_output 156 | 157 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 158 | .grunt 159 | 160 | # Bower dependency directory (https://bower.io/) 161 | bower_components 162 | 163 | # node-waf configuration 164 | .lock-wscript 165 | 166 | # Compiled binary addons (https://nodejs.org/api/addons.html) 167 | build/Release 168 | 169 | # Dependency directories 170 | node_modules/ 171 | jspm_packages/ 172 | 173 | # Snowpack dependency directory (https://snowpack.dev/) 174 | web_modules/ 175 | 176 | # TypeScript cache 177 | *.tsbuildinfo 178 | 179 | # Optional npm cache directory 180 | .npm 181 | 182 | # Optional eslint cache 183 | .eslintcache 184 | 185 | # Optional stylelint cache 186 | .stylelintcache 187 | 188 | # Microbundle cache 189 | .rpt2_cache/ 190 | .rts2_cache_cjs/ 191 | .rts2_cache_es/ 192 | .rts2_cache_umd/ 193 | 194 | # Optional REPL history 195 | .node_repl_history 196 | 197 | # Output of 'npm pack' 198 | *.tgz 199 | 200 | # Yarn Integrity file 201 | .yarn-integrity 202 | 203 | # dotenv environment variable files 204 | .env 205 | .env.development.local 206 | .env.test.local 207 | .env.production.local 208 | .env.local 209 | 210 | # parcel-bundler cache (https://parceljs.org/) 211 | .cache 212 | .parcel-cache 213 | 214 | # Next.js build output 215 | .next 216 | out 217 | 218 | # Nuxt.js build / generate output 219 | .nuxt 220 | dist 221 | 222 | # Gatsby files 223 | .cache/ 224 | # Comment in the public line in if your project uses Gatsby and not Next.js 225 | # https://nextjs.org/blog/next-9-1#public-directory-support 226 | # public 227 | 228 | # vuepress build output 229 | .vuepress/dist 230 | 231 | # vuepress v2.x temp and cache directory 232 | .temp 233 | 234 | # Docusaurus cache and generated files 235 | .docusaurus 236 | 237 | # Serverless directories 238 | .serverless/ 239 | 240 | # FuseBox cache 241 | .fusebox/ 242 | 243 | # DynamoDB Local files 244 | .dynamodb/ 245 | 246 | # TernJS port file 247 | .tern-port 248 | 249 | # Stores VSCode versions used for testing VSCode extensions 250 | .vscode-test 251 | 252 | # yarn v2 253 | .yarn/cache 254 | .yarn/unplugged 255 | .yarn/build-state.yml 256 | .yarn/install-state.gz 257 | .pnp.* 258 | 259 | ### Node Patch ### 260 | # Serverless Webpack directories 261 | .webpack/ 262 | 263 | # Optional stylelint cache 264 | 265 | # SvelteKit build / generate output 266 | .svelte-kit 267 | 268 | ### Nohup ### 269 | nohup.out 270 | 271 | ### Python ### 272 | # Byte-compiled / optimized / DLL files 273 | __pycache__/ 274 | *.py[cod] 275 | *$py.class 276 | 277 | # C extensions 278 | *.so 279 | 280 | # Distribution / packaging 281 | .Python 282 | build/ 283 | develop-eggs/ 284 | downloads/ 285 | eggs/ 286 | .eggs/ 287 | lib/ 288 | lib64/ 289 | parts/ 290 | sdist/ 291 | var/ 292 | wheels/ 293 | share/python-wheels/ 294 | *.egg-info/ 295 | .installed.cfg 296 | *.egg 297 | MANIFEST 298 | 299 | # PyInstaller 300 | # Usually these files are written by a python script from a template 301 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 302 | *.manifest 303 | *.spec 304 | 305 | # Installer logs 306 | pip-log.txt 307 | pip-delete-this-directory.txt 308 | 309 | # Unit test / coverage reports 310 | htmlcov/ 311 | .tox/ 312 | .nox/ 313 | .coverage 314 | .coverage.* 315 | nosetests.xml 316 | coverage.xml 317 | *.cover 318 | *.py,cover 319 | .hypothesis/ 320 | .pytest_cache/ 321 | cover/ 322 | 323 | # Translations 324 | *.mo 325 | *.pot 326 | 327 | # Django stuff: 328 | local_settings.py 329 | db.sqlite3 330 | db.sqlite3-journal 331 | 332 | # Flask stuff: 333 | instance/ 334 | .webassets-cache 335 | 336 | # Scrapy stuff: 337 | .scrapy 338 | 339 | # Sphinx documentation 340 | docs/_build/ 341 | 342 | # PyBuilder 343 | .pybuilder/ 344 | target/ 345 | 346 | # Jupyter Notebook 347 | .ipynb_checkpoints 348 | 349 | # IPython 350 | profile_default/ 351 | ipython_config.py 352 | 353 | # pyenv 354 | # For a library or package, you might want to ignore these files since the code is 355 | # intended to run in multiple environments; otherwise, check them in: 356 | # .python-version 357 | 358 | # pipenv 359 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 360 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 361 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 362 | # install all needed dependencies. 363 | #Pipfile.lock 364 | 365 | # poetry 366 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 367 | # This is especially recommended for binary packages to ensure reproducibility, and is more 368 | # commonly ignored for libraries. 369 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 370 | #poetry.lock 371 | 372 | # pdm 373 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 374 | #pdm.lock 375 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 376 | # in version control. 377 | # https://pdm.fming.dev/#use-with-ide 378 | .pdm.toml 379 | 380 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 381 | __pypackages__/ 382 | 383 | # Celery stuff 384 | celerybeat-schedule 385 | celerybeat.pid 386 | 387 | # SageMath parsed files 388 | *.sage.py 389 | 390 | # Environments 391 | .venv 392 | env/ 393 | venv/ 394 | ENV/ 395 | env.bak/ 396 | venv.bak/ 397 | 398 | # Spyder project settings 399 | .spyderproject 400 | .spyproject 401 | 402 | # Rope project settings 403 | .ropeproject 404 | 405 | # mkdocs documentation 406 | /site 407 | 408 | # mypy 409 | .mypy_cache/ 410 | .dmypy.json 411 | dmypy.json 412 | 413 | # Pyre type checker 414 | .pyre/ 415 | 416 | # pytype static type analyzer 417 | .pytype/ 418 | 419 | # Cython debug symbols 420 | cython_debug/ 421 | 422 | # PyCharm 423 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 424 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 425 | # and can be added to the global gitignore or merged into this file. For a more nuclear 426 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 427 | #.idea/ 428 | 429 | ### Python Patch ### 430 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 431 | poetry.toml 432 | 433 | # ruff 434 | .ruff_cache/ 435 | 436 | # LSP config files 437 | pyrightconfig.json 438 | 439 | ### SSH ### 440 | **/.ssh/id_* 441 | **/.ssh/*_id_* 442 | **/.ssh/known_hosts 443 | 444 | ### Vim ### 445 | # Swap 446 | [._]*.s[a-v][a-z] 447 | !*.svg # comment out if you don't need vector files 448 | [._]*.sw[a-p] 449 | [._]s[a-rt-v][a-z] 450 | [._]ss[a-gi-z] 451 | [._]sw[a-p] 452 | 453 | # Session 454 | Session.vim 455 | Sessionx.vim 456 | 457 | # Temporary 458 | .netrwhist 459 | # Auto-generated tag files 460 | tags 461 | # Persistent undo 462 | [._]*.un~ 463 | 464 | ### VirtualEnv ### 465 | # Virtualenv 466 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 467 | [Bb]in 468 | [Ii]nclude 469 | [Ll]ib 470 | [Ll]ib64 471 | [Ll]ocal 472 | [Ss]cripts 473 | pyvenv.cfg 474 | pip-selfcheck.json 475 | 476 | ### VisualStudioCode ### 477 | .vscode/* 478 | !.vscode/settings.json 479 | !.vscode/tasks.json 480 | !.vscode/launch.json 481 | !.vscode/extensions.json 482 | !.vscode/*.code-snippets 483 | 484 | # Local History for Visual Studio Code 485 | .history/ 486 | 487 | # Built Visual Studio Code Extensions 488 | *.vsix 489 | 490 | ### VisualStudioCode Patch ### 491 | # Ignore all local history of files 492 | .history 493 | .ionide 494 | 495 | ### Windows ### 496 | # Windows thumbnail cache files 497 | Thumbs.db 498 | Thumbs.db:encryptable 499 | ehthumbs.db 500 | ehthumbs_vista.db 501 | 502 | # Dump file 503 | *.stackdump 504 | 505 | # Folder config file 506 | [Dd]esktop.ini 507 | 508 | # Recycle Bin used on file shares 509 | $RECYCLE.BIN/ 510 | 511 | # Windows Installer files 512 | *.cab 513 | *.msi 514 | *.msix 515 | *.msm 516 | *.msp 517 | 518 | # Windows shortcuts 519 | *.lnk 520 | 521 | # End of https://www.toptal.com/developers/gitignore/api/certificates,emacs,git,gpg,linux,macos,node,nohup,python,ssh,vim,virtualenv,visualstudiocode,windows 522 | 523 | junit.xml 524 | 525 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # Generated by gha-utils mailmap-sync v4.4.2 - https://github.com/kdeldycke/workflows 2 | # Timestamp: 2024-08-21T06:20:31.291215 3 | # Format is: 4 | # Prefered Name Other Name 5 | # 6 | # Reference: https://git-scm.com/docs/git-blame#_mapping_authors 7 | 8 | BD103 <59022059+BD103@users.noreply.github.com> 9 | GitHub Actions dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> GitHub github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 10 | Jakob 11 | Kevin Deldycke kdeldycke <159718+kdeldycke@users.noreply.github.com> kdeldycke Kevin Deldycke Kevin Deldycke 12 | Kian-Meng Ang 13 | Raj 14 | Ross Smith II -------------------------------------------------------------------------------- /citation.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: "Click Extra" 3 | message: "If you use this software, please cite it as below." 4 | type: software 5 | authors: 6 | - family-names: "Deldycke" 7 | given-names: "Kevin" 8 | email: kevin@deldycke.com 9 | orcid: "https://orcid.org/0000-0001-9748-9014" 10 | doi: 10.5281/zenodo.7116050 11 | version: 5.0.3 12 | # The release date is kept up to date by the external workflows. See: 13 | # https://github.com/kdeldycke/workflows/blob/33b704b489c1aa18b7b7efbf963e153e91e1c810/.github/workflows/changelog.yaml#L135-L137 14 | date-released: 2025-05-31 15 | url: "https://github.com/kdeldycke/click-extra" -------------------------------------------------------------------------------- /click_extra/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Expose package-wide elements.""" 17 | 18 | __version__ = "5.0.3" 19 | """Examples of valid version strings according :pep:`440#version-scheme`: 20 | 21 | .. code-block:: python 22 | 23 | __version__ = "1.2.3.dev1" # Development release 1 24 | __version__ = "1.2.3a1" # Alpha Release 1 25 | __version__ = "1.2.3b1" # Beta Release 1 26 | __version__ = "1.2.3rc1" # RC Release 1 27 | __version__ = "1.2.3" # Final Release 28 | __version__ = "1.2.3.post1" # Post Release 1 29 | """ 30 | 31 | # Import all click's module-level content to allow for drop-in replacement. 32 | # XXX Star import is really badly supported by mypy for now and leads to lots of 33 | # "Module 'XXX' has no attribute 'YYY'". See: https://github.com/python/mypy/issues/4930 34 | # Overrides click helpers with cloup's. 35 | from click import * # noqa: E402, F403 36 | from click.core import ParameterSource # noqa: E402 37 | from cloup import * # type: ignore[no-redef, assignment] # noqa: E402, F403 38 | 39 | from .colorize import ( # noqa: E402 40 | ColorOption, 41 | HelpExtraFormatter, 42 | HelpExtraTheme, 43 | ) 44 | from .commands import ( # noqa: E402 45 | ExtraCommand, 46 | ExtraContext, 47 | ExtraGroup, 48 | ) 49 | from .config import ConfigOption # noqa: E402 50 | from .decorators import ( # type: ignore[no-redef, has-type, unused-ignore] # noqa: E402 51 | color_option, 52 | command, 53 | config_option, 54 | extra_command, 55 | extra_group, 56 | extra_version_option, 57 | group, 58 | help_option, 59 | show_params_option, 60 | table_format_option, 61 | telemetry_option, 62 | timer_option, 63 | verbose_option, 64 | verbosity_option, 65 | ) 66 | from .logging import ( # noqa: E402 67 | ExtraFormatter, 68 | ExtraStreamHandler, 69 | VerboseOption, 70 | VerbosityOption, 71 | extraBasicConfig, 72 | new_extra_logger, 73 | ) 74 | from .parameters import ( # noqa: E402 75 | ExtraOption, 76 | ParamStructure, 77 | ShowParamsOption, 78 | search_params, 79 | ) 80 | from .tabulate import TableFormatOption # noqa: E402 81 | from .telemetry import TelemetryOption # noqa: E402 82 | from .testing import ExtraCliRunner # noqa: E402 83 | from .timer import TimerOption # noqa: E402 84 | from .version import ExtraVersionOption # noqa: E402 85 | 86 | __all__ = [ 87 | "Abort", # noqa: F405 88 | "annotations", # noqa: F405 89 | "Argument", # noqa: F405 90 | "argument", # noqa: F405 91 | "BadArgumentUsage", # noqa: F405 92 | "BadOptionUsage", # noqa: F405 93 | "BadParameter", # noqa: F405 94 | "BOOL", # noqa: F405 95 | "Choice", # noqa: F405 96 | "clear", # noqa: F405 97 | "ClickException", # noqa: F405 98 | "Color", # noqa: F405 99 | "color_option", # noqa: F405 100 | "ColorOption", # noqa: F405 101 | "Command", # noqa: F405 102 | "command", # noqa: F405 103 | "CommandCollection", # noqa: F405 104 | "config_option", # noqa: F405 105 | "ConfigOption", # noqa: F405 106 | "confirm", # noqa: F405 107 | "confirmation_option", # noqa: F405 108 | "constrained_params", # noqa: F405 109 | "constraint", # noqa: F405 110 | "ConstraintMixin", # noqa: F405 111 | "Context", # noqa: F405 112 | "DateTime", # noqa: F405 113 | "dir_path", # noqa: F405 114 | "echo", # noqa: F405 115 | "echo_via_pager", # noqa: F405 116 | "edit", # noqa: F405 117 | "extra_command", # noqa: F405 118 | "extra_group", # noqa: F405 119 | "extra_version_option", # noqa: F405 120 | "extraBasicConfig", # noqa: F405 121 | "ExtraCliRunner", # noqa: F405 122 | "ExtraCommand", # noqa: F405 123 | "ExtraContext", # noqa: F405 124 | "ExtraFormatter", # noqa: F405 125 | "ExtraGroup", # noqa: F405 126 | "ExtraOption", # noqa: F405 127 | "ExtraStreamHandler", # noqa: F405 128 | "ExtraVersionOption", # noqa: F405 129 | "File", # noqa: F405 130 | "file_path", # noqa: F405 131 | "FileError", # noqa: F405 132 | "FLOAT", # noqa: F405 133 | "FloatRange", # noqa: F405 134 | "format_filename", # noqa: F405 135 | "get_app_dir", # noqa: F405 136 | "get_binary_stream", # noqa: F405 137 | "get_current_context", # noqa: F405 138 | "get_text_stream", # noqa: F405 139 | "getchar", # noqa: F405 140 | "Group", # noqa: F405 141 | "group", # noqa: F405 142 | "help_option", # noqa: F405 143 | "HelpExtraFormatter", # noqa: F405 144 | "HelpExtraTheme", # noqa: F405 145 | "HelpFormatter", # noqa: F405 146 | "HelpSection", # noqa: F405 147 | "HelpTheme", # noqa: F405 148 | "INT", # noqa: F405 149 | "IntRange", # noqa: F405 150 | "launch", # noqa: F405 151 | "make_pass_decorator", # noqa: F405 152 | "MissingParameter", # noqa: F405 153 | "new_extra_logger", # noqa: F405 154 | "NoSuchOption", # noqa: F405 155 | "open_file", # noqa: F405 156 | "Option", # noqa: F405 157 | "option", # noqa: F405 158 | "option_group", # noqa: F405 159 | "OptionGroup", # noqa: F405 160 | "OptionGroupMixin", # noqa: F405 161 | "Parameter", # noqa: F405 162 | "ParameterSource", # noqa: F405 163 | "ParamStructure", # noqa: F405 164 | "ParamType", # noqa: F405 165 | "pass_context", # noqa: F405 166 | "pass_obj", # noqa: F405 167 | "password_option", # noqa: F405 168 | "Path", # noqa: F405 169 | "path", # noqa: F405 170 | "pause", # noqa: F405 171 | "progressbar", # noqa: F405 172 | "prompt", # noqa: F405 173 | "search_params", # noqa: F405 174 | "secho", # noqa: F405 175 | "Section", # noqa: F405 176 | "SectionMixin", # noqa: F405 177 | "show_params_option", # noqa: F405 178 | "ShowParamsOption", # noqa: F405 179 | "STRING", # noqa: F405 180 | "Style", # noqa: F405 181 | "style", # noqa: F405 182 | "table_format_option", # noqa: F405 183 | "TableFormatOption", # noqa: F405 184 | "telemetry_option", # noqa: F405 185 | "TelemetryOption", # noqa: F405 186 | "timer_option", # noqa: F405 187 | "TimerOption", # noqa: F405 188 | "Tuple", # noqa: F405 189 | "UNPROCESSED", # noqa: F405 190 | "unstyle", # noqa: F405 191 | "UsageError", # noqa: F405 192 | "UUID", # noqa: F405 193 | "verbose_option", # noqa: F405 194 | "VerboseOption", # noqa: F405 195 | "verbosity_option", # noqa: F405 196 | "VerbosityOption", # noqa: F405 197 | "version_option", # noqa: F405 198 | "VersionOption", # noqa: F405 199 | "warnings", # noqa: F405 200 | "wrap_text", # noqa: F405 201 | ] 202 | """Expose all of Click, Cloup and Click Extra. 203 | 204 | .. note:: 205 | The content of ``__all__`` is checked and enforced in unittests. 206 | 207 | .. todo:: 208 | Test ruff __all__ formatting capabilities. And if good enough, remove ``__all__`` 209 | checks in unittests. 210 | """ 211 | -------------------------------------------------------------------------------- /click_extra/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Decorators for group, commands and options.""" 17 | 18 | from functools import wraps 19 | 20 | import click 21 | import cloup 22 | 23 | from .colorize import ColorOption 24 | from .commands import DEFAULT_HELP_NAMES, ExtraCommand, ExtraGroup, default_extra_params 25 | from .config import ConfigOption 26 | from .logging import VerboseOption, VerbosityOption 27 | from .parameters import ShowParamsOption 28 | from .tabulate import TableFormatOption 29 | from .telemetry import TelemetryOption 30 | from .timer import TimerOption 31 | from .version import ExtraVersionOption 32 | 33 | 34 | def allow_missing_parenthesis(dec_factory): 35 | """Allow to use decorators with or without parenthesis. 36 | 37 | As proposed in 38 | `Cloup issue #127 `_. 39 | """ 40 | 41 | @wraps(dec_factory) 42 | def new_factory(*args, **kwargs): 43 | if args and callable(args[0]): 44 | return dec_factory(*args[1:], **kwargs)(args[0]) 45 | return dec_factory(*args, **kwargs) 46 | 47 | return new_factory 48 | 49 | 50 | def decorator_factory(dec, *new_args, **new_defaults): 51 | """Clone decorator with a set of new defaults. 52 | 53 | Used to create our own collection of decorators for our custom options, based on 54 | Cloup's. 55 | """ 56 | 57 | @allow_missing_parenthesis 58 | def decorator(*args, **kwargs): 59 | """Returns a new decorator instantiated with custom defaults. 60 | 61 | These defaults values are merged with the user's own arguments. 62 | 63 | A special case is made for the ``params`` argument, to allow it to be callable. 64 | This limits the issue of the mutable options being shared between commands. 65 | 66 | This decorator can be used with or without arguments. 67 | """ 68 | if not args: 69 | args = new_args 70 | 71 | # Use a copy of the defaults to avoid modifying the original dict. 72 | new_kwargs = new_defaults.copy() 73 | new_kwargs.update(kwargs) 74 | 75 | # If the params argument is a callable, we need to call it to get the actual 76 | # list of options. 77 | params_func = new_kwargs.get("params") 78 | if callable(params_func): 79 | new_kwargs["params"] = params_func() 80 | 81 | # Return the original decorator with the new defaults. 82 | return dec(*args, **new_kwargs) 83 | 84 | return decorator 85 | 86 | 87 | # Redefine Cloup decorators to allow them to be used with or without parenthesis. 88 | command = decorator_factory(dec=cloup.command) 89 | group = decorator_factory(dec=cloup.group) 90 | 91 | # Customize existing Click decorators with better default parameters. 92 | help_option = decorator_factory( 93 | # XXX parameters are not named because of the way the default option names are 94 | # passed to HelpOption. 95 | # TODO: try the following instead once https://github.com/pallets/click/pull/2840 96 | # is merged: 97 | # dec=click.decorators.help_option, 98 | # param_decls=DEFAULT_HELP_NAMES, 99 | click.decorators.help_option, 100 | *DEFAULT_HELP_NAMES, 101 | ) 102 | 103 | # Copy Click and Cloup decorators with better defaults, and prefix them with "extra_". 104 | extra_command = decorator_factory( 105 | dec=cloup.command, 106 | cls=ExtraCommand, 107 | params=default_extra_params, 108 | ) 109 | extra_group = decorator_factory( 110 | dec=cloup.group, 111 | cls=ExtraGroup, 112 | params=default_extra_params, 113 | ) 114 | extra_version_option = decorator_factory(dec=cloup.option, cls=ExtraVersionOption) 115 | 116 | # New option decorators provided by Click Extra. 117 | color_option = decorator_factory(dec=cloup.option, cls=ColorOption) 118 | config_option = decorator_factory(dec=cloup.option, cls=ConfigOption) 119 | show_params_option = decorator_factory(dec=cloup.option, cls=ShowParamsOption) 120 | table_format_option = decorator_factory(dec=cloup.option, cls=TableFormatOption) 121 | telemetry_option = decorator_factory(dec=cloup.option, cls=TelemetryOption) 122 | timer_option = decorator_factory(dec=cloup.option, cls=TimerOption) 123 | verbose_option = decorator_factory(dec=cloup.option, cls=VerboseOption) 124 | verbosity_option = decorator_factory(dec=cloup.option, cls=VerbosityOption) 125 | -------------------------------------------------------------------------------- /click_extra/docs_update.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Automation to keep click-extra documentation up-to-date. 17 | 18 | .. tip:: 19 | 20 | When the module is called directly, it will update all documentation files in-place: 21 | 22 | .. code-block:: shell-session 23 | 24 | $ run python -m click_extra.docs_update 25 | 26 | See how it is `used in .github/workflows/docs.yaml workflow 27 | `_. 28 | """ 29 | 30 | from __future__ import annotations 31 | 32 | import sys 33 | from pathlib import Path 34 | 35 | from .tabulate import tabulate 36 | 37 | 38 | def replace_content( 39 | filepath: Path, 40 | new_content: str, 41 | start_tag: str, 42 | end_tag: str | None = None, 43 | ) -> None: 44 | """Replace in a file the content surrounded by the provided start end end tags. 45 | 46 | If no end tag is provided, the whole content found after the start tag will be 47 | replaced. 48 | """ 49 | filepath = filepath.resolve() 50 | assert filepath.exists(), f"File {filepath} does not exist." 51 | assert filepath.is_file(), f"File {filepath} is not a file." 52 | 53 | orig_content = filepath.read_text() 54 | 55 | # Extract pre-content before the start tag. 56 | assert start_tag, "Start tag must be empty." 57 | assert start_tag in orig_content, f"Start tag {start_tag!r} not found in content." 58 | pre_content, table_start = orig_content.split(start_tag, 1) 59 | 60 | # Extract the post-content after the end tag. 61 | if end_tag: 62 | _, post_content = table_start.split(end_tag, 1) 63 | # If no end tag is provided, we're going to replace the whole content found after 64 | # the start tag. 65 | else: 66 | end_tag = "" 67 | post_content = "" 68 | 69 | # Reconstruct the content with our updated table. 70 | filepath.write_text( 71 | f"{pre_content}{start_tag}{new_content}{end_tag}{post_content}", 72 | ) 73 | 74 | 75 | def generate_lexer_table() -> str: 76 | """Generate a Markdown table mapping original Pygments' lexers to their new ANSI 77 | variants implemented by Click Extra. 78 | 79 | Import ``pygments.lexer_map`` on function execution, to avoid referencing the 80 | optional ``pygments`` extra dependency. 81 | """ 82 | from .pygments import lexer_map 83 | 84 | table = [] 85 | for orig_lexer, ansi_lexer in sorted( 86 | lexer_map.items(), 87 | key=lambda i: i[0].__qualname__, 88 | ): 89 | table.append( 90 | [ 91 | f"[`{orig_lexer.__qualname__}`](https://pygments.org/docs/lexers/#" 92 | f"{orig_lexer.__module__}.{orig_lexer.__qualname__})", 93 | f"{', '.join(f'`{a}`' for a in sorted(orig_lexer.aliases))}", 94 | f"{', '.join(f'`{a}`' for a in sorted(ansi_lexer.aliases))}", 95 | ], 96 | ) 97 | return tabulate.tabulate( 98 | table, 99 | headers=[ 100 | "Original Lexer", 101 | "Original IDs", 102 | "ANSI variants", 103 | ], 104 | tablefmt="github", 105 | colalign=["left", "left", "left"], 106 | disable_numparse=True, 107 | ) 108 | 109 | 110 | def update_docs() -> None: 111 | """Update documentation with dynamic content.""" 112 | project_root = Path(__file__).parent.parent 113 | 114 | # Update the lexer table in Sphinx's documentation. 115 | replace_content( 116 | project_root.joinpath("docs/pygments.md"), 117 | generate_lexer_table(), 118 | "\n\n", 119 | "\n\n", 120 | ) 121 | 122 | 123 | if __name__ == "__main__": 124 | sys.exit(update_docs()) # type: ignore[func-returns-value] 125 | -------------------------------------------------------------------------------- /click_extra/envvar.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Implements environment variable utilities.""" 17 | 18 | from __future__ import annotations 19 | 20 | import os 21 | import re 22 | from typing import Any, Iterable, Mapping 23 | 24 | import click 25 | import click.testing 26 | from boltons.iterutils import flatten_iter 27 | 28 | TEnvVarID = str | None 29 | TEnvVarIDs = Iterable[TEnvVarID] 30 | TNestedEnvVarIDs = Iterable[TEnvVarID | Iterable["TNestedEnvVarIDs"]] 31 | """Types environment variable names.""" 32 | 33 | TEnvVars = Mapping[str, str | None] 34 | """Type for ``dict``-like environment variables.""" 35 | 36 | 37 | def merge_envvar_ids(*envvar_ids: TEnvVarID | TNestedEnvVarIDs) -> tuple[str, ...]: 38 | """Merge and deduplicate environment variables. 39 | 40 | Multiple parameters are accepted and can be single strings or arbitrary-nested 41 | iterables of strings. ``None`` values are ignored. 42 | 43 | Variable names are deduplicated while preserving their initial order. 44 | 45 | .. caution:: 46 | `On Windows, environment variable names are case-insensitive 47 | `_, so we `normalize them 48 | to uppercase as the standard library does 49 | `_. 50 | 51 | Returns a tuple of strings. The result is ready to be used as the ``envvar`` 52 | parameter for Click's options or arguments. 53 | """ 54 | ids = [] 55 | for envvar in flatten_iter(envvar_ids): 56 | if envvar: 57 | if os.name == "nt": 58 | envvar = envvar.upper() 59 | # Deduplicate names. 60 | if envvar not in ids: 61 | ids.append(envvar) 62 | return tuple(ids) 63 | 64 | 65 | def clean_envvar_id(envvar_id: str) -> str: 66 | """Utility to produce a user-friendly environment variable name from a string. 67 | 68 | Separates all contiguous alphanumeric string segments, eliminate empty strings, 69 | join them with an underscore and uppercase the result. 70 | 71 | .. attention:: 72 | We do not rely too much on this utility to try to reproduce the `current 73 | behavior of Click, which is is not consistent regarding case-handling of 74 | environment variable `_. 75 | """ 76 | return "_".join(p for p in re.split(r"[^a-zA-Z0-9]+", envvar_id) if p).upper() 77 | 78 | 79 | def param_auto_envvar_id( 80 | param: click.Parameter, 81 | ctx: click.Context | dict[str, Any], 82 | ) -> str | None: 83 | """Compute the auto-generated environment variable of an option or argument. 84 | 85 | Returns the auto envvar as it is exactly computed within Click's internals, i.e. 86 | ``click.core.Parameter.resolve_envvar_value()`` and 87 | ``click.core.Option.resolve_envvar_value()``. 88 | """ 89 | # Skip parameters that have their auto-envvar explicitly disabled. 90 | if not getattr(param, "allow_from_autoenv", None): 91 | return None 92 | 93 | if isinstance(ctx, click.Context): 94 | prefix = ctx.auto_envvar_prefix 95 | else: 96 | prefix = ctx.get("auto_envvar_prefix") 97 | if not prefix or not param.name: 98 | return None 99 | 100 | # Mimics Click's internals. 101 | return f"{prefix}_{param.name.upper()}" 102 | 103 | 104 | def param_envvar_ids( 105 | param: click.Parameter, 106 | ctx: click.Context | dict[str, Any], 107 | ) -> tuple[str, ...]: 108 | """Returns the deduplicated, ordered list of environment variables for an option or 109 | argument, including the auto-generated one. 110 | 111 | The auto-generated environment variable is added at the end of the list, so that 112 | user-defined envvars takes precedence. This respects the current implementation 113 | of ``click.core.Option.resolve_envvar_value()``. 114 | 115 | .. caution:: 116 | `On Windows, environment variable names are case-insensitive 117 | `_, so we `normalize them 118 | to uppercase as the standard library does 119 | `_. 120 | """ 121 | return merge_envvar_ids(param.envvar, param_auto_envvar_id(param, ctx)) 122 | 123 | 124 | def env_copy(extend: TEnvVars | None = None) -> TEnvVars | None: 125 | """Returns a copy of the current environment variables and eventually ``extend`` it. 126 | 127 | Mimics `Python's original implementation 128 | `_ by 129 | returning ``None`` if no ``extend`` content are provided. 130 | 131 | Environment variables are expected to be a ``dict`` of ``str:str``. 132 | """ 133 | if isinstance(extend, dict): 134 | for k, v in extend.items(): 135 | assert isinstance(k, str) 136 | assert isinstance(v, str) 137 | else: 138 | assert not extend 139 | env_copy: TEnvVars | None = None 140 | if extend: 141 | # By casting to dict we make a copy and prevent the modification of the 142 | # global environment. 143 | env_copy = dict(os.environ) 144 | env_copy.update(extend) 145 | return env_copy 146 | -------------------------------------------------------------------------------- /click_extra/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdeldycke/click-extra/ca2b8c7e0ad80288fb2de6a9fd0532e61a643b3e/click_extra/py.typed -------------------------------------------------------------------------------- /click_extra/pygments.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Helpers and utilities to allow Pygments to parse and render ANSI codes.""" 17 | 18 | from __future__ import annotations 19 | 20 | try: 21 | import pygments # noqa: F401 22 | except ImportError: 23 | raise ImportError( 24 | "You need to install click_extra[pygments] extra dependencies to use this " 25 | "module." 26 | ) 27 | 28 | from typing import Iterable, Iterator 29 | 30 | from pygments import lexers 31 | from pygments.filter import Filter 32 | from pygments.filters import TokenMergeFilter 33 | from pygments.formatter import _lookup_style # type: ignore[attr-defined] 34 | from pygments.formatters import HtmlFormatter 35 | from pygments.lexer import Lexer, LexerMeta 36 | from pygments.lexers.algebra import GAPConsoleLexer 37 | from pygments.lexers.dylan import DylanConsoleLexer 38 | from pygments.lexers.erlang import ElixirConsoleLexer, ErlangShellLexer 39 | from pygments.lexers.julia import JuliaConsoleLexer 40 | from pygments.lexers.matlab import MatlabSessionLexer 41 | from pygments.lexers.php import PsyshConsoleLexer 42 | from pygments.lexers.python import PythonConsoleLexer 43 | from pygments.lexers.r import RConsoleLexer 44 | from pygments.lexers.ruby import RubyConsoleLexer 45 | from pygments.lexers.shell import ShellSessionBaseLexer 46 | from pygments.lexers.special import OutputLexer 47 | from pygments.lexers.sql import PostgresConsoleLexer, SqliteConsoleLexer 48 | from pygments.style import StyleMeta 49 | from pygments.token import Generic, _TokenType, string_to_tokentype 50 | from pygments_ansi_color import ( 51 | AnsiColorLexer, 52 | ExtendedColorHtmlFormatterMixin, 53 | color_tokens, 54 | ) 55 | 56 | DEFAULT_TOKEN_TYPE = Generic.Output 57 | """Default Pygments' token type to render with ANSI support. 58 | 59 | We defaults to ``Generic.Output`` tokens, as this is the token type used by all REPL- 60 | like and terminal lexers. 61 | """ 62 | 63 | 64 | class AnsiFilter(Filter): 65 | """Custom filter transforming a particular kind of token (``Generic.Output`` by 66 | defaults) into ANSI tokens.""" 67 | 68 | def __init__(self, **options) -> None: 69 | """Initialize a ``AnsiColorLexer`` and configure the ``token_type`` to be 70 | colorized. 71 | 72 | .. todo:: 73 | 74 | Allow multiple ``token_type`` to be configured for colorization (if 75 | traditions are changed on Pygments' side). 76 | """ 77 | super().__init__(**options) 78 | self.ansi_lexer = AnsiColorLexer() 79 | self.token_type = string_to_tokentype( 80 | options.get("token_type", DEFAULT_TOKEN_TYPE), 81 | ) 82 | 83 | def filter( 84 | self, lexer: Lexer, stream: Iterable[tuple[_TokenType, str]] 85 | ) -> Iterator[tuple[_TokenType, str]]: 86 | """Transform each token of ``token_type`` type into a stream of ANSI tokens.""" 87 | for ttype, value in stream: 88 | if ttype == self.token_type: 89 | # TODO: Should we re-wrap the resulting list of token into their 90 | # original Generic.Output? 91 | yield from self.ansi_lexer.get_tokens(value) 92 | else: 93 | yield ttype, value 94 | 95 | 96 | class AnsiSessionLexer(LexerMeta): 97 | """Custom metaclass used as a class factory to derive an ANSI variant of default 98 | shell session lexers.""" 99 | 100 | def __new__(cls, name, bases, dct): 101 | """Setup class properties' defaults for new ANSI-capable lexers. 102 | 103 | - Adds an ``ANSI`` prefix to the lexer's name. 104 | - Replaces all ``aliases`` IDs from the parent lexer with variants prefixed with 105 | ``ansi-``. 106 | """ 107 | new_cls = super().__new__(cls, name, bases, dct) 108 | new_cls.name = f"ANSI {new_cls.name}" 109 | new_cls.aliases = tuple(f"ansi-{alias}" for alias in new_cls.aliases) 110 | return new_cls 111 | 112 | 113 | class AnsiLexerFiltersMixin(Lexer): 114 | def __init__(self, *args, **kwargs) -> None: 115 | """Adds a ``TokenMergeFilter`` and ``AnsiOutputFilter`` to the list of filters. 116 | 117 | The session lexers we inherits from are parsing the code block line by line so 118 | they can differentiate inputs and outputs. Each output line ends up 119 | encapsulated into a ``Generic.Output`` token. We apply the ``TokenMergeFilter`` 120 | filter to reduce noise and have each contiguous output lines part of the same 121 | single token. 122 | 123 | Then we apply our custom ``AnsiOutputFilter`` to transform any 124 | ``Generic.Output`` monoblocks into ANSI tokens. 125 | """ 126 | super().__init__(*args, **kwargs) 127 | self.filters.append(TokenMergeFilter()) 128 | self.filters.append(AnsiFilter()) 129 | 130 | 131 | def collect_session_lexers() -> Iterator[type[Lexer]]: 132 | """Retrieve all lexers producing shell-like sessions in Pygments. 133 | 134 | This function contain a manually-maintained list of lexers, to which we dynamiccaly 135 | adds lexers inheriting from ``ShellSessionBaseLexer``. 136 | 137 | .. hint:: 138 | 139 | To help maintain this list, there is `a test that will fail 140 | `_ 141 | if a new REPL/terminal-like lexer is added to Pygments but not referenced here. 142 | """ 143 | yield from [ 144 | DylanConsoleLexer, 145 | ElixirConsoleLexer, 146 | ErlangShellLexer, 147 | GAPConsoleLexer, 148 | JuliaConsoleLexer, 149 | MatlabSessionLexer, 150 | OutputLexer, 151 | PostgresConsoleLexer, 152 | PsyshConsoleLexer, 153 | PythonConsoleLexer, 154 | RConsoleLexer, 155 | RubyConsoleLexer, 156 | SqliteConsoleLexer, 157 | ] 158 | 159 | for lexer in lexers._iter_lexerclasses(): 160 | if ShellSessionBaseLexer in lexer.__bases__: 161 | yield lexer 162 | 163 | 164 | lexer_map = {} 165 | """Map original lexer to their ANSI variant.""" 166 | 167 | 168 | # Auto-generate the ANSI variant of all lexers we collected. 169 | for original_lexer in collect_session_lexers(): 170 | new_name = f"Ansi{original_lexer.__name__}" 171 | new_lexer = AnsiSessionLexer(new_name, (AnsiLexerFiltersMixin, original_lexer), {}) 172 | locals()[new_name] = new_lexer 173 | lexer_map[original_lexer] = new_lexer 174 | 175 | 176 | class AnsiHtmlFormatter(ExtendedColorHtmlFormatterMixin, HtmlFormatter): 177 | """Extend standard Pygments' ``HtmlFormatter``. 178 | 179 | `Adds support for ANSI 256 colors `_. 180 | """ 181 | 182 | name = "ANSI HTML" 183 | aliases = ["ansi-html"] 184 | 185 | def __init__(self, **kwargs) -> None: 186 | """Intercept the ``style`` argument to augment it with ANSI colors support. 187 | 188 | Creates a new style instance that inherits from the one provided by the user, 189 | but updates its ``styles`` attribute to add ANSI colors support from 190 | ``pygments_ansi_color``. 191 | """ 192 | # XXX Same default style as in Pygments' HtmlFormatter, which is... `default`: 193 | # https://github.com/pygments/pygments/blob/1d83928/pygments/formatter.py#LL89C33-L89C33 194 | base_style_id = kwargs.setdefault("style", "default") 195 | 196 | # Fetch user-provided style. 197 | base_style = _lookup_style(base_style_id) 198 | 199 | # Augment the style with ANSI colors support. 200 | augmented_styles = dict(base_style.styles) 201 | augmented_styles.update(color_tokens(enable_256color=True)) 202 | 203 | # Prefix the style name with `Ansi` to avoid name collision with the original 204 | # and ease debugging. 205 | new_name = f"Ansi{base_style.__name__}" 206 | new_lexer = StyleMeta(new_name, (base_style,), {"styles": augmented_styles}) 207 | 208 | kwargs["style"] = new_lexer 209 | 210 | super().__init__(**kwargs) 211 | -------------------------------------------------------------------------------- /click_extra/pytest.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Pytest fixtures and marks to help testing Click CLIs.""" 17 | 18 | from __future__ import annotations 19 | 20 | try: 21 | import pytest # noqa: F401 22 | except ImportError: 23 | raise ImportError( 24 | "You need to install click_extra[pytest] extra dependencies to use this module." 25 | ) 26 | 27 | from typing import TYPE_CHECKING, Any 28 | 29 | import click 30 | import click.testing 31 | import cloup 32 | import pytest 33 | 34 | from click_extra.decorators import command, extra_command, extra_group, group 35 | from click_extra.testing import ExtraCliRunner 36 | 37 | if TYPE_CHECKING: 38 | from pathlib import Path 39 | 40 | from _pytest.mark import MarkDecorator 41 | from _pytest.mark.structures import ParameterSet 42 | 43 | 44 | @pytest.fixture 45 | def extra_runner(): 46 | """Runner fixture for ``click.testing.ExtraCliRunner``.""" 47 | runner = ExtraCliRunner() 48 | with runner.isolated_filesystem(): 49 | yield runner 50 | 51 | 52 | @pytest.fixture 53 | def invoke(extra_runner): 54 | """Invoke fixture shorthand for ``click.testing.ExtraCliRunner.invoke``.""" 55 | return extra_runner.invoke 56 | 57 | 58 | skip_naked = pytest.mark.skip(reason="Naked decorator not supported yet.") 59 | """Mark to skip Cloup decorators without parenthesis. 60 | 61 | .. warning:: 62 | `Cloup does not yet support decorators without parenthesis 63 | `_. 64 | """ 65 | 66 | 67 | def command_decorators( 68 | no_commands: bool = False, 69 | no_groups: bool = False, 70 | no_click: bool = False, 71 | no_cloup: bool = False, 72 | no_redefined: bool = False, 73 | no_extra: bool = False, 74 | with_parenthesis: bool = True, 75 | with_types: bool = False, 76 | ) -> tuple[ParameterSet, ...]: 77 | """Returns collection of Pytest parameters to test all forms of click/cloup/click- 78 | extra command-like decorators.""" 79 | params: list[tuple[Any, set[str], str, tuple | MarkDecorator]] = [] 80 | 81 | if no_commands is False: 82 | if not no_click: 83 | params.append((click.command, {"click", "command"}, "click.command", ())) 84 | if with_parenthesis: 85 | params.append( 86 | (click.command(), {"click", "command"}, "click.command()", ()), 87 | ) 88 | 89 | if not no_cloup: 90 | params.append( 91 | (cloup.command, {"cloup", "command"}, "cloup.command", skip_naked), 92 | ) 93 | if with_parenthesis: 94 | params.append( 95 | (cloup.command(), {"cloup", "command"}, "cloup.command()", ()), 96 | ) 97 | 98 | if not no_redefined: 99 | params.append( 100 | (command, {"redefined", "command"}, "click_extra.command", ()), 101 | ) 102 | if with_parenthesis: 103 | params.append( 104 | (command(), {"redefined", "command"}, "click_extra.command()", ()), 105 | ) 106 | 107 | if not no_extra: 108 | params.append( 109 | ( 110 | extra_command, 111 | {"extra", "command"}, 112 | "click_extra.extra_command", 113 | (), 114 | ), 115 | ) 116 | if with_parenthesis: 117 | params.append( 118 | ( 119 | extra_command(), 120 | {"extra", "command"}, 121 | "click_extra.extra_command()", 122 | (), 123 | ), 124 | ) 125 | 126 | if not no_groups: 127 | if not no_click: 128 | params.append((click.group, {"click", "group"}, "click.group", ())) 129 | if with_parenthesis: 130 | params.append((click.group(), {"click", "group"}, "click.group()", ())) 131 | 132 | if not no_cloup: 133 | params.append((cloup.group, {"cloup", "group"}, "cloup.group", skip_naked)) 134 | if with_parenthesis: 135 | params.append((cloup.group(), {"cloup", "group"}, "cloup.group()", ())) 136 | 137 | if not no_redefined: 138 | params.append((group, {"redefined", "group"}, "click_extra.group", ())) 139 | if with_parenthesis: 140 | params.append( 141 | (group(), {"redefined", "group"}, "click_extra.group()", ()), 142 | ) 143 | 144 | if not no_extra: 145 | params.append( 146 | ( 147 | extra_group, 148 | {"extra", "group"}, 149 | "click_extra.extra_group", 150 | (), 151 | ), 152 | ) 153 | if with_parenthesis: 154 | params.append( 155 | ( 156 | extra_group(), 157 | {"extra", "group"}, 158 | "click_extra.extra_group()", 159 | (), 160 | ), 161 | ) 162 | 163 | decorator_params = [] 164 | for deco, deco_type, label, marks in params: 165 | args = [deco] 166 | if with_types: 167 | args.append(deco_type) 168 | decorator_params.append(pytest.param(*args, id=label, marks=marks)) 169 | 170 | return tuple(decorator_params) 171 | 172 | 173 | @pytest.fixture 174 | def create_config(tmp_path): 175 | """A generic fixture to produce a temporary configuration file.""" 176 | 177 | def _create_config(filename: str | Path, content: str) -> Path: 178 | """Create a fake configuration file.""" 179 | config_path: Path 180 | if isinstance(filename, str): 181 | config_path = tmp_path.joinpath(filename) 182 | else: 183 | config_path = filename.resolve() 184 | 185 | # Create the missing folder structure, like "mkdir -p" does. 186 | config_path.parent.mkdir(parents=True, exist_ok=True) 187 | config_path.write_text(content, encoding="utf-8") 188 | 189 | return config_path 190 | 191 | return _create_config 192 | 193 | 194 | default_options_uncolored_help = ( 195 | r" --time / --no-time Measure and print elapsed execution time." 196 | r" \[default:\n" 197 | r" no-time\]\n" 198 | r" --color, --ansi / --no-color, --no-ansi\n" 199 | r" Strip out all colors and all ANSI codes from" 200 | r" output.\n" 201 | r" \[default: color\]\n" 202 | r" -C, --config CONFIG_PATH Location of the configuration file. Supports glob\n" 203 | r" pattern of local path and remote URL." 204 | r" \[default:( \S+)?\n" 205 | r"( .+\n)*" 206 | r" \S+\.{toml,yaml,yml,json,ini,xml}\]\n" 207 | r" --show-params Show all CLI parameters, their provenance, defaults\n" 208 | r" and value, then exit.\n" 209 | r" --verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.\n" 210 | r" \[default: WARNING\]\n" 211 | r" -v, --verbose Increase the default WARNING verbosity by one level\n" 212 | r" for each additional repetition of the option.\n" 213 | r" \[default: 0\]\n" 214 | r" --version Show the version and exit.\n" 215 | r" -h, --help Show this message and exit.\n" 216 | ) 217 | 218 | 219 | default_options_colored_help = ( 220 | r" \x1b\[36m--time\x1b\[0m / \x1b\[36m--no-time\x1b\[0m" 221 | r" Measure and print elapsed execution time." 222 | r" \x1b\[2m\[\x1b\[0m\x1b\[2mdefault:\n" 223 | r" " 224 | r"\x1b\[0m\x1b\[32m\x1b\[2m\x1b\[3mno-time\x1b\[0m\x1b\[2m\]\x1b\[0m\n" 225 | r" \x1b\[36m--color\x1b\[0m, \x1b\[36m--ansi\x1b\[0m /" 226 | r" \x1b\[36m--no-color\x1b\[0m, \x1b\[36m--no-ansi\x1b\[0m\n" 227 | r" Strip out all colors and all ANSI codes from" 228 | r" output.\n" 229 | r" \x1b\[2m\[\x1b\[0m\x1b\[2mdefault:" 230 | r" \x1b\[0m\x1b\[32m\x1b\[2m\x1b\[3mcolor\x1b\[0m\x1b\[2m\]\x1b\[0m\n" 231 | r" \x1b\[36m-C\x1b\[0m, \x1b\[36m--config\x1b\[0m" 232 | r" \x1b\[36m\x1b\[2mCONFIG_PATH\x1b\[0m" 233 | r" Location of the configuration file. Supports glob\n" 234 | r" pattern of local path and remote URL." 235 | r" \x1b\[2m\[\x1b\[0m\x1b\[2mdefault:( \S+)?\n" 236 | r"( .+\n)*" 237 | r" " 238 | r"\S+\.{toml,yaml,yml,json,ini,xml}\x1b\[0m\x1b\[2m\]\x1b\[0m\n" 239 | r" \x1b\[36m--show-params\x1b\[0m" 240 | r" Show all CLI parameters, their provenance, defaults\n" 241 | r" and value, then exit.\n" 242 | r" \x1b\[36m--verbosity\x1b\[0m" 243 | r" \x1b\[36m\x1b\[2mLEVEL\x1b\[0m " 244 | r" Either \x1b\[35mCRITICAL\x1b\[0m, \x1b\[35mERROR\x1b\[0m, " 245 | r"\x1b\[35mWARNING\x1b\[0m, \x1b\[35mINFO\x1b\[0m, \x1b\[35mDEBUG\x1b\[0m.\n" 246 | r" \x1b\[2m\[\x1b\[0m\x1b\[2mdefault: " 247 | r"\x1b\[0m\x1b\[32m\x1b\[2m\x1b\[3mWARNING\x1b\[0m\x1b\[2m\]\x1b\[0m\n" 248 | r" \x1b\[36m-v\x1b\[0m, \x1b\[36m--verbose\x1b\[0m" 249 | r" Increase the default \x1b\[35mWARNING\x1b\[0m verbosity by one level\n" 250 | r" for each additional repetition of the option.\n" 251 | r" \x1b\[2m\[\x1b\[0m\x1b\[2mdefault: " 252 | r"\x1b\[0m\x1b\[32m\x1b\[2m\x1b\[3m0\x1b\[0m\x1b\[2m\]\x1b\[0m\n" 253 | r" \x1b\[36m--version\x1b\[0m Show the version and exit.\n" 254 | r" \x1b\[36m-h\x1b\[0m, \x1b\[36m--help\x1b\[0m" 255 | r" Show this message and exit.\n" 256 | ) 257 | 258 | 259 | default_debug_uncolored_logging = ( 260 | r"debug: Set to DEBUG.\n" 261 | r"debug: Set to DEBUG.\n" 262 | ) 263 | default_debug_colored_logging = ( 264 | r"\x1b\[34mdebug\x1b\[0m: Set to DEBUG.\n" 265 | r"\x1b\[34mdebug\x1b\[0m: Set to DEBUG.\n" 266 | ) 267 | 268 | 269 | default_debug_uncolored_verbose_log = ( 270 | r"debug: Increased log verbosity by \d+ levels: from WARNING to [A-Z]+.\n" 271 | ) 272 | default_debug_colored_verbose_log = ( 273 | r"\x1b\[34mdebug\x1b\[0m: Increased log verbosity " 274 | r"by \d+ levels: from WARNING to [A-Z]+.\n" 275 | ) 276 | 277 | 278 | default_debug_uncolored_config = ( 279 | r"debug: Load configuration matching .+\*\.{toml,yaml,yml,json,ini,xml}\n" 280 | r"debug: Pattern is not an URL: search local file system.\n" 281 | r"debug: No configuration file found.\n" 282 | ) 283 | default_debug_colored_config = ( 284 | r"\x1b\[34mdebug\x1b\[0m: Load configuration" 285 | r" matching .+\*\.{toml,yaml,yml,json,ini,xml}\n" 286 | r"\x1b\[34mdebug\x1b\[0m: Pattern is not an URL: search local file system.\n" 287 | r"\x1b\[34mdebug\x1b\[0m: No configuration file found.\n" 288 | ) 289 | 290 | 291 | default_debug_uncolored_version_details = ( 292 | "debug: Version string template variables:\n" 293 | r"debug: {module} : \n" 294 | r"debug: {module_name} : \S+\n" 295 | r"debug: {module_file} : .+\n" 296 | r"debug: {module_version} : \S+\n" 297 | r"debug: {package_name} : \S+\n" 298 | r"debug: {package_version}: \S+\n" 299 | r"debug: {exec_name} : \S+\n" 300 | r"debug: {version} : \S+\n" 301 | r"debug: {prog_name} : \S+\n" 302 | r"debug: {env_info} : {.*}\n" 303 | ) 304 | default_debug_colored_version_details = ( 305 | r"\x1b\[34mdebug\x1b\[0m: Version string template variables:\n" 306 | r"\x1b\[34mdebug\x1b\[0m: {module} : \n" 307 | r"\x1b\[34mdebug\x1b\[0m: {module_name} : \x1b\[97m\S+\x1b\[0m\n" 308 | r"\x1b\[34mdebug\x1b\[0m: {module_file} : .+\n" 309 | r"\x1b\[34mdebug\x1b\[0m: {module_version} : \x1b\[32m\S+\x1b\[0m\n" 310 | r"\x1b\[34mdebug\x1b\[0m: {package_name} : \x1b\[97m\S+\x1b\[0m\n" 311 | r"\x1b\[34mdebug\x1b\[0m: {package_version}: \x1b\[32m\S+\x1b\[0m\n" 312 | r"\x1b\[34mdebug\x1b\[0m: {exec_name} : \x1b\[97m\S+\x1b\[0m\n" 313 | r"\x1b\[34mdebug\x1b\[0m: {version} : \x1b\[32m\S+\x1b\[0m\n" 314 | r"\x1b\[34mdebug\x1b\[0m: {prog_name} : \x1b\[97m\S+\x1b\[0m\n" 315 | r"\x1b\[34mdebug\x1b\[0m: {env_info} : \x1b\[90m{.*}\x1b\[0m\n" 316 | ) 317 | 318 | 319 | default_debug_uncolored_log_start = ( 320 | default_debug_uncolored_logging 321 | + default_debug_uncolored_config 322 | + default_debug_uncolored_version_details 323 | ) 324 | default_debug_colored_log_start = ( 325 | default_debug_colored_logging 326 | + default_debug_colored_config 327 | + default_debug_colored_version_details 328 | ) 329 | 330 | 331 | default_debug_uncolored_log_end = ( 332 | r"debug: Reset to WARNING.\n" 333 | r"debug: Reset to WARNING.\n" 334 | ) 335 | default_debug_colored_log_end = ( 336 | r"\x1b\[34mdebug\x1b\[0m: Reset to WARNING.\n" 337 | r"\x1b\[34mdebug\x1b\[0m: Reset to WARNING.\n" 338 | ) 339 | -------------------------------------------------------------------------------- /click_extra/sphinx.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Helpers and utilities for Sphinx rendering of CLI based on Click Extra. 17 | 18 | .. danger:: 19 | This module is quite janky but does the job. Still, it would benefits from a total 20 | clean rewrite. This would require a better understanding of Sphinx, Click and MyST 21 | internals. And as a side effect will eliminate the dependency on 22 | ``pallets_sphinx_themes``. 23 | 24 | If you're up to the task, you can try to refactor it. I'll probably start by moving 25 | the whole ``pallets_sphinx_themes.themes.click.domain`` code here, merge it with 26 | the local collection of monkey-patches below, then clean the whole code to make it 27 | more readable and maintainable. And finally, address all the todo-list below. 28 | 29 | .. todo:: 30 | Add support for plain MyST directives to remove the need of wrapping rST into an 31 | ``{eval-rst}`` block. Ideally, this would allow for the following simpler syntax in 32 | MyST: 33 | 34 | .. code-block:: markdown 35 | 36 | ```{click-example} 37 | from click_extra import echo, extra_command, option, style 38 | 39 | @extra_command 40 | @option("--name", prompt="Your name", help="The person to greet.") 41 | def hello_world(name): 42 | "Simple program that greets NAME." 43 | echo(f"Hello, {style(name, fg='red')}!") 44 | ``` 45 | 46 | .. code-block:: markdown 47 | 48 | ```{click-run} 49 | invoke(hello_world, args=["--help"]) 50 | ``` 51 | 52 | .. todo:: 53 | Fix the need to have both ``.. click:example::`` and ``.. click:run::`` directives 54 | in the same ``{eval-rst}`` block in MyST. This is required to have both directives 55 | shares states and context. 56 | 57 | .. seealso:: 58 | This is based on `Pallets' Sphinx Themes 59 | `_, 60 | `released under a BSD-3-Clause license 61 | `_. 62 | 63 | Compared to the latter, it: 64 | 65 | - Forces the rendering of CLI results into ANSI shell sessions, via the 66 | ``.. code-block:: ansi-shell-session`` directive. 67 | """ 68 | 69 | from __future__ import annotations 70 | 71 | try: 72 | import sphinx # noqa: F401 73 | except ImportError: 74 | raise ImportError( 75 | "You need to install click_extra[sphinx] extra dependencies to use this module." 76 | ) 77 | 78 | import contextlib 79 | import shlex 80 | import subprocess 81 | import sys 82 | import tempfile 83 | from functools import partial 84 | from typing import Any 85 | 86 | import click 87 | from click.testing import EchoingStdin 88 | from docutils import nodes 89 | from docutils.parsers.rst import Directive 90 | from docutils.statemachine import ViewList 91 | from sphinx.domains import Domain 92 | from sphinx.highlighting import PygmentsBridge 93 | 94 | from .pygments import AnsiHtmlFormatter 95 | from .testing import ExtraCliRunner 96 | 97 | 98 | class EofEchoingStdin(EchoingStdin): 99 | """Like :class:`click.testing.EchoingStdin` but adds a visible 100 | ``^D`` in place of the EOT character (``\x04``). 101 | 102 | :meth:`ExampleRunner.invoke` adds ``\x04`` when 103 | ``terminate_input=True``. 104 | """ 105 | 106 | def _echo(self, rv: bytes) -> bytes: 107 | eof = rv[-1] == b"\x04"[0] 108 | 109 | if eof: 110 | rv = rv[:-1] 111 | 112 | if not self._paused: 113 | self._output.write(rv) 114 | 115 | if eof: 116 | self._output.write(b"^D\n") 117 | 118 | return rv 119 | 120 | 121 | @contextlib.contextmanager 122 | def patch_modules(): 123 | """Patch modules to work better with :meth:`ExampleRunner.invoke`. 124 | 125 | ``subprocess.call` output is redirected to ``click.echo`` so it 126 | shows up in the example output. 127 | """ 128 | old_call = subprocess.call 129 | 130 | def dummy_call(*args, **kwargs): 131 | with tempfile.TemporaryFile("wb+") as f: 132 | kwargs["stdout"] = f 133 | kwargs["stderr"] = f 134 | rv = subprocess.Popen(*args, **kwargs).wait() 135 | f.seek(0) 136 | click.echo(f.read().decode("utf-8", "replace").rstrip()) 137 | return rv 138 | 139 | subprocess.call = dummy_call 140 | 141 | try: 142 | yield 143 | finally: 144 | subprocess.call = old_call 145 | 146 | 147 | class ExampleRunner(ExtraCliRunner): 148 | """:class:`click.testing.CliRunner` with additional features. 149 | 150 | This class inherits from ``click_extra.testing.ExtraCliRunner`` to have full 151 | control of contextual color settings by the way of the ``color`` parameter. It also 152 | produce unfiltered ANSI codes so that the ``Directive`` sub-classes below can 153 | render colors in the HTML output. 154 | """ 155 | 156 | force_color = True 157 | """Force color rendering in ``invoke`` calls.""" 158 | 159 | def __init__(self): 160 | super().__init__(echo_stdin=True) 161 | self.namespace = {"click": click, "__file__": "dummy.py"} 162 | 163 | @contextlib.contextmanager 164 | def isolation(self, *args, **kwargs): 165 | iso = super().isolation(*args, **kwargs) 166 | 167 | with iso as streams: 168 | try: 169 | buffer = sys.stdin.buffer 170 | except AttributeError: 171 | buffer = sys.stdin 172 | 173 | # FIXME: We need to replace EchoingStdin with our custom 174 | # class that outputs "^D". At this point we know sys.stdin 175 | # has been patched so it's safe to reassign the class. 176 | # Remove this once EchoingStdin is overridable. 177 | buffer.__class__ = EofEchoingStdin 178 | yield streams 179 | 180 | def invoke( 181 | self, 182 | cli, 183 | args=None, 184 | prog_name=None, 185 | input=None, 186 | terminate_input=False, 187 | env=None, 188 | _output_lines=None, 189 | **extra, 190 | ): 191 | """Like :meth:`CliRunner.invoke` but displays what the user 192 | would enter in the terminal for env vars, command args, and 193 | prompts. 194 | 195 | :param terminate_input: Whether to display "^D" after a list of 196 | input. 197 | :param _output_lines: A list used internally to collect lines to 198 | be displayed. 199 | """ 200 | output_lines = _output_lines if _output_lines is not None else [] 201 | 202 | if env: 203 | for key, value in sorted(env.items()): 204 | value = shlex.quote(value) 205 | output_lines.append(f"$ export {key}={value}") 206 | 207 | args = args or [] 208 | 209 | if prog_name is None: 210 | prog_name = cli.name.replace("_", "-") 211 | 212 | output_lines.append(f"$ {prog_name} {shlex.join(args)}".rstrip()) 213 | # remove "python" from command 214 | prog_name = prog_name.rsplit(" ", 1)[-1] 215 | 216 | if isinstance(input, (tuple, list)): 217 | input = "\n".join(input) + "\n" 218 | 219 | if terminate_input: 220 | input += "\x04" 221 | 222 | result = super().invoke( 223 | cli=cli, args=args, input=input, env=env, prog_name=prog_name, **extra 224 | ) 225 | output_lines.extend(result.output.splitlines()) 226 | return result 227 | 228 | def declare_example(self, source): 229 | """Execute the given code, adding it to the runner's namespace.""" 230 | with patch_modules(): 231 | code = compile(source, "", "exec") 232 | exec(code, self.namespace) 233 | 234 | def run_example(self, source): 235 | """Run commands by executing the given code, returning the lines 236 | of input and output. The code should be a series of the 237 | following functions: 238 | 239 | * :meth:`invoke`: Invoke a command, adding env vars, input, 240 | and output to the output. 241 | * ``println(text="")``: Add a line of text to the output. 242 | * :meth:`isolated_filesystem`: A context manager that changes 243 | to a temporary directory while executing the block. 244 | """ 245 | code = compile(source, "", "exec") 246 | buffer = [] 247 | invoke = partial(self.invoke, _output_lines=buffer) 248 | 249 | def println(text=""): 250 | buffer.append(text) 251 | 252 | exec( 253 | code, 254 | self.namespace, 255 | { 256 | "invoke": invoke, 257 | "println": println, 258 | "isolated_filesystem": self.isolated_filesystem, 259 | }, 260 | ) 261 | return buffer 262 | 263 | def close(self): 264 | """Clean up the runner once the document has been read.""" 265 | pass 266 | 267 | 268 | def get_example_runner(document): 269 | """Get or create the :class:`ExampleRunner` instance associated with 270 | a document. 271 | """ 272 | runner = getattr(document, "click_example_runner", None) 273 | if runner is None: 274 | runner = document.click_example_runner = ExampleRunner() 275 | return runner 276 | 277 | 278 | class DeclareExampleDirective(Directive): 279 | """Add the source contained in the directive's content to the 280 | document's :class:`ExampleRunner`, to be run using 281 | :class:`RunExampleDirective`. 282 | 283 | See :meth:`ExampleRunner.declare_example`. 284 | """ 285 | 286 | has_content = True 287 | required_arguments = 0 288 | optional_arguments = 0 289 | final_argument_whitespace = False 290 | 291 | def run(self): 292 | doc = ViewList() 293 | runner = get_example_runner(self.state.document) 294 | 295 | try: 296 | runner.declare_example("\n".join(self.content)) 297 | except BaseException: 298 | runner.close() 299 | raise 300 | 301 | doc.append(".. code-block:: python", "") 302 | doc.append("", "") 303 | 304 | for line in self.content: 305 | doc.append(" " + line, "") 306 | 307 | node = nodes.section() 308 | self.state.nested_parse(doc, self.content_offset, node) 309 | return node.children 310 | 311 | 312 | class RunExampleDirective(Directive): 313 | """Run commands from :class:`DeclareExampleDirective` and display 314 | the input and output. 315 | 316 | See :meth:`ExampleRunner.run_example`. 317 | """ 318 | 319 | has_content = True 320 | required_arguments = 0 321 | optional_arguments = 0 322 | final_argument_whitespace = False 323 | 324 | def run(self): 325 | doc = ViewList() 326 | runner = get_example_runner(self.state.document) 327 | 328 | try: 329 | rv = runner.run_example("\n".join(self.content)) 330 | except BaseException: 331 | runner.close() 332 | raise 333 | 334 | doc.append(".. code-block:: ansi-shell-session", "") 335 | doc.append("", "") 336 | 337 | for line in rv: 338 | doc.append(" " + line, "") 339 | 340 | node = nodes.section() 341 | self.state.nested_parse(doc, self.content_offset, node) 342 | return node.children 343 | 344 | 345 | class ClickDomain(Domain): 346 | """Declares new directives: 347 | - ``.. click:example::`` 348 | - ``.. click:run::`` 349 | """ 350 | 351 | name = "click" 352 | label = "Click" 353 | directives = { 354 | "example": DeclareExampleDirective, 355 | "run": RunExampleDirective, 356 | } 357 | 358 | def merge_domaindata(self, docnames, otherdata): 359 | # Needed to support parallel build. 360 | # Not using self.data -- nothing to merge. 361 | pass 362 | 363 | 364 | def delete_example_runner_state(app, doctree): 365 | """Close and remove the :class:`ExampleRunner` instance once the 366 | document has been read. 367 | """ 368 | runner = getattr(doctree, "click_example_runner", None) 369 | 370 | if runner is not None: 371 | runner.close() 372 | del doctree.click_example_runner 373 | 374 | 375 | def setup(app: Any) -> None: 376 | """Register new directives, augmented with ANSI coloring. 377 | 378 | .. danger:: 379 | This function activates some monkey-patches: 380 | 381 | - ``sphinx.highlighting.PygmentsBridge`` is updated to set its default HTML 382 | formatter to an ANSI capable one for the whole Sphinx app. 383 | 384 | """ 385 | # Set Sphinx's default HTML formatter to an ANSI capable one. 386 | PygmentsBridge.html_formatter = AnsiHtmlFormatter 387 | 388 | # Register directives to Sphinx. 389 | app.add_domain(ClickDomain) 390 | app.connect("doctree-read", delete_example_runner_state) 391 | -------------------------------------------------------------------------------- /click_extra/tabulate.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Collection of table rendering utilities.""" 17 | 18 | from __future__ import annotations 19 | 20 | import csv 21 | from functools import partial 22 | from gettext import gettext as _ 23 | from io import StringIO 24 | from typing import Sequence 25 | 26 | import tabulate 27 | from tabulate import DataRow, Line, TableFormat 28 | 29 | from . import Choice, Context, Parameter, echo 30 | from .parameters import ExtraOption 31 | 32 | tabulate.MIN_PADDING = 0 33 | """Neutralize spurious double-spacing in table rendering.""" 34 | 35 | 36 | tabulate._table_formats.update( # type: ignore[attr-defined] 37 | { 38 | "github": TableFormat( 39 | lineabove=Line("| ", "-", " | ", " |"), 40 | linebelowheader=Line("| ", "-", " | ", " |"), 41 | linebetweenrows=None, 42 | linebelow=None, 43 | headerrow=DataRow("| ", " | ", " |"), 44 | datarow=DataRow("| ", " | ", " |"), 45 | padding=0, 46 | with_header_hide=["lineabove"], 47 | ), 48 | }, 49 | ) 50 | """Tweak table separators to match MyST and GFM syntax. 51 | 52 | I.e. add a space between the column separator and the dashes filling a cell: 53 | 54 | ``|---|---|---|`` → ``| --- | --- | --- |`` 55 | 56 | That way we produce a table that doesn't need any supplement linting. 57 | 58 | This has been proposed upstream at `python-tabulate#261 59 | `_. 60 | """ 61 | 62 | 63 | output_formats: list[str] = sorted( 64 | # Formats from tabulate. 65 | list(tabulate._table_formats) # type: ignore[attr-defined] 66 | # Formats inherited from previous legacy cli-helpers dependency. 67 | + ["csv", "vertical"] 68 | # Formats derived from CSV dialects. 69 | + [f"csv-{d}" for d in csv.list_dialects()], 70 | ) 71 | """All output formats supported by click-extra.""" 72 | 73 | 74 | def get_csv_dialect(format_id: str) -> str | None: 75 | """Extract, validate and normalize CSV dialect ID from format.""" 76 | assert format_id.startswith("csv") 77 | # Defaults to excel rendering, like in Python's csv module. 78 | dialect = "excel" 79 | parts = format_id.split("-", 1) 80 | assert parts[0] == "csv" 81 | if len(parts) > 1: 82 | dialect = parts[1] 83 | return dialect 84 | 85 | 86 | def render_csv( 87 | tabular_data: Sequence[Sequence[str]], 88 | headers: Sequence[str] = (), 89 | **kwargs, 90 | ) -> None: 91 | # StringIO is used to capture CSV output in memory. Hard-coded to default to UTF-8: 92 | # https://github.com/python/cpython/blob/9291095a746cbd266a3681a26e10989def6f8629/Lib/_pyio.py#L2652 93 | with StringIO(newline="") as output: 94 | writer = csv.writer(output, **kwargs) 95 | writer.writerow(headers) 96 | writer.writerows(tabular_data) 97 | # Use print instead of echo to conserve CSV dialect's line termination, 98 | # avoid extra line returns and keep ANSI coloring. 99 | print(output.getvalue(), end="") 100 | 101 | 102 | def render_vertical( 103 | tabular_data: Sequence[Sequence[str]], 104 | headers: Sequence[str] = (), 105 | **kwargs, 106 | ) -> None: 107 | """Re-implements ``cli-helpers``'s vertical table layout. 108 | 109 | See `cli-helpers source for reference 110 | `_. 111 | """ 112 | header_len = max(len(h) for h in headers) 113 | padded_headers = [h.ljust(header_len) for h in headers] 114 | 115 | for index, row in enumerate(tabular_data): 116 | # 27 has been hardcoded in cli-helpers: 117 | # https://github.com/dbcli/cli_helpers/blob/4e2c417/cli_helpers/tabular_output/vertical_table_adapter.py#L34 118 | echo(f"{'*' * 27}[ {index + 1}. row ]{'*' * 27}") 119 | for cell_label, cell_value in zip(padded_headers, row): 120 | echo(f"{cell_label} | {cell_value}") 121 | 122 | 123 | def render_table( 124 | tabular_data: Sequence[Sequence[str]], 125 | headers: Sequence[str] = (), 126 | **kwargs, 127 | ) -> None: 128 | """Render a table with tabulate and output it via echo.""" 129 | defaults = { 130 | "disable_numparse": True, 131 | "numalign": None, 132 | } 133 | defaults.update(kwargs) 134 | echo(tabulate.tabulate(tabular_data, headers, **defaults)) # type: ignore[arg-type] 135 | 136 | 137 | class TableFormatOption(ExtraOption): 138 | """A pre-configured option that is adding a ``-t``/``--table-format`` flag to select 139 | the rendering style of a table. 140 | 141 | The selected table format ID is made available in the context in 142 | ``ctx.meta["click_extra.table_format"]``. 143 | """ 144 | 145 | def __init__( 146 | self, 147 | param_decls: Sequence[str] | None = None, 148 | type=Choice(output_formats, case_sensitive=False), 149 | default="rounded_outline", 150 | expose_value=False, 151 | help=_("Rendering style of tables."), 152 | **kwargs, 153 | ) -> None: 154 | if not param_decls: 155 | param_decls = ("-t", "--table-format") 156 | 157 | kwargs.setdefault("callback", self.init_formatter) 158 | 159 | super().__init__( 160 | param_decls=param_decls, 161 | type=type, 162 | default=default, 163 | expose_value=expose_value, 164 | help=help, 165 | **kwargs, 166 | ) 167 | 168 | def init_formatter( 169 | self, 170 | ctx: Context, 171 | param: Parameter, 172 | value: str, 173 | ) -> None: 174 | """Save table format ID in the context, and adds ``print_table()`` to it. 175 | 176 | The ``print_table(tabular_data, headers)`` method added to the context is a 177 | ready-to-use helper that takes for parameters: 178 | - ``tabular_data``, a 2-dimensional iterable of iterables for cell values, 179 | - ``headers``, a list of string to be used as headers. 180 | """ 181 | ctx.meta["click_extra.table_format"] = value 182 | 183 | render_func = None 184 | if value.startswith("csv"): 185 | render_func = partial(render_csv, dialect=get_csv_dialect(value)) 186 | elif value == "vertical": 187 | render_func = render_vertical # type: ignore[assignment] 188 | else: 189 | render_func = partial(render_table, tablefmt=value) 190 | 191 | ctx.print_table = render_func # type: ignore[attr-defined] 192 | -------------------------------------------------------------------------------- /click_extra/telemetry.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Telemetry utilities.""" 17 | 18 | from __future__ import annotations 19 | 20 | from gettext import gettext as _ 21 | from typing import TYPE_CHECKING, Sequence 22 | 23 | from .envvar import merge_envvar_ids 24 | from .parameters import ExtraOption 25 | 26 | if TYPE_CHECKING: 27 | from . import Context, Parameter 28 | 29 | 30 | class TelemetryOption(ExtraOption): 31 | """A pre-configured ``--telemetry``/``--no-telemetry`` option flag. 32 | 33 | Respects the 34 | `proposed DO_NOT_TRACK environment variable `_ as a 35 | unified standard to opt-out of telemetry for TUI/console apps. 36 | 37 | The ``DO_NOT_TRACK`` convention takes precedence over the user-defined environment 38 | variables and the auto-generated values. 39 | 40 | .. seealso:: 41 | 42 | - A `knowledge base of telemetry disabling configuration options 43 | `_. 44 | 45 | - And another `list of environment variable to disable telemetry in desktop apps 46 | `_. 47 | """ 48 | 49 | def save_telemetry( 50 | self, 51 | ctx: Context, 52 | param: Parameter, 53 | value: bool, 54 | ) -> None: 55 | """Save the option value in the context, in ``ctx.telemetry``.""" 56 | ctx.telemetry = value # type: ignore[attr-defined] 57 | 58 | def __init__( 59 | self, 60 | param_decls: Sequence[str] | None = None, 61 | default=False, 62 | expose_value=False, 63 | envvar=None, 64 | show_envvar=True, 65 | help=_("Collect telemetry and usage data."), 66 | **kwargs, 67 | ) -> None: 68 | if not param_decls: 69 | param_decls = ("--telemetry/--no-telemetry",) 70 | 71 | envvar = merge_envvar_ids("DO_NOT_TRACK", envvar) 72 | 73 | kwargs.setdefault("callback", self.save_telemetry) 74 | 75 | super().__init__( 76 | param_decls=param_decls, 77 | default=default, 78 | expose_value=expose_value, 79 | envvar=envvar, 80 | show_envvar=show_envvar, 81 | help=help, 82 | **kwargs, 83 | ) 84 | -------------------------------------------------------------------------------- /click_extra/timer.py: -------------------------------------------------------------------------------- 1 | # Copyright Kevin Deldycke and contributors. 2 | # 3 | # This program is Free Software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 16 | """Command execution time measurement.""" 17 | 18 | from __future__ import annotations 19 | 20 | from gettext import gettext as _ 21 | from time import perf_counter 22 | from typing import Sequence 23 | 24 | from . import Context, Parameter, echo 25 | from .parameters import ExtraOption 26 | 27 | 28 | class TimerOption(ExtraOption): 29 | """A pre-configured option that is adding a ``--time``/``--no-time`` flag to print 30 | elapsed time at the end of CLI execution. 31 | 32 | The start time is made available in the context in 33 | ``ctx.meta["click_extra.start_time"]``. 34 | """ 35 | 36 | def print_timer(self): 37 | """Compute and print elapsed execution time.""" 38 | echo(f"Execution time: {perf_counter() - self.start_time:0.3f} seconds.") 39 | 40 | def register_timer_on_close( 41 | self, 42 | ctx: Context, 43 | param: Parameter, 44 | value: bool, 45 | ) -> None: 46 | """Callback setting up all timer's machinery. 47 | 48 | Computes and print the execution time at the end of the CLI, if option has been 49 | activated. 50 | """ 51 | # Take timestamp snapshot. 52 | self.start_time = perf_counter() 53 | 54 | ctx.meta["click_extra.start_time"] = self.start_time 55 | 56 | # Skip timekeeping if option is not active. 57 | if value: 58 | # Register printing at the end of execution. 59 | ctx.call_on_close(self.print_timer) 60 | 61 | def __init__( 62 | self, 63 | param_decls: Sequence[str] | None = None, 64 | default=False, 65 | expose_value=False, 66 | is_eager=True, 67 | help=_("Measure and print elapsed execution time."), 68 | **kwargs, 69 | ) -> None: 70 | if not param_decls: 71 | param_decls = ("--time/--no-time",) 72 | 73 | kwargs.setdefault("callback", self.register_timer_on_close) 74 | 75 | super().__init__( 76 | param_decls=param_decls, 77 | default=default, 78 | expose_value=expose_value, 79 | is_eager=is_eager, 80 | help=help, 81 | **kwargs, 82 | ) 83 | -------------------------------------------------------------------------------- /docs/assets/click-extra-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdeldycke/click-extra/ca2b8c7e0ad80288fb2de6a9fd0532e61a643b3e/docs/assets/click-extra-screen.png -------------------------------------------------------------------------------- /docs/assets/click-help-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdeldycke/click-extra/ca2b8c7e0ad80288fb2de6a9fd0532e61a643b3e/docs/assets/click-help-screen.png -------------------------------------------------------------------------------- /docs/assets/dependencies.mmd: -------------------------------------------------------------------------------- 1 | flowchart TD 2 | classDef missing stroke-dasharray: 5 3 | boltons["boltons\n25.0.0"] 4 | bracex["bracex\n2.5.post1"] 5 | certifi["certifi\n2025.4.26"] 6 | charset-normalizer["charset-normalizer\n3.4.2"] 7 | click-extra["click-extra\n5.0.2"] 8 | click_0["click\n8.2.1"] 9 | cloup["cloup\n3.0.7"] 10 | distro["distro\n1.9.0"] 11 | extra-platforms["extra-platforms\n3.2.2"] 12 | idna["idna\n3.10"] 13 | mergedeep["mergedeep\n1.3.4"] 14 | pyyaml["PyYAML\n6.0.2"] 15 | requests["requests\n2.32.3"] 16 | tabulate["tabulate\n0.9.0"] 17 | urllib3["urllib3\n2.4.0"] 18 | wcmatch["wcmatch\n10.0"] 19 | xmltodict["xmltodict\n0.14.2"] 20 | click-extra -- ">=3.1.0" --> extra-platforms 21 | click-extra -- "~=0.14.2" --> xmltodict 22 | click-extra -- "~=0.9" --> tabulate 23 | click-extra -- "~=1.3.4" --> mergedeep 24 | click-extra -- "~=10.0" --> wcmatch 25 | click-extra -- "~=2.32.3" --> requests 26 | click-extra -- "~=25.0.0" --> boltons 27 | click-extra -- "~=3.0.5" --> cloup 28 | click-extra -- "~=6.0.0" --> pyyaml 29 | click-extra -- "~=8.2.0" --> click_0 30 | cloup -- ">=8.0,<9.0" --> click_0 31 | extra-platforms -- "~=1.9.0" --> distro 32 | extra-platforms -- "~=25.0.0" --> boltons 33 | requests -- ">=1.21.1,<3" --> urllib3 34 | requests -- ">=2,<4" --> charset-normalizer 35 | requests -- ">=2.5,<4" --> idna 36 | requests -- ">=2017.4.17" --> certifi 37 | wcmatch -- ">=2.1.1" --> bracex 38 | 39 | -------------------------------------------------------------------------------- /docs/assets/logo-banner-transparent-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdeldycke/click-extra/ca2b8c7e0ad80288fb2de6a9fd0532e61a643b3e/docs/assets/logo-banner-transparent-background.png -------------------------------------------------------------------------------- /docs/assets/logo-banner-white-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdeldycke/click-extra/ca2b8c7e0ad80288fb2de6a9fd0532e61a643b3e/docs/assets/logo-banner-white-background.png -------------------------------------------------------------------------------- /docs/assets/logo-square-transparent-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdeldycke/click-extra/ca2b8c7e0ad80288fb2de6a9fd0532e61a643b3e/docs/assets/logo-square-transparent-background.png -------------------------------------------------------------------------------- /docs/assets/logo-square-white-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdeldycke/click-extra/ca2b8c7e0ad80288fb2de6a9fd0532e61a643b3e/docs/assets/logo-square-white-background.png -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | ```{include} ../changelog.md 2 | ``` 3 | -------------------------------------------------------------------------------- /docs/click_extra.rst: -------------------------------------------------------------------------------- 1 | click\_extra package 2 | ==================== 3 | 4 | .. automodule:: click_extra 5 | :members: 6 | :show-inheritance: 7 | :undoc-members: 8 | 9 | Submodules 10 | ---------- 11 | 12 | click\_extra.colorize module 13 | ---------------------------- 14 | 15 | .. automodule:: click_extra.colorize 16 | :members: 17 | :show-inheritance: 18 | :undoc-members: 19 | 20 | click\_extra.commands module 21 | ---------------------------- 22 | 23 | .. automodule:: click_extra.commands 24 | :members: 25 | :show-inheritance: 26 | :undoc-members: 27 | 28 | click\_extra.config module 29 | -------------------------- 30 | 31 | .. automodule:: click_extra.config 32 | :members: 33 | :show-inheritance: 34 | :undoc-members: 35 | 36 | click\_extra.decorators module 37 | ------------------------------ 38 | 39 | .. automodule:: click_extra.decorators 40 | :members: 41 | :show-inheritance: 42 | :undoc-members: 43 | 44 | click\_extra.docs\_update module 45 | -------------------------------- 46 | 47 | .. automodule:: click_extra.docs_update 48 | :members: 49 | :show-inheritance: 50 | :undoc-members: 51 | 52 | click\_extra.envvar module 53 | -------------------------- 54 | 55 | .. automodule:: click_extra.envvar 56 | :members: 57 | :show-inheritance: 58 | :undoc-members: 59 | 60 | click\_extra.logging module 61 | --------------------------- 62 | 63 | .. automodule:: click_extra.logging 64 | :members: 65 | :show-inheritance: 66 | :undoc-members: 67 | 68 | click\_extra.parameters module 69 | ------------------------------ 70 | 71 | .. automodule:: click_extra.parameters 72 | :members: 73 | :show-inheritance: 74 | :undoc-members: 75 | 76 | click\_extra.pygments module 77 | ---------------------------- 78 | 79 | .. automodule:: click_extra.pygments 80 | :members: 81 | :show-inheritance: 82 | :undoc-members: 83 | 84 | click\_extra.pytest module 85 | -------------------------- 86 | 87 | .. automodule:: click_extra.pytest 88 | :members: 89 | :show-inheritance: 90 | :undoc-members: 91 | 92 | click\_extra.sphinx module 93 | -------------------------- 94 | 95 | .. automodule:: click_extra.sphinx 96 | :members: 97 | :show-inheritance: 98 | :undoc-members: 99 | 100 | click\_extra.tabulate module 101 | ---------------------------- 102 | 103 | .. automodule:: click_extra.tabulate 104 | :members: 105 | :show-inheritance: 106 | :undoc-members: 107 | 108 | click\_extra.telemetry module 109 | ----------------------------- 110 | 111 | .. automodule:: click_extra.telemetry 112 | :members: 113 | :show-inheritance: 114 | :undoc-members: 115 | 116 | click\_extra.testing module 117 | --------------------------- 118 | 119 | .. automodule:: click_extra.testing 120 | :members: 121 | :show-inheritance: 122 | :undoc-members: 123 | 124 | click\_extra.timer module 125 | ------------------------- 126 | 127 | .. automodule:: click_extra.timer 128 | :members: 129 | :show-inheritance: 130 | :undoc-members: 131 | 132 | click\_extra.version module 133 | --------------------------- 134 | 135 | .. automodule:: click_extra.version 136 | :members: 137 | :show-inheritance: 138 | :undoc-members: 139 | -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | ```{include} ../.github/code-of-conduct.md 4 | --- 5 | start-line: 2 6 | --- 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/colorize.md: -------------------------------------------------------------------------------- 1 | # Colored help 2 | 3 | Extend 4 | [Cloup's own help formatter and theme](https://cloup.readthedocs.io/en/stable/pages/formatting.html#help-formatting-and-themes) 5 | to add colorization of: 6 | 7 | - Options 8 | 9 | - Choices 10 | 11 | - Metavars 12 | 13 | - Cli name 14 | 15 | - Sub-commands 16 | 17 | - Command aliases 18 | 19 | - Long and short options 20 | 21 | - Choices 22 | 23 | - Metavars 24 | 25 | - Environment variables 26 | 27 | - Defaults 28 | 29 | ```{todo} 30 | Write examples and tutorial. 31 | ``` 32 | 33 | ## Why not use `rich-click`? 34 | 35 | [`rich-click`](https://github.com/ewels/rich-click) is a good project that aims to integrate [Rich](https://github.com/Textualize/rich) with Click. Like Click Extra, it provides a ready-to-use help formatter for Click. 36 | 37 | But contrary to Click Extra, the [help screen is rendered within a table](https://github.com/ewels/rich-click#styling), which takes the whole width of the terminal. This is not ideal if you try to print the output of a command somewhere else. 38 | 39 | The typical use-case is users reporting issues on GitHub, and pasting the output of a command in the issue description. If the output is too wide, it will be akwardly wrapped, or [adds a horizontal scrollbar](https://github.com/callowayproject/bump-my-version/pull/23#issuecomment-1602007874) to the page. 40 | 41 | Without a table imposing a maximal width, the help screens from Click Extra will be rendered with the minimal width of the text, and will be more readable. 42 | 43 | ```{hint} 44 | This is just a matter of preference, as nothing prevents you to use both `rich-click` and Click Extra in the same project, and get the best from both. 45 | ``` 46 | 47 | ## `color_option` 48 | 49 | ```{todo} 50 | Write examples and tutorial. 51 | ``` 52 | 53 | ## `help_option` 54 | 55 | ```{todo} 56 | Write examples and tutorial. 57 | ``` 58 | 59 | ## Colors and styles 60 | 61 | Here is a little CLI to demonstrate the rendering of colors and styles, based on [`cloup.styling.Style`](https://cloup.readthedocs.io/en/stable/autoapi/cloup/styling/index.html#cloup.styling.Style): 62 | 63 | ```{eval-rst} 64 | .. click:example:: 65 | from click import command 66 | from click_extra import Color, style, Choice, option 67 | from click_extra.tabulate import render_table 68 | 69 | all_styles = [ 70 | "bold", 71 | "dim", 72 | "underline", 73 | "overline", 74 | "italic", 75 | "blink", 76 | "reverse", 77 | "strikethrough", 78 | ] 79 | 80 | all_colors = sorted(Color._dict.values()) 81 | 82 | @command 83 | @option("--matrix", type=Choice(["colors", "styles"])) 84 | def render_matrix(matrix): 85 | table = [] 86 | 87 | if matrix == "colors": 88 | table_headers = ["Foreground ↴ \ Background →"] + all_colors 89 | for fg_color in all_colors: 90 | line = [ 91 | style(fg_color, fg=fg_color) 92 | ] 93 | for bg_color in all_colors: 94 | line.append( 95 | style(fg_color, fg=fg_color, bg=bg_color) 96 | ) 97 | table.append(line) 98 | 99 | elif matrix == "styles": 100 | table_headers = ["Color ↴ \ Style →"] + all_styles 101 | for color_name in all_colors: 102 | line = [ 103 | style(color_name, fg=color_name) 104 | ] 105 | for prop in all_styles: 106 | line.append( 107 | style(color_name, fg=color_name, **{prop: True}) 108 | ) 109 | table.append(line) 110 | 111 | render_table(table, headers=table_headers) 112 | 113 | .. click:run:: 114 | result = invoke(render_matrix, ["--matrix=colors"]) 115 | assert "\x1b[95mbright_magenta\x1b[0m" in result.stdout 116 | assert "\x1b[95m\x1b[101mbright_magenta\x1b[0m" in result.stdout 117 | 118 | .. click:run:: 119 | result = invoke(render_matrix, ["--matrix=styles"]) 120 | assert "\x1b[97mbright_white\x1b[0m" in result.stdout 121 | assert "\x1b[97m\x1b[1mbright_white\x1b[0m" in result.stdout 122 | assert "\x1b[97m\x1b[2mbright_white\x1b[0m" in result.stdout 123 | assert "\x1b[97m\x1b[4mbright_white\x1b[0m" in result.stdout 124 | ``` 125 | 126 | ```{caution} 127 | The current rendering of colors and styles in this HTML documentation is not complete, and does not reflect the real output in a terminal. 128 | 129 | That is because [`pygments-ansi-color`](https://github.com/chriskuehl/pygments-ansi-color), the component we rely on to render ANSI code in Sphinx via Pygments, [only supports a subset of the ANSI codes](https://github.com/chriskuehl/pygments-ansi-color/issues/31) we use. 130 | ``` 131 | 132 | ```{tip} 133 | The code above is presented as a CLI, so you can copy and run it yourself in your environment, and see the output in your terminal. That way you can evaluate the real effect of these styles and colors for your end users. 134 | ``` 135 | 136 | ## `click_extra.colorize` API 137 | 138 | ```{eval-rst} 139 | .. autoclasstree:: click_extra.colorize 140 | :strict: 141 | ``` 142 | 143 | ```{eval-rst} 144 | .. automodule:: click_extra.colorize 145 | :members: 146 | :undoc-members: 147 | :show-inheritance: 148 | ``` 149 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | 6 | if sys.version_info >= (3, 11): 7 | import tomllib 8 | else: 9 | import tomli as tomllib # type: ignore[import-not-found] 10 | 11 | project_path = Path(__file__).parent.parent.resolve() 12 | 13 | # Fetch general information about the project from pyproject.toml. 14 | toml_path = project_path / "pyproject.toml" 15 | toml_config = tomllib.loads(toml_path.read_text()) 16 | 17 | # Redistribute pyproject.toml config to Sphinx. 18 | project_id = toml_config["project"]["name"] 19 | version = release = toml_config["project"]["version"] 20 | url = toml_config["project"]["urls"]["Homepage"] 21 | author = ", ".join(author["name"] for author in toml_config["project"]["authors"]) 22 | 23 | # Title-case each word of the project ID. 24 | project = " ".join(word.title() for word in project_id.split("-")) 25 | htmlhelp_basename = project_id 26 | 27 | # Addons. 28 | extensions = [ 29 | "sphinx.ext.autodoc", 30 | "sphinx.ext.todo", 31 | "sphinx.ext.intersphinx", 32 | "sphinx.ext.viewcode", 33 | # Adds a copy button to code blocks. 34 | "sphinx_copybutton", 35 | "sphinx_design", 36 | # Link to GitHub issues and PRs. 37 | "sphinx_issues", 38 | "sphinxext.opengraph", 39 | "myst_parser", 40 | "sphinx.ext.autosectionlabel", 41 | "sphinx_autodoc_typehints", 42 | "click_extra.sphinx", 43 | "sphinxcontrib.mermaid", 44 | ] 45 | 46 | # https://myst-parser.readthedocs.io/en/latest/syntax/optional.html 47 | myst_enable_extensions = [ 48 | "attrs_block", 49 | "attrs_inline", 50 | "deflist", 51 | "replacements", 52 | "smartquotes", 53 | "strikethrough", 54 | "tasklist", 55 | ] 56 | # XXX Allow ```mermaid``` directive to be used without curly braces (```{mermaid}```), see: 57 | # https://github.com/mgaitan/sphinxcontrib-mermaid/issues/99#issuecomment-2339587001 58 | myst_fence_as_directive = ["mermaid"] 59 | 60 | # Always use the latest version of Mermaid. 61 | mermaid_version = "latest" 62 | mermaid_d3_zoom = True 63 | 64 | master_doc = "index" 65 | 66 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 67 | 68 | nitpicky = True 69 | 70 | # Concatenates the docstrings of the class and the __init__ method. 71 | autoclass_content = "both" 72 | # Keep the same ordering as in original source code. 73 | autodoc_member_order = "bysource" 74 | autodoc_default_flags = ["members", "undoc-members", "show-inheritance"] 75 | 76 | # If true, `todo` and `todoList` produce output, else they produce nothing. 77 | todo_include_todos = True 78 | 79 | # GitHub pre-implemented shortcuts. 80 | github_user = "kdeldycke" 81 | issues_github_path = f"{github_user}/{project_id}" 82 | 83 | intersphinx_mapping = { 84 | "python": ("https://docs.python.org/3", None), 85 | "click": ("https://click.palletsprojects.com", None), 86 | } 87 | 88 | # Prefix document path to section labels, to use: 89 | # `path/to/file:heading` instead of just `heading` 90 | autosectionlabel_prefix_document = True 91 | 92 | # Theme config. 93 | html_theme = "furo" 94 | html_title = project 95 | html_logo = "assets/logo-square.svg" 96 | html_theme_options = { 97 | "sidebar_hide_name": True, 98 | # Activates edit links. 99 | "source_repository": f"https://github.com/{issues_github_path}", 100 | "source_branch": "main", 101 | "source_directory": "docs/", 102 | "announcement": ( 103 | f"{project} works fine, but is maintained by only one person " 104 | "😶‍🌫️.
You can help if you " 105 | "" 107 | "purchase business support 🤝 or " 108 | "" 110 | "sponsor the project 🫶." 111 | ), 112 | } 113 | 114 | # Footer content. 115 | html_last_updated_fmt = "%Y-%m-%d" 116 | copyright = f"{author} and contributors" 117 | html_show_copyright = True 118 | html_show_sphinx = False 119 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide-toc: true 3 | --- 4 | 5 | ```{include} ../readme.md 6 | ``` 7 | 8 | ```{toctree} 9 | --- 10 | maxdepth: 2 11 | hidden: 12 | --- 13 | install 14 | tutorial 15 | commands 16 | config 17 | colorize 18 | logging 19 | tabulate 20 | version 21 | timer 22 | parameters 23 | pygments 24 | sphinx 25 | testing 26 | pytest 27 | issues 28 | ``` 29 | 30 | ```{toctree} 31 | --- 32 | caption: Development 33 | maxdepth: 2 34 | hidden: 35 | --- 36 | API 37 | tests 38 | genindex 39 | modindex 40 | todolist 41 | changelog 42 | code-of-conduct 43 | license 44 | GitHub Repository 45 | Funding 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## With `pip` 4 | 5 | This package is 6 | [available on PyPi](https://pypi.python.org/pypi/click-extra), so you 7 | can install the latest stable release and its dependencies with a simple `pip` 8 | call: 9 | 10 | ```shell-session 11 | $ pip install click-extra 12 | ``` 13 | 14 | See also 15 | [pip installation instructions](https://pip.pypa.io/en/stable/installing/). 16 | 17 | ## Main dependencies 18 | 19 | This is a graph of the default, main dependencies of the Python package: 20 | 21 | ```mermaid assets/dependencies.mmd 22 | :align: center 23 | ``` 24 | 25 | ## Extra dependencies 26 | 27 | For additional features, and to facilitate integration of Click CLIs with third-party tools, you may need to install extra dependencies: 28 | 29 | - [For Pygments](pygments.md): 30 | 31 | ```shell-session 32 | $ pip install click-extra[pygments] 33 | ``` 34 | 35 | - [For Sphinx](sphinx.md): 36 | 37 | ```shell-session 38 | $ pip install click-extra[sphinx] 39 | ``` 40 | 41 | - [For Pytest](pytest.md): 42 | 43 | ```shell-session 44 | $ pip install click-extra[pytest] 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/issues.md: -------------------------------------------------------------------------------- 1 | # Fixed issues 2 | 3 | Click Extra was basically born as a collection of patches for unmaintained or slow moving [`click-contrib` addons](https://github.com/click-contrib). 4 | 5 | Here is the list of issues and bugs from other projects that `click-extra` has addressed over the years. 6 | 7 | ## [`configmanager`](https://github.com/jbasko/configmanager) 8 | 9 | - [`#164` - Should be used?](https://github.com/jbasko/configmanager/issues/164) 10 | - [`#152` - XML format](https://github.com/jbasko/configmanager/issues/152) 11 | - [`#139` - TOML format](https://github.com/jbasko/configmanager/issues/139) 12 | 13 | ## [`click`](https://github.com/pallets/click) 14 | 15 | - [`#2811` - Fix eagerness of help option generated by `help_option_names`](https://github.com/pallets/click/pull/2811) 16 | - [`#2680` - Fix closing of callbacks on CLI exit](https://github.com/pallets/click/pull/2680) 17 | - [`#2523` - Keep track of `` and `` mix in `CliRunner` results ](https://github.com/pallets/click/pull/2523) 18 | - [`#2522` - `CliRunner`: restrict `mix_stderr` influence to ``; keep `` and `` stable](https://github.com/pallets/click/issues/2522) 19 | - [`#2483` - Missing auto-generated environment variables in help screen & case-sensitivity](https://github.com/pallets/click/issues/2483) 20 | - [`#2331` - `version_option` module name and package name are not equivalent](https://github.com/pallets/click/issues/2331) 21 | - [`#2324` - Can't pass `click.version_option()` to `click.MultiCommand(params=)`](https://github.com/pallets/click/issues/2324) 22 | - [`#2313` - Add `show_envvar` as global setting via `context_settings` (like `show_default`)](https://github.com/pallets/click/issues/2313) 23 | - [`#2207` - Support `NO_COLOR` environment variable](https://github.com/pallets/click/issues/2207) 24 | - [`#2111` - `Context.color = False` doesn't overrides `echo(color=True)`](https://github.com/pallets/click/issues/2111) 25 | - [`#2110` - `testing.CliRunner.invoke` cannot pass color for `Context` instantiation](https://github.com/pallets/click/issues/2110) 26 | - [`#1756` - Path and Python version for version message formatting](https://github.com/pallets/click/issues/1756) 27 | - [`#1498` - Support for `NO_COLOR` proposal](https://github.com/pallets/click/issues/1498) 28 | - [`#1279` - Provide access to a normalized list of args](https://github.com/pallets/click/issues/1279) 29 | - [`#1090` - Color output from CI jobs](https://github.com/pallets/click/issues/1090) 30 | - [`#558` - Support `FORCE_COLOR` in `click.echo`](https://github.com/pallets/click/issues/558) 31 | 32 | ## [`click-config-file`](https://github.com/phha/click_config_file) 33 | 34 | - [`#26` - Don't require `FILE` for `--config` when `implicit=False`?](https://github.com/phha/click_config_file/issues/26) 35 | - [`#11` - Warn when providing unsupported options in the config file?](https://github.com/phha/click_config_file/issues/11) 36 | - [`#9` - Additional configuration providers](https://github.com/phha/click_config_file/issues/9) 37 | 38 | ## [`click-configfile`](https://github.com/click-contrib/click-configfile) 39 | 40 | - [`#9` - Inquiry on Repo Status](https://github.com/click-contrib/click-configfile/issues/9) 41 | - [`#8` - Exclude some options from config file](https://github.com/click-contrib/click-configfile/issues/8) 42 | - [`#5` - Interpolation](https://github.com/click-contrib/click-configfile/issues/5) 43 | - [`#2` - Order of configuration file and environment variable value](https://github.com/click-contrib/click-configfile/issues/2) 44 | 45 | ## [`click-help-color`](https://github.com/click-contrib/click-help-colors) 46 | 47 | - [`#17` - Highlighting of options, choices and metavars](https://github.com/click-contrib/click-help-colors/issues/17) 48 | 49 | ## [`click-log`](https://github.com/click-contrib/click-log) 50 | 51 | - [`#30` - Add a `no-color` option, method or parameter to disable coloring globally](https://github.com/click-contrib/click-log/issues/30) 52 | - [`#29` - Log level is leaking between invocations: hack to force-reset it](https://github.com/click-contrib/click-log/issues/29) 53 | - [`#24` - Add missing string interpolation in error message](https://github.com/click-contrib/click-log/pull/24) 54 | - [`#18` - Add trailing dot to help text](https://github.com/click-contrib/click-log/pull/18) 55 | 56 | ## [`cli-helper`](https://github.com/dbcli/cli_helpers) 57 | 58 | - [`#79` - Replace local tabulate formats with those available upstream](https://github.com/dbcli/cli_helpers/issues/79) 59 | 60 | ## [`cloup`](https://github.com/janluke/cloup) 61 | 62 | - [`#127` - Optional parenthesis for `@command` and `@option`](https://github.com/janluke/cloup/issues/127) 63 | - [`#98` - Add support for option groups on `cloup.Group`](https://github.com/janluke/cloup/issues/98) 64 | - [`#97` - Styling metavars, default values, env var, choices](https://github.com/janluke/cloup/issues/97) 65 | - [`#96` - Add loading of options from a TOML configuration file](https://github.com/janluke/cloup/issues/96) 66 | - [`#95` - Highlights options, choices and metavars](https://github.com/janluke/cloup/issues/95) 67 | - [`#92` - Use sphinx directive to generate output of full examples in the docs](https://github.com/janluke/cloup/issues/92) 68 | 69 | ## [`kitty`](https://github.com/kovidgoyal/kitty) 70 | 71 | - [`#5482` - ANSI shell sessions in Sphinx documentation](https://github.com/kovidgoyal/kitty/discussions/5482) 72 | 73 | ## [`pallets-sphinx-themes`](https://github.com/pallets/pallets-sphinx-themes) 74 | 75 | - [`#62` - Render `.. click:run::` code blocks with `shell-session` lexer](https://github.com/pallets/pallets-sphinx-themes/pull/62) 76 | - [`#61` - Move `.. click:example::` and `.. click:run::` implementation to `sphinx-click`](https://github.com/pallets/pallets-sphinx-themes/issues/61) 77 | 78 | ## [`pygments`](https://github.com/pygments/pygments) 79 | 80 | - [`#1148` - Can't format console/shell-session output that includes ANSI colors](https://github.com/pygments/pygments/issues/1148) 81 | - [`#477` - Support ANSI (ECMA-48) color-coded text input](https://github.com/pygments/pygments/issues/477) 82 | 83 | ## [`python-tabulate`](https://github.com/astanin/python-tabulate) 84 | 85 | - [`#261` - Add support for alignments in Markdown tables](https://github.com/astanin/python-tabulate/pull/261) 86 | - [`#260` - Renders GitHub-Flavored Markdown tables in canonical format](https://github.com/astanin/python-tabulate/pull/260) 87 | - [`#151` - Add new {`rounded`,`simple`,`double`}\_(`grid`,`outline`} formats](https://github.com/astanin/python-tabulate/pull/151) 88 | 89 | ## [`rich-click`](https://github.com/ewels/rich-click) 90 | 91 | - [`#101` - Command Aliases?](https://github.com/ewels/rich-click/issues/101) 92 | - [`#18` - Options inherited from context settings aren't applied](https://github.com/ewels/rich-click/issues/18) 93 | 94 | ## [`sphinx-click`](https://github.com/click-contrib/sphinx-click) 95 | 96 | - [`#117` - Add reference to complementary Click CLI documentation helpers](https://github.com/click-contrib/sphinx-click/pull/117) 97 | - [`#110` - Supports `.. click:example::` and `.. click:run::` directives](https://github.com/click-contrib/sphinx-click/issues/110) 98 | 99 | ## [`sphinx-contrib/ansi`](https://github.com/sphinx-contrib/ansi) 100 | 101 | - [`#9` - ANSI Codes in output](https://github.com/sphinx-contrib/ansi/issues/9) 102 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | This software is licensed under the 4 | [GNU General Public License v2 or later (GPLv2+)](https://github.com/kdeldycke/click-extra/blob/main/license). 5 | 6 | ```{literalinclude} ../license 7 | ``` 8 | 9 | ## Additional credits 10 | 11 | The following images are sourced from [Open Clipart](https://openclipart.org) 12 | which are distributed under a 13 | [Creative Commons Zero 1.0 Public Domain License](http://creativecommons.org/publicdomain/zero/1.0/): 14 | 15 | - [cube logo](https://github.com/kdeldycke/click-extra/blob/main/docs/assets/logo-banner.svg) 16 | is based on a modified 17 | [Prismatic Isometric Cube Extra Pattern](https://openclipart.org/detail/266153/prismatic-isometric-cube-extra-pattern-no-background) 18 | -------------------------------------------------------------------------------- /docs/parameters.md: -------------------------------------------------------------------------------- 1 | # Parameters 2 | 3 | Click Extra provides a set of tools to help you manage parameters in your CLI. 4 | 5 | Like the magical `--show-params` option, which is a X-ray scanner for your CLI's parameters. 6 | 7 | ## Parameter structure 8 | 9 | ```{todo} 10 | Write example and tutorial. 11 | ``` 12 | 13 | ## Introspecting parameters 14 | 15 | If for any reason you need to dive into parameters and their values, there is a lot of intermediate and metadata available in the context. Here are some pointers: 16 | 17 | ```{code-block} python 18 | from click import option, echo, pass_context 19 | 20 | from click_extra import config_option, extra_group 21 | 22 | @extra_group 23 | @option("--dummy-flag/--no-flag") 24 | @option("--my-list", multiple=True) 25 | @config_option 26 | @pass_context 27 | def my_cli(ctx, dummy_flag, my_list): 28 | echo(f"dummy_flag is {dummy_flag!r}") 29 | echo(f"my_list is {my_list!r}") 30 | echo(f"Raw parameters: {ctx.meta.get('click_extra.raw_args', [])}") 31 | echo(f"Loaded, default values: {ctx.default_map}") 32 | echo(f"Values passed to function: {ctx.params}") 33 | 34 | @my_cli.command() 35 | @option("--int-param", type=int, default=10) 36 | def subcommand(int_param): 37 | echo(f"int_parameter is {int_param!r}") 38 | ``` 39 | 40 | ```{caution} 41 | The `click_extra.raw_args` metadata field in the context referenced above is not a standard feature from Click, but a helper introduced by Click Extra. It is only available with `@extra_group` and `@extra_command` decorators. 42 | 43 | In the mean time, it is [being discussed in the Click community at `click#1279`](https://github.com/pallets/click/issues/1279#issuecomment-1493348208). 44 | ``` 45 | 46 | ```{todo} 47 | Propose the `raw_args` feature upstream to Click. 48 | ``` 49 | 50 | Now if we feed the following `~/configuration.toml` configuration file: 51 | 52 | ```toml 53 | [my-cli] 54 | verbosity = "DEBUG" 55 | dummy_flag = true 56 | my_list = ["item 1", "item #2", "Very Last Item!"] 57 | 58 | [my-cli.subcommand] 59 | int_param = 3 60 | ``` 61 | 62 | Here is what we get: 63 | 64 | ```{code-block} shell-session 65 | $ cli --config ~/configuration.toml default-command 66 | dummy_flag is True 67 | my_list is ('item 1', 'item #2', 'Very Last Item!') 68 | Raw parameters: ['--config', '~/configuration.toml', 'default-command'] 69 | Loaded, default values: {'dummy_flag': True, 'my_list': ['pip', 'npm', 'gem'], 'verbosity': 'DEBUG', 'default-command': {'int_param': 3}} 70 | Values passed to function: {'dummy_flag': True, 'my_list': ('pip', 'npm', 'gem')} 71 | ``` 72 | 73 | ## `--show-params` option 74 | 75 | Click Extra provides a ready-to-use `--show-params` option, which is enabled by default. 76 | 77 | It produces a comprehensive table of your CLI parameters, normalized IDs, types and corresponding environment variables. And because it dynamiccaly print their default value, actual value and its source, it is a practical tool for users to introspect and debug the parameters of a CLI. 78 | 79 | See how the default `@extra_command` decorator come with the default `--show-params` option and the result of its use: 80 | 81 | ```{eval-rst} 82 | .. click:example:: 83 | from click_extra import extra_command, option, echo 84 | 85 | @extra_command 86 | @option("--int-param1", type=int, default=10) 87 | @option("--int-param2", type=int, default=555) 88 | def cli(int_param1, int_param2): 89 | echo(f"int_param1 is {int_param1!r}") 90 | echo(f"int_param2 is {int_param2!r}") 91 | 92 | .. click:run:: 93 | result = invoke(cli, args=["--verbosity", "Debug", "--int-param1", "3", "--show-params"]) 94 | assert "click_extra.raw_args: ['--verbosity', 'Debug', '--int-param1', '3', '--show-params']" in result.stderr 95 | assert "│ \x1b[33m\x1b[2mCLI_INT_PARAM1\x1b[0m │ \x1b[32m\x1b[2m\x1b[3m10\x1b[0m " in result.stdout 96 | assert "│ \x1b[33m\x1b[2mCLI_INT_PARAM2\x1b[0m │ \x1b[32m\x1b[2m\x1b[3m555\x1b[0m " in result.stdout 97 | ``` 98 | 99 | ```{note} 100 | Notice how `--show-params` is showing all parameters, even those provided to the `excluded_params` argument. You can still see the `--help`, `--version`, `-C`/`--config` and `--show-params` options in the table. 101 | ``` 102 | 103 | ## `click_extra.parameters` API 104 | 105 | ```{eval-rst} 106 | .. autoclasstree:: click_extra.parameters 107 | :strict: 108 | ``` 109 | 110 | ```{eval-rst} 111 | .. automodule:: click_extra.parameters 112 | :members: 113 | :undoc-members: 114 | :show-inheritance: 115 | ``` 116 | -------------------------------------------------------------------------------- /docs/pytest.md: -------------------------------------------------------------------------------- 1 | # Pytest utilities and fixtures 2 | 3 | ````{important} 4 | For these helpers to work, you need to install ``click_extra``'s additional dependencies from the ``pytest`` extra group: 5 | 6 | ```shell-session 7 | $ pip install click_extra[pytest] 8 | ``` 9 | ```` 10 | 11 | ```{todo} 12 | Write example and tutorial. 13 | ``` 14 | 15 | ## `click_extra.pytest` API 16 | 17 | ```{eval-rst} 18 | .. autoclasstree:: click_extra.pytest 19 | :strict: 20 | ``` 21 | 22 | ```{eval-rst} 23 | .. automodule:: click_extra.pytest 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/tabulate.md: -------------------------------------------------------------------------------- 1 | # Table 2 | 3 | Click Extra provides a way to render tables in the terminal. 4 | 5 | Here how to use the standalone table rendering option decorator: 6 | 7 | ```{eval-rst} 8 | .. click:example:: 9 | from click_extra import command, echo, pass_context, table_format_option 10 | 11 | @command 12 | @table_format_option 13 | @pass_context 14 | def table_command(ctx): 15 | data = ((1, 87), (2, 80), (3, 79)) 16 | headers = ("day", "temperature") 17 | ctx.print_table(data, headers) 18 | 19 | As you can see above, this option adds a ready-to-use ``print_table()`` method to the context object. 20 | 21 | The default help message for this option list all available table formats: 22 | 23 | .. click:run:: 24 | result = invoke(table_command, args=["--help"]) 25 | assert "-t, --table-format" in result.stdout 26 | 27 | So you can use the ``--table-format`` option to change the table format: 28 | 29 | .. click:run:: 30 | from textwrap import dedent 31 | 32 | result = invoke(table_command, args=["--table-format", "fancy_outline"]) 33 | assert result.stdout == dedent("""\ 34 | ╒═════╤═════════════╕ 35 | │ day │ temperature │ 36 | ╞═════╪═════════════╡ 37 | │ 1 │ 87 │ 38 | │ 2 │ 80 │ 39 | │ 3 │ 79 │ 40 | ╘═════╧═════════════╛ 41 | """) 42 | 43 | .. click:run:: 44 | from textwrap import dedent 45 | 46 | result = invoke(table_command, args=["--table-format", "jira"]) 47 | assert result.stdout == dedent("""\ 48 | || day || temperature || 49 | | 1 | 87 | 50 | | 2 | 80 | 51 | | 3 | 79 | 52 | """) 53 | ``` 54 | 55 | ### Table formats 56 | 57 | Available table [formats are inherited from `python-tabulate`](https://github.com/astanin/python-tabulate#table-format). 58 | 59 | This list is augmented with extra formats: 60 | 61 | - `csv` 62 | - `csv-excel` 63 | - `csv-excel-tab` 64 | - `csv-unix` 65 | - `vertical` 66 | 67 | ```{todo} 68 | Explicitly list all formats IDs and render an example of each format. 69 | ``` 70 | 71 | ```{todo} 72 | Explain extra parameters supported by `print_table()` for each category of formats. 73 | ``` 74 | 75 | ### Get table format 76 | 77 | You can get the ID of the current table format from the context: 78 | 79 | ```{eval-rst} 80 | .. click:example:: 81 | from click_extra import command, echo, pass_context, table_format_option 82 | 83 | @command 84 | @table_format_option 85 | @pass_context 86 | def vanilla_command(ctx): 87 | format_id = ctx.meta["click_extra.table_format"] 88 | echo(f"Table format: {format_id}") 89 | 90 | data = ((1, 87), (2, 80), (3, 79)) 91 | headers = ("day", "temperature") 92 | ctx.print_table(data, headers) 93 | 94 | .. click:run:: 95 | result = invoke(vanilla_command, args=["--table-format", "fancy_outline"]) 96 | assert "Table format: fancy_outline" in result.stdout 97 | ``` 98 | 99 | ## `click_extra.tabulate` API 100 | 101 | ```{eval-rst} 102 | .. autoclasstree:: click_extra.tabulate 103 | :strict: 104 | ``` 105 | 106 | ```{eval-rst} 107 | .. automodule:: click_extra.tabulate 108 | :members: 109 | :undoc-members: 110 | :show-inheritance: 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # CLI testing and execution 2 | 3 | ```{todo} 4 | Write example and tutorial. 5 | ``` 6 | 7 | ## `click_extra.testing` API 8 | 9 | ```{eval-rst} 10 | .. autoclasstree:: click_extra.testing 11 | :strict: 12 | ``` 13 | 14 | ```{eval-rst} 15 | .. automodule:: click_extra.testing 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/tests.rst: -------------------------------------------------------------------------------- 1 | tests package 2 | ============= 3 | 4 | .. automodule:: tests 5 | :members: 6 | :show-inheritance: 7 | :undoc-members: 8 | 9 | Submodules 10 | ---------- 11 | 12 | tests.conftest module 13 | --------------------- 14 | 15 | .. automodule:: tests.conftest 16 | :members: 17 | :show-inheritance: 18 | :undoc-members: 19 | 20 | tests.test\_colorize module 21 | --------------------------- 22 | 23 | .. automodule:: tests.test_colorize 24 | :members: 25 | :show-inheritance: 26 | :undoc-members: 27 | 28 | tests.test\_commands module 29 | --------------------------- 30 | 31 | .. automodule:: tests.test_commands 32 | :members: 33 | :show-inheritance: 34 | :undoc-members: 35 | 36 | tests.test\_config module 37 | ------------------------- 38 | 39 | .. automodule:: tests.test_config 40 | :members: 41 | :show-inheritance: 42 | :undoc-members: 43 | 44 | tests.test\_envvar module 45 | ------------------------- 46 | 47 | .. automodule:: tests.test_envvar 48 | :members: 49 | :show-inheritance: 50 | :undoc-members: 51 | 52 | tests.test\_logging module 53 | -------------------------- 54 | 55 | .. automodule:: tests.test_logging 56 | :members: 57 | :show-inheritance: 58 | :undoc-members: 59 | 60 | tests.test\_parameters module 61 | ----------------------------- 62 | 63 | .. automodule:: tests.test_parameters 64 | :members: 65 | :show-inheritance: 66 | :undoc-members: 67 | 68 | tests.test\_pygments module 69 | --------------------------- 70 | 71 | .. automodule:: tests.test_pygments 72 | :members: 73 | :show-inheritance: 74 | :undoc-members: 75 | 76 | tests.test\_tabulate module 77 | --------------------------- 78 | 79 | .. automodule:: tests.test_tabulate 80 | :members: 81 | :show-inheritance: 82 | :undoc-members: 83 | 84 | tests.test\_telemetry module 85 | ---------------------------- 86 | 87 | .. automodule:: tests.test_telemetry 88 | :members: 89 | :show-inheritance: 90 | :undoc-members: 91 | 92 | tests.test\_testing module 93 | -------------------------- 94 | 95 | .. automodule:: tests.test_testing 96 | :members: 97 | :show-inheritance: 98 | :undoc-members: 99 | 100 | tests.test\_timer module 101 | ------------------------ 102 | 103 | .. automodule:: tests.test_timer 104 | :members: 105 | :show-inheritance: 106 | :undoc-members: 107 | 108 | tests.test\_version module 109 | -------------------------- 110 | 111 | .. automodule:: tests.test_version 112 | :members: 113 | :show-inheritance: 114 | :undoc-members: 115 | -------------------------------------------------------------------------------- /docs/timer.md: -------------------------------------------------------------------------------- 1 | # Timer 2 | 3 | ## Option 4 | 5 | Click Extra can measure the execution time of a CLI via a dedicated `--time`/`--no-time` option. 6 | 7 | Here how to use the standalone decorator: 8 | 9 | ```{eval-rst} 10 | .. click:example:: 11 | from time import sleep 12 | 13 | from click_extra import command, echo, pass_context, timer_option 14 | 15 | @command 16 | @timer_option 17 | def timer(): 18 | sleep(0.2) 19 | echo("Hello world!") 20 | 21 | .. click:run:: 22 | result = invoke(timer, args=["--help"]) 23 | assert "--time / --no-time" in result.stdout 24 | 25 | .. click:run:: 26 | import re 27 | 28 | result = invoke(timer, ["--time"]) 29 | assert re.fullmatch( 30 | r"Hello world!\n" 31 | r"Execution time: (?P