diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 18cdff1942..0000000000 --- a/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[run] - -source = - typer - tests - docs_src - -parallel = True diff --git a/.env b/.env new file mode 100644 index 0000000000..aa298973cb --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +# Environment variables automatically read by VS Code, e.g. running tests + +# For tests, a large terminal width +TERMINAL_WIDTH=3000 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 0ffc101a3f..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: [tiangolo] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 55749398fd..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,4 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Security Contact - about: Please report security vulnerabilities to security@tiangolo.com diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index faa58a6d08..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: Feature Request -description: Suggest an idea or ask for a feature that you would like to have in Typer -labels: [enhancement] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in Typer! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. - - All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. - - If more Typer users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title to this issue. - required: true - - label: I used the GitHub search to find a similar issue and didn't find it. - required: true - - label: I searched the Typer documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in Typer" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to Typer but to [Click](https://github.com/pallets/click). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - After submitting this, I commit to one of: - - * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. - * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - import typer - - - def main(name: str): - typer.echo(f"Hello {name}") - - - if __name__ == "__main__": - typer.run(main) - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is your feature request? - - Write a short description telling me what you are trying to solve and what you are currently doing. - placeholder: | - * Create a Typer CLI that says "Hello". - * Run it on the command line without a name. - * I would like it to use Privacy-Aware Artificial Intelligence based on Block Chain and predict my name without me typing it. - validations: - required: true - - type: textarea - id: wanted-solution - attributes: - label: Wanted Solution - description: | - Tell me what's the solution you would like. - placeholder: | - I would like it to say my name without me having to type it. - validations: - required: true - - type: textarea - id: wanted-code - attributes: - label: Wanted Code - description: Show me an example of how you would want the code to look like. - placeholder: | - import typer - - - def main(name: str): - typer.guess_name(f"Hello {name}") - - - if __name__ == "__main__": - typer.run(main) - render: python - validations: - required: true - - type: textarea - id: alternatives - attributes: - label: Alternatives - description: | - Tell me about alternatives you've considered. - placeholder: | - To hardcode my name, so that if I don't type it it will be the default. - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: typer-version - attributes: - label: Typer Version - description: | - What Typer version are you using? - - You can find the Typer version with: - - ```bash - python -c "import typer; print(typer.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml deleted file mode 100644 index 62ea9eb30d..0000000000 --- a/.github/ISSUE_TEMPLATE/question.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: Question or Problem -description: Ask a question or ask about a problem -labels: [question] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in Typer! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. - - All that, on top of all the incredible help provided by a bunch of community members that give a lot of their time to come here and help others. - - If more Typer users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title to this issue. - required: true - - label: I used the GitHub search to find a similar issue and didn't find it. - required: true - - label: I searched the Typer documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in Typer" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to Typer but to [Click](https://github.com/pallets/click). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - After submitting this, I commit to one of: - - * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. - * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - import typer - - - def main(name: str): - typer.echo(f"Hello {name}") - - - if __name__ == "__main__": - typer.run(main) - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is the problem, question, or error? - - Write a short description telling me what you are doing, what you expect to happen, and what is currently happening. - placeholder: | - * Create a small Typer script. - * Open a Terminal with Ninja-Turtle-Shell. - * Trigger autocompletion hitting TAB. - * I don't see any completion in the terminal using Ninja-Turtle-Shell. - * I expected to see autocompletion there. - validations: - required: true - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: typer-version - attributes: - label: Typer Version - description: | - What Typer version are you using? - - You can find the Typer version with: - - ```bash - python -c "import typer; print(typer.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. diff --git a/.github/actions/comment-docs-preview-in-pr/Dockerfile b/.github/actions/comment-docs-preview-in-pr/Dockerfile deleted file mode 100644 index 4f20c5f10b..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.7 - -RUN pip install httpx "pydantic==1.5.1" pygithub - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/comment-docs-preview-in-pr/action.yml b/.github/actions/comment-docs-preview-in-pr/action.yml deleted file mode 100644 index 0eb64402d2..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/action.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Comment Docs Preview in PR -description: Comment with the docs URL preview in the PR -author: Sebastián Ramírez -inputs: - token: - description: Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }} - required: true - deploy_url: - description: The deployment URL to comment in the PR - required: true -runs: - using: docker - image: Dockerfile diff --git a/.github/actions/comment-docs-preview-in-pr/app/main.py b/.github/actions/comment-docs-preview-in-pr/app/main.py deleted file mode 100644 index c9fb7cbbef..0000000000 --- a/.github/actions/comment-docs-preview-in-pr/app/main.py +++ /dev/null @@ -1,68 +0,0 @@ -import logging -import sys -from pathlib import Path -from typing import Optional - -import httpx -from github import Github -from github.PullRequest import PullRequest -from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError - -github_api = "https://api.github.com" - - -class Settings(BaseSettings): - github_repository: str - github_event_path: Path - github_event_name: Optional[str] = None - input_token: SecretStr - input_deploy_url: str - - -class PartialGithubEventHeadCommit(BaseModel): - id: str - - -class PartialGithubEventWorkflowRun(BaseModel): - head_commit: PartialGithubEventHeadCommit - - -class PartialGithubEvent(BaseModel): - workflow_run: PartialGithubEventWorkflowRun - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - try: - event = PartialGithubEvent.parse_file(settings.github_event_path) - except ValidationError as e: - logging.error(f"Error parsing event file: {e.errors()}") - sys.exit(0) - use_pr: Optional[PullRequest] = None - for pr in repo.get_pulls(): - if pr.head.sha == event.workflow_run.head_commit.id: - use_pr = pr - break - if not use_pr: - logging.error(f"No PR found for hash: {event.workflow_run.head_commit.id}") - sys.exit(0) - github_headers = { - "Authorization": f"token {settings.input_token.get_secret_value()}" - } - url = f"{github_api}/repos/{settings.github_repository}/issues/{use_pr.number}/comments" - logging.info(f"Using comments URL: {url}") - response = httpx.post( - url, - headers=github_headers, - json={ - "body": f"📝 Docs preview for commit {use_pr.head.sha} at: {settings.input_deploy_url}" - }, - ) - if not (200 <= response.status_code <= 300): - logging.error(f"Error posting comment: {response.text}") - sys.exit(1) - logging.info("Finished") diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b38df29f46..d2fd71e929 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,57 @@ version: 2 updates: - - package-ecosystem: "pip" + # GitHub Actions + - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "monthly" + cooldown: + default-days: 7 + commit-message: + prefix: ⬆ + labels: + - "internal" + - "dependencies" + - "github_actions" + groups: + github-actions: + patterns: + - "*" + # Python + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "monthly" + cooldown: + default-days: 7 + commit-message: + prefix: ⬆ + groups: + python-packages: + dependency-type: "development" + patterns: + - "*" + - package-ecosystem: "docker" + directory: "/scripts/docker" + schedule: + interval: "monthly" + cooldown: + default-days: 7 + commit-message: + prefix: ⬆ + groups: + docker-images: + patterns: + - "*" + - package-ecosystem: "docker-compose" + directory: "/scripts/docker" + schedule: + interval: "monthly" + cooldown: + default-days: 7 + commit-message: + prefix: ⬆ + groups: + docker-compose: + patterns: + - "*" diff --git a/.github/latest-changes.yml b/.github/latest-changes.yml new file mode 100644 index 0000000000..d811d979e4 --- /dev/null +++ b/.github/latest-changes.yml @@ -0,0 +1,10 @@ +auto-labels: + docs: + - docs/** + - docs_src/** + internal: + - .github/** + - scripts/** + - .gitignore + - .pre-commit-config.yaml + - uv.lock diff --git a/.github/pr-push.yml b/.github/pr-push.yml new file mode 100644 index 0000000000..1591365077 --- /dev/null +++ b/.github/pr-push.yml @@ -0,0 +1,2 @@ +workflows: + - .github/workflows/pre-commit.yml diff --git a/.github/pr-submit.yml b/.github/pr-submit.yml new file mode 100644 index 0000000000..c9aa524192 --- /dev/null +++ b/.github/pr-submit.yml @@ -0,0 +1,3 @@ +workflows: + - .github/workflows/bump-pre-commit-hooks.yml + - .github/workflows/prepare-release.yml diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml new file mode 100644 index 0000000000..35d089860c --- /dev/null +++ b/.github/workflows/add-to-project.yml @@ -0,0 +1,21 @@ +name: Add to Project + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] + issues: + types: + - opened + - reopened + +permissions: {} + +jobs: + add-to-project: + name: Add to project + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0 + with: + project-url: https://github.com/orgs/fastapi/projects/2 + github-token: ${{ secrets.PROJECTS_TOKEN }} # zizmor: ignore[secrets-outside-env] diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 94950f4a15..962180943f 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -4,69 +4,85 @@ on: branches: - master pull_request: - types: [opened, synchronize] - workflow_dispatch: - inputs: - debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' - required: false - default: false +permissions: {} + jobs: + changes: + runs-on: ubuntu-latest + # Required permissions + permissions: + pull-requests: read + timeout-minutes: 5 + # Set job outputs to values from filter step + outputs: + docs: ${{ steps.filter.outputs.docs }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # For pull requests it's not necessary to checkout the code but for the main branch it is + with: + persist-credentials: false + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: filter + with: + filters: | + docs: + - README.md + - docs/** + - docs_src/** + - pyproject.toml + - uv.lock + - mkdocs.yml + - mkdocs.env.yml + - .github/workflows/build-docs.yml + - .github/workflows/deploy-docs.yml + - data/** + build-docs: - runs-on: ubuntu-20.04 + needs: + - changes + if: ${{ needs.changes.outputs.docs == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - python-version: "3.7" - # Allow debugging with tmate - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - limit-access-to-actor: true - - uses: actions/cache@v2 - id: cache + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v01 - - name: Install Flit - if: steps.cache.outputs.cache-hit != 'true' - run: python3.7 -m pip install flit + version: "latest-known" + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock - name: Install docs extras - if: steps.cache.outputs.cache-hit != 'true' - run: python3.7 -m flit install --extras doc - - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: python3.7 -m pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git - - uses: actions/cache@v2 - with: - key: mkdocs-cards-${{ github.ref }}-v1 - path: .cache + run: uv sync --locked --no-dev --group docs - name: Build Docs - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true - run: python3.7 -m mkdocs build - - name: Build Docs with Insiders - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false - run: python3.7 -m mkdocs build --config-file mkdocs.insiders.yml - - name: Zip docs - run: bash ./scripts/zip-docs.sh - - uses: actions/upload-artifact@v2 + run: uv run ./scripts/docs.py build + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: docs-zip - path: ./docs.zip - - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v1.1.5 + name: docs-site + path: ./site/** + include-hidden-files: true + + # https://github.com/marketplace/actions/alls-green#why + docs-all-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - build-docs + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0 with: - publish-dir: './site' - production-branch: master - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + jobs: ${{ toJSON(needs) }} + allowed-skips: build-docs diff --git a/.github/workflows/bump-pre-commit-hooks.yml b/.github/workflows/bump-pre-commit-hooks.yml new file mode 100644 index 0000000000..fa08b0768d --- /dev/null +++ b/.github/workflows/bump-pre-commit-hooks.yml @@ -0,0 +1,71 @@ +name: Bump pre-commit hooks + +on: + schedule: + - cron: "0 12 1 * *" + workflow_dispatch: + +permissions: {} + +jobs: + bump-pre-commit-hooks: + if: github.repository_owner == 'fastapi' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Bump pre-commit hooks + run: uv run prek update --freeze --cooldown-days 7 + - name: Get PR Submit token + id: pr-submit + uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1 + - name: Create pull request + env: + GH_TOKEN: ${{ steps.pr-submit.outputs.token }} + BASE_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + if git diff --quiet; then + echo "No pre-commit hook updates available" + exit 0 + fi + git config user.name "pr-submit[bot]" + git config user.email "pr-submit[bot]@users.noreply.github.com" + branch="bump-pre-commit-hooks" + git switch -C "$branch" + git add .pre-commit-config.yaml + git commit -m "⬆ Bump pre-commit hooks" + gh auth setup-git + git push --force origin "$branch" + if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then + gh pr create \ + --base "$BASE_BRANCH" \ + --head "$branch" \ + --title "⬆ Bump pre-commit hooks" \ + --body "Bump pre-commit hook versions via \`prek update --freeze --cooldown-days 7\`." \ + --label internal \ + --label dependencies \ + --label pre-commit + else + echo "PR for \"$branch\" already open; branch updated in place." + fi diff --git a/.github/workflows/conflict.yml b/.github/workflows/conflict.yml new file mode 100644 index 0000000000..ecd288ba19 --- /dev/null +++ b/.github/workflows/conflict.yml @@ -0,0 +1,22 @@ +name: "Conflict detector" +on: + push: + pull_request_target: # zizmor: ignore[dangerous-triggers] + types: [synchronize] + +permissions: {} + +jobs: + main: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check if PRs have merge conflicts + uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0 + with: + dirtyLabel: "conflicts" + repoToken: "${{ secrets.GITHUB_TOKEN }}" + commentOnDirty: "This pull request has a merge conflict that needs to be resolved." diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml new file mode 100644 index 0000000000..0fcc58ec92 --- /dev/null +++ b/.github/workflows/create-draft-release.yml @@ -0,0 +1,54 @@ +name: Create Draft Release + +on: + pull_request: + types: + - closed + +permissions: {} + +jobs: + create-draft-release: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + env: + PREPARE_RELEASE_VERSION_FILE: typer/__init__.py + PREPARE_RELEASE_RELEASE_NOTES_FILE: docs/release-notes.md + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.repository.default_branch }} + persist-credentials: true + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + - name: Install uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + - name: Extract release details + id: release-details + run: | + set -euo pipefail + version="$(uv run python scripts/prepare_release.py current-version)" + uv run python scripts/prepare_release.py release-notes > draft-release-notes.md + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Create draft release + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.release-details.outputs.version }} + run: | + set -euo pipefail + gh release create "$VERSION" \ + --draft \ + --title "$VERSION" \ + --notes-file draft-release-notes.md \ + --target "$(git rev-parse HEAD)" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..ca148c95f0 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,84 @@ +name: Deploy Docs +on: + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: + - Build Docs + types: + - completed + +permissions: {} + +jobs: + deploy-docs: + runs-on: ubuntu-latest + permissions: + deployments: write + issues: write + pull-requests: write + statuses: write + timeout-minutes: 5 + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + enable-cache: false + - name: Install GitHub Actions dependencies + run: uv sync --locked --no-dev --group github-actions + - name: Deploy Docs Status Pending + run: uv run ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "pending" + - name: Clean site + run: | + rm -rf ./site + mkdir ./site + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + path: ./site/ + pattern: docs-site + merge-multiple: true + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + - name: Deploy to Cloudflare Pages + # hashFiles returns an empty string if there are no files + if: hashFiles('./site/*') + id: deploy + env: + PROJECT_NAME: typertiangolo + BRANCH: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' && 'main' ) || ( github.event.workflow_run.head_sha ) }} + uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} # zizmor: ignore[secrets-outside-env] + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # zizmor: ignore[secrets-outside-env] + command: pages deploy ./site --project-name=${{ env.PROJECT_NAME }} --branch=${{ env.BRANCH }} + - name: Deploy Docs Status Error + if: failure() + run: uv run ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "error" + - name: Comment Deploy + run: uv run ./scripts/deploy_docs_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_ID: ${{ github.run_id }} + STATE: "success" diff --git a/.github/workflows/guard-dependencies.yml b/.github/workflows/guard-dependencies.yml new file mode 100644 index 0000000000..c3f97c3752 --- /dev/null +++ b/.github/workflows/guard-dependencies.yml @@ -0,0 +1,52 @@ +name: Guard Dependencies + +on: + pull_request_target: # zizmor: ignore[dangerous-triggers] -- This workflow only reads context.payload metadata, never checks out PR code + branches: [master] + paths: + - pyproject.toml + - uv.lock + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + check-author: + runs-on: ubuntu-latest + steps: + - name: Check if author is org member or allowed bot + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const pr = context.payload.pull_request; + const author = pr.user.login; + const assoc = pr.author_association; + + const botAllowlist = new Set(['dependabot[bot]']); + const orgAuthorAssociations = new Set(['MEMBER', 'OWNER']); + + const allowed = + botAllowlist.has(author) || + (assoc != null && orgAuthorAssociations.has(assoc)); + + if (!allowed) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: `This PR modifies dependency files (\`pyproject.toml\` or \`uv.lock\`), which is restricted to members of the **${context.repo.owner}** organization on GitHub.\n\nIf you need a dependency change, please [open a discussion](https://github.com/${context.repo.owner}/${context.repo.repo}/discussions/new) describing what you need and why.\n\nClosing this PR automatically.` + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + state: 'closed' + }); + + core.setFailed('Dependency changes are restricted to organization members.'); + } else { + console.log(`Author ${author} (author_association=${assoc}) is allowed to make dependency changes.`); + } diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 7b074fd1a7..6f63422373 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,25 +2,33 @@ name: Issue Manager on: schedule: - - cron: "0 0 * * *" + - cron: "13 21 * * *" issue_comment: types: - created - - edited issues: types: - labeled + pull_request_target: # zizmor: ignore[dangerous-triggers] + types: + - labeled + workflow_dispatch: + +permissions: {} jobs: issue-manager: + if: github.repository_owner == 'fastapi' runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + timeout-minutes: 5 steps: - - uses: tiangolo/issue-manager@0.2.0 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: tiangolo/issue-manager@dc846170c36eb62fb434b3d943b36399fe240fb5 # 0.8.1 with: token: ${{ secrets.GITHUB_TOKEN }} - config: > - { - "answered": { - "message": "Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues." - } - } diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml deleted file mode 100644 index d7f138f5fc..0000000000 --- a/.github/workflows/latest-changes.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Latest Changes - -on: - pull_request_target: - branches: - - master - types: - - closed - workflow_dispatch: - inputs: - number: - description: PR number - required: true - debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' - required: false - default: false - -jobs: - latest-changes: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - # To allow latest-changes to commit to the main branch - token: ${{ secrets.ACTIONS_TOKEN }} - # Allow debugging with tmate - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - with: - limit-access-to-actor: true - - uses: docker://tiangolo/latest-changes:0.0.3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - latest_changes_file: docs/release-notes.md - latest_changes_header: '## Latest Changes\n\n' - debug_logs: true diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000000..2808236e2d --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,108 @@ +name: pre-commit + +on: + pull_request: +permissions: {} + +env: + CAN_PUSH: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} + +jobs: + pre-commit: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + timeout-minutes: 5 + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + name: Checkout PR for own repo + if: env.CAN_PUSH == 'true' + with: + # To be able to commit it needs to fetch the head of the branch, not the + # merge commit + ref: ${{ github.head_ref }} + # And it needs the full history to be able to compute diffs + fetch-depth: 0 + persist-credentials: false + # pre-commit lite ci needs the default checkout configs to work + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + name: Checkout PR for fork + if: env.CAN_PUSH == 'false' + with: + # To be able to commit it needs the head branch of the PR, the remote one + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Install Dependencies + run: uv sync --locked + - name: Run prek - pre-commit + id: precommit + run: uv run prek run --from-ref origin/${GITHUB_BASE_REF} --to-ref HEAD --show-diff-on-failure + continue-on-error: true + - name: Check for changes + id: changes + run: | + if [[ -n "$(git status --porcelain)" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + - name: Get PR Push token + id: pr-push + if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' + uses: tiangolo/pr-push@f336b3817f32ea9b8273a8c15f8ecb739ac38167 # 0.0.4 + - name: Commit and push changes + if: env.CAN_PUSH == 'true' && steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.pr-push.outputs.token }} + run: | + git config user.name "pr-push[bot]" + git config user.email "pr-push[bot]@users.noreply.github.com" + gh auth setup-git + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "🎨 Auto format" + git push + fi + - uses: pre-commit-ci/lite-action@062bca0919bc9d6e66755cc05074b70c77e111fc # v1.2.0 + if: env.CAN_PUSH == 'false' + with: + msg: 🎨 Auto format + - name: Error out on pre-commit errors + if: steps.precommit.outcome == 'failure' + run: exit 1 + + # https://github.com/marketplace/actions/alls-green#why + pre-commit-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - pre-commit + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000000..0427e0c094 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,80 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + bump: + description: Release bump + required: true + type: choice + options: + - patch + - minor + - major + date: + description: Release date in YYYY-MM-DD format. Defaults to today. + required: false + type: string + +permissions: {} + +jobs: + prepare-release: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + id-token: write + env: + PREPARE_RELEASE_VERSION_FILE: typer/__init__.py + PREPARE_RELEASE_RELEASE_NOTES_FILE: docs/release-notes.md + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + - name: Install uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + - name: Prepare release + env: + PREPARE_RELEASE_BUMP: ${{ inputs.bump }} + PREPARE_RELEASE_DATE: ${{ inputs.date }} + run: uv run python scripts/prepare_release.py prepare + - name: Get release version + id: release-version + run: | + version="$(uv run python scripts/prepare_release.py current-version)" + echo "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Get PR Submit token + id: pr-submit + uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1 + - name: Create release pull request + env: + GH_TOKEN: ${{ steps.pr-submit.outputs.token }} + VERSION: ${{ steps.release-version.outputs.version }} + run: | + set -euo pipefail + branch="release-${VERSION}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + git config user.name "pr-submit[bot]" + git config user.email "pr-submit[bot]@users.noreply.github.com" + git switch -c "$branch" + git add $PREPARE_RELEASE_VERSION_FILE $PREPARE_RELEASE_RELEASE_NOTES_FILE + git commit -m "🔖 Release version ${VERSION}" + gh auth setup-git + git push --set-upstream origin "$branch" + gh pr create \ + --base master \ + --head "$branch" \ + --title "🔖 Release version ${VERSION}" \ + --body "Prepare release ${VERSION}." \ + --label release diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml deleted file mode 100644 index 47656794b1..0000000000 --- a/.github/workflows/preview-docs.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Preview Docs -on: - workflow_run: - workflows: - - Build Docs - types: - - completed - -jobs: - preview-docs: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.9.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - workflow: build-docs.yml - run_id: ${{ github.event.workflow_run.id }} - name: docs-zip - - name: Unzip docs - run: | - rm -rf ./site - unzip docs.zip - rm -f docs.zip - - name: Deploy to Netlify - id: netlify - uses: nwtgck/actions-netlify@v1.1.5 - with: - publish-dir: './site' - production-deploy: false - github-token: ${{ secrets.GITHUB_TOKEN }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - - name: Comment Deploy - uses: ./.github/actions/comment-docs-preview-in-pr - with: - token: ${{ secrets.GITHUB_TOKEN }} - deploy_url: "${{ steps.netlify.outputs.deploy-url }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a0feaebb8a..24b677d964 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,23 +3,35 @@ name: Publish on: release: types: - - created + - published + +permissions: {} jobs: publish: runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + timeout-minutes: 5 steps: - - uses: actions/checkout@v2 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: - python-version: "3.7" - - name: Install Flit - run: pip install flit - - name: Install Dependencies - run: flit install --symlink + python-version-file: ".python-version" + - name: Install uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + enable-cache: false + - name: Build distribution + run: uv build - name: Publish - env: - FLIT_USERNAME: ${{ secrets.FLIT_USERNAME }} - FLIT_PASSWORD: ${{ secrets.FLIT_PASSWORD }} - run: bash scripts/publish.sh + run: uv publish diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml new file mode 100644 index 0000000000..4ca9e50df9 --- /dev/null +++ b/.github/workflows/smokeshow.yml @@ -0,0 +1,50 @@ +name: Smokeshow + +on: + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: + - Test + types: + - completed + +permissions: {} + +jobs: + smokeshow: + runs-on: ubuntu-latest + permissions: + statuses: write + timeout-minutes: 5 + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + cache-dependency-glob: | + pyproject.toml + uv.lock + - run: uv sync --locked --no-dev --group github-actions + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: coverage-html + path: htmlcov + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + - run: uv run smokeshow upload htmlcov + env: + SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100 + SMOKESHOW_GITHUB_CONTEXT: coverage + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} # zizmor: ignore[secrets-outside-env] diff --git a/.github/workflows/test-cpython-nightly.yml b/.github/workflows/test-cpython-nightly.yml new file mode 100644 index 0000000000..0555460138 --- /dev/null +++ b/.github/workflows/test-cpython-nightly.yml @@ -0,0 +1,59 @@ +name: Test CPython Nightly + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +permissions: {} + +env: + UV_NO_SYNC: true + +jobs: + test-latest-python: + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Checkout CPython main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: python/cpython + ref: main + path: ./.cpython + - name: Install CPython build dependencies + run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config libssl-dev zlib1g-dev + - name: Build CPython + run: | + cd ./.cpython + ./configure && make -j $(nproc) + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Install Dependencies + run: uv sync --locked --no-dev --group tests --python ./.cpython/python + - name: Lint + run: uv run bash scripts/lint.sh + - run: mkdir coverage + - run: uv run bash scripts/test-files.sh + - name: Test + run: uv run bash scripts/test.sh + env: + COVERAGE_FILE: coverage/.coverage.ubuntu-cpython-main + CONTEXT: ubuntu-cpython-main + - name: Store coverage files + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: coverage-ubuntu-cpython-main + path: coverage + include-hidden-files: true diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml new file mode 100644 index 0000000000..85c97b97dd --- /dev/null +++ b/.github/workflows/test-redistribute.yml @@ -0,0 +1,60 @@ +name: Test Redistribute + +on: + push: + branches: + - master + pull_request: +permissions: {} + +jobs: + test-redistribute: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" + # cache-dependency-path: pyproject.toml + - name: Install build dependencies + run: pip install build + - name: Build source distribution + run: python -m build --sdist + - name: Decompress source distribution + run: | + cd dist + tar xvf typer*.tar.gz + - name: Install test dependencies + run: | + cd dist/typer*/ + pip install --group tests --editable . + - name: Run source distribution tests + run: | + cd dist/typer*/ + bash scripts/test.sh + - name: Build wheel distribution + run: | + cd dist + pip wheel --no-deps typer*.tar.gz + + # https://github.com/marketplace/actions/alls-green#why + test-redistribute-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - test-redistribute + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6eec01b364..5639836333 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,34 +5,134 @@ on: branches: - master pull_request: - types: [opened, synchronize] + schedule: + # cron every week on monday + - cron: "0 0 * * 1" + +permissions: {} + +env: + UV_NO_SYNC: true jobs: test: - runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] - click-7: [true, false] + os: [ ubuntu-latest, windows-latest, macos-latest ] + python-version: [ "3.14", "3.15", "3.15t" ] + uv-resolution: + - highest + include: + - os: ubuntu-latest + python-version: "3.10" + uv-resolution: lowest-direct + - os: macos-latest + python-version: "3.11" + uv-resolution: highest + - os: macos-latest + python-version: "3.12" + uv-resolution: lowest-direct + - os: windows-latest + python-version: "3.13" + uv-resolution: highest fail-fast: false - + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + env: + UV_PYTHON: ${{ matrix.python-version }} + UV_RESOLUTION: ${{ matrix.uv-resolution }} steps: - - uses: actions/checkout@v2 + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: ${{ matrix.python-version }} - - name: Install Flit - run: pip install flit + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock - name: Install Dependencies - run: flit install --deps=develop --symlink - - name: Install Click 7 - if: matrix.click-7 - run: pip install "click<8.0.0" - - name: Lint - if: ${{ matrix.python-version != '3.6' && matrix.click-7 == false }} - run: bash scripts/lint.sh + run: uv sync --no-dev --group tests + - run: mkdir coverage + - run: uv run bash scripts/test-files.sh - name: Test - run: bash scripts/test.sh - - name: Upload coverage - uses: codecov/codecov-action@v2 + run: uv run bash scripts/test.sh + env: + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} + CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} + - name: Store coverage files + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: coverage-${{ runner.os }}-${{ matrix.python-version }} + path: coverage + include-hidden-files: true + + coverage-combine: + needs: [test] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version-file: ".python-version" + - name: Setup uv + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: "latest-known" + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock + - name: Get coverage files + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + pattern: coverage-* + path: coverage + merge-multiple: true + - name: Install Dependencies + run: uv sync --locked --no-dev --group tests + - run: ls -la coverage + - run: uv run coverage combine coverage + - run: uv run coverage html --show-contexts --title "Coverage for ${{ github.sha }}" + - name: Store coverage HTML + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: coverage-html + path: htmlcov + include-hidden-files: true + - run: uv run coverage report --fail-under=100 + + # https://github.com/marketplace/actions/alls-green#why + check: # This job does nothing and is only used for the branch protection + if: always() + needs: + - coverage-combine + - test + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..8188eaccd0 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,25 @@ +name: Zizmor + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +permissions: {} + +jobs: + zizmor: + name: Run zizmor + runs-on: ubuntu-latest + permissions: + security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Run zizmor + uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0 diff --git a/.gitignore b/.gitignore index 8ab16b46cb..230c8c0c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ .vscode *.pyc __pycache__ -env3.7 -env3.6 +.venv* env dist .mypy_cache .idea site -.coverage htmlcov .pytest_cache coverage.xml +.coverage* +.cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3eaa2a221..9af33749ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,52 +2,70 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 hooks: - - id: check-added-large-files - - id: check-toml - - id: check-yaml + - id: check-added-large-files + - id: check-toml + - id: check-yaml args: - --unsafe - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: https://github.com/crate-ci/typos + rev: 7f9d0819ca1c008ade73f4b41fa9d258d48f605b # frozen: v1.51.1 hooks: - - id: pyupgrade - args: - - --py3-plus - - --keep-runtime-typing - # This file is more readable without yield from - exclude: ^docs_src/progressbar/tutorial002\.py -- repo: https://github.com/myint/autoflake - rev: v1.4 - hooks: - - id: autoflake - args: - - --recursive - - --in-place - - --remove-all-unused-imports - - --remove-unused-variables - - --expand-star-imports - - --exclude - - __init__.py - - --remove-duplicate-keys -- repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - name: isort (python) - - id: isort - name: isort (cython) - types: [cython] - - id: isort - name: isort (pyi) - types: [pyi] -- repo: https://github.com/psf/black - rev: 22.3.0 + - id: typos + args: [--force-exclude] + +- repo: local hooks: - - id: black -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate + - id: local-ruff-check + name: ruff check + entry: uv run ruff check --force-exclude --fix --exit-non-zero-on-fix + require_serial: true + language: unsupported + types: [python] + + - id: local-ruff-format + name: ruff format + entry: uv run ruff format --force-exclude --exit-non-zero-on-format + require_serial: true + language: unsupported + types_or: [python, markdown] + + - id: local-mypy + name: mypy check + entry: uv run mypy typer + require_serial: true + language: unsupported + pass_filenames: false + + - id: local-ty + name: ty check + entry: uv run ty check typer + require_serial: true + language: unsupported + pass_filenames: false + + - id: add-release-date + language: unsupported + name: add date to latest release header + entry: uv run python scripts/add_latest_release_date.py + files: ^docs/release-notes\.md$ + pass_filenames: false + + - id: generate-readme + language: unsupported + name: generate README.md from index.md + entry: uv run ./scripts/docs.py generate-readme + files: ^docs/index\.md|scripts/docs\.py$ + pass_filenames: false + + - id: zizmor + name: zizmor + language: python + entry: uv run zizmor . + files: ^\.github\/workflows\/ + require_serial: true + pass_filenames: false diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..c8cfe39591 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..b4902c6ab1 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,23 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: Typer +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Sebastián + family-names: Ramírez + email: tiangolo@gmail.com +identifiers: +repository-code: 'https://github.com/fastapi/typer' +url: 'https://typer.tiangolo.com' +abstract: >- + Typer, build great CLIs. Easy to code. Based on Python type hints. +keywords: + - typer + - cli + - python +license: MIT diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 466bbbac26..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -Please read the [Development - Contributing](https://typer.tiangolo.com/contributing/) guidelines in the documentation site. diff --git a/README.md b/README.md index bb1ccf96fc..a5dff812f8 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,32 @@

- Typer + Typer +

Typer, build great CLIs. Easy to code. Based on Python type hints.

- - Test - - - Publish - - - Coverage + + Test - + + Coverage + Package version

--- -**Documentation**: https://typer.tiangolo.com +**Documentation**: [https://typer.tiangolo.com](https://typer.tiangolo.com) -**Source Code**: https://github.com/tiangolo/typer +**Source Code**: [https://github.com/fastapi/typer](https://github.com/fastapi/typer) --- -Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python 3.6+ type hints. +Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python type hints. + +It's also a command line tool to run scripts, automatically converting them to CLI applications. The key features are: @@ -36,84 +35,134 @@ The key features are: * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**. * **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments. +* **Run scripts**: Typer includes a `typer` command/program that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally. ## FastAPI of CLIs - - -**Typer** is FastAPI's little sibling. - -And it's intended to be the FastAPI of CLIs. - -## Requirements - -Python 3.6+ - -**Typer** stands on the shoulders of a giant. Its only internal dependency is Click. +**Typer** is [FastAPI](https://fastapi.tiangolo.com)'s little sibling, it's the FastAPI of CLIs. ## Installation +First, [install `uv`](https://docs.astral.sh/uv/getting-started/installation/), and then add **Typer** to your project: +
```console -$ pip install typer +$ uv add typer ---> 100% -Successfully installed typer ```
+This installs both the Typer library and the `typer` command in the project's virtual environment. To use `typer` directly with shell completion, [activate the project environment and install completion](tutorial/install.md#activate-the-virtual-environment). + +If you prefer to use `pip`, install `typer` inside a virtual environment. See the [installation guide](tutorial/install.md) for the alternative steps. + ## Example ### The absolute minimum * Create a file `main.py` with: +```Python +def main(name: str): + print(f"Hello {name}") +``` + +This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application. + +### Run it + +Run your application with the `typer` command: + +
+ +```console +// Run your application +$ typer main.py run + +// You get a nice error, you are missing 'name' +Usage: typer [PATH_OR_MODULE] run [OPTIONS] {name} +Try 'typer [PATH_OR_MODULE] run --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'name'. │ +╰───────────────────────────────────────────────────╯ + + +// You get a --help for free +$ typer main.py run --help + +Usage: typer [PATH_OR_MODULE] run [OPTIONS] {name} + +Run the provided Typer app. + +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ + +// Now pass the 'name' argument +$ typer main.py run Camila + +Hello Camila + +// It works! 🎉 +``` + +
+ +This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts. + +**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command. + +## Use Typer in your code + +Now let's start using Typer in your own code, update `main.py` with: + ```Python import typer def main(name: str): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": typer.run(main) ``` -### Run it - -Run your application: +Now you could run it with Python directly:
```console // Run your application -$ python main.py +$ uv run python main.py -// You get a nice error, you are missing NAME -Usage: main.py [OPTIONS] NAME -Try "main.py --help" for help. +// You get a nice error, you are missing 'name' +Usage: main.py [OPTIONS] {name} +Try 'main.py --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'name'. │ +╰───────────────────────────────────────────────────╯ -Error: Missing argument 'NAME'. // You get a --help for free -$ python main.py --help - -Usage: main.py [OPTIONS] NAME - -Arguments: - NAME [required] +$ uv run python main.py --help -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +Usage: main.py [OPTIONS] {name} -// When you create a package you get ✨ auto-completion ✨ for free, installed with --install-completion +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ -// Now pass the NAME argument -$ python main.py Camila +// Now pass the 'name' argument +$ uv run python main.py Camila Hello Camila @@ -122,7 +171,7 @@ Hello Camila
-**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use Typer CLI. +**Note**: you can also call this same script with the `typer` command, but you don't need to. ## Example upgrade @@ -144,15 +193,15 @@ app = typer.Typer() @app.command() def hello(name: str): - typer.echo(f"Hello {name}") + print(f"Hello {name}") @app.command() def goodbye(name: str, formal: bool = False): if formal: - typer.echo(f"Goodbye Ms. {name}. Have a good day.") + print(f"Goodbye Ms. {name}. Have a good day.") else: - typer.echo(f"Bye {name}!") + print(f"Bye {name}!") if __name__ == "__main__": @@ -168,73 +217,109 @@ And that will: ### Run the upgraded example +Check the new help: +
```console -// Check the --help -$ python main.py --help +$ uv run python main.py --help + + Usage: main.py [OPTIONS] COMMAND [ARGS]... + +╭─ Options ─────────────────────────────────────────╮ +│ --install-completion Install completion │ +│ for the current │ +│ shell. │ +│ --show-completion Show completion for │ +│ the current shell, │ +│ to copy it or │ +│ customize the │ +│ installation. │ +│ --help Show this message │ +│ and exit. │ +╰───────────────────────────────────────────────────╯ +╭─ Commands ────────────────────────────────────────╮ +│ hello │ +│ goodbye │ +╰───────────────────────────────────────────────────╯ -Usage: main.py [OPTIONS] COMMAND [ARGS]... +// When you create a package you get ✨ auto-completion ✨ for free, installed with --install-completion -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +// You have 2 subcommands (the 2 functions): hello and goodbye +``` -Commands: - goodbye - hello +
-// You have 2 subcommands (the 2 functions): goodbye and hello +Now check the help for the `hello` command: -// Now get the --help for hello +
-$ python main.py hello --help +```console +$ uv run python main.py hello --help -Usage: main.py hello [OPTIONS] NAME + Usage: main.py hello [OPTIONS] {name} -Arguments: - NAME [required] +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ +``` -Options: - --help Show this message and exit. +
-// And now get the --help for goodbye +And now check the help for the `goodbye` command: -$ python main.py goodbye --help +
-Usage: main.py goodbye [OPTIONS] NAME +```console +$ uv run python main.py goodbye --help -Arguments: - NAME [required] + Usage: main.py goodbye [OPTIONS] {name} -Options: - --formal / --no-formal [default: False] - --help Show this message and exit. +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --formal --no-formal [default: no-formal] │ +│ --help Show this message │ +│ and exit. │ +╰───────────────────────────────────────────────────╯ // Automatic --formal and --no-formal for the bool option 🎉 +``` -// And if you use it with the hello command +
-$ python main.py hello Camila +Now you can try out the new command line application: + +
+ +```console +// Use it with the hello command + +$ uv run python main.py hello Camila Hello Camila // And with the goodbye command -$ python main.py goodbye Camila +$ uv run python main.py goodbye Camila Bye Camila! // And with --formal -$ python main.py goodbye --formal Camila +$ uv run python main.py goodbye --formal Camila Goodbye Ms. Camila. Have a good day. ```
+**Note**: If your app only has one command, by default the command name is **omitted** in usage: `python main.py Camila`. However, when there are multiple commands, you must **explicitly include the command name**: `python main.py hello Camila`. See [One or Multiple Commands](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/) for more details. + ### Recap In summary, you declare **once** the types of parameters (*CLI arguments* and *CLI options*) as function parameters. @@ -243,7 +328,7 @@ You do that with standard modern Python types. You don't have to learn a new syntax, the methods or classes of a specific library, etc. -Just standard **Python 3.6+**. +Just standard **Python**. For example, for an `int`: @@ -261,36 +346,35 @@ And similarly for **files**, **paths**, **enums** (choices), etc. And there are **You get**: great editor support, including **completion** and **type checks** everywhere. -**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. +**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command. For a more complete example including more features, see the Tutorial - User Guide. -## Optional Dependencies +## Dependencies + +**Typer** requires only a few dependencies: + +* [`rich`](https://rich.readthedocs.io/en/stable/index.html): to show nicely formatted errors automatically. +* [`pydantic`](https://pydantic.dev/docs/validation/latest/get-started/): to validate input parameters. +* [`shellingham`](https://github.com/sarugaku/shellingham): to automatically detect the current shell when installing completion. +* [`annotated-doc`](https://github.com/fastapi/annotated-doc): to generate documentation from Python type annotations. +* [`colorama`](https://github.com/tartley/colorama) (only on Windows): for producing colored terminal text on Windows. -Typer uses Click internally. That's the only dependency. +### Click code -But you can also install extras: +Typer used to depend on [Click](https://click.palletsprojects.com/) as well, a popular tool for building CLIs in Python. -* colorama: and Click will automatically use it to make sure your terminal's colors always work correctly, even in Windows. - * Then you can use any tool you want to output your terminal's colors in all the systems, including the integrated `typer.style()` and `typer.secho()` (provided by Click). - * Or any other tool, e.g. wasabi, blessings. -* shellingham: and Typer will automatically detect the current shell when installing completion. - * With `shellingham` you can just use `--install-completion`. - * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`. +Since version 0.26.0, Typer has vendored Click (included Click's source code internally, instead of installing it as a third party package) and has unified the code interactions between Typer and the embedded Click source code for easier maintainability in the future. -You can install `typer` with `colorama` and `shellingham` with `pip install typer[all]`. +Note that some Click functionality will not be available anymore in the future, as we continue to improve and extend Typer's codebase. -## Other tools and plug-ins +### `typer-slim` -Click has many plug-ins available that you can use. And there are many tools that help with command line applications that you can use as well, even if they are not related to Typer or Click. +There used to be a slimmed-down version of Typer called `typer-slim`, which didn't include the dependencies `rich` and `shellingham`, nor the `typer` command. -For example: +However, since version 0.22.0, we have stopped supporting this, and `typer-slim` now simply installs (all of) Typer. -* click-spinner: to show the user that you are loading data. A Click plug-in. - * There are several other Click plug-ins at click-contrib that you can explore. -* tabulate: to automatically display tabular data nicely. Independent of Click or Typer. -* tqdm: a fast, extensible progress bar, alternative to Typer's own `typer.progressbar()`. -* etc... you can re-use many of the great available tools for building CLIs. +If you want to disable Rich globally, you can set an environmental variable `TYPER_USE_RICH` to `False` or `0`. ## License diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index fd3a6b14f0..0000000000 --- a/SECURITY.md +++ /dev/null @@ -1,29 +0,0 @@ -# Security Policy - -Security is very important for Typer and its community. 🔒 - -Learn more about it below. 👇 - -## Versions - -The latest versions of Typer are supported. - -You are encouraged to [write tests](https://typer.tiangolo.com/tutorial/testing/) for your application and update your Typer version frequently after ensuring that your tests are passing. This way you will benefit from the latest features, bug fixes, and **security fixes**. - -## Reporting a Vulnerability - -If you think you found a vulnerability, and even if you are not sure about it, please report it right away by sending an email to: security@tiangolo.com. Please try to be as explicit as possible, describing all the steps and example code to reproduce the security issue. - -I (the author, [@tiangolo](https://twitter.com/tiangolo)) will review it thoroughly and get back to you. - -## Public Discussions - -Please restrain from publicly discussing a potential security vulnerability. 🙊 - -It's better to discuss privately and try to find a solution first, to limit the potential impact as much as possible. - ---- - -Thanks for your help! - -The Typer community and I thank you for that. 🙇 diff --git a/docs/about/index.md b/docs/about/index.md new file mode 100644 index 0000000000..1688bf193d --- /dev/null +++ b/docs/about/index.md @@ -0,0 +1,3 @@ +# About + +About **Typer**, its design, inspiration, and more. 🤓 diff --git a/docs/ai-development.md b/docs/ai-development.md new file mode 100644 index 0000000000..0aaf647527 --- /dev/null +++ b/docs/ai-development.md @@ -0,0 +1,29 @@ +# Develop with AI + +Typer includes an official **agent skill**, bundled with the Python package, with guidance for the version installed in your project. + +## Install the skill + +After [installing Typer](tutorial/install.md), run [Library Skills](https://library-skills.io/use/) from your project directory: + +```bash +uvx library-skills +``` + +Select the Typer skill and `.agents/skills` for Codex and most agents, or `.claude/skills` for Claude Code. You can select both. + +Your agent can then use the skill automatically when working on Typer code. + +### Install without prompts + +For agents and scripts, select the Typer skill without interactive prompts: + +```bash +uvx library-skills install --skill typer --yes +``` + +For Claude Code, add `--claude` to also install it in `.claude/skills`: + +```bash +uvx library-skills install --skill typer --yes --claude +``` diff --git a/docs/alternatives.md b/docs/alternatives.md index 0f93697557..03912e80ef 100644 --- a/docs/alternatives.md +++ b/docs/alternatives.md @@ -1,3 +1,5 @@ +# Alternatives, Inspiration and Comparisons + What inspired **Typer**, how it compares to other alternatives and what it learned from them. ## Intro @@ -8,32 +10,41 @@ There have been many tools created before that have helped inspire its creation. ## Previous tools -### `argparse` +### [`argparse`](https://docs.python.org/3/library/argparse.html) `argparse` is the Python standard library's module to write CLIs. It provides a better alternative than reading the *CLI Parameters* as a `list` of `str` and parsing everything by hand. -!!! check "Inspired **Typer** to" - Provide a better development experience than just reading *CLI Parameters* by hand. +/// tip | Inspired **Typer** to + +Provide a better development experience than just reading *CLI Parameters* by hand. -### Hug +/// + +### [Hug](https://hugapi.github.io/hug/) Hug is a library to create APIs and CLIs, it uses parameters in functions to declare the required data. It inspired a lot of the ideas in **FastAPI** and **Typer**. -!!! check "Inspired **Typer** to" - Use function parameters to declare *CLI arguments* and *CLI options* as it simplifies a lot the development experience. +/// tip | Inspired **Typer** to + +Use function parameters to declare *CLI arguments* and *CLI options* as it simplifies a lot the development experience. -### Plac +/// + +### [Plac](https://plac.readthedocs.io/en/latest/) Plac is another library to create CLIs using parameters in functions, similar to Hug. -!!! check "Inspired **Typer** to" - Provide a simple way to use a function as a command line app, without having to create a complete app, with `typer.run(some_function)`. +/// tip | Inspired **Typer** to + +Provide a simple way to use a function as a command line app, without having to create a complete app, with `typer.run(some_function)`. + +/// -### Pydantic +### [Pydantic](https://pydantic-docs.helpmanual.io/) Pydantic is a library to handle data validation using standard modern Python type annotations. @@ -41,10 +52,13 @@ It powers **FastAPI** underneath. It is not used by **Typer**, but it inspired a lot of the design (through **FastAPI**). -!!! check "Inspired **Typer** to" - Use standard Python type annotations to declare types instead of library-specific types or classes and use them for data validation and documentation. +/// tip | Inspired **Typer** to -### Click +Use standard Python type annotations to declare types instead of library-specific types or classes and use them for data validation and documentation. + +/// + +### [Click](https://click.palletsprojects.com) Click is one of the most widely used libraries to create CLIs in Python. @@ -56,14 +70,17 @@ It uses decorators on top of functions to modify the actual value of those funct It was built with some great ideas and design using the features available in the language at the time (Python 2.x). -!!! check "**Typer** uses it for" - Everything. 🚀 +/// tip | **Typer** builds on top of Click functionality + +It has vendored Click version 8.3.1 and adds a layer on top of it. - **Typer** mainly adds a layer on top of Click, making the code simpler and easier to use, with autocompletion everywhere, etc, but providing all the powerful features of Click underneath. +Typer aims to make the code simpler and easier to use, with autocompletion everywhere, etc, while still providing many of the powerful features of Click underneath. - As someone pointed out: "Nice to see it is built on Click but adds the type stuff. Me gusta!" +As someone pointed out: ["Nice to see it is built on Click but adds the type stuff. Me gusta!"](https://twitter.com/fishnets88/status/1210126833745838080) -### `click-completion` +/// + +### [`click-completion`](https://github.com/click-contrib/click-completion) `click-completion` is a plug-in for Click. It was created to extend completion support for shells when Click only had support for Bash completion. @@ -71,12 +88,15 @@ Previous versions of **Typer** had deep integrations with `click-completion` and And now **Typer** improved it to have new features, tests, some bug fixes (for issues in plain `click-completion` and Click), and better support for shells, including modern versions of PowerShell (e.g. the default versions that come with Windows 10). -!!! check "Inspired **Typer** to" - Provide auto completion for all the shells. +/// tip | Inspired **Typer** to + +Provide auto completion for all the shells. + +/// -### FastAPI +### [FastAPI](https://fastapi.tiangolo.com/) -I created **FastAPI** to provide an easy way to build APIs with autocompletion for everything in the code (and some other features). +I created **FastAPI** to provide an easy way to build APIs with autocompletion for everything in the code (and some other [features](https://fastapi.tiangolo.com/features/)). **Typer** is the "FastAPI of CLIs". diff --git a/docs/contributing.md b/docs/contributing.md index dd038fc408..e9f057727d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,231 +1,148 @@ -First, you might want to see the basic ways to [help Typer and get help](help-typer.md){.internal-link target=_blank}. +# Development - Contributing -## Developing - -If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. - -### Virtual environment with `venv` - -You can create a virtual environment in a directory using Python's `venv` module: - -
- -```console -$ python -m venv env -``` - -
- -That will create a directory `./env/` with the Python binaries and then you will be able to install packages for that isolated environment. - -### Activate the environment - -Activate the new environment with: - -=== "Linux, macOS" - -
- - ```console - $ source ./env/bin/activate - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ .\env\Scripts\Activate.ps1 - ``` - -
- -=== "Windows Bash" - - Or if you use Bash for Windows (e.g. Git Bash): - -
- - ```console - $ source ./env/Scripts/activate - ``` - -
- -To check it worked, use: - -=== "Linux, macOS, Windows Bash" - -
- - ```console - $ which pip - - some/directory/typer/env/bin/pip - ``` - -
+First, you might want to see the basic ways to [help Typer and get help](https://typer.tiangolo.com/help-typer/). -=== "Windows PowerShell" - -
- - ```console - $ Get-Command pip +## Developing - some/directory/typer/env/bin/pip - ``` +To contribute code to the project, please follow the guidelines in [tiangolo.com - Contributing](https://tiangolo.com/open-source/contributing/). -
+## Completion -If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 +To try and test the completion for different shells and check that they are working you can use a Docker container. -!!! tip - Every time you install a new package with `pip` under that environment, activate the environment again. +There's a `Dockerfile` and a Docker Compose file `compose.yaml` at `./scripts/docker/`. - This makes sure that if you use a terminal program installed by that package (like `flit`), you use the one from your local environment and not any other that could be installed globally. +It has installed `bash`, `zsh`, `fish`, and `pwsh` (PowerShell for Linux). -### Flit +It also has installed `nano` and `vim`, so that you can check the modified configuration files for the shells (for example `.bashrc`, `.zshrc`, etc). -**Typer** uses Flit to build, package and publish the project. +It also has `uv` installed, so you can install the dependencies and the project quickly. -After activating the environment as described above, install `flit`: +The Docker Compose file mounts the main directory as `/code` inside the container, so you can change things and try them out. -
+Go to the `./scripts/docker/` directory: ```console -$ pip install flit - ----> 100% +$ cd scripts/docker/ ``` -
- -Now re-activate the environment to make sure you are using the `flit` you just installed (and not a global one). - -And now use `flit` to install the development dependencies: - -=== "Linux, macOS" - -
+Then run an interactive session with `bash` inside the container: - ```console - $ flit install --deps develop --symlink - - ---> 100% - ``` +```console +$ docker compose run typer bash -
+root@79c4b9b70cbe:/code# +``` -=== "Windows" +Then inside the container, you can install `typer` with: - If you are on Windows, use `--pth-file` instead of `--symlink`: +```console +$ uv pip install -r requirements.txt +``` -
+Then, you can start the shell you want to use, the one where you want to try out completion: - ```console - $ flit install --deps develop --pth-file +* `bash` +* `fish` +* `pwsh` +* `zsh` - ---> 100% - ``` +For example: -
+```console +$ zsh +``` -It will install all the dependencies and your local Typer in your local environment. +Then install `typer` completion: -#### Using your local Typer +```console +$ typer --install-completion +``` -If you create a Python file that imports and uses Typer, and run it with the Python from your local environment, it will use your local Typer source code. +/// note -And if you update that local Typer source code, as it is installed with `--symlink` (or `--pth-file` on Windows), when you run that Python file again, it will use the fresh version of Typer you just edited. +In `pwsh` you will probably get a warning of: -That way, you don't have to "install" your local version to be able to test every change. +```plaintext +Set-ExecutionPolicy: Operation is not supported on this platform. +``` -### Format +this is because that configuration is only available in Windows (and needed there), not in PowerShell for Linux. -There is a script that you can run that will format and clean all your code: +/// -
+For completion to take effect, you need to restart the shell. So, exit the current shell: ```console -$ bash scripts/format.sh +$ exit ``` -
- -It will also auto-sort all your imports. - -For it to sort them correctly, you need to have Typer installed locally in your environment, with the command in the section above using `--symlink` (or `--pth-file` on Windows). - -### Format imports - -There is another script that formats all the imports and makes sure you don't have unused imports: - -
+and start a new shell (for the same shell you installed completion in) again. For example: ```console -$ bash scripts/format-imports.sh +$ zsh ``` -
+Now you could create a demo file on the same Typer directory in your editor, for example `demo.py`: -As it runs one command after the other and modifies and reverts many files, it takes a bit longer to run, so it might be easier to use `scripts/format.sh` frequently and `scripts/format-imports.sh` only before committing. +```python +import typer -## Docs +app = typer.Typer() -The documentation uses MkDocs. -All the documentation is in Markdown format in the directory `./docs`. +@app.command() +def hello(): + print("Hello") -Many of the tutorials have blocks of code. -In most of the cases, these blocks of code are actual complete applications that can be run as is. +@app.command() +def goodbye(): + print("Goodbye") -In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. -And those Python files are included/injected in the documentation when generating the site. +if __name__ == "__main__": + app() +``` -### Docs for tests +Because the directory is mounted as a volume, you will be able to access the file from inside the container. -Most of the tests actually run against the example source files in the documentation. +So, you can try running it with the `typer` command, that will use the installed shell completion: -This helps making sure that: +```console +$ typer demo.py +``` -* The documentation is up to date. -* The documentation examples can be run as is. -* Most of the features are covered by the documentation, ensured by test coverage. +And you should see the completion working: -During local development, there is a script that builds the site and checks for any changes, live-reloading: +```console +run -- Run the provided Typer app. +utils -- Extra utility commands for Typer apps. +``` -
+And the same for the commands in your `demo.py` file: ```console -$ bash scripts/docs-live.sh +$ typer demo.py run -[INFO] - Building documentation... -[INFO] - Cleaning site directory -[INFO] - Documentation built in 2.74 seconds -[INFO] - Serving on http://127.0.0.1:8008 +hello goodbye ``` -
+You can also check the configuration file using `nano` or `vim`, for example: -It will serve the documentation on `http://127.0.0.1:8008`. - -That way, you can edit the documentation/source files and see the changes live. +```bash +nano ~/.zshrc +``` -## Tests +It will show some content like: -There is a script that you can run locally to test all the code and generate coverage reports in HTML: +```bash +fpath+=~/.zfunc; autoload -Uz compinit; compinit -
-```console -$ bash scripts/test-cov-html.sh +zstyle ':completion:*' menu select ``` -
+If you exit from the container, you can start a new one, you will probably have to install the packages again and install completion again. -This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. +Using this process, you can test all the shells, with their completions, being able to start from scratch quickly in a fresh container, and verifying that everything works as expected. diff --git a/docs/css/custom.css b/docs/css/custom.css index 954b0cf485..8e441842d3 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -1,3 +1,18 @@ +/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */ +@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css); +/* Noto Color Emoji for emoji support with the same font everywhere */ +@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap); + +/* Use Fira Code for code blocks */ +:root { + --md-code-font: "Fira Code", monospace, "Noto Color Emoji"; +} + +/* Keep the modern theme font and include Noto Color Emoji */ +:root { + --md-text-font: "Inter", "Noto Color Emoji"; +} + .termynal-comment { color: #4a968f; font-style: italic; @@ -8,16 +23,122 @@ white-space: pre-wrap; } -a.external-link::after { - /* \00A0 is a non-breaking space - to make the mark be on the same line as the link - */ - content: "\00A0[↪]"; +.termy .linenos { + display: none; +} + +/* External links: detected by JS comparing origin to site origin + JS sets data-external-link on links pointing outside the site + Skip image links, .no-link-icon, and .announce-link */ +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link) { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link)::after { + content: ""; + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + vertical-align: middle; + opacity: 0.55; + background: currentColor; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cline x1='10' y1='14' x2='21' y2='3'/%3E%3C/svg%3E"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cline x1='10' y1='14' x2='21' y2='3'/%3E%3C/svg%3E"); + mask-size: contain; + mask-repeat: no-repeat; +} + +a[data-external-link]:not(:has(img)):not(.no-link-icon):not(.announce-link):hover::after { + opacity: 0.85; +} + +/* Internal links opening in new tab: same-origin links with target=_blank + JS sets data-internal-link on links pointing to the same site origin + Skip image links, .no-link-icon, and .announce-link */ +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link) { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link)::after { + content: ""; + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + vertical-align: middle; + opacity: 0.55; + background: currentColor; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='7' width='14' height='14' rx='2'/%3E%3Cpath d='M7 3h14v14'/%3E%3C/svg%3E"); + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='7' width='14' height='14' rx='2'/%3E%3Cpath d='M7 3h14v14'/%3E%3C/svg%3E"); + mask-size: contain; + mask-repeat: no-repeat; +} + +a[data-internal-link][target="_blank"]:not(:has(img)):not(.no-link-icon):not(.announce-link):hover::after { + opacity: 0.85; +} + +/* Disable link icons in footer and header nav */ +.md-footer a::after, +.md-header a::after { + content: none !important; +} + +.shadow { + box-shadow: 5px 5px 10px #999; +} + +.user-list { + display: flex; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +.user-list-center { + justify-content: space-evenly; +} + +.user { + margin: 1em; + min-width: 7em; +} + +.user .avatar-wrapper { + width: 80px; + height: 80px; + margin: 10px auto; + overflow: hidden; + border-radius: 50%; + position: relative; +} + +.user .avatar-wrapper img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.user .title { + text-align: center; +} + +.user .count { + font-size: 80%; + text-align: center; } -a.internal-link::after { - /* \00A0 is a non-breaking space - to make the mark be on the same line as the link - */ - content: "\00A0↪"; +.doc-param-details .highlight { + overflow-x: auto; + width: 0; + min-width: 100%; } diff --git a/docs/css/termynal.css b/docs/css/termynal.css index 0484e65d4d..daa6038a0a 100644 --- a/docs/css/termynal.css +++ b/docs/css/termynal.css @@ -17,14 +17,17 @@ max-width: 100%; background: var(--color-bg); color: var(--color-text); - font-size: 18px; + /* font-size: 18px; */ + font-size: 15px; /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ - font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; border-radius: 4px; padding: 75px 45px 35px; position: relative; -webkit-box-sizing: border-box; box-sizing: border-box; + /* Custom line-height */ + line-height: 1.2; } [data-termynal]:before { @@ -52,10 +55,19 @@ text-align: center; } -a[data-terminal-control] { +button[data-terminal-control] { + cursor: pointer; text-align: right; display: block; + width: 100%; color: #aebbff; + border: 0; + background: transparent; + font: inherit; +} + +button[data-terminal-control]:hover { + color: var(--color-text); } [data-ty] { diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 0000000000..e32e0fced5 --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,9 @@ +# Environment Variables + +An **environment variable** (also known as an **env var**) is a value that lives outside of your Python code, in the operating system, and can be read by your application and other programs. + +Typer can use environment variables as alternative values for command-line parameters. You will learn how in [CLI Arguments with Environment Variables](tutorial/arguments/envvar.md). + +## Learn More { #learn-more } + +Read the [Environment Variables guide](https://tiangolo.com/guides/environment-variables/) for a detailed, cross-platform explanation, including how to create and read environment variables and how the `PATH` environment variable works. diff --git a/docs/features.md b/docs/features.md index 6b2bbb2b51..25490ec610 100644 --- a/docs/features.md +++ b/docs/features.md @@ -1,18 +1,20 @@ +# Features + ## Design based on **FastAPI** - + -**Typer** is FastAPI's little sibling. +**Typer** is [FastAPI](https://fastapi.tiangolo.com)'s little sibling. It follows the same design and ideas. If you know **FastAPI**, you already know **Typer**... more or less. ## Just Modern Python -It's all based on standard **Python 3.6 type** declarations. No new syntax to learn. Just standard modern Python. +It's all based on standard **Python type** declarations. No new syntax to learn. Just standard modern Python. -If you need a 2 minute refresher of how to use Python types (even if you don't use FastAPI or Typer), check the FastAPI tutorial section: Python types intro. +If you need a 2 minute refresher of how to use Python types (even if you don't use FastAPI or Typer), check the FastAPI tutorial section: [Python types intro](https://fastapi.tiangolo.com/python-types/). -You will also see a 20 seconds refresher on the section [Tutorial - User Guide: First Steps](tutorial/first-steps.md){.internal-link target=_blank}. +You will also see a 20 seconds refresher on the section [Tutorial - User Guide: First Steps](tutorial/first-steps.md). ## Editor support @@ -22,11 +24,11 @@ You will rarely need to come back to the docs. Here's how your editor might help you: -* in Visual Studio Code: +* in [Visual Studio Code](https://code.visualstudio.com/): ![editor support](img/vscode-completion.png) -* in PyCharm: +* in [PyCharm](https://www.jetbrains.com/pycharm/): ![editor support](img/pycharm-completion.png) @@ -48,35 +50,18 @@ The resulting CLI apps created with **Typer** have the nice features of many "pr * Automatic command and subcommand structure handling (you will see more about subcommands in the Tutorial - User Guide). * Automatic completion for the CLI app in all operating systems, in all the shells (Bash, Zsh, Fish, PowerShell), so that the final user of your app can just hit TAB and get the available options or subcommands. * -!!! note "* Auto completion" - Auto completion works when you create a package (installable with `pip`). Or when using [Typer CLI](typer-cli.md){.internal-link target=_blank}. - - If you also add `shellingham` as a dependency, **Typer** will use it to auto-detect the current shell when installing completion. - - **Typer** will automatically create 2 *CLI options*: - - * `--install-completion`: Install completion for the current shell. - * `--show-completion`: Show completion for the current shell, to copy it or customize the installation. - - If you didn't add `shellingham` those *CLI options* take a value with the name of the shell to install completion for, e.g.: - - * `--install-completion bash`. - * `--show-completion powershell`. - - Then you can tell the user to install completion after installing your CLI program and the rest will just work. - -!!! tip - **Typer**'s completion is implemented internally, it uses ideas and components from Click and ideas from `click-completion`, but it doesn't use `click-completion` and re-implements some of the relevant parts of Click. +/// note | * Auto completion - Then it extends those ideas with features and bug fixes. For example, **Typer** programs also support modern versions of PowerShell (e.g. in Windows 10) among all the other shells. +Auto completion works when you create a package (installable with `pip`). Or when using the `typer` command. -## The power of Click +**Typer** uses `shellingham` to auto-detect the current shell when installing completion. -Click is one of the most popular tools for building CLIs in Python. +**Typer** will automatically create 2 *CLI options*: -**Typer** is based on it, so you get all its benefits, plug-ins, robustness, etc. +* `--install-completion`: Install completion for the current shell. +* `--show-completion`: Show completion for the current shell, to copy it or customize the installation. -But you can write simpler code with the benefits of modern Python. +/// ## Tested diff --git a/docs/help-typer.md b/docs/help-typer.md index b5c0dd2283..841e02f525 100644 --- a/docs/help-typer.md +++ b/docs/help-typer.md @@ -1,106 +1,73 @@ -Are you liking **Typer**? +# Help -Would you like to help Typer, other users, and the author? +Would you like to help Typer or get help about Typer? -Or would you like to get help with **Typer**? +There are very simple ways to help and get help. -There are very simple ways to help (several involve just one or two clicks). +## Subscribe to the newsletter -And there are several ways to get help too. +You can subscribe to the (infrequent) [**FastAPI and friends** newsletter](https://fastapi.tiangolo.com/newsletter/) to stay updated about: -## Star **Typer** in GitHub - -You can "star" Typer in GitHub (clicking the star button at the top right): https://github.com/tiangolo/typer. - -By adding a star, other users will be able to find it more easily and see that it has been already useful for others. - -## Watch the GitHub repository for releases - -You can "watch" Typer in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/typer. - -There you can select "Releases only". - -Doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **Typer** with bug fixes and new features. +* News about FastAPI and friends, including Typer 🚀 +* Guides 📝 +* Features ✨ +* Breaking changes 🚨 +* Tips and tricks ✅ -## Connect with the author +## Follow FastAPI online -You can connect with me (Sebastián Ramírez / `tiangolo`), the author. +You can follow **FastAPI** online in several places: -You can: +* [@fastapi on **X / Twitter**](https://x.com/fastapi) +* [@fastapi.tiangolo.com on **Bluesky**](https://bsky.app/profile/fastapi.tiangolo.com) +* [FastAPI on **LinkedIn**](https://www.linkedin.com/company/fastapi/) -* Follow me on **GitHub**. - * See other Open Source projects I have created that could help you. - * Follow me to see when I create a new Open Source project. -* Follow me on **Twitter**. - * Tell me how you use Typer (I love to hear that). - * Ask questions. -* Connect with me on **Linkedin**. - * Talk to me. - * Endorse me or recommend me :) -* Read what I write (or follow me): - * Read other ideas, articles and tools I have created. - * Follow me to see when I publish something new. - * On **Dev.to**. - * On **Medium**. - -## Tweet about **Typer** - -Tweet about **Typer** and let me and others know why you like it. - -## Let me know how are you using **Typer** - -I love to hear about how **Typer** is being used, what have you liked in it, in which project/company you are using it, etc. - -You can let me know: +## Star **Typer** in GitHub -* On **Twitter**. -* On **Linkedin**. -* On **Dev.to**. -* On **Medium**. +You can "star" Typer in GitHub (clicking the star button at the top right): [https://github.com/fastapi/typer](https://github.com/fastapi/typer). ⭐️ -## Help others with issues in GitHub +By adding a star, other users will be able to find it more easily and see that it has been already useful for others. -You can see existing issues and try and help others. +## Watch the GitHub repository for releases -## Watch the GitHub repository +You can "watch" Typer in GitHub (clicking the "watch" button at the top right): [https://github.com/fastapi/typer](https://github.com/fastapi/typer). 👀 -You can "watch" Typer in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/typer. +There you can select "Releases only". -If you select "Watching" instead of "Releases only", you will receive notifications when someone creates a new issue. +By doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **Typer** with bug fixes and new features. -Then you can try and help them solving those issues. +## Follow the author -## Create issues +You can follow [me (Sebastián Ramírez / `tiangolo`)](https://tiangolo.com), the author in a few places, to hear when I have news to share about FastAPI and friends: -You can create a new issue in the GitHub repository, for example to: +* [@tiangolo on **GitHub**](https://github.com/tiangolo). +* [@tiangolo on **X (Twitter)**](https://x.com/tiangolo) +* [@tiangolo.com on **Bluesky**](https://bsky.app/profile/tiangolo.com) +* [@tiangolo on **LinkedIn**](https://www.linkedin.com/in/tiangolo/). -* Report a bug/issue. -* Suggest a new feature. -* Ask a question. +## Ask Questions -## Create a Pull Request +You can [create a new question](https://github.com/fastapi/typer/discussions/new?category=questions) in the GitHub repository, for example to: -You can create a Pull Request, for example: +* Ask a **question** or ask about a **problem**. +* Suggest a new **feature**. -* To fix a typo you found on the documentation. -* To propose new documentation sections. -* To fix an existing issue/bug. -* To add a new feature. +## Join the Chat -## Sponsor the author +Join the 👥 [FastAPI and Friends Discord chat server](https://discord.gg/VQjSZaeJmf) 👥 and hang out with others in the community. There's a `#typer` channel. -You can also financially support the author (me) through GitHub sponsors. +/// tip -There you could buy me a coffee ☕️ to say thanks. 😄 +For questions, ask them in [GitHub Discussions](https://github.com/fastapi/typer/discussions/new?category=questions), there's a much better chance you will receive help there. -## Sponsor the tools that power Typer +Use the chat only for other general conversations. -As you have seen in the documentation, Typer is built on top of Click. +/// -You can also sponsor: +### Don't use the Chat for Questions -* Pallets Project (Click maintainers)via the PSF or via Tidelift +Keep in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. ---- +In GitHub, the template will guide you to write the right question so that you can more easily get a good answer, or even solve the problem yourself even before asking. -Thanks! 🚀 +Conversations in the chat systems are also not as easily searchable as in GitHub, they get lost. diff --git a/docs/img/favicon.png b/docs/img/favicon.png old mode 100755 new mode 100644 index b5ce368d78..05752c5c07 Binary files a/docs/img/favicon.png and b/docs/img/favicon.png differ diff --git a/docs/img/github-social-preview.png b/docs/img/github-social-preview.png index 73674f2e24..12ec338123 100644 Binary files a/docs/img/github-social-preview.png and b/docs/img/github-social-preview.png differ diff --git a/docs/img/github-social-preview.svg b/docs/img/github-social-preview.svg index 3743f3ceff..dd99136570 100644 --- a/docs/img/github-social-preview.svg +++ b/docs/img/github-social-preview.svg @@ -1,22 +1,22 @@ + inkscape:export-ydpi="96" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + inkscape:current-layer="g818" + inkscape:showpageshadow="2" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="mm" /> + id="defs2"> + + + @@ -63,32 +102,71 @@ - + Typer - + style="font-style:normal;font-weight:normal;font-size:127.57px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:3.18925" + xml:space="preserve">Typer + + + + + + + Build great CLIs. Easy to code. Based on Python type hints. diff --git a/docs/img/icon-black.svg b/docs/img/icon-black.svg deleted file mode 100644 index bc987f38b6..0000000000 --- a/docs/img/icon-black.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/img/icon-square.svg b/docs/img/icon-square.svg new file mode 100644 index 0000000000..8e2b3fcd1b --- /dev/null +++ b/docs/img/icon-square.svg @@ -0,0 +1,68 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/docs/img/icon-white.svg b/docs/img/icon-white.svg deleted file mode 100644 index 1de612ccd4..0000000000 --- a/docs/img/icon-white.svg +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - diff --git a/docs/img/icon.svg b/docs/img/icon.svg new file mode 100644 index 0000000000..6bdf83703d --- /dev/null +++ b/docs/img/icon.svg @@ -0,0 +1,68 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/docs/img/logo-margin/logo-margin-vector.svg b/docs/img/logo-margin/logo-margin-vector.svg index 815a4e77fc..ef28325eb0 100644 --- a/docs/img/logo-margin/logo-margin-vector.svg +++ b/docs/img/logo-margin/logo-margin-vector.svg @@ -1,39 +1,15 @@ - + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> image/svg+xml - @@ -56,33 +31,43 @@ id="rect824" style="opacity:0.98000004;fill:none;fill-opacity:1;stroke-width:0.42965442" /> - - - - + id="g818" + transform="translate(-5.3444232)"> + style="font-weight:bold;font-size:127.57px;line-height:1.25;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono Bold';letter-spacing:0px;word-spacing:0px;stroke-width:3.18925" + d="m 294.86852,68.451101 v 13.01214 H 274.07461 V 147.41693 H 258.25593 V 81.463241 h -20.79391 v -13.01214 z m 63.40204,19.00793 q -2.42383,8.41962 -4.72009,16.584099 -2.16869,8.03691 -4.59252,15.81868 -2.42383,7.6542 -5.23037,15.05326 -2.67897,7.39906 -5.86822,14.41541 -2.42383,5.1028 -4.84766,8.80233 -2.29626,3.69953 -5.23037,5.99579 -2.80654,2.42383 -6.3785,3.57196 -3.44439,1.14813 -8.16448,1.14813 -3.95467,0 -7.27149,-0.63785 -3.18925,-0.63785 -5.35794,-1.65841 l 2.93411,-13.13971 q 2.5514,1.14813 4.46495,1.53084 2.04112,0.51028 4.33738,0.51028 4.59252,0 7.39906,-3.06168 2.80654,-3.06168 4.46495,-7.27149 -5.61308,-11.22616 -11.22616,-25.64157 -5.61308,-14.54298 -10.2056,-32.020069 h 16.32896 q 1.14813,4.72009 2.5514,10.07803 1.53084,5.357939 3.18925,10.715879 1.65841,5.35794 3.44439,10.58831 1.91355,5.1028 3.8271,9.56775 1.2757,-4.46495 2.67897,-9.56775 1.40327,-5.23037 2.67897,-10.46074 1.2757,-5.35794 2.42383,-10.715879 1.2757,-5.35794 2.29626,-10.2056 z m 47.4558,30.489229 q 0,-8.03691 -3.18925,-13.13971 -3.18925,-5.230369 -9.69532,-5.230369 -1.78598,0 -3.8271,0.25514 -2.04112,0.12757 -3.44439,0.255139 v 32.91306 q 1.53084,1.02056 4.08224,1.65841 2.5514,0.63785 5.1028,0.63785 5.61308,0 8.29205,-4.46495 2.67897,-4.59252 2.67897,-12.88457 z m 15.94625,-0.51028 q 0,6.88878 -1.53084,12.757 -1.53084,5.74065 -4.46495,9.95046 -2.93411,4.08224 -7.39906,6.3785 -4.46495,2.29626 -10.33317,2.29626 -5.99579,0 -12.37429,-2.93411 v 22.57989 H 369.87919 V 89.372581 q 4.33738,-1.40327 10.58831,-2.29626 6.3785,-0.89299 12.50186,-0.89299 13.90513,0 21.30419,8.41962 7.39906,8.419619 7.39906,22.835029 z m 7.90909,0.51028 q 0,-7.90934 2.42383,-13.90513 2.42383,-5.995789 6.3785,-9.950459 3.95467,-4.08224 9.05747,-6.12336 5.1028,-2.04112 10.46074,-2.04112 13.26728,0 20.15606,7.90934 6.88878,7.781769 6.88878,22.835029 0,1.53084 -0.12757,3.18925 0,1.65841 -0.12757,2.67897 h -38.90885 q 0,5.86822 4.84766,9.31261 4.84766,3.31682 12.50186,3.31682 4.72009,0 8.9299,-1.02056 4.33738,-1.02056 7.27149,-2.04112 l 2.16869,13.39485 q -4.08224,1.40327 -8.67476,2.29626 -4.59252,1.02056 -10.33317,1.02056 -7.6542,0 -13.77756,-1.91355 -5.99579,-2.04112 -10.33317,-5.86822 -4.20981,-3.95467 -6.50607,-9.69532 -2.29626,-5.74065 -2.29626,-13.39485 z m 40.18455,-6.25093 q 0,-2.42383 -0.63785,-4.59252 -0.63785,-2.29626 -2.04112,-4.08224 -1.40327,-1.78598 -3.57196,-2.80654 -2.16869,-1.148129 -5.35794,-1.148129 -3.06168,0 -5.35794,1.020559 -2.16869,1.02056 -3.69953,2.80654 -1.40327,1.78598 -2.29626,4.08224 -0.76542,2.29626 -1.02056,4.72009 z m 75.39363,-8.29205 q -1.53084,-0.38271 -3.69953,-0.76542 -2.04112,-0.38271 -4.20981,-0.63785 -2.16869,-0.38271 -4.20981,-0.51028 -2.04112,-0.12757 -3.44439,-0.12757 -3.31682,0 -6.50607,0.38271 -3.18925,0.25514 -6.50607,1.14813 v 44.52193 H 500.76552 V 91.413701 q 6.25093,-2.29626 12.757,-3.69953 6.63364,-1.40327 15.43597,-1.40327 1.2757,0 3.57196,0.12757 2.42383,0.12757 5.1028,0.51028 2.67897,0.25514 5.35794,0.76542 2.80654,0.38271 4.97523,1.14813 z" + id="text861" + aria-label="Typer" /> + + + + + + - diff --git a/docs/img/logo-margin/logo-margin-white-vector.svg b/docs/img/logo-margin/logo-margin-white-vector.svg new file mode 100644 index 0000000000..d4c32dddc4 --- /dev/null +++ b/docs/img/logo-margin/logo-margin-white-vector.svg @@ -0,0 +1,73 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/docs/img/logo-margin/logo-margin-white.svg b/docs/img/logo-margin/logo-margin-white.svg new file mode 100644 index 0000000000..b758dc2481 --- /dev/null +++ b/docs/img/logo-margin/logo-margin-white.svg @@ -0,0 +1,78 @@ + + + + + + + image/svg+xml + + + + + + + Typer + + + + + + + + diff --git a/docs/img/logo-margin/logo-margin.svg b/docs/img/logo-margin/logo-margin.svg index a7cf9b14f5..4501f53abd 100644 --- a/docs/img/logo-margin/logo-margin.svg +++ b/docs/img/logo-margin/logo-margin.svg @@ -1,39 +1,15 @@ - + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> image/svg+xml - @@ -62,17 +37,42 @@ id="text861" y="147.41693" x="234.27277" - style="font-style:normal;font-weight:normal;font-size:127.57019043px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.189255" + style="font-style:normal;font-weight:normal;font-size:127.57px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.18925" xml:space="preserve">Typer - + id="tspan859">Typer + + + + + + diff --git a/docs/index.md b/docs/index.md index bb1ccf96fc..b65242172e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,33 +1,42 @@ +# + + +

- Typer + Typer + + Typer +

Typer, build great CLIs. Easy to code. Based on Python type hints.

- - Test - - - Publish + + Test - - Coverage - - + + Coverage + Package version

--- -**Documentation**: https://typer.tiangolo.com +**Documentation**: [https://typer.tiangolo.com](https://typer.tiangolo.com) -**Source Code**: https://github.com/tiangolo/typer +**Source Code**: [https://github.com/fastapi/typer](https://github.com/fastapi/typer) --- -Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python 3.6+ type hints. +Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python type hints. + +It's also a command line tool to run scripts, automatically converting them to CLI applications. + + The key features are: @@ -36,84 +45,134 @@ The key features are: * **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. * **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**. * **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments. +* **Run scripts**: Typer includes a `typer` command/program that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally. ## FastAPI of CLIs - - -**Typer** is FastAPI's little sibling. - -And it's intended to be the FastAPI of CLIs. - -## Requirements - -Python 3.6+ - -**Typer** stands on the shoulders of a giant. Its only internal dependency is Click. +**Typer** is [FastAPI](https://fastapi.tiangolo.com)'s little sibling, it's the FastAPI of CLIs. ## Installation +First, [install `uv`](https://docs.astral.sh/uv/getting-started/installation/), and then add **Typer** to your project: +
```console -$ pip install typer +$ uv add typer ---> 100% -Successfully installed typer ```
+This installs both the Typer library and the `typer` command in the project's virtual environment. To use `typer` directly with shell completion, [activate the project environment and install completion](tutorial/install.md#activate-the-virtual-environment). + +If you prefer to use `pip`, install `typer` inside a virtual environment. See the [installation guide](tutorial/install.md) for the alternative steps. + ## Example ### The absolute minimum * Create a file `main.py` with: +```Python +def main(name: str): + print(f"Hello {name}") +``` + +This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application. + +### Run it + +Run your application with the `typer` command: + +
+ +```console +// Run your application +$ typer main.py run + +// You get a nice error, you are missing 'name' +Usage: typer [PATH_OR_MODULE] run [OPTIONS] {name} +Try 'typer [PATH_OR_MODULE] run --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'name'. │ +╰───────────────────────────────────────────────────╯ + + +// You get a --help for free +$ typer main.py run --help + +Usage: typer [PATH_OR_MODULE] run [OPTIONS] {name} + +Run the provided Typer app. + +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ + +// Now pass the 'name' argument +$ typer main.py run Camila + +Hello Camila + +// It works! 🎉 +``` + +
+ +This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts. + +**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command. + +## Use Typer in your code + +Now let's start using Typer in your own code, update `main.py` with: + ```Python import typer def main(name: str): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": typer.run(main) ``` -### Run it - -Run your application: +Now you could run it with Python directly:
```console // Run your application -$ python main.py +$ uv run python main.py -// You get a nice error, you are missing NAME -Usage: main.py [OPTIONS] NAME -Try "main.py --help" for help. +// You get a nice error, you are missing 'name' +Usage: main.py [OPTIONS] {name} +Try 'main.py --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'name'. │ +╰───────────────────────────────────────────────────╯ -Error: Missing argument 'NAME'. // You get a --help for free -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} -Arguments: - NAME [required] +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. - -// When you create a package you get ✨ auto-completion ✨ for free, installed with --install-completion - -// Now pass the NAME argument -$ python main.py Camila +// Now pass the 'name' argument +$ uv run python main.py Camila Hello Camila @@ -122,7 +181,7 @@ Hello Camila
-**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use Typer CLI. +**Note**: you can also call this same script with the `typer` command, but you don't need to. ## Example upgrade @@ -144,15 +203,15 @@ app = typer.Typer() @app.command() def hello(name: str): - typer.echo(f"Hello {name}") + print(f"Hello {name}") @app.command() def goodbye(name: str, formal: bool = False): if formal: - typer.echo(f"Goodbye Ms. {name}. Have a good day.") + print(f"Goodbye Ms. {name}. Have a good day.") else: - typer.echo(f"Bye {name}!") + print(f"Bye {name}!") if __name__ == "__main__": @@ -168,73 +227,109 @@ And that will: ### Run the upgraded example +Check the new help: +
```console -// Check the --help -$ python main.py --help +$ uv run python main.py --help + + Usage: main.py [OPTIONS] COMMAND [ARGS]... + +╭─ Options ─────────────────────────────────────────╮ +│ --install-completion Install completion │ +│ for the current │ +│ shell. │ +│ --show-completion Show completion for │ +│ the current shell, │ +│ to copy it or │ +│ customize the │ +│ installation. │ +│ --help Show this message │ +│ and exit. │ +╰───────────────────────────────────────────────────╯ +╭─ Commands ────────────────────────────────────────╮ +│ hello │ +│ goodbye │ +╰───────────────────────────────────────────────────╯ -Usage: main.py [OPTIONS] COMMAND [ARGS]... +// When you create a package you get ✨ auto-completion ✨ for free, installed with --install-completion -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +// You have 2 subcommands (the 2 functions): hello and goodbye +``` -Commands: - goodbye - hello +
-// You have 2 subcommands (the 2 functions): goodbye and hello +Now check the help for the `hello` command: -// Now get the --help for hello +
-$ python main.py hello --help +```console +$ uv run python main.py hello --help -Usage: main.py hello [OPTIONS] NAME + Usage: main.py hello [OPTIONS] {name} -Arguments: - NAME [required] +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────╯ +``` -Options: - --help Show this message and exit. +
-// And now get the --help for goodbye +And now check the help for the `goodbye` command: -$ python main.py goodbye --help +
-Usage: main.py goodbye [OPTIONS] NAME +```console +$ uv run python main.py goodbye --help -Arguments: - NAME [required] + Usage: main.py goodbye [OPTIONS] {name} -Options: - --formal / --no-formal [default: False] - --help Show this message and exit. +╭─ Arguments ───────────────────────────────────────╮ +│ * name [required] │ +╰───────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────╮ +│ --formal --no-formal [default: no-formal] │ +│ --help Show this message │ +│ and exit. │ +╰───────────────────────────────────────────────────╯ // Automatic --formal and --no-formal for the bool option 🎉 +``` -// And if you use it with the hello command +
-$ python main.py hello Camila +Now you can try out the new command line application: + +
+ +```console +// Use it with the hello command + +$ uv run python main.py hello Camila Hello Camila // And with the goodbye command -$ python main.py goodbye Camila +$ uv run python main.py goodbye Camila Bye Camila! // And with --formal -$ python main.py goodbye --formal Camila +$ uv run python main.py goodbye --formal Camila Goodbye Ms. Camila. Have a good day. ```
+**Note**: If your app only has one command, by default the command name is **omitted** in usage: `python main.py Camila`. However, when there are multiple commands, you must **explicitly include the command name**: `python main.py hello Camila`. See [One or Multiple Commands](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/) for more details. + ### Recap In summary, you declare **once** the types of parameters (*CLI arguments* and *CLI options*) as function parameters. @@ -243,7 +338,7 @@ You do that with standard modern Python types. You don't have to learn a new syntax, the methods or classes of a specific library, etc. -Just standard **Python 3.6+**. +Just standard **Python**. For example, for an `int`: @@ -261,36 +356,35 @@ And similarly for **files**, **paths**, **enums** (choices), etc. And there are **You get**: great editor support, including **completion** and **type checks** everywhere. -**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using Typer CLI. +**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command. For a more complete example including more features, see the Tutorial - User Guide. -## Optional Dependencies +## Dependencies + +**Typer** requires only a few dependencies: + +* [`rich`](https://rich.readthedocs.io/en/stable/index.html): to show nicely formatted errors automatically. +* [`pydantic`](https://pydantic.dev/docs/validation/latest/get-started/): to validate input parameters. +* [`shellingham`](https://github.com/sarugaku/shellingham): to automatically detect the current shell when installing completion. +* [`annotated-doc`](https://github.com/fastapi/annotated-doc): to generate documentation from Python type annotations. +* [`colorama`](https://github.com/tartley/colorama) (only on Windows): for producing colored terminal text on Windows. -Typer uses Click internally. That's the only dependency. +### Click code -But you can also install extras: +Typer used to depend on [Click](https://click.palletsprojects.com/) as well, a popular tool for building CLIs in Python. -* colorama: and Click will automatically use it to make sure your terminal's colors always work correctly, even in Windows. - * Then you can use any tool you want to output your terminal's colors in all the systems, including the integrated `typer.style()` and `typer.secho()` (provided by Click). - * Or any other tool, e.g. wasabi, blessings. -* shellingham: and Typer will automatically detect the current shell when installing completion. - * With `shellingham` you can just use `--install-completion`. - * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`. +Since version 0.26.0, Typer has vendored Click (included Click's source code internally, instead of installing it as a third party package) and has unified the code interactions between Typer and the embedded Click source code for easier maintainability in the future. -You can install `typer` with `colorama` and `shellingham` with `pip install typer[all]`. +Note that some Click functionality will not be available anymore in the future, as we continue to improve and extend Typer's codebase. -## Other tools and plug-ins +### `typer-slim` -Click has many plug-ins available that you can use. And there are many tools that help with command line applications that you can use as well, even if they are not related to Typer or Click. +There used to be a slimmed-down version of Typer called `typer-slim`, which didn't include the dependencies `rich` and `shellingham`, nor the `typer` command. -For example: +However, since version 0.22.0, we have stopped supporting this, and `typer-slim` now simply installs (all of) Typer. -* click-spinner: to show the user that you are loading data. A Click plug-in. - * There are several other Click plug-ins at click-contrib that you can explore. -* tabulate: to automatically display tabular data nicely. Independent of Click or Typer. -* tqdm: a fast, extensible progress bar, alternative to Typer's own `typer.progressbar()`. -* etc... you can re-use many of the great available tools for building CLIs. +If you want to disable Rich globally, you can set an environmental variable `TYPER_USE_RICH` to `False` or `0`. ## License diff --git a/docs/js/custom.js b/docs/js/custom.js index 45243eb7ef..232b2ceb3e 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -1,105 +1,169 @@ -document.querySelectorAll(".use-termynal").forEach(node => { - node.style.display = "block"; - new Termynal(node, { - lineDelay: 500 +function escapePlainCliLine(line) { + return line + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function prepareTermynalLines(lines, isRichHtml) { + if (isRichHtml) { + return lines.join("
"); + } + return lines.map(escapePlainCliLine).join("
"); +} + +function setupTermynal() { + document.querySelectorAll(".use-termynal").forEach(node => { + node.style.display = "block"; + new Termynal(node, { + lineDelay: 500 + }); }); -}); -const progressLiteralStart = "---> 100%"; -const promptLiteralStart = "$ "; -const customPromptLiteralStart = "# "; -const termynalActivateClass = "termy"; -let termynals = []; + const progressLiteralStart = "---> 100%"; + const promptLiteralStart = "$ "; + const customPromptLiteralStart = "# "; + const termynalActivateClass = "termy"; + let termynals = []; -function createTermynals() { - document - .querySelectorAll(`.${termynalActivateClass} .highlight`) - .forEach(node => { - const text = node.textContent; - const lines = text.split("\n"); - const useLines = []; - let buffer = []; - function saveBuffer() { - if (buffer.length) { - let isBlankSpace = true; - buffer.forEach(line => { - if (line) { - isBlankSpace = false; + function createTermynals() { + document + .querySelectorAll(`.${termynalActivateClass} .highlight code`) + .forEach(node => { + const text = node.textContent; + const lines = text.split("\n"); + // If it is a type in brackets like or , it won't have + // HTML attributes (=) or a closing tag, while Rich HTML tags do. + const hasHtmlAttribute = /<[A-Za-z][^>]*=/.test(text); + const hasClosingTag = /<\/[a-zA-Z0-9]/.test(text); + const isRichHtml = hasHtmlAttribute || hasClosingTag; + const useLines = []; + let buffer = []; + function saveBuffer() { + if (buffer.length) { + let isBlankSpace = true; + buffer.forEach(line => { + if (line) { + isBlankSpace = false; + } + }); + dataValue = {}; + if (isBlankSpace) { + dataValue["delay"] = 0; } - }); - dataValue = {}; - if (isBlankSpace) { - dataValue["delay"] = 0; - } - if (buffer[buffer.length - 1] === "") { - // A last single
won't have effect - // so put an additional one - buffer.push(""); + if (buffer[buffer.length - 1] === "") { + // A last single
won't have effect + // so put an additional one + buffer.push(""); + } + const bufferValue = prepareTermynalLines(buffer, isRichHtml); + dataValue["value"] = bufferValue; + useLines.push(dataValue); + buffer = []; } - const bufferValue = buffer.join("
"); - dataValue["value"] = bufferValue; - useLines.push(dataValue); - buffer = []; } - } - for (let line of lines) { - if (line === progressLiteralStart) { - saveBuffer(); - useLines.push({ - type: "progress" - }); - } else if (line.startsWith(promptLiteralStart)) { - saveBuffer(); - const value = line.replace(promptLiteralStart, "").trimEnd(); - useLines.push({ - type: "input", - value: value - }); - } else if (line.startsWith("// ")) { - saveBuffer(); - const value = "💬 " + line.replace("// ", "").trimEnd(); - useLines.push({ - value: value, - class: "termynal-comment", - delay: 0 - }); - } else if (line.startsWith(customPromptLiteralStart)) { - saveBuffer(); - const promptStart = line.indexOf(promptLiteralStart); - if (promptStart === -1) { - console.error("Custom prompt found but no end delimiter", line) + for (let line of lines) { + if (line === progressLiteralStart) { + saveBuffer(); + useLines.push({ + type: "progress" + }); + } else if (line.startsWith(promptLiteralStart)) { + saveBuffer(); + const value = line.replace(promptLiteralStart, "").trimEnd(); + useLines.push({ + type: "input", + value: value + }); + } else if (line.startsWith("// ")) { + saveBuffer(); + let comment = line.replace("// ", "").trimEnd(); + if (!isRichHtml) { + comment = escapePlainCliLine(comment); + } + const value = "💬 " + comment; + useLines.push({ + value: value, + class: "termynal-comment", + delay: 0 + }); + } else if (line.startsWith(customPromptLiteralStart)) { + saveBuffer(); + const promptStart = line.indexOf(promptLiteralStart); + if (promptStart === -1) { + console.error("Custom prompt found but no end delimiter", line) + } + const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") + let value = line.slice(promptStart + promptLiteralStart.length); + useLines.push({ + type: "input", + value: value, + prompt: prompt + }); + } else { + buffer.push(line); } - const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") - let value = line.slice(promptStart + promptLiteralStart.length); - useLines.push({ - type: "input", - value: value, - prompt: prompt - }); - } else { - buffer.push(line); } - } - saveBuffer(); - const div = document.createElement("div"); - node.replaceWith(div); - const termynal = new Termynal(div, { - lineData: useLines, - noInit: true, - lineDelay: 500 + saveBuffer(); + const inputCommands = useLines.filter(line => line.type === "input").map(line => line.value).join("\n"); + node.textContent = inputCommands; + const div = document.createElement("div"); + node.style.display = "none"; + node.after(div); + const termynal = new Termynal(div, { + lineData: useLines, + noInit: true, + lineDelay: 500 + }); + termynals.push(termynal); }); - termynals.push(termynal); + } + + function loadVisibleTermynals() { + termynals = termynals.filter(termynal => { + if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { + termynal.init(); + return false; + } + return true; }); + } + window.addEventListener("scroll", loadVisibleTermynals); + createTermynals(); + loadVisibleTermynals(); } -function loadVisibleTermynals() { - termynals = termynals.filter(termynal => { - if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { - termynal.init(); - return false; - } - return true; +function openLinksInNewTab() { + const siteUrl = document.querySelector("link[rel='canonical']")?.href + || window.location.origin; + const siteOrigin = new URL(siteUrl).origin; + document.querySelectorAll(".md-content a[href]").forEach(a => { + if (a.getAttribute("target") === "_self") return; + const href = a.getAttribute("href"); + if (!href) return; + try { + const url = new URL(href, window.location.href); + // Skip same-page anchor links (only the hash differs) + if (url.origin === window.location.origin + && url.pathname === window.location.pathname + && url.search === window.location.search) return; + if (!a.hasAttribute("target")) { + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "noopener"); + } + if (url.origin !== siteOrigin) { + a.dataset.externalLink = ""; + } else { + a.dataset.internalLink = ""; + } + } catch (_) {} }); } -window.addEventListener("scroll", loadVisibleTermynals); -createTermynals(); -loadVisibleTermynals(); + +async function main() { + setupTermynal(); + openLinksInNewTab(); +} + +document$.subscribe(() => { + main() +}) diff --git a/docs/js/termynal.js b/docs/js/termynal.js index 4ac32708a3..de4d8d1442 100644 --- a/docs/js/termynal.js +++ b/docs/js/termynal.js @@ -127,27 +127,29 @@ class Termynal { } generateRestart() { - const restart = document.createElement('a') - restart.onclick = (e) => { - e.preventDefault() + const restart = document.createElement('button') + restart.type = 'button' + + restart.onclick = () => { this.container.innerHTML = '' this.init() } - restart.href = '#' + restart.setAttribute('data-terminal-control', '') restart.innerHTML = "restart ↻" return restart } generateFinish() { - const finish = document.createElement('a') - finish.onclick = (e) => { - e.preventDefault() + const finish = document.createElement('button') + finish.type = 'button' + + finish.onclick = () => { this.lineDelay = 0 this.typeDelay = 0 this.startDelay = 0 } - finish.href = '#' + finish.setAttribute('data-terminal-control', '') finish.innerHTML = "fast →" this.finishElement = finish @@ -249,7 +251,6 @@ class Termynal { attrs += `${this.pfx}-${prop}="${line[prop]}" ` } } - return attrs; } } diff --git a/docs/management.md b/docs/management.md new file mode 100644 index 0000000000..03f91279f3 --- /dev/null +++ b/docs/management.md @@ -0,0 +1,15 @@ +# Repository Management + +Here's a short description of how the Typer repository is managed and maintained. + +## Owner + +I, [@tiangolo](https://github.com/tiangolo), am the creator and owner of the Typer repository. 🤓 + +I normally give the final review to each PR before merging them. I make the final decisions on the project, I'm the [BDFL](https://en.wikipedia.org/wiki/Benevolent_dictator_for_life). 😅 + +## Team + +There's a team of people that help manage and maintain the project. 😎 + +Learn more about it in [tiangolo.com - GitHub FastAPI](https://tiangolo.com/github-fastapi/). diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 7e501ba0fb..94d9808cc7 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,29 +1 @@ {% extends "base.html" %} -{%- block scripts %} -{{ super() }} - - - -
-
You can ask questions about Typer. Try:
-
How can I terminate a program?
-
How to launch applications?
-
How to add help to CLI argument?
-
-
-{%- endblock %} diff --git a/docs/reference/context.md b/docs/reference/context.md new file mode 100644 index 0000000000..55cbecb5f9 --- /dev/null +++ b/docs/reference/context.md @@ -0,0 +1,39 @@ +# Context + +Every app has a special internal object that keeps track of state relevant to the script's execution. +For some advanced use-cases, you may want to access it directly. +This can be done by declaring a function parameter of type `typer.Context`. + +Similarly, you can also declare a function parameter with type `typer.CallbackParam` in case a callback could be used +by several CLI parameters, and you need to figure out which one it was. + + +```python +from typing import Annotated + +import typer + +app = typer.Typer() + + +def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): + if ctx.resilient_parsing: + return + print(f"Validating param: {param.name}") + if value != "Rick": + raise typer.BadParameter("Only Rick is allowed") + return value + + +@app.command() +def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() +``` + +::: typer.Context + +::: typer.CallbackParam diff --git a/docs/reference/file_objects.md b/docs/reference/file_objects.md new file mode 100644 index 0000000000..f9a95b617f --- /dev/null +++ b/docs/reference/file_objects.md @@ -0,0 +1,18 @@ +# File objects + +When you want to declare some types of files, you can use `Path`. +However, in some cases you may need to have access to a file-like object, and then you can use these [special File types](https://typer.tiangolo.com/tutorial/parameter-types/file/). + +These objects can be imported from `typer` directly: + +```python +from typer import FileBinaryRead, FileBinaryWrite, FileText, FileTextWrite +``` + +::: typer.FileText + +::: typer.FileTextWrite + +::: typer.FileBinaryRead + +::: typer.FileBinaryWrite diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000000..6a932833ec --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,7 @@ +# Reference + +Here's the reference or code API, the classes, functions, parameters, attributes, and +all the Typer parts you can use in your applications. + +If you want to **learn Typer** you are much better off reading the +[Typer Tutorial](https://typer.tiangolo.com/tutorial/). diff --git a/docs/reference/parameters.md b/docs/reference/parameters.md new file mode 100644 index 0000000000..20960d2b46 --- /dev/null +++ b/docs/reference/parameters.md @@ -0,0 +1,33 @@ +# Parameters + +Parameters to our command line interface may be both [CLI Options](https://typer.tiangolo.com/tutorial/options/) and [CLI Arguments](https://typer.tiangolo.com/tutorial/arguments/): + +```python +from typing import Annotated +import typer + +app = typer.Typer() + + +@app.command() +def register( + user: Annotated[str, typer.Argument()], + age: Annotated[int, typer.Option(min=18)], + score: Annotated[float, typer.Option(max=100)] = 0, +): + print(f"User is {user}") + print(f"--age is {age}") + print(f"--score is {score}") + + +if __name__ == "__main__": + app() +``` + +::: typer.Argument + options: + show_overloads: false + +::: typer.Option + options: + show_overloads: false diff --git a/docs/reference/run_launch.md b/docs/reference/run_launch.md new file mode 100644 index 0000000000..9f82672a08 --- /dev/null +++ b/docs/reference/run_launch.md @@ -0,0 +1,11 @@ +# `run` and `launch` + +These two functions can be imported from `typer` directly: + +```python +from typer import launch, run +``` + +::: typer.run + +::: typer.launch diff --git a/docs/reference/typer.md b/docs/reference/typer.md new file mode 100644 index 0000000000..1655fec788 --- /dev/null +++ b/docs/reference/typer.md @@ -0,0 +1,16 @@ +# `Typer` class + +Here's the reference information for the `Typer` class, with all its parameters, attributes and methods. + +You can import the `Typer` class directly from `typer`: + +```python +from typer import Typer +``` + +::: typer.Typer + options: + members: + - callback + - command + - add_typer diff --git a/docs/release-notes.md b/docs/release-notes.md index 1bb1875efe..2c1e14511c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,1487 @@ +# Release Notes + ## Latest Changes +## 0.29.1 (2026-11-11) + +### Upgrades + +* ⬆️ Add support for Python 3.15 and 3.15t. PR [#2013](https://github.com/fastapi/typer/pull/2013) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Clarify compatibility changes in the 0.29.0 release notes. PR [#2014](https://github.com/fastapi/typer/pull/2014) by [@tiangolo](https://github.com/tiangolo). +* 📝 Adopt unversioned documentation snippets. PR [#2010](https://github.com/fastapi/typer/pull/2010) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔥 Remove obsolete documentation snippet generators. PR [#2012](https://github.com/fastapi/typer/pull/2012) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Preserve snippet variants in documentation cleanup. PR [#2011](https://github.com/fastapi/typer/pull/2011) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade markdown-include-variants to 0.0.10. PR [#2009](https://github.com/fastapi/typer/pull/2009) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add an agent skills note to GitHub releases. PR [#2008](https://github.com/fastapi/typer/pull/2008) by [@tiangolo](https://github.com/tiangolo). + +## 0.29.0 (2026-11-09) + +### Features + +* ✨ Use Pydantic for parameter validation. PR [#1831](https://github.com/fastapi/typer/pull/1831) by [@svlandeg](https://github.com/svlandeg). + + Typer now uses Pydantic internally to validate command-line parameters. This is a gradual migration focused on preserving compatibility with existing Typer applications. Some validation error messages, help text, and usage output have changed. + + Compatibility changes: + + * Pydantic `>=2.5.3` is now a required dependency. + * `click_type` has been removed from `typer.Option()` and `typer.Argument()`. New ways to provide custom types will be provided with Pydantic, with much more flexibility. + * Some defaults are now validated more strictly. The previous tuple example's `(None, None, None)` default now fails validation. Use `tuple[str, int, bool] | None` with a default of `None` and check for `None` before unpacking, as shown in the [updated tuple example](https://typer.tiangolo.com/tutorial/multiple-values/options-with-multiple-values/). This also makes the code more correct, so you could consider this a bug fix, but if you depended on this behavior, there's a change needed in your code. + * Default [datetime parsing](https://typer.tiangolo.com/tutorial/parameter-types/datetime/) now follows Pydantic's rules, accepting Unix timestamps, timezone offsets, and fractional seconds. + * Callback parameter objects no longer expose Click's `.type`, `.make_metavar()`, `.human_readable_name`, or `.type_cast_value()`. Custom integrations using these need updating. + +### Fixes + +* 🐛 Escape terminal controls in Pydantic validation errors. PR [#2005](https://github.com/fastapi/typer/pull/2005) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Explain Pydantic validation in the release notes. PR [#2006](https://github.com/fastapi/typer/pull/2006) by [@tiangolo](https://github.com/tiangolo). +* 🎨 Use the Zensical modern theme for docs. PR [#2004](https://github.com/fastapi/typer/pull/2004) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add AI development guidance and coding agent notices. PR [#2003](https://github.com/fastapi/typer/pull/2003) by [@tiangolo](https://github.com/tiangolo). + +## 0.28.0 (2026-11-06) + +### Fixes + +* 🐛 Ensure that the default option name is not changed by setting a metavar. PR [#1957](https://github.com/fastapi/typer/pull/1957) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 🎨 Enable Ruff formatting for Python in Markdown. PR [#1978](https://github.com/fastapi/typer/pull/1978) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump certifi from 2025.11.12 to 2026.10.18. PR [#1990](https://github.com/fastapi/typer/pull/1990) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump the python-packages group with 6 updates. PR [#1994](https://github.com/fastapi/typer/pull/1994) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump the github-actions group with 6 updates. PR [#1993](https://github.com/fastapi/typer/pull/1993) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pre-commit hooks. PR [#1995](https://github.com/fastapi/typer/pull/1995) by [@pr-submit[bot]](https://github.com/apps/pr-submit). + +## 0.27.3 (2026-10-06) + +### Fixes + +* 🐛 Escape terminal control characters in error messages. PR [#1972](https://github.com/fastapi/typer/pull/1972) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump pyjwt from 2.13.0 to 2.15.0. PR [#1969](https://github.com/fastapi/typer/pull/1969) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump urllib3 from 2.7.0 to 2.8.0. PR [#1968](https://github.com/fastapi/typer/pull/1968) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pymdown-extensions from 11.0 to 11.0.1. PR [#1924](https://github.com/fastapi/typer/pull/1924) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump the python-packages group across 1 directory with 7 updates. PR [#1966](https://github.com/fastapi/typer/pull/1966) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cryptography from 48.0.1 to 50.0.2. PR [#1920](https://github.com/fastapi/typer/pull/1920) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pre-commit hooks. PR [#1967](https://github.com/fastapi/typer/pull/1967) by [@pr-submit[bot]](https://github.com/apps/pr-submit). +* ⬆ Bump the github-actions group with 3 updates. PR [#1965](https://github.com/fastapi/typer/pull/1965) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump anyio from 4.12.0 to 4.14.2. PR [#1959](https://github.com/fastapi/typer/pull/1959) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Fix flaky `app_dir` and `launch` tutorial tests under `xdist`. PR [#1951](https://github.com/fastapi/typer/pull/1951) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump the python-packages group across 1 directory with 8 updates. PR [#1948](https://github.com/fastapi/typer/pull/1948) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump annotated-doc from 0.0.4 to 0.0.5. PR [#1949](https://github.com/fastapi/typer/pull/1949) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pre-commit hooks. PR [#1950](https://github.com/fastapi/typer/pull/1950) by [@pr-submit[bot]](https://github.com/apps/pr-submit). +* ⬆ Bump the github-actions group with 4 updates. PR [#1947](https://github.com/fastapi/typer/pull/1947) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.27.2 (2026-08-28) + +### Refactors + +* ♻️ Create `exceptions` module and `TyperException` base class. PR [#1942](https://github.com/fastapi/typer/pull/1942) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 🐛 Fix showing `fast` button as external link in animated terminals in docs. PR [#1912](https://github.com/fastapi/typer/pull/1912) by [@phalberg](https://github.com/phalberg). + +### Internal + +* ⬆️ Bump setup-uv action to 10.0.1. PR [#1940](https://github.com/fastapi/typer/pull/1940) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Migrate automatic labels to Latest Changes. PR [#1931](https://github.com/fastapi/typer/pull/1931) by [@tiangolo](https://github.com/tiangolo). +* 👷 Remove legacy label check. PR [#1930](https://github.com/fastapi/typer/pull/1930) by [@tiangolo](https://github.com/tiangolo). +* 🔐 Use PR Submit for pull requests. PR [#1928](https://github.com/fastapi/typer/pull/1928) by [@tiangolo](https://github.com/tiangolo). +* 👷 Use GitHub CLI for Git authentication. PR [#1927](https://github.com/fastapi/typer/pull/1927) by [@tiangolo](https://github.com/tiangolo). +* 👷 Use PR Push commit identity. PR [#1926](https://github.com/fastapi/typer/pull/1926) by [@tiangolo](https://github.com/tiangolo). +* 🔒 Replace pre-commit PAT with PR Push. PR [#1923](https://github.com/fastapi/typer/pull/1923) by [@tiangolo](https://github.com/tiangolo). +* 👷 Remove Zensical docs cache. PR [#1922](https://github.com/fastapi/typer/pull/1922) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove the old Latest Changes workflow. PR [#1921](https://github.com/fastapi/typer/pull/1921) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump the python-packages group with 6 updates. PR [#1917](https://github.com/fastapi/typer/pull/1917) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pymdown-extensions from 10.21.3 to 11.0. PR [#1910](https://github.com/fastapi/typer/pull/1910) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump the github-actions group with 5 updates. PR [#1916](https://github.com/fastapi/typer/pull/1916) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.27.1 (2026-08-03) + +### Features + +* ✨ Make `epilog` formatting consistent with other parts of the help string. PR [#1405](https://github.com/fastapi/typer/pull/1405) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 📝 Add Library Skills documentation. PR [#1906](https://github.com/fastapi/typer/pull/1906) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Prevent scroll-to-top on restart/fast buttons in the documentation. PR [#1904](https://github.com/fastapi/typer/pull/1904) by [@phalberg](https://github.com/phalberg). +* 📝 Recommend uv projects by default in docs. PR [#1903](https://github.com/fastapi/typer/pull/1903) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update help docs. PR [#1902](https://github.com/fastapi/typer/pull/1902) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆️ Upgrade latest-changes to 0.7.1. PR [#1909](https://github.com/fastapi/typer/pull/1909) by [@tiangolo](https://github.com/tiangolo). + +## 0.27.0 (2026-07-15) + +### Breaking Changes + +* 💥 Update metavar printing. PR [#1863](https://github.com/fastapi/typer/pull/1863) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆ Bump the python-packages group across 1 directory with 5 updates. PR [#1896](https://github.com/fastapi/typer/pull/1896) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Bump ty to 0.0.53. PR [#1877](https://github.com/fastapi/typer/pull/1877) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump the github-actions group across 1 directory with 4 updates. PR [#1897](https://github.com/fastapi/typer/pull/1897) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Set Dependabot schedule interval to "monthly". PR [#1873](https://github.com/fastapi/typer/pull/1873) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump pre-commit hooks. PR [#1895](https://github.com/fastapi/typer/pull/1895) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GH workflow to bump pre-commit hook versions. PR [#1872](https://github.com/fastapi/typer/pull/1872) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Fix latest-changes checkout target. PR [#1883](https://github.com/fastapi/typer/pull/1883) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Update issue-manager to 0.8.1. PR [#1880](https://github.com/fastapi/typer/pull/1880) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump the github-actions group across 1 directory with 8 updates. PR [#1874](https://github.com/fastapi/typer/pull/1874) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump the python-packages group across 1 directory with 5 updates. PR [#1876](https://github.com/fastapi/typer/pull/1876) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆️ Update latest-changes to 0.6.1. PR [#1879](https://github.com/fastapi/typer/pull/1879) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump https://github.com/crate-ci/typos from v1.46.0 to 1.47.2 in the pre-commit group across 1 directory. PR [#1834](https://github.com/fastapi/typer/pull/1834) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.14.1 to 2.14.2. PR [#1849](https://github.com/fastapi/typer/pull/1849) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.26.8 (2026-06-25) + +### Fixes + +* 🐛 Make second column of Rich help output reflect the type consistently, even when using `metavar`. PR [#1410](https://github.com/fastapi/typer/pull/1410) by [@svlandeg](https://github.com/svlandeg). +* 🐛 Fix formatting in `NoSuchOption.format_message()`. PR [#1843](https://github.com/fastapi/typer/pull/1843) by [@foomunleong](https://github.com/foomunleong). + +### Docs + +* 📝 Update docs badges: remove Publish badge, it doesn't give extra information. PR [#1850](https://github.com/fastapi/typer/pull/1850) by [@tiangolo](https://github.com/tiangolo). +* 📝 Fix formatting for help link to support GitHub-specific overview edge-case. PR [#1826](https://github.com/fastapi/typer/pull/1826) by [@phalberg](https://github.com/phalberg). + +### Internal + +* 👷 Simplify pull request workflow triggers. PR [#1858](https://github.com/fastapi/typer/pull/1858) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update issue-manager to 0.7.1. PR [#1857](https://github.com/fastapi/typer/pull/1857) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Update issue-manager to 0.7.0. PR [#1856](https://github.com/fastapi/typer/pull/1856) by [@tiangolo](https://github.com/tiangolo). +* 🔒️ Update zizmor workflow security checks. PR [#1854](https://github.com/fastapi/typer/pull/1854) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Group Dependabot updates. PR [#1853](https://github.com/fastapi/typer/pull/1853) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Update Dependabot ecosystem coverage. PR [#1851](https://github.com/fastapi/typer/pull/1851) by [@tiangolo](https://github.com/tiangolo). +* 👷 Fix alls-green test dependency. PR [#1848](https://github.com/fastapi/typer/pull/1848) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump cryptography from 46.0.7 to 48.0.1. PR [#1841](https://github.com/fastapi/typer/pull/1841) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pyjwt from 2.12.0 to 2.13.0. PR [#1839](https://github.com/fastapi/typer/pull/1839) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump the github-actions group across 1 directory with 3 updates. PR [#1828](https://github.com/fastapi/typer/pull/1828) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump the python-packages group with 3 updates. PR [#1829](https://github.com/fastapi/typer/pull/1829) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.26.7 (2026-06-03) + +### Fixes + +* 🐛 Respect wait=False when launching URLs with xdg-open. PR [#1820](https://github.com/fastapi/typer/pull/1820) by [@patrick91](https://github.com/patrick91). + +## 0.26.6 (2026-06-02) + +### Fixes + +* 🐛 Ensure that the default of a list argument is used correctly. PR [#1821](https://github.com/fastapi/typer/pull/1821) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* 👷 Speed up the CI by shuffling the test matrix. PR [#1815](https://github.com/fastapi/typer/pull/1815) by [@svlandeg](https://github.com/svlandeg). + +## 0.26.5 (2026-06-01) + +### Fixes + +* 🐛 Ensure that hidden commands are not shown when Rich markup is disabled. PR [#1812](https://github.com/fastapi/typer/pull/1812) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* 🔥 Remove old stub packages. PR [#1810](https://github.com/fastapi/typer/pull/1810) by [@tiangolo](https://github.com/tiangolo). + +## 0.26.4 (2026-05-30) + +### Features + +* 📝 Update AI Library Skill to avoid verbose code for CLI Options. PR [#1808](https://github.com/fastapi/typer/pull/1808) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 👷 Add CI to create draft release after merging a `release` PR. PR [#1807](https://github.com/fastapi/typer/pull/1807) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update labeler to accept label `release`. PR [#1806](https://github.com/fastapi/typer/pull/1806) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update GitHub Action permissions for prepare-release. PR [#1804](https://github.com/fastapi/typer/pull/1804) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GitHub Actions prepare release workflow. PR [#1802](https://github.com/fastapi/typer/pull/1802) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update publish action, do not use uv cache. PR [#1803](https://github.com/fastapi/typer/pull/1803) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump the python-packages group across 1 directory with 5 updates. PR [#1793](https://github.com/fastapi/typer/pull/1793) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.26.3 (2026-05-28) + +### Refactors + +* ♻️ Unify the testing functionality. PR [#1792](https://github.com/fastapi/typer/pull/1792) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* 👷 Update version of latest-changes GitHub action. PR [#1798](https://github.com/fastapi/typer/pull/1798) by [@tiangolo](https://github.com/tiangolo). + +## 0.26.2 (2026-05-27) + +### Fixes + +* 🐛 Ensure that an envvar set for a `typer.Option` list is split on whitespace. PR [#1791](https://github.com/fastapi/typer/pull/1791) by [@svlandeg](https://github.com/svlandeg). + +## 0.26.1 (2026-05-26) + +### Fixes + +* 🐛 Ensure that an envvar set for `typer.Option` works as expected. PR [#1788](https://github.com/fastapi/typer/pull/1788) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆ Bump python-dotenv from 1.2.1 to 1.2.2. PR [#1714](https://github.com/fastapi/typer/pull/1714) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.26.0 (2026-05-26) + +### Breaking Changes + +* ➖ Vendor Click and streamline Typer's functionality and code base. PR [#1774](https://github.com/fastapi/typer/pull/1774) by [@svlandeg](https://github.com/svlandeg). + * Typer no longer depends on Click as a third party dependency, it vendors (includes the source code of) Click. + * This simplifies the work done by both Click and Typer teams. + * It allows Typer to evolve independently, and enables several new planned features. + * It will solve several dependency conflict situations for projects that use some packages that depend on Click and some that depend on Typer. + * This also means that Click-specific functionality is no longer supported, like extracting the Click app and adding Click-specific plug-ins, or customizing the field types with Click-specific types. + * You can read more about it in the docs for [Vendored Click](https://typer.tiangolo.com/tutorial/click/). + +### Docs + +* 📝 Update and simplify docs about help and management. PR [#1778](https://github.com/fastapi/typer/pull/1778) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update contributing docs, reference central docs. PR [#1777](https://github.com/fastapi/typer/pull/1777) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update security policy. PR [#1775](https://github.com/fastapi/typer/pull/1775) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update link syntax to minimal Markdown. PR [#1623](https://github.com/fastapi/typer/pull/1623) by [@tiangolo](https://github.com/tiangolo). +* 📝 Update and simplify usage of admonitions. PR [#1755](https://github.com/fastapi/typer/pull/1755) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 📌 Pin max version of Click to `>= 8.2.1, < 8.4` temporarily to prevent incompatibilities. PR [#1753](https://github.com/fastapi/typer/pull/1753) by [@tiangolo](https://github.com/tiangolo). + * Superseded by vendoring Click. +* 👷 Configure Dependabot to group updates and update weekly. PR [#1768](https://github.com/fastapi/typer/pull/1768) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🔥 Remove config files now in central GitHub repo. PR [#1780](https://github.com/fastapi/typer/pull/1780) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump idna from 3.11 to 3.15. PR [#1771](https://github.com/fastapi/typer/pull/1771) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pymdown-extensions from 10.21.2 to 10.21.3. PR [#1772](https://github.com/fastapi/typer/pull/1772) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 📝 Fix categorization of PR 1475 in release notes. PR [#1769](https://github.com/fastapi/typer/pull/1769) by [@svlandeg](https://github.com/svlandeg). +* ✅ Add Fish shell completion tests for colon options. PR [#1475](https://github.com/fastapi/typer/pull/1475) by [@Mohamed-Elwasila](https://github.com/Mohamed-Elwasila). +* ✅ Extend completion unit tests for zsh, powershell and pwsh. PR [#1767](https://github.com/fastapi/typer/pull/1767) by [@ADiTyaRaj8969](https://github.com/ADiTyaRaj8969). +* ⬆ Bump actions/github-script from 7.1.0 to 9.0.0. PR [#1746](https://github.com/fastapi/typer/pull/1746) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Fix GitHub link in docs. PR [#1754](https://github.com/fastapi/typer/pull/1754) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Migrate to Zensical. PR [#1470](https://github.com/fastapi/typer/pull/1470) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump urllib3 from 2.6.3 to 2.7.0. PR [#1741](https://github.com/fastapi/typer/pull/1741) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Remove unnecessary Ruff rule. PR [#1752](https://github.com/fastapi/typer/pull/1752) by [@tiangolo](https://github.com/tiangolo). +* 🔒️ Add zizmor and fix audit findings. PR [#1705](https://github.com/fastapi/typer/pull/1705) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🔒️ Only allow team members to modify dependencies. PR [#1744](https://github.com/fastapi/typer/pull/1744) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump mypy from 2.0.0 to 2.1.0. PR [#1742](https://github.com/fastapi/typer/pull/1742) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.14.0 to 2.14.1. PR [#1739](https://github.com/fastapi/typer/pull/1739) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.34 to 0.0.35. PR [#1740](https://github.com/fastapi/typer/pull/1740) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/add-to-project from 1.0.2 to 2.0.0. PR [#1731](https://github.com/fastapi/typer/pull/1731) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/labeler from 6.0.1 to 6.1.0. PR [#1734](https://github.com/fastapi/typer/pull/1734) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.13.3 to 2.13.4. PR [#1736](https://github.com/fastapi/typer/pull/1736) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.11 to 0.3.13. PR [#1735](https://github.com/fastapi/typer/pull/1735) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mypy from 1.20.2 to 2.0.0. PR [#1737](https://github.com/fastapi/typer/pull/1737) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Add pre-commit for typos. PR [#1730](https://github.com/fastapi/typer/pull/1730) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump ty from 0.0.33 to 0.0.34. PR [#1729](https://github.com/fastapi/typer/pull/1729) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.32 to 0.0.33. PR [#1724](https://github.com/fastapi/typer/pull/1724) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.25.1 (2026-04-30) + +### Features + +* 🔧 Add Typer Library Skill for Agents. PR [#1620](https://github.com/fastapi/typer/pull/1620) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆ Bump ruff from 0.15.11 to 0.15.12. PR [#1722](https://github.com/fastapi/typer/pull/1722) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.10 to 0.3.11. PR [#1723](https://github.com/fastapi/typer/pull/1723) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.25.0 (2026-04-26) + +### Features + +* 🚸 Don't truncate code lines in traceback when formatted with Rich. PR [#1695](https://github.com/fastapi/typer/pull/1695) by [@YuriiMotov](https://github.com/YuriiMotov). + +## 0.24.2 (2026-04-22) + +### Fixes + +* 🐛 Ensure that `typer.launch` forwards correctly when launching a file. PR [#1708](https://github.com/fastapi/typer/pull/1708) by [@svlandeg](https://github.com/svlandeg). + +### Refactors + +* 🎨 Ensure `ty` runs without errors. PR [#1628](https://github.com/fastapi/typer/pull/1628) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 📝 Add dates to release notes. PR [#1612](https://github.com/fastapi/typer/pull/1612) by [@YuriiMotov](https://github.com/YuriiMotov). +* 💄 Fix code blocks in reference docs overflowing table width. PR [#1630](https://github.com/fastapi/typer/pull/1630) by [@YuriiMotov](https://github.com/YuriiMotov). +* 📝 Fix broken link to FastAPI and Friends newsletter. PR [#1540](https://github.com/fastapi/typer/pull/1540) by [@Karlemami](https://github.com/Karlemami). +* 🔨 Handle external links `target=_blank` and CSS automatically in JS and CSS. PR [#1622](https://github.com/fastapi/typer/pull/1622) by [@tiangolo](https://github.com/tiangolo). +* 📝 Remove link to Typer developer survey. PR [#1609](https://github.com/fastapi/typer/pull/1609) by [@svlandeg](https://github.com/svlandeg). +* ✏️ Clean up documentation in `install.md` file. PR [#1606](https://github.com/fastapi/typer/pull/1606) by [@Johandielangman](https://github.com/Johandielangman). + +### Internal + +* ⬆ Bump mypy from 1.20.1 to 1.20.2. PR [#1715](https://github.com/fastapi/typer/pull/1715) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.9 to 0.3.10. PR [#1716](https://github.com/fastapi/typer/pull/1716) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.13.1 to 2.14.0. PR [#1713](https://github.com/fastapi/typer/pull/1713) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.31 to 0.0.32. PR [#1711](https://github.com/fastapi/typer/pull/1711) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.13.2 to 2.13.3. PR [#1712](https://github.com/fastapi/typer/pull/1712) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pygments from 2.19.2 to 2.20.0. PR [#1667](https://github.com/fastapi/typer/pull/1667) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pymdown-extensions from 10.20 to 10.21.2. PR [#1710](https://github.com/fastapi/typer/pull/1710) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump actions/cache from 5.0.4 to 5.0.5. PR [#1700](https://github.com/fastapi/typer/pull/1700) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.13.1 to 2.13.2. PR [#1703](https://github.com/fastapi/typer/pull/1703) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 7.0.0 to 7.0.1. PR [#1701](https://github.com/fastapi/typer/pull/1701) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.10 to 0.15.11. PR [#1704](https://github.com/fastapi/typer/pull/1704) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cloudflare/wrangler-action from 3.14.1 to 3.15.0. PR [#1702](https://github.com/fastapi/typer/pull/1702) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 7.6.0 to 8.1.0. PR [#1699](https://github.com/fastapi/typer/pull/1699) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.30 to 0.0.31. PR [#1696](https://github.com/fastapi/typer/pull/1696) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.13.0 to 2.13.1. PR [#1697](https://github.com/fastapi/typer/pull/1697) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔒 Pin GitHub actions by commit SHA. PR [#1666](https://github.com/fastapi/typer/pull/1666) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump ty from 0.0.29 to 0.0.30. PR [#1693](https://github.com/fastapi/typer/pull/1693) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.8 to 0.3.9. PR [#1691](https://github.com/fastapi/typer/pull/1691) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pygithub from 2.9.0 to 2.9.1. PR [#1692](https://github.com/fastapi/typer/pull/1692) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump rich from 14.3.3 to 15.0.0. PR [#1688](https://github.com/fastapi/typer/pull/1688) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic from 2.12.5 to 2.13.0. PR [#1687](https://github.com/fastapi/typer/pull/1687) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mypy from 1.20.0 to 1.20.1. PR [#1689](https://github.com/fastapi/typer/pull/1689) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cryptography from 46.0.6 to 46.0.7. PR [#1682](https://github.com/fastapi/typer/pull/1682) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.9 to 0.15.10. PR [#1684](https://github.com/fastapi/typer/pull/1684) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pytest from 9.0.2 to 9.0.3. PR [#1681](https://github.com/fastapi/typer/pull/1681) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.28 to 0.0.29. PR [#1678](https://github.com/fastapi/typer/pull/1678) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.8 to 0.15.9. PR [#1674](https://github.com/fastapi/typer/pull/1674) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.27 to 0.0.28. PR [#1675](https://github.com/fastapi/typer/pull/1675) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 12.1.1 to 12.2.0. PR [#1672](https://github.com/fastapi/typer/pull/1672) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mypy from 1.19.1 to 1.20.0. PR [#1670](https://github.com/fastapi/typer/pull/1670) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.26 to 0.0.27. PR [#1669](https://github.com/fastapi/typer/pull/1669) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cryptography from 46.0.5 to 46.0.6. PR [#1661](https://github.com/fastapi/typer/pull/1661) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.7 to 0.15.8. PR [#1659](https://github.com/fastapi/typer/pull/1659) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.25 to 0.0.26. PR [#1660](https://github.com/fastapi/typer/pull/1660) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Configure `ty` to exit with error code on warnings. PR [#1657](https://github.com/fastapi/typer/pull/1657) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump requests from 2.32.5 to 2.33.0. PR [#1656](https://github.com/fastapi/typer/pull/1656) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.24 to 0.0.25. PR [#1652](https://github.com/fastapi/typer/pull/1652) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.6 to 0.3.8. PR [#1648](https://github.com/fastapi/typer/pull/1648) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pytest-cov from 7.0.0 to 7.1.0. PR [#1649](https://github.com/fastapi/typer/pull/1649) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pygithub from 2.8.1 to 2.9.0. PR [#1650](https://github.com/fastapi/typer/pull/1650) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.7.5 to 9.7.6. PR [#1645](https://github.com/fastapi/typer/pull/1645) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.6 to 0.15.7. PR [#1644](https://github.com/fastapi/typer/pull/1644) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.23 to 0.0.24. PR [#1646](https://github.com/fastapi/typer/pull/1646) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.5 to 0.3.6. PR [#1638](https://github.com/fastapi/typer/pull/1638) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.22 to 0.0.23. PR [#1639](https://github.com/fastapi/typer/pull/1639) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pyjwt from 2.10.1 to 2.12.0. PR [#1636](https://github.com/fastapi/typer/pull/1636) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cairosvg from 2.8.2 to 2.9.0. PR [#1635](https://github.com/fastapi/typer/pull/1635) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.5 to 0.15.6. PR [#1633](https://github.com/fastapi/typer/pull/1633) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.21 to 0.0.22. PR [#1634](https://github.com/fastapi/typer/pull/1634) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dorny/paths-filter from 3 to 4. PR [#1632](https://github.com/fastapi/typer/pull/1632) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.7.4 to 9.7.5. PR [#1629](https://github.com/fastapi/typer/pull/1629) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.4 to 0.3.5. PR [#1627](https://github.com/fastapi/typer/pull/1627) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.20 to 0.0.21. PR [#1624](https://github.com/fastapi/typer/pull/1624) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.4 to 0.15.5. PR [#1625](https://github.com/fastapi/typer/pull/1625) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.7.3 to 9.7.4. PR [#1621](https://github.com/fastapi/typer/pull/1621) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.19 to 0.0.20. PR [#1618](https://github.com/fastapi/typer/pull/1618) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.3.3 to 0.3.4. PR [#1617](https://github.com/fastapi/typer/pull/1617) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.2 to 0.15.4. PR [#1616](https://github.com/fastapi/typer/pull/1616) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ty from 0.0.18 to 0.0.19. PR [#1615](https://github.com/fastapi/typer/pull/1615) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 7 to 8. PR [#1614](https://github.com/fastapi/typer/pull/1614) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 6 to 7. PR [#1613](https://github.com/fastapi/typer/pull/1613) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.7.2 to 9.7.3. PR [#1611](https://github.com/fastapi/typer/pull/1611) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocstrings-python from 2.0.1 to 2.0.3. PR [#1605](https://github.com/fastapi/typer/pull/1605) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump ty from 0.0.17 to 0.0.18. PR [#1602](https://github.com/fastapi/typer/pull/1602) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump griffe-warnings-deprecated from 1.1.0 to 1.1.1. PR [#1603](https://github.com/fastapi/typer/pull/1603) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump griffe-typingdoc from 0.3.0 to 0.3.1. PR [#1604](https://github.com/fastapi/typer/pull/1604) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Upgrade pytest version and config. PR [#1570](https://github.com/fastapi/typer/pull/1570) by [@tiangolo](https://github.com/tiangolo). + +## 0.24.1 (2026-02-21) + +### Internal + +* 👷 Fix CI, do not attempt to build `typer-slim`, nor `typer-cli`. PR [#1569](https://github.com/fastapi/typer/pull/1569) by [@tiangolo](https://github.com/tiangolo). +* ➖ Drop support for `typer-slim` and `typer-cli`, no more versions will be released, use only `typer`. PR [#1568](https://github.com/fastapi/typer/pull/1568) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump rich from 14.3.2 to 14.3.3. PR [#1565](https://github.com/fastapi/typer/pull/1565) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.13.0 to 2.13.1. PR [#1566](https://github.com/fastapi/typer/pull/1566) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.1 to 0.15.2. PR [#1567](https://github.com/fastapi/typer/pull/1567) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.7.1 to 9.7.2. PR [#1561](https://github.com/fastapi/typer/pull/1561) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 11.3.0 to 12.1.1. PR [#1550](https://github.com/fastapi/typer/pull/1550) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pytest from 8.4.2 to 9.0.2. PR [#1551](https://github.com/fastapi/typer/pull/1551) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pydantic-settings from 2.12.0 to 2.13.0. PR [#1552](https://github.com/fastapi/typer/pull/1552) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🎨 Upgrade types for Python 3.10. PR [#1549](https://github.com/fastapi/typer/pull/1549) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Add internal scripts to migrate docs from Python 3.9 to 3.10. PR [#1547](https://github.com/fastapi/typer/pull/1547) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump ty from 0.0.16 to 0.0.17. PR [#1544](https://github.com/fastapi/typer/pull/1544) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.24.0 (2026-02-16) + +### Breaking Changes + +* ➖ Drop support for Python 3.9. PR [#1546](https://github.com/fastapi/typer/pull/1546) by [@tiangolo](https://github.com/tiangolo). + +## 0.23.2 (2026-02-16) + +### Features + +* ✅ Monkeypatch console width to allow running `pytest` directly. PR [#1542](https://github.com/fastapi/typer/pull/1542) by [@SwaatiR](https://github.com/SwaatiR). + +### Internal + +* 👷 Run tests with lower bound uv sync, update minimum dependencies. PR [#1526](https://github.com/fastapi/typer/pull/1526) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump prek from 0.3.2 to 0.3.3. PR [#1545](https://github.com/fastapi/typer/pull/1545) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.15.0 to 0.15.1. PR [#1541](https://github.com/fastapi/typer/pull/1541) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.23.1 (2026-02-13) + +### Fixes + +* 🐛 Fix `TYPER_USE_RICH` parsing to allow disabling Rich completely. PR [#1539](https://github.com/fastapi/typer/pull/1539) by [@bckohan](https://github.com/bckohan). + +### Docs + +* 📝 Remove documentation pages that reference using Click directly. PR [#1538](https://github.com/fastapi/typer/pull/1538) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆ Bump ty from 0.0.15 to 0.0.16. PR [#1533](https://github.com/fastapi/typer/pull/1533) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.23.0 (2026-02-11) + +### Breaking Changes + +* ♻️ When printing error tracebacks with Rich, default to not showing locals, which are sometimes verbose. PR [#1072](https://github.com/fastapi/typer/pull/1072) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add more explicit deprecation note in shell packages. PR [#1534](https://github.com/fastapi/typer/pull/1534) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump cryptography from 46.0.4 to 46.0.5. PR [#1532](https://github.com/fastapi/typer/pull/1532) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔨 Tweak PDM hook script, remove unnecessary default. PR [#1536](https://github.com/fastapi/typer/pull/1536) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Simplify build setup scripts and configs for deprecated wrapper packages. PR [#1535](https://github.com/fastapi/typer/pull/1535) by [@tiangolo](https://github.com/tiangolo). + +## 0.22.0 (2026-02-11) + +### Breaking Changes + +* 💥 Make `typer-slim` a shallow wrapper around `typer`, always requiring `rich` and `shellingham`. PR [#1522](https://github.com/fastapi/typer/pull/1522) by [@svlandeg](https://github.com/svlandeg). + +## 0.21.2 (2026-02-10) + +### Fixes + +* 🐛 Fix highlighting of optional variadic argument metavars. PR [#1508](https://github.com/fastapi/typer/pull/1508) by [@BenjyWiener](https://github.com/BenjyWiener). +* 🐛 Fix `--help` text alignment when using `typer.style()` in option descriptions. PR [#1356](https://github.com/fastapi/typer/pull/1356) by [@mahimairaja](https://github.com/mahimairaja). + +### Refactors + +* 🎨 Update types and errors, comment ty for now. PR [#1531](https://github.com/fastapi/typer/pull/1531) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* ➖ Drop `typing-extensions` as external dependency. PR [#1467](https://github.com/fastapi/typer/pull/1467) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 📝 Add reference (code API) docs. PR [#1504](https://github.com/fastapi/typer/pull/1504) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update the "Building a Package" tutorial to use `uv` instead of `poetry`. PR [#1474](https://github.com/fastapi/typer/pull/1474) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update `management-tasks.md` to be in line with `management-tasks.md` in FastAPI repo. PR [#1519](https://github.com/fastapi/typer/pull/1519) by [@YuriiMotov](https://github.com/YuriiMotov). +* 📝 Add link to Typer developer survey. PR [#1514](https://github.com/fastapi/typer/pull/1514) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#1489](https://github.com/fastapi/typer/pull/1489) by [@alejsdev](https://github.com/alejsdev). +* 🐛 Fix copy button in `custom.js`. PR [#1488](https://github.com/fastapi/typer/pull/1488) by [@alejsdev](https://github.com/alejsdev). + +### Internal + +* 🔧 Update config for labeler. PR [#1530](https://github.com/fastapi/typer/pull/1530) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Remove no longer used requirements.txt file. PR [#1528](https://github.com/fastapi/typer/pull/1528) by [@tiangolo](https://github.com/tiangolo). +* 📌 Update internal dependency limits. PR [#1529](https://github.com/fastapi/typer/pull/1529) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add ty to pre-commit. PR [#1527](https://github.com/fastapi/typer/pull/1527) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add `ty` to the CI `lint` check. PR [#1477](https://github.com/fastapi/typer/pull/1477) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump prek from 0.3.1 to 0.3.2. PR [#1524](https://github.com/fastapi/typer/pull/1524) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.14 to 0.15.0. PR [#1516](https://github.com/fastapi/typer/pull/1516) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Add generate-readme to pre-commit. PR [#1515](https://github.com/fastapi/typer/pull/1515) by [@tiangolo](https://github.com/tiangolo). +* 👷 Run mypy in pre-commit instead of `Lint` step in `test.yml` workflow. PR [#1511](https://github.com/fastapi/typer/pull/1511) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump prek from 0.3.0 to 0.3.1. PR [#1510](https://github.com/fastapi/typer/pull/1510) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump rich from 14.3.1 to 14.3.2. PR [#1509](https://github.com/fastapi/typer/pull/1509) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Update test to use `mod` fixture. PR [#1506](https://github.com/fastapi/typer/pull/1506) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump rich from 14.2.0 to 14.3.1. PR [#1502](https://github.com/fastapi/typer/pull/1502) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.13 to 0.14.14. PR [#1499](https://github.com/fastapi/typer/pull/1499) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#1497](https://github.com/fastapi/typer/pull/1497) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump ruff from 0.14.10 to 0.14.13. PR [#1493](https://github.com/fastapi/typer/pull/1493) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump urllib3 from 2.6.2 to 2.6.3. PR [#1496](https://github.com/fastapi/typer/pull/1496) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump prek from 0.2.24 to 0.3.0. PR [#1495](https://github.com/fastapi/typer/pull/1495) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.21.1 (2026-01-06) + +### Fixes + +* 🐛 Fix escaping in help text when `rich` is installed but not used. PR [#1089](https://github.com/fastapi/typer/pull/1089) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆️ Migrate to uv. PR [#1472](https://github.com/fastapi/typer/pull/1472) by [@DoctorJohn](https://github.com/DoctorJohn). +* ⬆ Bump mypy from 1.18.2 to 1.19.1. PR [#1469](https://github.com/fastapi/typer/pull/1469) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/checkout from 5 to 6. PR [#1456](https://github.com/fastapi/typer/pull/1456) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 6 to 7. PR [#1444](https://github.com/fastapi/typer/pull/1444) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.8 to 0.14.10. PR [#1449](https://github.com/fastapi/typer/pull/1449) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.7.0 to 9.7.1. PR [#1446](https://github.com/fastapi/typer/pull/1446) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 5 to 6. PR [#1443](https://github.com/fastapi/typer/pull/1443) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/cache from 4 to 5. PR [#1441](https://github.com/fastapi/typer/pull/1441) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Update secrets check. PR [#1471](https://github.com/fastapi/typer/pull/1471) by [@YuriiMotov](https://github.com/YuriiMotov). +* ✅ Add missing tests for code examples. PR [#1465](https://github.com/fastapi/typer/pull/1465) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🔧 Update pre-commit to use local Ruff instead of hook, unpin `prek`. PR [#1466](https://github.com/fastapi/typer/pull/1466) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump mypy from 1.14.1 to 1.18.2. PR [#1382](https://github.com/fastapi/typer/pull/1382) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.21.0 (2025-12-25) + +### Breaking Changes + +* ➖ Drop support for Python 3.8. PR [#1464](https://github.com/fastapi/typer/pull/1464) by [@tiangolo](https://github.com/tiangolo). +* ➖ Drop support for Python 3.8 in CI. PR [#1463](https://github.com/fastapi/typer/pull/1463) by [@YuriiMotov](https://github.com/YuriiMotov) and [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Update code examples to Python 3.9. PR [#1459](https://github.com/fastapi/typer/pull/1459) by [@YuriiMotov](https://github.com/YuriiMotov). + +### Internal + +* 💚 Move `ruff` dependency to shared `requirements-docs-tests.txt` to fix "Build docs" workflow in CI. PR [#1458](https://github.com/fastapi/typer/pull/1458) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.8. PR [#1442](https://github.com/fastapi/typer/pull/1442) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Add pre-commit workflow. PR [#1453](https://github.com/fastapi/typer/pull/1453) by [@tiangolo](https://github.com/tiangolo). +* 👷 Configure coverage, error on main tests, don't wait for Smokeshow. PR [#1448](https://github.com/fastapi/typer/pull/1448) by [@YuriiMotov](https://github.com/YuriiMotov). +* 👷 Run Smokeshow always, even on test failures. PR [#1447](https://github.com/fastapi/typer/pull/1447) by [@YuriiMotov](https://github.com/YuriiMotov). +* 🔨 Add Typer script to generate example variants for Python files. PR [#1452](https://github.com/fastapi/typer/pull/1452) by [@tiangolo](https://github.com/tiangolo). + +## 0.20.1 (2025-12-19) + +### Features + +* ✨ Add support for standard tracebacks via the env `TYPER_STANDARD_TRACEBACK`. PR [#1299](https://github.com/fastapi/typer/pull/1299) by [@colin-nolan](https://github.com/colin-nolan). + +### Fixes + +* 🐛 Ensure that `options_metavar` is passed through correctly. PR [#816](https://github.com/fastapi/typer/pull/816) by [@gar1t](https://github.com/gar1t). +* 🐛 Ensure an optional argument is shown in brackets, even when `metavar` is set. PR [#1409](https://github.com/fastapi/typer/pull/1409) by [@svlandeg](https://github.com/svlandeg). +* 🐛 Ensure that the default `rich_markup_mode` is interpreted correctly. PR [#1304](https://github.com/fastapi/typer/pull/1304) by [@svlandeg](https://github.com/svlandeg). + +### Refactors + +* ♻️ Refactor the handling of `shellingham`. PR [#1347](https://github.com/fastapi/typer/pull/1347) by [@nathanjmcdougall](https://github.com/nathanjmcdougall). + +### Docs + +* 📝 Ensure that bold letters are rendered correctly in `printing.md`. PR [#1365](https://github.com/fastapi/typer/pull/1365) by [@svlandeg](https://github.com/svlandeg). +* 🩺 Update test badge to only reflect pushes to `master`. PR [#1414](https://github.com/fastapi/typer/pull/1414) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update console output on the Rich help formatting page. PR [#1430](https://github.com/fastapi/typer/pull/1430) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update emoji used in Rich help formatting tutorial. PR [#1429](https://github.com/fastapi/typer/pull/1429) by [@svlandeg](https://github.com/svlandeg). +* 📝 Remove duplicate explanation how the path is resolved. PR [#956](https://github.com/fastapi/typer/pull/956) by [@dennis-rall](https://github.com/dennis-rall). +* 📝 Update docs to use `Typer()` more prominently. PR [#1418](https://github.com/fastapi/typer/pull/1418) by [@svlandeg](https://github.com/svlandeg). +* 💄 Use font 'Fira Code' to fix display of Rich panels in docs in Windows. PR [#1419](https://github.com/fastapi/typer/pull/1419) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔨 Add `--showlocals` to `test.sh`. PR [#1169](https://github.com/fastapi/typer/pull/1169) by [@rickwporter](https://github.com/rickwporter). +* ⬆ Bump ruff from 0.14.6 to 0.14.8. PR [#1436](https://github.com/fastapi/typer/pull/1436) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1434](https://github.com/fastapi/typer/pull/1434) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ✅ Update tests to use `mod.app` . PR [#1427](https://github.com/fastapi/typer/pull/1427) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump actions/checkout from 5 to 6. PR [#1426](https://github.com/fastapi/typer/pull/1426) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1425](https://github.com/fastapi/typer/pull/1425) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.14.5 to 0.14.6. PR [#1423](https://github.com/fastapi/typer/pull/1423) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/checkout from 5 to 6. PR [#1417](https://github.com/fastapi/typer/pull/1417) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#1424](https://github.com/fastapi/typer/pull/1424) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#1416](https://github.com/fastapi/typer/pull/1416) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump mkdocs-material from 9.6.23 to 9.7.0. PR [#1404](https://github.com/fastapi/typer/pull/1404) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-macros-plugin from 1.4.1 to 1.5.0. PR [#1406](https://github.com/fastapi/typer/pull/1406) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.14.4 to 0.14.5. PR [#1407](https://github.com/fastapi/typer/pull/1407) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1413](https://github.com/fastapi/typer/pull/1413) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.14.3 to 0.14.4. PR [#1402](https://github.com/fastapi/typer/pull/1402) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1403](https://github.com/fastapi/typer/pull/1403) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.14.2 to 0.14.3. PR [#1396](https://github.com/fastapi/typer/pull/1396) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1399](https://github.com/fastapi/typer/pull/1399) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-material from 9.6.22 to 9.6.23. PR [#1398](https://github.com/fastapi/typer/pull/1398) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1394](https://github.com/fastapi/typer/pull/1394) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.14.1 to 0.14.2. PR [#1383](https://github.com/fastapi/typer/pull/1383) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/upload-artifact from 4 to 5. PR [#1388](https://github.com/fastapi/typer/pull/1388) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-macros-plugin from 1.4.0 to 1.4.1. PR [#1389](https://github.com/fastapi/typer/pull/1389) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 5 to 6. PR [#1391](https://github.com/fastapi/typer/pull/1391) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Add PEP-639 license metadata. PR [#1387](https://github.com/fastapi/typer/pull/1387) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump mypy from 1.11.2 to 1.14.1. PR [#1375](https://github.com/fastapi/typer/pull/1375) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1380](https://github.com/fastapi/typer/pull/1380) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.14.0 to 0.14.1. PR [#1379](https://github.com/fastapi/typer/pull/1379) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.20.0 (2025-10-20) + +### Features + +* ✨ Enable command suggestions on typo by default. PR [#1371](https://github.com/fastapi/typer/pull/1371) by [@savannahostrowski](https://github.com/savannahostrowski). + +### Upgrades + +* ⬆️ Add support for Python 3.14. PR [#1372](https://github.com/fastapi/typer/pull/1372) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* 👷 Add nightly workflow to run tests against CPython main branch. PR [#1374](https://github.com/fastapi/typer/pull/1374) by [@savannahostrowski](https://github.com/savannahostrowski). +* ⬆ Bump mkdocs-material from 9.6.21 to 9.6.22. PR [#1377](https://github.com/fastapi/typer/pull/1377) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🔧 Configure reminder for `waiting` label in `issue-manager`. PR [#1378](https://github.com/fastapi/typer/pull/1378) by [@YuriiMotov](https://github.com/YuriiMotov). +* ⬆ Bump ruff from 0.13.3 to 0.14.0. PR [#1368](https://github.com/fastapi/typer/pull/1368) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1376](https://github.com/fastapi/typer/pull/1376) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-macros-plugin from 1.3.9 to 1.4.0. PR [#1354](https://github.com/fastapi/typer/pull/1354) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.6.20 to 9.6.21. PR [#1360](https://github.com/fastapi/typer/pull/1360) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mypy from 1.4.1 to 1.11.2. PR [#957](https://github.com/fastapi/typer/pull/957) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 6 to 7. PR [#1369](https://github.com/fastapi/typer/pull/1369) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.13.2 to 0.13.3. PR [#1366](https://github.com/fastapi/typer/pull/1366) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1367](https://github.com/fastapi/typer/pull/1367) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump tiangolo/issue-manager from 0.5.1 to 0.6.0. PR [#1361](https://github.com/fastapi/typer/pull/1361) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.13.1 to 0.13.2. PR [#1357](https://github.com/fastapi/typer/pull/1357) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1358](https://github.com/fastapi/typer/pull/1358) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 👷 Update docs previews comment, single comment, add failure status. PR [#1359](https://github.com/fastapi/typer/pull/1359) by [@tiangolo](https://github.com/tiangolo). + +## 0.19.2 (2025-09-23) + +### Fixes + +* 🐛 Fix list convertor with an empty list default factory . PR [#1350](https://github.com/fastapi/typer/pull/1350) by [@svlandeg](https://github.com/svlandeg). + +### Refactors + +* 🔥 Drop support for Python 3.7. PR [#830](https://github.com/fastapi/typer/pull/830) by [@kinuax](https://github.com/kinuax). + +### Internal + +* ⬆ Bump ruff from 0.13.0 to 0.13.1. PR [#1339](https://github.com/fastapi/typer/pull/1339) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1351](https://github.com/fastapi/typer/pull/1351) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump markdown-include-variants from 0.0.4 to 0.0.5. PR [#1348](https://github.com/fastapi/typer/pull/1348) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.19.1 (2025-09-20) + +**Note**: this is the last version to support Python 3.7, going forward Typer will require Python 3.8+. And soon Python 3.8 will also be dropped as [Python 3.8 reached it's end of life](https://devguide.python.org/versions/) almost a year ago. + +### Fixes + +* 🐛 Ensure that `Optional[list]` values work correctly with callbacks. PR [#1018](https://github.com/fastapi/typer/pull/1018) by [@solesensei](https://github.com/solesensei). + +## 0.19.0 (2025-09-20) + +### Features + +* ✨ Support `typing.Literal` to define a set of predefined choices. PR [#429](https://github.com/fastapi/typer/pull/429) by [@blackary](https://github.com/blackary). +* ✨ Allow setting an environment variable to `None` in `CliRunner.invoke`. PR [#1303](https://github.com/fastapi/typer/pull/1303) by [@arjenzorgdoc](https://github.com/arjenzorgdoc). + +### Refactors + +* ✅ Use Ruff rules to ensure safe lazy-loading of `rich`. PR [#1297](https://github.com/fastapi/typer/pull/1297) by [@nathanjmcdougall](https://github.com/nathanjmcdougall). +* ✅ Avoid rich formatting in number test. PR [#1305](https://github.com/fastapi/typer/pull/1305) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 📝 Clarify single-command vs multi-command behaviour in README. PR [#1268](https://github.com/fastapi/typer/pull/1268) by [@MorgenPronk](https://github.com/MorgenPronk). + +## 0.18.0 (2025-09-19) + +### Fixes + +* 👽️ Ensure compatibility with Click 8.3.0 by restoring the original `value_is_missing` function. PR [#1333](https://github.com/fastapi/typer/pull/1333) by [@svlandeg](https://github.com/svlandeg). + +### Upgrades + +* 📌 Remove pin for Click < 8.3.0 now that there's a fix for the changes. PR [#1346](https://github.com/fastapi/typer/pull/1346) by [@tiangolo](https://github.com/tiangolo). + +## 0.17.5 (2025-09-19) + +### Fixes + +* ⬇️ Restrict Click to below 8.3.0 to handle changes in the new version. PR [#1336](https://github.com/fastapi/typer/pull/1336) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆ Bump mkdocs-material from 9.6.14 to 9.6.20. PR [#1308](https://github.com/fastapi/typer/pull/1308) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.50 to 9.6.14. PR [#1223](https://github.com/fastapi/typer/pull/1223) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/download-artifact from 4 to 5. PR [#1269](https://github.com/fastapi/typer/pull/1269) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.12.12 to 0.13.0. PR [#1302](https://github.com/fastapi/typer/pull/1302) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1307](https://github.com/fastapi/typer/pull/1307) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Update pytest-cov requirement from <7.0.0,>=2.10.0 to >=2.10.0,<8.0.0. PR [#1301](https://github.com/fastapi/typer/pull/1301) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/setup-python from 5 to 6. PR [#1291](https://github.com/fastapi/typer/pull/1291) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.12.11 to 0.12.12. PR [#1295](https://github.com/fastapi/typer/pull/1295) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1300](https://github.com/fastapi/typer/pull/1300) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/labeler from 5 to 6. PR [#1296](https://github.com/fastapi/typer/pull/1296) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.17.4 (2025-09-05) + +### Fixes + +* 🐛 Make sure `rich.markup` is imported when rendering help text. PR [#1290](https://github.com/fastapi/typer/pull/1290) by [@g-arjones](https://github.com/g-arjones). + +### Internal + +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0. PR [#1292](https://github.com/fastapi/typer/pull/1292) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.12.10 to 0.12.11. PR [#1283](https://github.com/fastapi/typer/pull/1283) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1288](https://github.com/fastapi/typer/pull/1288) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 👷 Set permissions for conflict detector workflow. PR [#1287](https://github.com/fastapi/typer/pull/1287) by [@svlandeg](https://github.com/svlandeg). +* 👷 Detect and label merge conflicts on PRs automatically. PR [#1286](https://github.com/fastapi/typer/pull/1286) by [@svlandeg](https://github.com/svlandeg). + +## 0.17.3 (2025-08-30) + +### Features + +* ✨ Allow annotated parsing with a subclass of `Path`. PR [#1183](https://github.com/fastapi/typer/pull/1183) by [@emfdavid](https://github.com/emfdavid). + +## 0.17.2 (2025-08-30) + +### Fixes + +* 🐛 Avoid printing `default: None` in the help section when using Rich. PR [#1120](https://github.com/fastapi/typer/pull/1120) by [@mattmess1221](https://github.com/mattmess1221). + +## 0.17.1 (2025-08-30) + +### Fixes + +* 🐛 Fix markdown formatting in `--help` output. PR [#815](https://github.com/fastapi/typer/pull/815) by [@gar1t](https://github.com/gar1t). + +## 0.17.0 (2025-08-30) + +### Features + +* ⚡️ Lazy-load `rich_utils` to reduce startup time. PR [#1128](https://github.com/fastapi/typer/pull/1128) by [@oefe](https://github.com/oefe). + +### Internal + +* ⬆ Bump ruff from 0.12.9 to 0.12.10. PR [#1280](https://github.com/fastapi/typer/pull/1280) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1281](https://github.com/fastapi/typer/pull/1281) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Update pytest-sugar requirement from <1.1.0,>=0.9.4 to >=0.9.4,<1.2.0. PR [#1279](https://github.com/fastapi/typer/pull/1279) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.16.1 (2025-08-18) + +### Fixes + +* 🐛 Avoid printing additional output with `no_args_is_help=True` and Click 8.2. PR [#1278](https://github.com/fastapi/typer/pull/1278) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* 📝 Remove duplicated line in `environment-variables.md`. PR [#1277](https://github.com/fastapi/typer/pull/1277) by [@neirzhei](https://github.com/neirzhei). +* 📝 Fix reference to `count` parameter in the documentation. PR [#1201](https://github.com/fastapi/typer/pull/1201) by [@PokkaKiyo](https://github.com/PokkaKiyo). + +### Internal + +* ⬆ Bump ruff from 0.11.13 to 0.12.9. PR [#1276](https://github.com/fastapi/typer/pull/1276) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1247](https://github.com/fastapi/typer/pull/1247) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/checkout from 4 to 5. PR [#1271](https://github.com/fastapi/typer/pull/1271) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-macros-plugin from 1.3.7 to 1.3.9. PR [#1272](https://github.com/fastapi/typer/pull/1272) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/latest-changes from 0.3.2 to 0.4.0. PR [#1265](https://github.com/fastapi/typer/pull/1265) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 11.2.1 to 11.3.0. PR [#1249](https://github.com/fastapi/typer/pull/1249) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1206](https://github.com/fastapi/typer/pull/1206) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump pillow from 11.1.0 to 11.2.1. PR [#1198](https://github.com/fastapi/typer/pull/1198) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump cairosvg from 2.7.1 to 2.8.2. PR [#1226](https://github.com/fastapi/typer/pull/1226) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.11.6 to 0.11.13. PR [#1241](https://github.com/fastapi/typer/pull/1241) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.16.0 (2025-05-26) + +### Upgrades + +* ⬆️ Add compatibility with Click 8.2. PR [#1222](https://github.com/fastapi/typer/pull/1222) by [@tiangolo](https://github.com/tiangolo). + +When using the `CliRunner` with Click < 8.2, to be able to access the `stderr` output, you needed to set the `mix_stderr` parameter to `True`. Since Click 8.2 (and Typer 0.160 this release supporting it) this is no longer necessary, so this parameter has been removed. + +### Refactors + +* ✅ Refactor tests for compatibility with Click 8.2. PR [#1230](https://github.com/fastapi/typer/pull/1230) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Remove Google Analytics. PR [#1229](https://github.com/fastapi/typer/pull/1229) by [@tiangolo](https://github.com/tiangolo). + +## 0.15.4 (2025-05-14) + +### Upgrades + +* 📌 Pin Click to < 8.2, compatibility for Click >= 8.2 will be added in a future version. PR [#1225](https://github.com/fastapi/typer/pull/1225) by [@tiangolo](https://github.com/tiangolo). + +## 0.15.3 (2025-04-28) + +### Fixes + +* 🐛 Ensure that autocompletion works for `Path` arguments/options. PR [#1138](https://github.com/fastapi/typer/pull/1138) by [@svlandeg](https://github.com/svlandeg). +* 🐛 Fix newline after header in help text, and add more tests for the behaviour of `rich_markup_mode` . PR [#964](https://github.com/fastapi/typer/pull/964) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ⬆ Bump astral-sh/setup-uv from 5 to 6. PR [#1203](https://github.com/fastapi/typer/pull/1203) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.11.2 to 0.11.6. PR [#1200](https://github.com/fastapi/typer/pull/1200) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1196](https://github.com/fastapi/typer/pull/1196) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.11.1 to 0.11.2. PR [#1186](https://github.com/fastapi/typer/pull/1186) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1187](https://github.com/fastapi/typer/pull/1187) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.11.0 to 0.11.1. PR [#1185](https://github.com/fastapi/typer/pull/1185) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.9.10 to 0.11.0. PR [#1180](https://github.com/fastapi/typer/pull/1180) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1181](https://github.com/fastapi/typer/pull/1181) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1176](https://github.com/fastapi/typer/pull/1176) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.9.9 to 0.9.10. PR [#1175](https://github.com/fastapi/typer/pull/1175) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1171](https://github.com/fastapi/typer/pull/1171) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.9.7 to 0.9.9. PR [#1166](https://github.com/fastapi/typer/pull/1166) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✏️ Fix typo in test name. PR [#1165](https://github.com/fastapi/typer/pull/1165) by [@svlandeg](https://github.com/svlandeg). + +## 0.15.2 (2025-02-27) + +### Features + +* ✨ Allow custom styles for commands in help output. PR [#1103](https://github.com/fastapi/typer/pull/1103) by [@TheTechromancer](https://github.com/TheTechromancer). +* ✨ Avoid the unnecessary import of `typing_extensions` in newer Python versions. PR [#1048](https://github.com/fastapi/typer/pull/1048) by [@horta](https://github.com/horta). + +### Fixes + +* 🐛 Fix shell completions for the fish shell. PR [#1069](https://github.com/fastapi/typer/pull/1069) by [@goraje](https://github.com/goraje). + +### Refactors + +* 🚚 Rename test to corner-cases to make it more explicit. PR [#1083](https://github.com/fastapi/typer/pull/1083) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏️ Fix small typos in the tutorial documentation. PR [#1137](https://github.com/fastapi/typer/pull/1137) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update optional CLI argument section in tutorial with `Annotated`. PR [#983](https://github.com/fastapi/typer/pull/983) by [@gkeuccsr](https://github.com/gkeuccsr). +* 📝 Clarify the need for `mix_stderr` when accessing the output of `stderr` in tests. PR [#1045](https://github.com/fastapi/typer/pull/1045) by [@mrchrisadams](https://github.com/mrchrisadams). + +### Internal + +* 🔧 Add support for Python 3.13, tests in CI and add PyPI trove classifier. PR [#1091](https://github.com/fastapi/typer/pull/1091) by [@edgarrmondragon](https://github.com/edgarrmondragon). +* ⬆ Bump ruff from 0.9.6 to 0.9.7. PR [#1161](https://github.com/fastapi/typer/pull/1161) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1162](https://github.com/fastapi/typer/pull/1162) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.9.5 to 0.9.6. PR [#1153](https://github.com/fastapi/typer/pull/1153) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1151](https://github.com/fastapi/typer/pull/1151) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.9.4 to 0.9.5. PR [#1146](https://github.com/fastapi/typer/pull/1146) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1142](https://github.com/fastapi/typer/pull/1142) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.9.3 to 0.9.4. PR [#1139](https://github.com/fastapi/typer/pull/1139) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1135](https://github.com/fastapi/typer/pull/1135) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.9.1 to 0.9.3. PR [#1136](https://github.com/fastapi/typer/pull/1136) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1130](https://github.com/fastapi/typer/pull/1130) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.8.6 to 0.9.1. PR [#1118](https://github.com/fastapi/typer/pull/1118) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4. PR [#1132](https://github.com/fastapi/typer/pull/1132) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.49 to 9.5.50. PR [#1129](https://github.com/fastapi/typer/pull/1129) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 💚 Fix test matrix for Python 3.7. PR [#1116](https://github.com/fastapi/typer/pull/1116) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump ruff from 0.8.4 to 0.8.6. PR [#1107](https://github.com/fastapi/typer/pull/1107) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1109](https://github.com/fastapi/typer/pull/1109) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump pillow from 11.0.0 to 11.1.0. PR [#1104](https://github.com/fastapi/typer/pull/1104) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1102](https://github.com/fastapi/typer/pull/1102) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.8.3 to 0.8.4. PR [#1097](https://github.com/fastapi/typer/pull/1097) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 4 to 5. PR [#1098](https://github.com/fastapi/typer/pull/1098) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump markdown-include-variants from 0.0.3 to 0.0.4. PR [#1100](https://github.com/fastapi/typer/pull/1100) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.8.2 to 0.8.3. PR [#1090](https://github.com/fastapi/typer/pull/1090) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1093](https://github.com/fastapi/typer/pull/1093) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-material from 9.5.48 to 9.5.49. PR [#1092](https://github.com/fastapi/typer/pull/1092) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3. PR [#1088](https://github.com/fastapi/typer/pull/1088) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1087](https://github.com/fastapi/typer/pull/1087) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.8.1 to 0.8.2. PR [#1084](https://github.com/fastapi/typer/pull/1084) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.47 to 9.5.48. PR [#1086](https://github.com/fastapi/typer/pull/1086) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.15.1 (2024-12-04) + +### Features + +* 🗑️ Deprecate `shell_complete` and continue to use `autocompletion` for CLI parameters. PR [#974](https://github.com/fastapi/typer/pull/974) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* ✏️ Fix a few typos in the source and documentation. PR [#1028](https://github.com/fastapi/typer/pull/1028) by [@kkirsche](https://github.com/kkirsche). +* 📝 Fix minor inconsistencies and typos in tutorial. PR [#1067](https://github.com/fastapi/typer/pull/1067) by [@tvoirand](https://github.com/tvoirand). +* ✏️ Fix a few small typos in the documentation. PR [#1077](https://github.com/fastapi/typer/pull/1077) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* 🔧 Update build-docs filter patterns. PR [#1080](https://github.com/fastapi/typer/pull/1080) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update deploy docs preview script. PR [#1079](https://github.com/fastapi/typer/pull/1079) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update members. PR [#1078](https://github.com/fastapi/typer/pull/1078) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1071](https://github.com/fastapi/typer/pull/1071) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Update httpx requirement from <0.28.0,>=0.27.0 to >=0.27.0,<0.29.0. PR [#1065](https://github.com/fastapi/typer/pull/1065) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.15.0 (2024-12-03) + +### Features + +* ✨ Add support for extending typer apps without passing a name, add commands to the top level. PR [#1037](https://github.com/fastapi/typer/pull/1037) by [@patrick91](https://github.com/patrick91). + * New docs: [One File Per Command](https://typer.tiangolo.com/tutorial/one-file-per-command/). + +### Internal + +* ⬆ Bump mkdocs-material from 9.5.46 to 9.5.47. PR [#1070](https://github.com/fastapi/typer/pull/1070) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.8.0 to 0.8.1. PR [#1066](https://github.com/fastapi/typer/pull/1066) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.14.0 (2024-11-28) + +### Breaking Changes + +* 🔥 Remove auto naming of groups added via `add_typer` based on the group's callback function name. PR [#1052](https://github.com/fastapi/typer/pull/1052) by [@patrick91](https://github.com/patrick91). + +Before, it was supported to infer the name of a command group from the callback function name in the sub-app, so, in this code: + +```python +import typer + +app = typer.Typer() +users_app = typer.Typer() + +app.add_typer(users_app) + + +@users_app.callback() +def users(): # <-- This was the inferred command group name + """ + Manage users in the app. + """ + + +@users_app.command() +def create(name: str): + print(f"Creating user: {name}") +``` + +...the command group would be named `users`, based on the name of the function `def users()`. + +Now you need to set it explicitly: + +```python +import typer + +app = typer.Typer() +users_app = typer.Typer() + +app.add_typer(users_app, name="users") # <-- Explicitly set the command group name + + +@users_app.callback() +def users(): + """ + Manage users in the app. + """ + + +@users_app.command() +def create(name: str): + print(f"Creating user: {name}") +``` + +Updated docs [SubCommand Name and Help](https://typer.tiangolo.com/tutorial/subcommands/name-and-help/). + +**Note**: this change will enable important features in the next release. 🤩 + +### Internal + +* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.12.2. PR [#1043](https://github.com/fastapi/typer/pull/1043) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.44 to 9.5.46. PR [#1062](https://github.com/fastapi/typer/pull/1062) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.7.4 to 0.8.0. PR [#1059](https://github.com/fastapi/typer/pull/1059) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1061](https://github.com/fastapi/typer/pull/1061) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1053](https://github.com/fastapi/typer/pull/1053) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + +## 0.13.1 (2024-11-18) + +### Features + +* ✨ Remove Rich tags when showing completion text. PR [#877](https://github.com/fastapi/typer/pull/877) by [@svlandeg](https://github.com/svlandeg). +* ✨ Render Rich markup as HTML in Markdown docs. PR [#847](https://github.com/fastapi/typer/pull/847) by [@svlandeg](https://github.com/svlandeg). +* ✨ Support cp850 encoding for auto-completion in PowerShell. PR [#808](https://github.com/fastapi/typer/pull/808) by [@svlandeg](https://github.com/svlandeg). +* ✨ Allow gettext translation of help message. PR [#886](https://github.com/fastapi/typer/pull/886) by [@svlandeg](https://github.com/svlandeg). + +### Refactors + +* 🐛 Fix printing HTML from Rich output. PR [#1055](https://github.com/fastapi/typer/pull/1055) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Update markdown includes to use the new simpler format. PR [#1054](https://github.com/fastapi/typer/pull/1054) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump ruff from 0.7.3 to 0.7.4. PR [#1051](https://github.com/fastapi/typer/pull/1051) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1047](https://github.com/fastapi/typer/pull/1047) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.7.2 to 0.7.3. PR [#1046](https://github.com/fastapi/typer/pull/1046) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/latest-changes from 0.3.1 to 0.3.2. PR [#1044](https://github.com/fastapi/typer/pull/1044) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pytest-cov requirement from <6.0.0,>=2.10.0 to >=2.10.0,<7.0.0. PR [#1033](https://github.com/fastapi/typer/pull/1033) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.13.0 (2024-11-07) + +### Features + +* ✨ Handle `KeyboardInterrupt` separately from other exceptions. PR [#1039](https://github.com/fastapi/typer/pull/1039) by [@patrick91](https://github.com/patrick91). +* ✨ Update `launch` to not print anything when opening urls. PR [#1035](https://github.com/fastapi/typer/pull/1035) by [@patrick91](https://github.com/patrick91). +* ✨ Show help items in order of definition. PR [#944](https://github.com/fastapi/typer/pull/944) by [@svlandeg](https://github.com/svlandeg). + +### Fixes + +* 🐛 Fix equality check for custom classes. PR [#979](https://github.com/fastapi/typer/pull/979) by [@AryazE](https://github.com/AryazE). +* 🐛 Allow colon in zsh autocomplete values and descriptions. PR [#988](https://github.com/fastapi/typer/pull/988) by [@snapbug](https://github.com/snapbug). + +### Refactors + +* 🗑️ Deprecate support for `is_flag` and `flag_value` parameters. PR [#987](https://github.com/fastapi/typer/pull/987) by [@svlandeg](https://github.com/svlandeg). +* 🔥 Remove unused functionality from `_typing.py` file. PR [#805](https://github.com/fastapi/typer/pull/805) by [@ivantodorovich](https://github.com/ivantodorovich). +* ✏️ Fix typo in function name `_make_rich_text`. PR [#959](https://github.com/fastapi/typer/pull/959) by [@svlandeg](https://github.com/svlandeg). + +### Internal + +* ✅ Only run completion installation tests when the env var `_TYPER_RUN_INSTALL_COMPLETION_TESTS` is set. PR [#995](https://github.com/fastapi/typer/pull/995) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update the docstring of the `_make_rich_text` method. PR [#972](https://github.com/fastapi/typer/pull/972) by [@svlandeg](https://github.com/svlandeg). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1040](https://github.com/fastapi/typer/pull/1040) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump mkdocs-material from 9.5.42 to 9.5.44. PR [#1042](https://github.com/fastapi/typer/pull/1042) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.7.1 to 0.7.2. PR [#1038](https://github.com/fastapi/typer/pull/1038) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-macros-plugin from 1.3.6 to 1.3.7. PR [#1031](https://github.com/fastapi/typer/pull/1031) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1032](https://github.com/fastapi/typer/pull/1032) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.7.0 to 0.7.1. PR [#1029](https://github.com/fastapi/typer/pull/1029) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 10.4.0 to 11.0.0. PR [#1023](https://github.com/fastapi/typer/pull/1023) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.35 to 9.5.42. PR [#1027](https://github.com/fastapi/typer/pull/1027) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.6.5 to 0.7.0. PR [#1026](https://github.com/fastapi/typer/pull/1026) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-macros-plugin from 1.2.0 to 1.3.6. PR [#1025](https://github.com/fastapi/typer/pull/1025) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pre-commit requirement from <4.0.0,>=2.17.0 to >=2.17.0,<5.0.0. PR [#1012](https://github.com/fastapi/typer/pull/1012) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.1 to 1.10.3. PR [#1009](https://github.com/fastapi/typer/pull/1009) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1001](https://github.com/fastapi/typer/pull/1001) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 👷 Update Deploy docs CI to use uv. PR [#1021](https://github.com/fastapi/typer/pull/1021) by [@tiangolo](https://github.com/tiangolo). +* 👷 Fix smokeshow, checkout files on CI. PR [#1020](https://github.com/fastapi/typer/pull/1020) by [@tiangolo](https://github.com/tiangolo). +* 👷 Use uv in CI. PR [#1019](https://github.com/fastapi/typer/pull/1019) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update `labeler.yml`. PR [#1014](https://github.com/fastapi/typer/pull/1014) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update worfkow deploy-docs-notify URL. PR [#1011](https://github.com/fastapi/typer/pull/1011) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade Cloudflare GitHub Action. PR [#1010](https://github.com/fastapi/typer/pull/1010) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump mkdocs-macros-plugin from 1.0.5 to 1.2.0. PR [#992](https://github.com/fastapi/typer/pull/992) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.6.4 to 0.6.5. PR [#991](https://github.com/fastapi/typer/pull/991) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.34 to 9.5.35. PR [#996](https://github.com/fastapi/typer/pull/996) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#993](https://github.com/fastapi/typer/pull/993) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#982](https://github.com/fastapi/typer/pull/982) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump tiangolo/issue-manager from 0.5.0 to 0.5.1. PR [#980](https://github.com/fastapi/typer/pull/980) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Update `issue-manager.yml`. PR [#978](https://github.com/fastapi/typer/pull/978) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump ruff from 0.6.3 to 0.6.4. PR [#975](https://github.com/fastapi/typer/pull/975) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.33 to 9.5.34. PR [#963](https://github.com/fastapi/typer/pull/963) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.10.1. PR [#973](https://github.com/fastapi/typer/pull/973) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#966](https://github.com/fastapi/typer/pull/966) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 💚 Set `include-hidden-files` to `True` when using the `upload-artifact` GH action. PR [#967](https://github.com/fastapi/typer/pull/967) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump ruff from 0.6.1 to 0.6.3. PR [#961](https://github.com/fastapi/typer/pull/961) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#689](https://github.com/fastapi/typer/pull/689) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump ruff from 0.2.0 to 0.6.1. PR [#938](https://github.com/fastapi/typer/pull/938) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Update `latest-changes` GitHub Action. PR [#955](https://github.com/fastapi/typer/pull/955) by [@tiangolo](https://github.com/tiangolo). + +## 0.12.5 (2024-08-24) + +### Features + +* 💄 Unify the width of the Rich console for help and errors. PR [#788](https://github.com/fastapi/typer/pull/788) by [@racinmat](https://github.com/racinmat). +* 🚸 Improve assertion error message if a group is not a valid subclass. PR [#425](https://github.com/fastapi/typer/pull/425) by [@chrisburr](https://github.com/chrisburr). + +### Fixes + +* 🐛 Ensure `rich_markup_mode=None` disables Rich formatting. PR [#859](https://github.com/fastapi/typer/pull/859) by [@svlandeg](https://github.com/svlandeg). +* 🐛 Fix sourcing of completion path for Git Bash. PR [#801](https://github.com/fastapi/typer/pull/801) by [@svlandeg](https://github.com/svlandeg). +* 🐛 Fix PowerShell completion with incomplete word. PR [#360](https://github.com/fastapi/typer/pull/360) by [@patricksurry](https://github.com/patricksurry). + +### Refactors + +* 🔥 Remove Python 3.6 specific code paths. PR [#850](https://github.com/fastapi/typer/pull/850) by [@svlandeg](https://github.com/svlandeg). +* 🔥 Clean up redundant code. PR [#858](https://github.com/fastapi/typer/pull/858) by [@svlandeg](https://github.com/svlandeg). + +### Docs + +* ♻️ Use F-strings in Click examples in docs. PR [#891](https://github.com/fastapi/typer/pull/891) by [@svlandeg](https://github.com/svlandeg). +* 📝Add missing `main.py` in tutorial on CLI option names. PR [#868](https://github.com/fastapi/typer/pull/868) by [@fsramalho](https://github.com/fsramalho). +* 📝 Fix broken link. PR [#835](https://github.com/fastapi/typer/pull/835) by [@OhioDschungel6](https://github.com/OhioDschungel6). +* 📝 Update package docs with the latest versions of Typer and Poetry. PR [#781](https://github.com/fastapi/typer/pull/781) by [@kinuax](https://github.com/kinuax). +* 📝 Update the Progress Bar tutorial with correct output. PR [#199](https://github.com/fastapi/typer/pull/199) by [@n1ckdm](https://github.com/n1ckdm). +* 📝 Add docs and scripts to test completion in different shells. PR [#953](https://github.com/fastapi/typer/pull/953) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix a typo in `docs/virtual-environments.md`. PR [#952](https://github.com/fastapi/typer/pull/952) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix typo in `docs/contributing.md`. PR [#947](https://github.com/fastapi/typer/pull/947) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add docs for virtual environments, environment variables, and update contributing. PR [#946](https://github.com/fastapi/typer/pull/946) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔨 Pre-install dependencies in Docker so that testing in Docker is faster. PR [#954](https://github.com/fastapi/typer/pull/954) by [@tiangolo](https://github.com/tiangolo). +* ✅ Add `needs_bash` test fixture. PR [#888](https://github.com/fastapi/typer/pull/888) by [@svlandeg](https://github.com/svlandeg). +* ⬆ Bump mkdocs-material from 9.5.18 to 9.5.33. PR [#945](https://github.com/fastapi/typer/pull/945) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 10.3.0 to 10.4.0. PR [#939](https://github.com/fastapi/typer/pull/939) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Fix issue-manager. PR [#948](https://github.com/fastapi/typer/pull/948) by [@tiangolo](https://github.com/tiangolo). +* 🙈 Remove extra line in .gitignore. PR [#936](https://github.com/fastapi/typer/pull/936) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Update pytest-cov requirement from <5.0.0,>=2.10.0 to >=2.10.0,<6.0.0. PR [#844](https://github.com/fastapi/typer/pull/844) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.9.0. PR [#865](https://github.com/fastapi/typer/pull/865) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pytest requirement from <8.0.0,>=4.4.0 to >=4.4.0,<9.0.0. PR [#915](https://github.com/fastapi/typer/pull/915) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pytest-sugar requirement from <0.10.0,>=0.9.4 to >=0.9.4,<1.1.0. PR [#841](https://github.com/fastapi/typer/pull/841) by [@dependabot[bot]](https://github.com/apps/dependabot). + +## 0.12.4 (2024-08-17) + +### Features + +* ✨ Add support for Python 3.12, tests in CI and official marker. PR [#807](https://github.com/tiangolo/typer/pull/807) by [@ivantodorovich](https://github.com/ivantodorovich). + +### Fixes + +* 🐛 Fix support for `UnionType` (e.g. `str | None`) with Python 3.11. PR [#548](https://github.com/fastapi/typer/pull/548) by [@jonaslb](https://github.com/jonaslb). +* 🐛 Fix `zsh` autocompletion installation. PR [#237](https://github.com/fastapi/typer/pull/237) by [@alexjurkiewicz](https://github.com/alexjurkiewicz). +* 🐛 Fix usage of `Annotated` with future annotations in Python 3.7+. PR [#814](https://github.com/fastapi/typer/pull/814) by [@ivantodorovich](https://github.com/ivantodorovich). +* 🐛 Fix `shell_complete` not working for Arguments. PR [#737](https://github.com/fastapi/typer/pull/737) by [@bckohan](https://github.com/bckohan). + +### Docs + +* 📝 Update docs links, from tiangolo to new fastapi org. PR [#919](https://github.com/fastapi/typer/pull/919) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add docs for team and repo management. PR [#917](https://github.com/tiangolo/typer/pull/917) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Add URLs to `pyproject.toml`, show up in PyPI. PR [#931](https://github.com/fastapi/typer/pull/931) by [@tiangolo](https://github.com/tiangolo). +* 👷 Do not sync labels as it overrides manually added labels. PR [#930](https://github.com/fastapi/typer/pull/930) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update labeler GitHub Action to add only one label. PR [#927](https://github.com/fastapi/typer/pull/927) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update labeler GitHub Actions permissions and dependencies. PR [#926](https://github.com/fastapi/typer/pull/926) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GitHub Action label-checker. PR [#925](https://github.com/fastapi/typer/pull/925) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GitHub Action labeler. PR [#924](https://github.com/fastapi/typer/pull/924) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add GitHub Action add-to-project. PR [#922](https://github.com/fastapi/typer/pull/922) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update docs.py script to enable dirty reload conditionally. PR [#918](https://github.com/tiangolo/typer/pull/918) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update MkDocs previews. PR [#916](https://github.com/tiangolo/typer/pull/916) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade build docs configs. PR [#914](https://github.com/tiangolo/typer/pull/914) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update MkDocs to have titles in Markdown files instead of config. PR [#913](https://github.com/tiangolo/typer/pull/913) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add alls-green for test-redistribute. PR [#911](https://github.com/tiangolo/typer/pull/911) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update docs-previews to handle no docs changes. PR [#912](https://github.com/tiangolo/typer/pull/912) by [@tiangolo](https://github.com/tiangolo). +* 👷🏻 Show docs deployment status and preview URLs in comment. PR [#910](https://github.com/tiangolo/typer/pull/910) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Enable auto dark mode from system. PR [#908](https://github.com/tiangolo/typer/pull/908) by [@tiangolo](https://github.com/tiangolo). +* 💄 Add dark mode logo. PR [#907](https://github.com/tiangolo/typer/pull/907) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update tabs and admonitions with new syntax and new MkDocs features. PR [#906](https://github.com/tiangolo/typer/pull/906) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Enable MkDocs Material features. PR [#905](https://github.com/tiangolo/typer/pull/905) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Enable dark mode for docs. PR [#904](https://github.com/tiangolo/typer/pull/904) by [@tiangolo](https://github.com/tiangolo). +* ➖ Do not install jieba for MkDocs Material as there are no chinese translations. PR [#903](https://github.com/tiangolo/typer/pull/903) by [@tiangolo](https://github.com/tiangolo). +* 🙈 Add MkDocs Material cache to gitignore. PR [#902](https://github.com/tiangolo/typer/pull/902) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update lint script. PR [#901](https://github.com/tiangolo/typer/pull/901) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update MkDocs configs and docs build setup. PR [#900](https://github.com/tiangolo/typer/pull/900) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/cache from 3 to 4. PR [#839](https://github.com/tiangolo/typer/pull/839) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 🍱 Update Typer icon and logo. PR [#899](https://github.com/tiangolo/typer/pull/899) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update issue-manager.yml GitHub Action permissions. PR [#897](https://github.com/tiangolo/typer/pull/897) by [@tiangolo](https://github.com/tiangolo). +* 👷 Refactor GitHub Action to comment docs deployment URLs and update token, preparing for GitHub org. PR [#896](https://github.com/tiangolo/typer/pull/896) by [@tiangolo](https://github.com/tiangolo). +* 🔨 Update docs Termynal scripts to not include line nums for local dev. PR [#882](https://github.com/tiangolo/typer/pull/882) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump black from 23.3.0 to 24.3.0. PR [#837](https://github.com/tiangolo/typer/pull/837) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pillow from 10.1.0 to 10.3.0. PR [#836](https://github.com/tiangolo/typer/pull/836) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✅ Add CI configs to run tests on Windows and MacOS. PR [#824](https://github.com/tiangolo/typer/pull/824) by [@svlandeg](https://github.com/svlandeg). +* 👷 Update GitHub Actions to upload and download artifacts. PR [#829](https://github.com/tiangolo/typer/pull/829) by [@tiangolo](https://github.com/tiangolo). +* 👷 Tweak CI for test-redistribute, add needed env vars for slim. PR [#827](https://github.com/tiangolo/typer/pull/827) by [@tiangolo](https://github.com/tiangolo). +* ✅ Generalize test suite to run on Windows. PR [#810](https://github.com/tiangolo/typer/pull/810) by [@svlandeg](https://github.com/svlandeg). +* ✅ Add `__init__.py` files to fix test suite. PR [#809](https://github.com/tiangolo/typer/pull/809) by [@svlandeg](https://github.com/svlandeg). +* 🔧 Update MkDocs Material, enable plugins. PR [#813](https://github.com/tiangolo/typer/pull/813) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Tweak development scripts and configs after migration to PDM, Ruff, etc.. PR [#797](https://github.com/tiangolo/typer/pull/797) by [@tiangolo](https://github.com/tiangolo). + +## 0.12.3 (2024-04-09) + +### Fixes + +* 🐛 Fix Rich formatting with no commands. PR [#796](https://github.com/tiangolo/typer/pull/796) by [@svlandeg](https://github.com/svlandeg). + +## 0.12.2 (2024-04-08) + +### Features + +* ✨ Improve column help display, ensure commands column width is the same on all panels. PR [#567](https://github.com/tiangolo/typer/pull/567) by [@ssbarnea](https://github.com/ssbarnea). + +### Fixes + +* 🐛 Add support for an argument of type `Optional[Tuple]` and default value `None`. PR [#757](https://github.com/tiangolo/typer/pull/757) by [@Asthestarsfalll](https://github.com/Asthestarsfalll). + +### Docs + +* 🔧 Fix typo in Github template. PR [#793](https://github.com/tiangolo/typer/pull/793) by [@svlandeg](https://github.com/svlandeg). +* 📝 Fix typos in documentation. PR [#761](https://github.com/tiangolo/typer/pull/761) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update console output with Click 8 messages. PR [#789](https://github.com/tiangolo/typer/pull/789) by [@svlandeg](https://github.com/svlandeg). +* 📝 Remove references to a .rst README generated by poetry new. PR [#632](https://github.com/tiangolo/typer/pull/632) by [@jonasmmiguel](https://github.com/jonasmmiguel). + +## 0.12.1 (2024-04-05) + +Now you don't need to install `typer[all]`. When you install `typer` it comes with the default optional dependencies and the `typer` command. + +If you don't want the extra optional dependencies (`rich` and `shellingham`), you can install `typer-slim` instead. + +You can also install `typer-slim[standard]`, which includes the default optional dependencies, but not the `typer` command. + +Now the package `typer-cli` doesn't add anything on top of what `typer` has, it only depends on `typer`, and is there only for backwards compatibility, so that projects that depend on `typer-cli` can get the latest features of the `typer` command while they upgrade their dependencies to require `typer` directly. + +### Features + +* ✨ Add support for `typer ./someprogram.py utils docs --title`. PR [#782](https://github.com/tiangolo/typer/pull/782) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* 🐛 Fix broken installation when upgrading from `typer <0.12.0` to `typer >=0.12.0`, make `typer` independent of `typer-slim`, include `typer` command in `typer` package. PR [#791](https://github.com/tiangolo/typer/pull/791) by [@tiangolo](https://github.com/tiangolo). + +This fixes a problem that would break the `typer` installation directory when upgrading from `typer <0.12.0` to `typer >=0.12.0`, see issue [#790](https://github.com/tiangolo/typer/issues/790). + +By installing the latest version (`0.12.1`) it fixes it, for any previous version, even if the installation directory was already broken by the previous upgrade. + +### Internal + +* 👷 Add cron to run test once a week on monday. PR [#783](https://github.com/tiangolo/typer/pull/783) by [@estebanx64](https://github.com/estebanx64). + +## 0.12.0 (2024-03-30) + +In version `0.12.0`, the `typer` package depends on `typer-slim[standard]` which includes the default dependencies (instead of `typer[all]`) and `typer-cli` (that provides the `typer` command). + +If you don't want the extra optional dependencies (`rich` and `shellingham`), you can install `typer-slim` instead. + +You can also install `typer-slim[standard]`, which includes the default optional dependencies, but not the `typer` command. + +In version `0.12.0` the `typer-cli` package only provides the `typer` command, but the code is still in the main code, so even without installing `typer-cli`, it can be called with `python -m typer`. + +This approach of having `typer` depend on `typer-slim[standard]` instead of including the whole code and dependencies itself caused an issue when upgrading from `typer <0.12.0` to `typer >=0.12.0`, see issue [#790](https://github.com/tiangolo/typer/issues/790). This is fixed in version `0.12.1`. + +### Features + +* ✨ Add `typer-slim` package without extras, make `typer` include `typer-slim[default]` and integrate Typer CLI (`typer` command) into Typer. PR [#780](https://github.com/tiangolo/typer/pull/780) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 🔧 Temporarily disable social plugin while a MkDocs issue is handled. PR [#779](https://github.com/tiangolo/typer/pull/779) by [@tiangolo](https://github.com/tiangolo). +* 👷 Fix install MkDocs Insiders only when available. PR [#778](https://github.com/tiangolo/typer/pull/778) by [@tiangolo](https://github.com/tiangolo). + +## 0.11.1 (2024-03-28) + +### Fixes + +* 🔧 Explicitly include testing files in sdist for redistributors (e.g. OpenSUSE) and add CI to test redistribution. PR [#773](https://github.com/tiangolo/typer/pull/773) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 👷 Do not use the cache for dependencies when publishing to PyPI. PR [#774](https://github.com/tiangolo/typer/pull/774) by [@tiangolo](https://github.com/tiangolo). + +## 0.11.0 (2024-03-26) + +### Breaking Changes + +* 🔧 Refactor package manager, move from Flit to PDM, remove private pip extras for `test`, `doc`, `dev`. PR [#764](https://github.com/tiangolo/typer/pull/764) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove support for Click 7, require Click 8+. PR [#760](https://github.com/tiangolo/typer/pull/760) by [@tiangolo](https://github.com/tiangolo). +* 🔥 Remove support for Python 3.6. PR [#758](https://github.com/tiangolo/typer/pull/758) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* 🔧 Migrate from Black, isort, flake8, autoflake, pyupgrade to Ruff. PR [#763](https://github.com/tiangolo/typer/pull/763) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆️ Upgrade coverage and configs. PR [#769](https://github.com/tiangolo/typer/pull/769) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Upgrade mypy and config. PR [#768](https://github.com/tiangolo/typer/pull/768) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade Smokeshow GitHub action. PR [#767](https://github.com/tiangolo/typer/pull/767) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade latest-changes GitHub Action. PR [#766](https://github.com/tiangolo/typer/pull/766) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade issue-manager GitHub Action. PR [#765](https://github.com/tiangolo/typer/pull/765) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add alls-green to CI. PR [#759](https://github.com/tiangolo/typer/pull/759) by [@tiangolo](https://github.com/tiangolo). + +## 0.10.0 (2024-03-23) + +### Fixes + +* 🐛 Fix default value of `None` for CLI Parameters when the type is `list | None` and the default value is `None`. PR [#664](https://github.com/tiangolo/typer/pull/664) by [@theowisear](https://github.com/theowisear). + +## 0.9.4 (2024-03-23) + +### Features + +* ✨ Improve support for CLI translations using gettext. PR [#417](https://github.com/tiangolo/typer/pull/417) by [@mjodmj](https://github.com/mjodmj). + +## 0.9.3 (2024-03-23) + +### Fixes + +* 🐛 Fix evaluating stringified annotations in Python 3.10 (also `from __future__ import annotations`). PR [#721](https://github.com/tiangolo/typer/pull/721) by [@heckad](https://github.com/heckad). + +## 0.9.2 (2024-03-23) + +### Fixes + +* 🐛 Fix display of default value for Enum parameters inside of a list, include docs and tests. PR [#473](https://github.com/tiangolo/typer/pull/473) by [@asieira](https://github.com/asieira). +* 🐛 Update type annotations for `show_default` parameter and update docs for setting a "Custom default string". PR [#501](https://github.com/tiangolo/typer/pull/501) by [@plannigan](https://github.com/plannigan). + +### Docs + +* 📝 Add docs and test for `no_args_is_help` feature. PR [#751](https://github.com/tiangolo/typer/pull/751) by [@svlandeg](https://github.com/svlandeg). + +## 0.9.1 (2024-03-23) + +### Fixes + +* 🐛 Add missing `default_factory` in `Argument` overloads. PR [#750](https://github.com/tiangolo/typer/pull/750) by [@m9810223](https://github.com/m9810223). +* 🐛 Fix preserving case in enum values. PR [#571](https://github.com/tiangolo/typer/pull/571) by [@avaldebe](https://github.com/avaldebe). + +### Docs + +* 📝 Remove obsolete references to `--install-completion` for `typer.run()` scripts. PR [#595](https://github.com/tiangolo/typer/pull/595) by [@tiangolo](https://github.com/tiangolo). + +* 📝 Update docs example for a Typer/Click group to make new subcommands explicit. PR [#755](https://github.com/tiangolo/typer/pull/755) by [@svlandeg](https://github.com/svlandeg). +* 📝 Update docs for building a package, file structure example. PR [#683](https://github.com/tiangolo/typer/pull/683) by [@davidbgk](https://github.com/davidbgk). +* 📝 Update link in docs to the newest stable version of click. PR [#675](https://github.com/tiangolo/typer/pull/675) by [@javier171188](https://github.com/javier171188). +* 🔧 Add `CITATION.cff` file for academic citations. PR [#681](https://github.com/tiangolo/typer/pull/681) by [@tiangolo](https://github.com/tiangolo). +* ✏ Fix typo in `docs/tutorial/exceptions.md`. PR [#702](https://github.com/tiangolo/typer/pull/702) by [@menzenski](https://github.com/menzenski). +* ✏ Fix typo in `docs/tutorial/options/name.md`. PR [#725](https://github.com/tiangolo/typer/pull/725) by [@bwagner](https://github.com/bwagner). +* ✏ Fix typo in `docs/tutorial/arguments/optional.md`. PR [#602](https://github.com/tiangolo/typer/pull/602) by [@tadasgedgaudas](https://github.com/tadasgedgaudas). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#606](https://github.com/tiangolo/typer/pull/606) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 👷 Install MkDocs Material Insiders only when secrets are available, for Dependabot. PR [#685](https://github.com/tiangolo/typer/pull/685) by [@tiangolo](https://github.com/tiangolo). +* ⚒️ Update build-docs.yml, do not zip docs. PR [#645](https://github.com/tiangolo/typer/pull/645) by [@tiangolo](https://github.com/tiangolo). +* 👷 Deploy docs to Cloudflare. PR [#644](https://github.com/tiangolo/typer/pull/644) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade CI for docs. PR [#642](https://github.com/tiangolo/typer/pull/642) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update token for latest changes. PR [#635](https://github.com/tiangolo/typer/pull/635) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update CI workflow dispatch for latest changes. PR [#643](https://github.com/tiangolo/typer/pull/643) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update token for Material for MkDocs Insiders. PR [#636](https://github.com/tiangolo/typer/pull/636) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix internal type annotations and bump mypy version. PR [#638](https://github.com/tiangolo/typer/pull/638) by [@paulo-raca](https://github.com/paulo-raca). +* 💡 Add comments to document overload definitions in code. PR [#752](https://github.com/tiangolo/typer/pull/752) by [@svlandeg](https://github.com/svlandeg). +* 🔥 Remove Jina QA Bot as it has been discontinued. PR [#749](https://github.com/tiangolo/typer/pull/749) by [@tiangolo](https://github.com/tiangolo). +* 👷 Update build docs CI cache paths. PR [#707](https://github.com/tiangolo/typer/pull/707) by [@tiangolo](https://github.com/tiangolo). +* 👷 Upgrade latest-changes GitHub Action. PR [#691](https://github.com/tiangolo/typer/pull/691) by [@tiangolo](https://github.com/tiangolo). + +## 0.9.0 (2023-05-02) + +### Features + +* ✨ Add support for PEP-593 `Annotated` for specifying options and arguments. Initial PR [#584](https://github.com/tiangolo/typer/pull/584) by [@ryangalamb](https://github.com/ryangalamb). + * New docs: [Optional CLI arguments](https://typer.tiangolo.com/tutorial/arguments/optional/#an-alternative-cli-argument-declaration). + * It is no longer required to pass a default value of `...` to mark a *CLI Argument* or *CLI Option* as required. + * It is now recommended to use `Annotated` for `typer.Option()` and `typer.Argument()`. + * All the docs have been updated to recommend `Annotated`. + +### Docs + +* 📝 Update docs examples for custom param types using `Annotated`, fix overloads for `typer.Argument`. PR [#594](https://github.com/tiangolo/typer/pull/594) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#592](https://github.com/tiangolo/typer/pull/592) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + +## 0.8.0 (2023-05-01) + +### Features + +* ✨ Add support for custom types and parsers. Initial PR [#583](https://github.com/tiangolo/typer/pull/583) by [@jpurviance](https://github.com/jpurviance). Based on original PR [#443](https://github.com/tiangolo/typer/pull/443) by [@paulo-raca](https://github.com/paulo-raca). + * New docs: [CLI Parameter Types: Custom Types](https://typer.tiangolo.com/tutorial/parameter-types/custom-types/). + +### Upgrades + +* ⬆ Upgrade Rich, support 13.x. PR [#524](https://github.com/tiangolo/typer/pull/524) by [@musicinmybrain](https://github.com/musicinmybrain). + +### Docs + +* 📝 Tweak docs, Custom Types path, main page and READAME colors, broken links. PR [#588](https://github.com/tiangolo/typer/pull/588) by [@tiangolo](https://github.com/tiangolo). +* ✏ Fix spelling (shinny -> shiny). PR [#586](https://github.com/tiangolo/typer/pull/586) by [@runofthemill](https://github.com/runofthemill). +* 📝 Update docs about helping Typer. PR [#547](https://github.com/tiangolo/typer/pull/547) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix typo in datetime docs. PR [#495](https://github.com/tiangolo/typer/pull/495) by [@huxuan](https://github.com/huxuan). +* ✏️ Add quotes to package name that includes brackets in docs. PR [#475](https://github.com/tiangolo/typer/pull/475) by [@gjolga](https://github.com/gjolga). + +### Internal + +* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.26.0. PR [#558](https://github.com/tiangolo/typer/pull/558) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#549](https://github.com/tiangolo/typer/pull/549) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* 🔧 Add `exclude_lines` to coverage configuration. PR [#585](https://github.com/tiangolo/typer/pull/585) by [@dmontagu](https://github.com/dmontagu). +* ⬆️ Upgrade analytics. PR [#557](https://github.com/tiangolo/typer/pull/557) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update new issue chooser to suggest GitHub Discussions. PR [#544](https://github.com/tiangolo/typer/pull/544) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Add GitHub Discussion templates for questions. PR [#541](https://github.com/tiangolo/typer/pull/541) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update pre-commit, Python version, isort version. PR [#542](https://github.com/tiangolo/typer/pull/542) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#512](https://github.com/tiangolo/typer/pull/512) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#513](https://github.com/tiangolo/typer/pull/513) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Refactor CI artifact upload/download for docs previews. PR [#516](https://github.com/tiangolo/typer/pull/516) by [@tiangolo](https://github.com/tiangolo). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#500](https://github.com/tiangolo/typer/pull/500) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump actions/cache from 2 to 3. PR [#496](https://github.com/tiangolo/typer/pull/496) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.24.1 to 2.24.2. PR [#494](https://github.com/tiangolo/typer/pull/494) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.24.1. PR [#491](https://github.com/tiangolo/typer/pull/491) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/setup-python from 2 to 4. PR [#492](https://github.com/tiangolo/typer/pull/492) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷‍♂️ Consistently use `sys.executable` to run subprocesses, needed by OpenSUSE. PR [#408](https://github.com/tiangolo/typer/pull/408) by [@theMarix](https://github.com/theMarix). +* 👷‍♂️ Ensure the `PYTHONPATH` is set properly when testing the tutorial scripts. PR [#407](https://github.com/tiangolo/typer/pull/407) by [@theMarix](https://github.com/theMarix). + +## 0.7.0 (2022-11-05) + +### Features + +* ✨ Make `typer.run()` not add completion scripts by default, it only makes sense in installed apps. Also update docs for handling [autocompletion in CLI options](https://typer.tiangolo.com/tutorial/options-autocompletion/). PR [#488](https://github.com/tiangolo/typer/pull/488) by [@tiangolo](https://github.com/tiangolo). +* ✨ Add support for Python 3.11, tests in CI and official marker. PR [#487](https://github.com/tiangolo/typer/pull/487) by [@tiangolo](https://github.com/tiangolo). +* 👷 Add CI for Python 3.10. PR [#384](https://github.com/tiangolo/typer/pull/384) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* 🎨 Fix type annotation of `typer.run()`. PR [#284](https://github.com/tiangolo/typer/pull/284) by [@yassu](https://github.com/yassu). +* 🎨 Fix type annotations for `get_group`. PR [#430](https://github.com/tiangolo/typer/pull/430) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add note about how subcommands with function names using underscores are converted to dashes. PR [#403](https://github.com/tiangolo/typer/pull/403) by [@targhs](https://github.com/targhs). +* 📝 Fix typo in docs at `docs/tutorial/commands/help.md`. PR [#466](https://github.com/tiangolo/typer/pull/466) by [@fepegar](https://github.com/fepegar). +* ✏ Fix link in docs to `datetime.strptime()`. PR [#464](https://github.com/tiangolo/typer/pull/464) by [@Kobu](https://github.com/Kobu). +* ✏ Update `first-steps.md`, clarify distinction between parameter and argument. PR [#176](https://github.com/tiangolo/typer/pull/176) by [@mccarthysean](https://github.com/mccarthysean). +* ✏ Fix broken plac link. PR [#275](https://github.com/tiangolo/typer/pull/275) by [@mgielda](https://github.com/mgielda). + +### Internal + +* ✅ Add extra tests just for coverage because monkeypatching with strange imports confuses coverage. PR [#490](https://github.com/tiangolo/typer/pull/490) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Tweak pytest coverage. PR [#485](https://github.com/tiangolo/typer/pull/485) by [@tiangolo](https://github.com/tiangolo). +* ➕ Bring back pytest-cov because coverage can't detect pytest-xdist. PR [#484](https://github.com/tiangolo/typer/pull/484) by [@tiangolo](https://github.com/tiangolo). +* ⬆ Bump actions/upload-artifact from 2 to 3. PR [#477](https://github.com/tiangolo/typer/pull/477) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump actions/checkout from 2 to 3. PR [#478](https://github.com/tiangolo/typer/pull/478) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#411](https://github.com/tiangolo/typer/pull/411) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.4. PR [#479](https://github.com/tiangolo/typer/pull/479) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump tiangolo/issue-manager from 0.2.0 to 0.4.0. PR [#481](https://github.com/tiangolo/typer/pull/481) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Move from pytest-cov to coverage and Codecov to Smokeshow. PR [#483](https://github.com/tiangolo/typer/pull/483) by [@tiangolo](https://github.com/tiangolo). +* ➕ Add extra Material for MkDocs deps for docs. PR [#482](https://github.com/tiangolo/typer/pull/482) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update Dependabot config. PR [#476](https://github.com/tiangolo/typer/pull/476) by [@tiangolo](https://github.com/tiangolo). + +## 0.6.1 (2022-07-12) + +### Fixes + +* 🐛 Fix setting `FORCE_TERMINAL` with colors 2. PR [#424](https://github.com/tiangolo/typer/pull/424) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix setting `FORCE_TERMINAL` with colors. PR [#423](https://github.com/tiangolo/typer/pull/423) by [@tiangolo](https://github.com/tiangolo). + +## 0.6.0 (2022-07-12) + +This release adds deep integrations with [Rich](https://rich.readthedocs.io/en/stable/). ✨ + +`rich` is an optional dependency, you can install it directly or it will be included when you install with: + +```console +$ pip install "typer[all]" +``` + +If Rich is available, it will be used to show the content from `--help` options, validation errors, and even errors in your app (exception tracebacks). + +There are new options to group commands, *CLI arguments*, and *CLI options*, support for [Rich Console Markup](https://rich.readthedocs.io/en/stable/markup.html), and more! 🎉 + +### Features + +* ✨ Richify, add integrations with Rich everywhere. PR [#419](https://github.com/tiangolo/typer/pull/419) by [@tiangolo](https://github.com/tiangolo). + * Recommend Rich as the main information displaying tool, new docs: [Printing and Colors](https://typer.tiangolo.com/tutorial/printing/). + * For most use cases not using Rich, use plain `print()` instead of `typer.echo()` in the docs, to simplify the concepts and avoid confusions. New docs: [Printing and Colors - typer Echo](https://typer.tiangolo.com/tutorial/printing/#typer-echo). + * Define help panels for *CLI arguments*, new docs: [CLI Arguments with Help - CLI Argument help panels](https://typer.tiangolo.com/tutorial/arguments/help/#cli-argument-help-panels). + * Define help panels for *CLI options*, new docs: [CLI Options with Help - CLI Options help panels](https://typer.tiangolo.com/tutorial/options/help/#cli-options-help-panels). + * New docs for deprecating commands: [Commands - Command Help - Deprecate a Command](https://typer.tiangolo.com/tutorial/commands/help/#deprecate-a-command). + * Support for Rich Markdown in docstrings, *CLI parameters* `help`, and `epilog` with the new parameter `typer.Typer(rich_markup_mode="markdown")`, new docs: [Commands - Command Help - Rich Markdown and Markup](https://typer.tiangolo.com/tutorial/commands/help/#rich-markdown-and-markup). + * Support for Rich Markup (different from Markdown) in docstrings, *CLI parameters* `help`, and `epilog` with the new parameter `typer.Typer(rich_markup_mode="rich")`, new docs: [Commands - Command Help - Rich Markdown and Markup](https://typer.tiangolo.com/tutorial/commands/help/#rich-markdown-and-markup). + * Define help panels for *commands*, new docs: [Commands - Command Help - Help Panels](https://typer.tiangolo.com/tutorial/commands/help/#help-panels). + * New docs for setting an `epilog`, with support for Rich Markdown and Console Markup, new docs: [Commands - Command Help - Epilog](https://typer.tiangolo.com/tutorial/commands/help/#epilog). +* ✨ Refactor and document handling pretty exceptions. PR [#422](https://github.com/tiangolo/typer/pull/422) by [@tiangolo](https://github.com/tiangolo). + * Add support for customizing pretty short errors, new docs: [Exceptions and Errors](https://typer.tiangolo.com/tutorial/exceptions/). +* ✨ Allow configuring pretty errors when creating the Typer instance. PR [#416](https://github.com/tiangolo/typer/pull/416) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add docs for using Rich with Typer. PR [#421](https://github.com/tiangolo/typer/pull/421) by [@tiangolo](https://github.com/tiangolo). + * Add new docs: [Ask with Prompt - Prompt with Rich](https://typer.tiangolo.com/tutorial/prompt/#prompt-with-rich). + * Add new docs to handle progress bars and spinners with Rich: [Progress Par](https://typer.tiangolo.com/tutorial/progressbar/). + +### Internal + +* ⬆️ Upgrade codecov GitHub Action. PR [#420](https://github.com/tiangolo/typer/pull/420) by [@tiangolo](https://github.com/tiangolo). + +## 0.5.0 (2022-07-06) + +### Features + +* ✨ Add pretty error tracebacks for user errors and support for Rich. PR [#412](https://github.com/tiangolo/typer/pull/412) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* ✏ Fix typo, "ASCII codes" to "ANSI escape sequences". PR [#308](https://github.com/tiangolo/typer/pull/308) by [@septatrix](https://github.com/septatrix). + +## 0.4.2 (2022-07-02) + +### Fixes + +* 🐛 Fix type conversion for `List` and `Tuple` and their internal types. PR [#143](https://github.com/tiangolo/typer/pull/143) by [@hellowhistler](https://github.com/hellowhistler). +* 🐛 Fix `context_settings` for a Typer app with a single command. PR [#210](https://github.com/tiangolo/typer/pull/210) by [@daddycocoaman](https://github.com/daddycocoaman). + +### Docs + +* 📝 Clarify testing documentation about checking `stderr`. PR [#335](https://github.com/tiangolo/typer/pull/335) by [@cgabard](https://github.com/cgabard). * ✏ Fix typo in docs for CLI Option autocompletion. PR [#288](https://github.com/tiangolo/typer/pull/288) by [@graue70](https://github.com/graue70). * 🎨 Fix header format for "Standard Input" in `docs/tutorial/printing.md`. PR [#386](https://github.com/tiangolo/typer/pull/386) by [@briancohan](https://github.com/briancohan). * ✏ Fix typo in `docs/tutorial/terminating.md`. PR [#382](https://github.com/tiangolo/typer/pull/382) by [@kianmeng](https://github.com/kianmeng). @@ -8,13 +1490,18 @@ * ✏ Fix minor typo in `index.md`. PR [#274](https://github.com/tiangolo/typer/pull/274) by [@RmStorm](https://github.com/RmStorm). * ✏ Fix double "and" typo in first-steps tutorial. PR [#225](https://github.com/tiangolo/typer/pull/225) by [@softwarebloat](https://github.com/softwarebloat). * 🎨 Fix format in docs explaining `datetime` parameter type. PR [#220](https://github.com/tiangolo/typer/pull/220) by [@DiegoPiloni](https://github.com/DiegoPiloni). + +### Internal + +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#404](https://github.com/tiangolo/typer/pull/404) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 👷 Fix Material for MkDocs install in CI. PR [#395](https://github.com/tiangolo/typer/pull/395) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit CI config. PR [#394](https://github.com/tiangolo/typer/pull/394) by [@tiangolo](https://github.com/tiangolo). * 👷 Clear MkDocs Insiders cache. PR [#393](https://github.com/tiangolo/typer/pull/393) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add pre-commit config and formatting. PR [#392](https://github.com/tiangolo/typer/pull/392) by [@tiangolo](https://github.com/tiangolo). * 👷 Disable installing MkDocs Insiders in forks. PR [#391](https://github.com/tiangolo/typer/pull/391) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Codecov GitHub Action. PR [#383](https://github.com/tiangolo/typer/pull/383) by [@tiangolo](https://github.com/tiangolo). -## 0.4.1 + +## 0.4.1 (2022-03-30) ### Fixes @@ -28,7 +1515,7 @@ * ⬆️ Upgrade MkDocs Material, MDX-Include, and MkDocs structure. PR [#356](https://github.com/tiangolo/typer/pull/356) by [@tiangolo](https://github.com/tiangolo). * 👷 Update publish GitHub action. PR [#325](https://github.com/tiangolo/typer/pull/325) by [@tiangolo](https://github.com/tiangolo). -## 0.4.0 +## 0.4.0 (2021-08-30) ### Features @@ -43,7 +1530,7 @@ * 👷 Update docs CI name. PR [#320](https://github.com/tiangolo/typer/pull/320) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add sponsors docs and badge. PR [#319](https://github.com/tiangolo/typer/pull/319) by [@tiangolo](https://github.com/tiangolo). -## 0.3.2 +## 0.3.2 (2020-08-16) ### Features @@ -60,14 +1547,14 @@ * Report coverage in XML to support GitHub Actions. PR [#146](https://github.com/tiangolo/typer/pull/146). * Update badges and remove Travis, now that GitHub Actions is the main CI. PR [#145](https://github.com/tiangolo/typer/pull/145). -## 0.3.1 +## 0.3.1 (2020-07-19) * Add GitHub Actions, move from Travis. PR [#144](https://github.com/tiangolo/typer/pull/144). * Pin dependencies. PR [#138](https://github.com/tiangolo/typer/pull/138). * Add Dependabot. PR [#136](https://github.com/tiangolo/typer/pull/136). * Upgrade Isort to version 5.x.x. PR [#137](https://github.com/tiangolo/typer/pull/137). -## 0.3.0 +## 0.3.0 (2020-06-24) * Add support for `help` parameter in *CLI arguments*: * As `help` in *CLI arguments* is not supported by Click, there are two new internal classes (Click sub-classes) to support it: @@ -95,11 +1582,11 @@ * Fix typos and rewording in docs. PR [#97](https://github.com/tiangolo/typer/pull/97) by [@mrcartoonster](https://github.com/mrcartoonster). * Update GitHub Action issue-manager. PR [#114](https://github.com/tiangolo/typer/pull/114). -## 0.2.1 +## 0.2.1 (2020-04-26) * Add support for forward references (types declared inside of strings). PR [#93](https://github.com/tiangolo/typer/pull/93). -## 0.2.0 +## 0.2.0 (2020-04-26) * Add support for completion for commands/programs not available on startup. * This allows installing a Typer program/script in a virtual environment and still have completion globally installed. @@ -107,14 +1594,14 @@ * Add note about `typer.echo()` and `print()` for colors in Windows. PR [#89](https://github.com/tiangolo/typer/pull/89). * Upgrade Mkdocs-Material version, update contributing guide style. PR [#90](https://github.com/tiangolo/typer/pull/90). -## 0.1.1 +## 0.1.1 (2020-03-29) * Fix completion evaluation for Bash and Zsh when the program is not installed/found. PR [#83](https://github.com/tiangolo/typer/pull/83). * Fix completion script for Fish. PR [#82](https://github.com/tiangolo/typer/pull/82). * Fix shell installation for Bash to `~/.bashrc` and update Windows development docs. PR [#81](https://github.com/tiangolo/typer/pull/81). * Update coverage badge. PR [#78](https://github.com/tiangolo/typer/pull/78). -## 0.1.0 +## 0.1.0 (2020-03-19) * Fix coverage instructions. PR [#72](https://github.com/tiangolo/typer/pull/72). * Add docs for [Building a Package](https://typer.tiangolo.com/tutorial/package/). PR [#71](https://github.com/tiangolo/typer/pull/71). @@ -132,7 +1619,7 @@ * Fix Zsh completion install script. PR [#69](https://github.com/tiangolo/typer/pull/69). * Fix typo in progressbar example. PR [#63](https://github.com/tiangolo/typer/pull/63) by [@ValentinCalomme](https://github.com/ValentinCalomme). -## 0.0.11 +## 0.0.11 (2020-03-16) * Re-implement completion system: * Remove optional dependency `click-completion` (with its sub-dependencies, like Jinja). @@ -153,11 +1640,11 @@ * Add docs about [Alternatives, Inspiration and Comparisons](https://typer.tiangolo.com/alternatives/). PR [#62](https://github.com/tiangolo/typer/pull/62). * Add [Development - Contributing Guide](https://typer.tiangolo.com/contributing/). PR [#61](https://github.com/tiangolo/typer/pull/61). -## 0.0.10 +## 0.0.10 (2020-03-10) * Add support for Click version 7.1.1. PR [#60](https://github.com/tiangolo/typer/pull/60). -## 0.0.9 +## 0.0.9 (2020-03-08) * Add support for PEP 561, to allow `mypy` to type check applications built with **Typer**. PR [#58](https://github.com/tiangolo/typer/pull/58). * Upgrade deploy docs to Netlify GitHub action. PR [#57](https://github.com/tiangolo/typer/pull/57). @@ -166,7 +1653,7 @@ * Update format for internal links. PR [#38](https://github.com/tiangolo/typer/pull/38). * Tweak external links' format. PR [#36](https://github.com/tiangolo/typer/pull/36). -## 0.0.8 +## 0.0.8 (2020-01-05) * Update docs and add latest changes to MkDocs/website. PR [#33](https://github.com/tiangolo/typer/pull/33). * Add extra tests for edge cases that don't belong in docs' examples. PR [#32](https://github.com/tiangolo/typer/pull/32). @@ -183,18 +1670,18 @@ * Add colors module and docs for [Printing and Colors](https://typer.tiangolo.com/tutorial/printing/) and for [Terminating](https://typer.tiangolo.com/tutorial/terminating/), including tests. PR [#20](https://github.com/tiangolo/typer/pull/20). * Refactor docs to make each individual page/section "bite-sized" / small. Add docs for [CLI option names](https://typer.tiangolo.com/tutorial/options/name/). Update `typer.Argument()` to remove invalid positional `param_decls`. PR [#19](https://github.com/tiangolo/typer/pull/19). -## 0.0.7 +## 0.0.7 (2019-12-31) * Add docs for [*CLI parameter* types](https://typer.tiangolo.com/tutorial/parameter-types/). Includes tests and file classes refactor. PR [#17](https://github.com/tiangolo/typer/pull/17). * Add tests for completion. PR [#15](https://github.com/tiangolo/typer/pull/15) and [#16](https://github.com/tiangolo/typer/pull/16). -## 0.0.6 +## 0.0.6 (2019-12-30) * Add docs for [Commands](https://typer.tiangolo.com/tutorial/commands/). Includes a bug fix for handling default values set in `typer.Typer()` parameters. PR [#14](https://github.com/tiangolo/typer/pull/14). * Add docs for [CLI Arguments](https://typer.tiangolo.com/tutorial/arguments/). PR [#13](https://github.com/tiangolo/typer/pull/13). * Add docs for [CLI Options](https://typer.tiangolo.com/tutorial/options/). PR [#12](https://github.com/tiangolo/typer/pull/12). -## 0.0.5 +## 0.0.5 (2019-12-28) * Clean exports from Typer. Remove unneeded components from Click and add needed `Exit` exception. PR [#11](https://github.com/tiangolo/typer/pull/11). * Fix and document extracting help from a function's docstring [First Steps: Document your CLI app](https://typer.tiangolo.com/tutorial/first-steps/#document-your-cli-app). PR [#10](https://github.com/tiangolo/typer/pull/10). @@ -204,19 +1691,19 @@ * Update Termynal to have fixed sizes, add "fast" button, and use it in [First Steps](https://typer.tiangolo.com/tutorial/first-steps/). PR [#6](https://github.com/tiangolo/typer/pull/6). * Add custom automatic [Termynal](https://github.com/tiangolo/termynal) for docs. PR [#5](https://github.com/tiangolo/typer/pull/5). -## 0.0.4 +## 0.0.4 (2019-12-24) * Update short descriptions and assets. * Docs rewording and fix typos. PR [#1](https://github.com/tiangolo/typer/pull/1) by [@mariacamilagl](https://github.com/mariacamilagl). -## 0.0.3 +## 0.0.3 (2019-12-24) * Fix group creation without name. -## 0.0.2 +## 0.0.2 (2019-12-24) * Add initial version of code, docs, etc. -## 0.0.1 +## 0.0.1 (2019-12-20) * First commit. Publish to PyPI to reserve package name. diff --git a/docs/resources/index.md b/docs/resources/index.md new file mode 100644 index 0000000000..d233a7833b --- /dev/null +++ b/docs/resources/index.md @@ -0,0 +1,3 @@ +# Resources + +Additional resources, how to **help** and get help, how to **contribute**, and more. ✈️ diff --git a/docs/tutorial/app-dir.md b/docs/tutorial/app-dir.md index 3d09346b22..8e1258e90c 100644 --- a/docs/tutorial/app-dir.md +++ b/docs/tutorial/app-dir.md @@ -1,8 +1,8 @@ +# CLI Application Directory + You can get the application directory where you can, for example, save configuration files with `typer.get_app_dir()`: -```Python hl_lines="9" -{!../docs_src/app_dir/tutorial001.py!} -``` +{* docs_src/app_dir/tutorial001.py hl[12] *} It will give you a directory for storing configurations appropriate for your CLI program for the current user in each operating system. @@ -11,7 +11,7 @@ Check it:
```console -$ python main.py +$ uv run python main.py Config file doesn't exist yet ``` @@ -32,7 +32,7 @@ If the first element is a `Path` object the next ones (after the `/`) can be `st And it will create a new `Path` object from that. -If you want a quick guide on using `Path()` you can check this post on Real Python or this post by Trey Hunner. +If you want a quick guide on using `Path()` you can check [this post on Real Python](https://realpython.com/python-pathlib/) or [this post by Trey Hunner](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/). In the code above, we are also explicitly declaring `config_path` as having type `Path` to help the editor provide completion and type checks: diff --git a/docs/tutorial/arguments/default.md b/docs/tutorial/arguments/default.md index 0ab7f15e01..5bf2eafa6f 100644 --- a/docs/tutorial/arguments/default.md +++ b/docs/tutorial/arguments/default.md @@ -1,3 +1,5 @@ +# CLI Arguments with Default + We can also use the same `typer.Argument()` to set a default value. That way the *CLI argument* will be optional *and also* have a default value. @@ -6,14 +8,15 @@ That way the *CLI argument* will be optional *and also* have a default value. We can also use `typer.Argument()` to make a *CLI argument* have a default value other than `None`: -```Python hl_lines="4" -{!../docs_src/arguments/default/tutorial001.py!} -``` +{* docs_src/arguments/default/tutorial001.py hl[9] *} + +/// tip + +Because now the value will be a `str` passed by the user or the default value of `"Wade Wilson"` which is also a `str`, we know the value will never be `None`, so we don't have to (and shouldn't) use `Optional[str]`. -!!! tip - Because now the value will be a `str` passed by the user or the default value of `"Wade Wilson"` which is also a `str`, we know the value will never be `None`, so we don't have to (and shouldn't) use `Optional[str]`. +Have in mind that the `Optional[something]` tells Python that a value "could be `None`". But the use of `Optional` doesn't affect Typer in any way, e.g. it doesn't tell Typer if a value is required or not. - Have in mind that the `Optional[something]` tells Python that a value "could be `None`". But the use of `Optional` doesn't affect Typer in any way, e.g. it doesn't tell Typer if a value is required or not. +/// Check it: @@ -21,26 +24,24 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the [default: Wade Wilson] ✨ -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] Arguments: - [NAME] [default: Wade Wilson] + name [default: Wade Wilson] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // With no optional CLI argument -$ python main.py +$ uv run python main.py Hello Wade Wilson // With one CLI argument -$ python main.py Camila +$ uv run python main.py Camila Hello Camila ``` @@ -49,49 +50,51 @@ Hello Camila ## Dynamic default value -And we can even make the default value be dynamically generated by passing a function as the first function argument: +And we can even make the default value be dynamically generated by passing a function as the `default_factory` argument: -```Python hl_lines="6 7 10" -{!../docs_src/arguments/default/tutorial002.py!} -``` +{* docs_src/arguments/default/tutorial002.py hl[9:10,14] *} In this case, we created the function `get_name` that will just return a random `str` each time. And we pass it as the first function argument to `typer.Argument()`. +/// tip + +The word "factory" in `default_factory` is just a fancy way of saying "function that will create the default value". + +/// + Check it:
```console // Check the help -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] Arguments: - [NAME] [default: (dynamic)] + name [default: (dynamic)] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Try it several times, it will use a random default each time -$ python main.py +$ uv run python main.py Hello Deadpool -$ python main.py +$ uv run python main.py Hello Hiro -$ python main.py +$ uv run python main.py Hello Rick // Now pass a value for the CLI argument -$ python main.py Camila +$ uv run python main.py Camila Hello Camila ``` diff --git a/docs/tutorial/arguments/envvar.md b/docs/tutorial/arguments/envvar.md index 4277870557..fdd10464db 100644 --- a/docs/tutorial/arguments/envvar.md +++ b/docs/tutorial/arguments/envvar.md @@ -1,10 +1,18 @@ +# CLI Arguments with Environment Variables + You can also configure a *CLI argument* to read a value from an environment variable if it is not provided in the command line as a *CLI argument*. +An **environment variable** (also known as an **env var**) is a value that lives outside of your Python code, in the operating system, and can be read by your application and other programs. + +/// tip + +Read the [Environment Variables guide](https://tiangolo.com/guides/environment-variables/) for a detailed, cross-platform explanation. + +/// + To do that, use the `envvar` parameter for `typer.Argument()`: -```Python hl_lines="4" -{!../docs_src/arguments/envvar/tutorial001.py!} -``` +{* docs_src/arguments/envvar/tutorial001.py hl[9] *} In this case, the *CLI argument* `name` will have a default value of `"World"`, but will also read any value passed to the environment variable `AWESOME_NAME` if no value is provided in the command line: @@ -12,48 +20,55 @@ In this case, the *CLI argument* `name` will have a default value of `"World"`, ```console // Check the help -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] Arguments: - [NAME] [env var: AWESOME_NAME;default: World] + name [env var: AWESOME_NAME; default: World] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Call it without a CLI argument -$ python main.py +$ uv run python main.py Hello Mr. World // Now pass a value for the CLI argument -$ python main.py Czernobog +$ uv run python main.py Czernobog Hello Mr. Czernobog // And now use the environment variable -$ AWESOME_NAME=Wednesday python main.py +$ AWESOME_NAME=Wednesday uv run python main.py Hello Mr. Wednesday // CLI arguments take precedence over env vars -$ AWESOME_NAME=Wednesday python main.py Czernobog +$ AWESOME_NAME=Wednesday uv run python main.py Czernobog Hello Mr. Czernobog ```
+/// tip | Windows PowerShell + +In PowerShell, set the environment variable first and then run the command: + +```console +$ $Env:AWESOME_NAME = "Wednesday" +$ uv run python main.py +``` + +/// + ## Multiple environment variables You are not restricted to a single environment variable, you can declare a list of environment variables that could be used to get a value if it was not passed in the command line: -```Python hl_lines="4" -{!../docs_src/arguments/envvar/tutorial002.py!} -``` +{* docs_src/arguments/envvar/tutorial002.py hl[10] *} Check it: @@ -61,25 +76,23 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] Arguments: - [NAME] [env var: AWESOME_NAME, GOD_NAME;default: World] + name [env var: AWESOME_NAME, GOD_NAME; default: World] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Try the first env var -$ AWESOME_NAME=Wednesday python main.py +$ AWESOME_NAME=Wednesday uv run python main.py Hello Mr. Wednesday // Try the second env var -$ GOD_NAME=Anubis python main.py +$ GOD_NAME=Anubis uv run python main.py Hello Mr. Anubis ``` @@ -90,9 +103,7 @@ Hello Mr. Anubis By default, environment variables used will be shown in the help text, but you can disable them with `show_envvar=False`: -```Python hl_lines="4" -{!../docs_src/arguments/envvar/tutorial003.py!} -``` +{* docs_src/arguments/envvar/tutorial003.py hl[11] *} Check it: @@ -100,28 +111,21 @@ Check it: ```console //Check the help -$ python main.py --help +$ uv run python main.py --help // It won't show the env var -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] Arguments: - [NAME] [default: World] + name [default: World] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // But it will still be able to use it -$ AWESOME_NAME=Wednesday python main.py +$ AWESOME_NAME=Wednesday uv run python main.py Hello Mr. Wednesday ```
- -!!! note "Technical Details" - In Click applications the env vars are hidden by default. 🙈 - - In **Typer** these env vars are shown by default. 👀 diff --git a/docs/tutorial/arguments/help.md b/docs/tutorial/arguments/help.md index 4a18d0d8a9..d81e2fd556 100644 --- a/docs/tutorial/arguments/help.md +++ b/docs/tutorial/arguments/help.md @@ -1,10 +1,10 @@ +# CLI Arguments with Help + In the *First Steps* section you saw how to add help for a CLI app/command by adding it to a function's docstring. Here's how that last example looked like: -```Python -{!../docs_src/first_steps/tutorial006.py!} -``` +{* docs_src/first_steps/tutorial006.py *} Now that you also know how to use `typer.Argument()`, let's use it to add documentation specific for a *CLI argument*. @@ -12,26 +12,22 @@ Now that you also know how to use `typer.Argument()`, let's use it to add docume You can use the `help` parameter to add a help text for a *CLI argument*: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial001.py!} -``` +{* docs_src/arguments/help/tutorial001.py hl[9] *} And it will be used in the automatic `--help` option:
```console -$ python main.py --help +$ uv run python main.py --help // Check the section with Arguments below 🚀 -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} Arguments: - NAME The name of the user to greet [required] + name The name of the user to greet [required] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. ``` @@ -41,28 +37,24 @@ Options: And of course, you can also combine that `help` with the docstring: -```Python hl_lines="4 5 6 7" -{!../docs_src/arguments/help/tutorial002.py!} -``` +{* docs_src/arguments/help/tutorial002.py hl[9:12] *} And the `--help` option will combine all the information:
```console -$ python main.py --help +$ uv run python main.py --help // Notice that we have the help text from the docstring and also the Arguments 📝 -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. Arguments: - NAME The name of the user to greet [required] + name The name of the user to greet [required] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. ``` @@ -72,28 +64,24 @@ Options: If you have a *CLI argument* with a default value, like `"World"`: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial003.py!} -``` +{* docs_src/arguments/help/tutorial003.py hl[9] *} It will show that default value in the help text:
```console -$ python main.py --help +$ uv run python main.py --help // Notice the [default: World] 🔍 -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. Arguments: - [NAME] Who to greet [default: World] + name Who to greet [default: World] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. ``` @@ -101,63 +89,49 @@ Options: But you can disable that if you want to, with `show_default=False`: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial004.py!} -``` +{* docs_src/arguments/help/tutorial004.py hl[11] *} And then it won't show the default value:
```console -$ python main.py --help +$ uv run python main.py --help // Notice the there's no [default: World] now 🔥 -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. Arguments: - [NAME] Who to greet + name Who to greet Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. ```
-!!! note "Technical Details" - In Click applications the default values are hidden by default. 🙈 - - In **Typer** these default values are shown by default. 👀 - ## Custom default string You can use the same `show_default` to pass a custom string (instead of a `bool`) to customize the default value to be shown in the help text: -```Python hl_lines="6" -{!../docs_src/arguments/help/tutorial005.py!} -``` +{* docs_src/arguments/help/tutorial005.py hl[13] *} And it will be used in the help text:
```console -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] Arguments: - [NAME] Who to greet [default: (Deadpoolio the amazing's name)] + name Who to greet [default: (Deadpoolio the amazing's name)] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // See it shows "(Deadpoolio the amazing's name)" instead of the actual default of "Wade Wilson" @@ -169,7 +143,7 @@ Options: You can also customize the text used in the generated help text to represent a *CLI argument*. -By default, it will be the same name you declared, in uppercase letters. +By default, it will be the same name you declared, with casing preserved. So, if you declare it as: @@ -180,91 +154,114 @@ name: str It will be shown as: ``` -NAME +name ``` But you can customize it with the `metavar` parameter for `typer.Argument()`. -For example, let's say you don't want to have the default of `NAME`, you want to have `username`, in lowercase, and you really want ✨ emojis ✨ everywhere: +For example, let's say you don't want to have the default of `name`, you want to have `username`, and you really want ✨ emojis ✨ everywhere: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial006.py!} -``` +{* docs_src/arguments/help/tutorial006.py hl[9] *} -Now the generated help text will have `✨username✨` instead of `NAME`: +Now the generated help text will have `✨username✨` instead of `name`:
```console -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] ✨username✨ +Usage: main.py [OPTIONS] [✨username✨] Arguments: - ✨username✨ [default: World] + ✨username✨ [default: World] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. ```
+## *CLI Argument* help panels + +You might want to show the help information for *CLI arguments* in different panels when using the `--help` option. + +If you have installed Rich as described in the docs for [Printing and Colors](../printing.md), you can set the `rich_help_panel` parameter to the name of the panel where you want this *CLI argument* to be shown: + +{* docs_src/arguments/help/tutorial007.py hl[12,16] *} + +Then, if you check the `--help` option, you will see a default panel named "`Arguments`" for the *CLI arguments* that don't have a custom `rich_help_panel`. + +And next you will see other panels for the *CLI arguments* that have a custom panel set in the `rich_help_panel` parameter: + +
+ +```console +$ uv run python main.py --help + + Usage: main.py [OPTIONS] {name} [lastname] [age] + + Say hi to 'name' very gently, like Dirk. + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * name str Who to greet [default: None] [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Secondary Arguments ─────────────────────────────────────────────╮ +│ lastname lastname The last name │ +│ age age The user's age │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +In this example we have a custom *CLI arguments* panel named "`Secondary Arguments`". + +## Help with style using Rich + +In a future section you will see how to use custom markup in the `help` for *CLI arguments* when reading about [Commands - Command Help](../commands/help.md#rich-markdown-and-markup). + +If you are in a hurry you can jump there, but otherwise, it would be better to continue reading here and following the tutorial in order. + ## Hide a *CLI argument* from the help text If you want, you can make a *CLI argument* **not** show up in the `Arguments` section in the help text. You will probably not want to do this normally, but it's possible: -```Python hl_lines="4" -{!../docs_src/arguments/help/tutorial007.py!} -``` +{* docs_src/arguments/help/tutorial008.py hl[9] *} Check it:
```console -$ python main.py --help +$ uv run python main.py --help // Notice there's no Arguments section at all 🔥 -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. ```
-!!! info - Have in mind that the *CLI argument* will still show up in the first line with `Usage`. - - But it won't show up in the main help text under the `Arguments` section. +/// note -### Help text for *CLI arguments* in Click +Have in mind that the *CLI argument* will still show up in the first line with `Usage`. -Click itself doesn't support adding help for *CLI arguments*, and it doesn't generate help for them as in the "`Arguments:`" sections in the examples above. +But it won't show up in the main help text under the `Arguments` section. -Not supporting `help` in *CLI arguments* is an intentional design decision in Click: +/// -> This is to follow the general convention of Unix tools of using arguments for only the most necessary things, and to document them in the command help text by referring to them by name. +### Help text for *CLI arguments* -So, in Click applications, you are expected to write all the documentation for *CLI arguments* by hand in the docstring. - ---- - -Nevertheless, **Typer supports `help` for *CLI arguments***. ✨ 🤷‍♂ - -**Typer** doesn't follow that convention and instead supports `help` to make it easier to have consistent help texts with a consistent format for your CLI programs. 🎨 +**Typer supports `help` for *CLI arguments*** to make it easier to have consistent help texts with a consistent format for your CLI programs. 🎨 This is also to help you create CLI programs that are ✨ awesome ✨ *by default*. With very little code. -If you want to keep Click's convention in a **Typer** app, you can do it with the `hidden` parameter as described above. - -!!! note "Technical Details" - To support `help` in *CLI arguments* **Typer** does a lot of internal work in its own sub-classes of Click's internal classes. +If you don't want the CLI Argument to be shown in help outputs, you can set the `hidden` parameter to `True`. diff --git a/docs/tutorial/arguments/index.md b/docs/tutorial/arguments/index.md index 23c97706ce..a52816e457 100644 --- a/docs/tutorial/arguments/index.md +++ b/docs/tutorial/arguments/index.md @@ -1,3 +1,5 @@ +# CLI Arguments + In the next few sections we'll see some ways to modify how *CLI arguments* work. We'll create optional *CLI arguments*, we'll add integrated help for *CLI arguments*, etc. diff --git a/docs/tutorial/arguments/optional.md b/docs/tutorial/arguments/optional.md index 4821ff923e..1d8ba55198 100644 --- a/docs/tutorial/arguments/optional.md +++ b/docs/tutorial/arguments/optional.md @@ -1,3 +1,5 @@ +# Optional CLI Arguments + We said before that *by default*: * *CLI options* are **optional** @@ -29,19 +31,29 @@ __init__.py test_tutorial
-### An alternative *CLI argument* declaration +## An alternative *CLI argument* declaration -In the [First Steps](../first-steps.md#add-a-cli-argument){.internal-link target=_blank} you saw how to add a *CLI argument*: +In the [First Steps](../first-steps.md#add-a-cli-argument) you saw how to add a *CLI argument*: -```Python hl_lines="4" -{!../docs_src/first_steps/tutorial002.py!} -``` +{* docs_src/first_steps/tutorial002.py hl[4] *} Now let's see an alternative way to create the same *CLI argument*: -```Python hl_lines="4" -{!../docs_src/arguments/optional/tutorial001.py!} -``` +{* docs_src/arguments/optional/tutorial000.py hl[6] *} + +Or, using an explicit `Typer()` instance creation: + +{* docs_src/arguments/optional/tutorial001.py hl[9] *} + +/// note + +Typer added support for `Annotated` (and started recommending it) in version 0.9.0. + +If you have an older version, you would get errors when trying to use `Annotated`. + +Make sure you upgrade the Typer version to at least 0.9.0 before using `Annotated`. + +/// Before, you had this function parameter: @@ -49,92 +61,95 @@ Before, you had this function parameter: name: str ``` -And because `name` didn't have any default value it would be a **required parameter** for the Python function, in Python terms. - -**Typer** does the same and makes it a **required** *CLI argument*. - -And then we changed it to: +And now we wrap it with `Annotated`: ```Python -name: str = typer.Argument(...) +name: Annotated[str] ``` -But now as `typer.Argument()` is the "default value" of the function's parameter, it would mean that "it is no longer required" (in Python terms). +Both of these versions mean the same thing, `Annotated` is part of standard Python and is there for this. -As we no longer have the Python function default value (or its absence) to tell if something is required or not and what is the default value, the first parameter to `typer.Argument()` serves the same purpose of defining that default value, or making it required. +But the second version using `Annotated` allows us to pass additional metadata that can be used by **Typer**: -To make it *required*, we pass `...` as the first function argument passed to `typer.Argument(...)`. +```Python +name: Annotated[str, typer.Argument()] +``` -!!! info - If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". +Now we are being explicit that `name` is a *CLI argument*. It's still a `str` and it's still required (it doesn't have a default value). All we did there achieves the same thing as before, a **required** *CLI argument*:
```console -$ python main.py +$ uv run python main.py -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} Try "main.py --help" for help. -Error: Missing argument 'NAME'. +Error: Missing argument 'name'. ```
It's still not very useful, but it works correctly. -And being able to declare a **required** *CLI argument* using `name: str = typer.Argument(...)` that works exactly the same as `name: str` will come handy later. +And being able to declare a **required** *CLI argument* using -### Make an optional *CLI argument* +```Python +name: Annotated[str, typer.Argument()] +``` + +...that works exactly the same as + +```Python +name: str +``` + +...will come handy later. + +## Make an optional *CLI argument* Now, finally what we came for, an optional *CLI argument*. -To make a *CLI argument* optional, use `typer.Argument()` and pass a different "default" as the first parameter to `typer.Argument()`, for example `None`: +To make a *CLI argument* optional, use `typer.Argument()` and make sure to provide a "default" value, for example `"World"`: -```Python hl_lines="6" -{!../docs_src/arguments/optional/tutorial002.py!} -``` +{* docs_src/arguments/optional/tutorial002.py hl[9] *} Now we have: ```Python -name: Optional[str] = typer.Argument(None) +name: Annotated[str, typer.Argument()] = "World" ``` Because we are using `typer.Argument()` **Typer** will know that this is a *CLI argument* (no matter if *required* or *optional*). -And because the first parameter passed to `typer.Argument(None)` (the new "default" value) is `None`, **Typer** knows that this is an **optional** *CLI argument*, if no value is provided when calling it in the command line, it will have that default value of `None`. - -!!! tip - By using `Optional` your editor will be able to know that the value *could* be `None`, and will be able to warn you if you do something assuming it is a `str` that would break if it was `None`. - Check the help:
```console // First check the help -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] [NAME] +Usage: main.py [OPTIONS] [name] Arguments: - [NAME] + name [default: World] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. ```
-!!! tip - Notice that `NAME` is still a *CLI argument*, it's shown up there in the "`Usage: main.py` ...". +/// tip + +Notice that `name` is still a *CLI argument*, it's shown up there in the "`Usage: main.py` ...". - Also notice that now `[NAME]` has brackets ("`[`" and "`]`") around (before it was just `NAME`) to denote that it's **optional**, not **required**. +Also notice that now `[name]` has rectangular brackets ("`[`" and "`]`") around it to denote that it's **optional**, not **required**. If it would have been required, curly brackets ("`{`" and "`}`") would be used instead. + +/// Now run it and test it: @@ -142,17 +157,82 @@ Now run it and test it: ```console // With no CLI argument -$ python main.py +$ uv run python main.py Hello World! // With one optional CLI argument -$ python main.py Camila +$ uv run python main.py Camila Hello Camila ```
-!!! tip - Notice that "`Camila`" here is an optional *CLI argument*, not a *CLI option*, because we didn't use something like "`--name Camila`", we just passed "`Camila`" directly to the program. +/// tip + +Notice that "`Camila`" here is an optional *CLI argument*, not a *CLI option*, because we didn't use something like "`--name Camila`", we just passed "`Camila`" directly to the program. + +/// + +## Alternative (old) `typer.Argument()` as the default value + +**Typer** also supports another older alternative syntax for declaring *CLI arguments* with additional metadata. + +Instead of using `Annotated`, you can use `typer.Argument()` as the default value: + +{* docs_src/arguments/optional/tutorial001_non_an.py hl[7] *} + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +Before, because `name` didn't have any default value it would be a **required parameter** for the Python function, in Python terms. + +When using `typer.Argument()` as the default value **Typer** does the same and makes it a **required** *CLI argument*. + +We changed it to: + +```Python +name: str = typer.Argument() +``` + +But now as `typer.Argument()` is the "default value" of the function's parameter, it would mean that "it is no longer required" (in Python terms). + +As we no longer have the Python function default value (or its absence) to tell if something is required or not and what is the default value, `typer.Argument()` receives a first parameter `default` that serves the same purpose of defining that default value, or making it required. + +Not passing any value to the `default` argument is the same as marking it as required. But you can also explicitly mark it as *required* by passing `...` as the `default` argument, passed to `typer.Argument(default=...)`. + +```Python +name: str = typer.Argument(default=...) +``` + +/// note + +If you hadn't seen that `...` before: it is a special single value, it is [part of Python and is called "Ellipsis"](https://docs.python.org/3/library/constants.html#Ellipsis). + +/// + +{* docs_src/arguments/optional/tutorial003.py hl[7] *} + +And the same way, you can make it optional by passing a different `default` value, for example `"World"`: + +{* docs_src/arguments/optional/tutorial002_non_an.py hl[7] *} + +Because the first parameter passed to `typer.Argument(default="World")` (the new "default" value) is `"World"`, **Typer** knows that this is an **optional** *CLI argument*, if no value is provided when calling it in the command line, it will have that default value of `"World"`. + +The `default` argument is the first one, so it's possible that you see code that passes the value without explicitly using `default=`, like: + +```Python +name: str = typer.Argument(...) +``` + +...or like: + +```Python +name: str = typer.Argument("World") +``` + +...but again, try to use `Annotated` if possible, that way your code in terms of Python will mean the same thing as with **Typer** and you won't have to remember any of these details. diff --git a/docs/tutorial/arguments/other-uses.md b/docs/tutorial/arguments/other-uses.md index f7f398f341..1870e9c6c3 100644 --- a/docs/tutorial/arguments/other-uses.md +++ b/docs/tutorial/arguments/other-uses.md @@ -1,3 +1,5 @@ +# Other uses + `typer.Argument()` has several other use cases. Such as for data validation, to enable other features, etc. You will see about these use cases later in the docs. diff --git a/docs/tutorial/click.md b/docs/tutorial/click.md new file mode 100644 index 0000000000..2f21f0dada --- /dev/null +++ b/docs/tutorial/click.md @@ -0,0 +1,69 @@ +# Vendored Click + +/// note + +This is historical information, if you are just learning Typer from scratch, you don't need to read it. ☕️ + +/// + +Typer used to depend on [Click](https://click.palletsprojects.com/), a popular tool for building CLIs in Python, as an external dependency. + +Since version 0.26.0, Typer has vendored Click (included Click's source code internally, instead of installing it as a third party package) and has unified the code interactions between Typer and the embedded Click source code for easier maintainability in the future. + +Note that some Click functionality will not be available anymore in the future, as we continue to improve and extend Typer's codebase. + +## Breaking Changes + +Typer used to support extracting the internal Click app from a Typer app to use and modify it with any Click functionality. For example, to add Click-specific plug-ins. + +The same way, it supported adding Click-specific types to override the default Typer ones. + +Using Click directly was an edge case feature that was not commonly used, and it is no longer supported. If your app depended specifically on this, you will need to either migrate it to use plain Typer, or migrate it to use Click directly instead of Typer. + +## Codebase Compatibility Improvements + +Because Typer used to depend on Click, any new features or changes in newer Click versions could break compatibility in Typer. + +The Click team has always been very helpful and supportive with Typer. But still, this dependency interaction would cause extra effort and burden for both the Typer team and the Click team. + +Now that Typer continues evolving, starting from a fixed copy of Click's source code, any changes in Click's codebase will not affect Typer. + +The Typer team will not need to make sure there are workarounds for changes in new versions of Click, and the Click team will not need to consider additional edge cases caused by Typer. + +## Compatibility Improvements for Your Apps + +The fact that Typer depended on Click caused an additional ongoing issue that could happen from time to time to user projects. + +Many packages come with a CLI, some of them could use Click, some could use Typer. + +Some of these packages could require a recent version of Click that Typer still didn't support, but as all these package dependencies would belong to the same project, there would be conflicts. + +In these cases, the package that depended on the newer version of Click would require installing it, but the other package that used Typer would break. Or if there were version pins, some combinations of packages would not be installable together. + +Across different versions of Click through time, there were many changes needed in Typer to make it all compatible with multiple versions of Click at the same time. + +Now that Typer and Click are decoupled, a package could depend on a newer version of Click, while another package that uses Typer would continue working as normally, as that Typer version would bundle anything necessary from Click's source code to work. + +## Future Improvements + +Both the Click team and the Typer team have future improvements planned, not having to coordinate with each other for compatibility will simplify the work of both teams. + +In some cases feature ideas could overlap, and could have caused incompatibilities. Now this won't be a problem as each team can focus on each project independently. + +## Typer Changes + +After vendoring Click, Typer will reduce, simplify and refactor parts of the vendored Click code that are not necessary for Typer, or that could be done in a different way to facilitate future improvements in Typer. + +Then Typer will gradually introduce new features and improvements that have been planned for a while but were too difficult to implement before this. + +## User Focus + +These decisions are all carefully planned and based on real world use cases extracted from the official Typer developer survey, including the tradeoff between the potential breaking changes for some use cases and the planned future features and improvements that will be enabled. + +## Thank You Click + +Click has been the foundation building block of Typer and most CLI's in Python (through Typer or directly with Click). + +We wouldn't be here without Click, and we are very grateful for all the work that the Click team has done. + +Thank you Click! 🙇 diff --git a/docs/tutorial/commands/arguments.md b/docs/tutorial/commands/arguments.md index 9829082c39..077debb228 100644 --- a/docs/tutorial/commands/arguments.md +++ b/docs/tutorial/commands/arguments.md @@ -1,39 +1,48 @@ +# Command CLI Arguments + The same way as with a CLI application with a single command, subcommands (or just "commands") can also have their own *CLI arguments*: -```Python hl_lines="7 12" -{!../docs_src/commands/arguments/tutorial001.py!} -``` +{* docs_src/commands/arguments/tutorial001.py hl[7,12] *}
```console // Check the help for create -$ python main.py create --help +$ uv run python main.py create --help -Usage: main.py create [OPTIONS] USERNAME +Usage: main.py create [OPTIONS] {username} + +Arguments: + username [required] Options: --help Show this message and exit. // Call it with a CLI argument -$ python main.py create Camila +$ uv run python main.py create Camila Creating user: Camila // The same for delete -$ python main.py delete Camila +$ uv run python main.py delete Camila Deleting user: Camila ```
-!!! tip - Everything to the *right* of the *command* are *CLI parameters* (*CLI arguments* and *CLI options*) for that command. +/// tip + +Everything to the *right* of the *command* are *CLI parameters* (*CLI arguments* and *CLI options*) for that command. + +/// + +/// note | Technical Details + +Actually, it's everything to the right of that command, *before any subcommand*. -!!! note "Technical Details" - Actually, it's everything to the right of that command, *before any subcommand*. +It's possible to have groups of *subcommands*, it's like if one *command* also had *subcommands*. And then those *subcommands* could have their own *CLI parameters*, taking their own *CLI parameters*. - It's possible to have groups of *subcommands*, it's like if one *command* also had *subcommands*. And then those *subcommands* could have their own *CLI parameters*, taking their own *CLI parameters*. +You will see about them later in another section. - You will see about them later in another section. +/// diff --git a/docs/tutorial/commands/callback.md b/docs/tutorial/commands/callback.md index 6235ade00f..231da52bc0 100644 --- a/docs/tutorial/commands/callback.md +++ b/docs/tutorial/commands/callback.md @@ -1,3 +1,5 @@ +# Typer Callback + When you create an `app = typer.Typer()` it works as a group of commands. And you can create multiple commands with it. @@ -10,16 +12,17 @@ But we can use `@app.callback()` for that. It's very similar to `@app.command()`, but it declares the *CLI parameters* for the main CLI application (before the commands): -```Python hl_lines="25 26 27 28 29 30 31 32" -{!../docs_src/commands/callback/tutorial001.py!} -``` +{* docs_src/commands/callback/tutorial001.py hl[25,26,27,28,29,30,31,32] *} Here we create a `callback` with a `--verbose` *CLI option*. -!!! tip - After getting the `--verbose` flag, we modify a global `state`, and we use it in the other commands. +/// tip + +After getting the `--verbose` flag, we modify a global `state`, and we use it in the other commands. + +There are other ways to achieve the same, but this will suffice for this example. - There are other ways to achieve the same, but this will suffice for this example. +/// And as we added a docstring to the callback function, by default it will be extracted and used as the help text. @@ -29,7 +32,7 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the main help text, extracted from the callback function: "Manage users in the awesome CLI app." Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -37,7 +40,7 @@ Usage: main.py [OPTIONS] COMMAND [ARGS]... Manage users in the awesome CLI app. Options: - --verbose / --no-verbose [default: False] + --verbose / --no-verbose [default: no-verbose] --install-completion Install completion for the current shell. --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. @@ -49,12 +52,12 @@ Commands: // Check the new top level CLI option --verbose // Try it normally -$ python main.py create Camila +$ uv run python main.py create Camila Creating user: Camila // And now with --verbose -$ python main.py --verbose create Camila +$ uv run python main.py --verbose create Camila Will write verbose output About to create a user @@ -62,12 +65,12 @@ Creating user: Camila Just created a user // Notice that --verbose belongs to the callback, it has to go before create or delete ⛔️ -$ python main.py create --verbose Camila +$ uv run python main.py create --verbose Camila -Usage: main.py create [OPTIONS] USERNAME +Usage: main.py create [OPTIONS] {username} Try "main.py create --help" for help. -Error: no such option: --verbose +Error: No such option: --verbose ```
@@ -76,9 +79,7 @@ Error: no such option: --verbose It's also possible to add a callback when creating the `typer.Typer()` app: -```Python hl_lines="4 5 8" -{!../docs_src/commands/callback/tutorial002.py!} -``` +{* docs_src/commands/callback/tutorial002.py hl[4,5,8] *} That achieves the same as with `@app.callback()`. @@ -87,7 +88,7 @@ Check it:
```console -$ python main.py create Camila +$ uv run python main.py create Camila Running a command Creating user: Camila @@ -99,9 +100,7 @@ Creating user: Camila If you added a callback when creating the `typer.Typer()` app, it's possible to override it with `@app.callback()`: -```Python hl_lines="11 12 13" -{!../docs_src/commands/callback/tutorial003.py!} -``` +{* docs_src/commands/callback/tutorial003.py hl[11,12,13] *} Now `new_callback()` will be the one used. @@ -110,7 +109,7 @@ Check it:
```console -$ python main.py create Camila +$ uv run python main.py create Camila // Notice that the message is the one from new_callback() Override callback, running a command @@ -125,9 +124,7 @@ You can also add a callback just to add the documentation in the docstring. It can be convenient especially if you have several lines of text, as the indentation will be automatically handled for you: -```Python hl_lines="8 9 10 11 12 13 14 15 16" -{!../docs_src/commands/callback/tutorial004.py!} -``` +{* docs_src/commands/callback/tutorial004.py hl[8,9,10,11,12,13,14,15,16] *} Now the callback will be used mainly to extract the docstring for the help text. @@ -136,7 +133,7 @@ Check it:
```console -$ python main.py --help +$ uv run python main.py --help // Notice all the help text extracted from the callback docstring Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -145,7 +142,7 @@ Usage: main.py [OPTIONS] COMMAND [ARGS]... Use it with the create command. - A new user with the given NAME will be created. + A new user with the given 'name' will be created. Options: --install-completion Install completion for the current shell. @@ -156,30 +153,9 @@ Commands: create // And it just works as normally -$ python main.py create Camila +$ uv run python main.py create Camila Creating user: Camila ```
- -## Click Group - -If you come from Click, this **Typer** callback is the equivalent of the function in a Click Group. - -For example: - -```Python -import click - -@click.group() -def cli(): - pass -``` - -The original function `cli` would be the equivalent of a Typer callback. - -!!! note "Technical Details" - When using Click, it converts that `cli` variable to a Click `Group` object. And then the original function no longer exists in that variable. - - **Typer** doesn't do that, the callback function is not modified, only registered in the `typer.Typer` app. This is intentional, it's part of **Typer**'s design, to allow having editor auto completion and type checks. diff --git a/docs/tutorial/commands/context.md b/docs/tutorial/commands/context.md index 81119977cd..7ecc5d84ae 100644 --- a/docs/tutorial/commands/context.md +++ b/docs/tutorial/commands/context.md @@ -1,8 +1,10 @@ -When you create a **Typer** application it uses Click underneath. And every Click application has a special object called a "Context" that is normally hidden. +# Using the Context + +When you create a **Typer** application it always has a special, hidden object underneath called the "Context". But you can access the context by declaring a function parameter of type `typer.Context`. -You might have read it in [CLI Option Callback and Context](../options/callback-and-context.md){.internal-link target=_blank}. +You might have read it in [CLI Option Callback and Context](../options/callback-and-context.md). The same way, in commands or in the main `Typer` callback you can access the context by declaring a function parameter of type `typer.Context`. @@ -12,22 +14,20 @@ For example, let's say that you want to execute some logic in a `Typer` callback You can get the name of the subcommand from the context: -```Python hl_lines="17 21" -{!../docs_src/commands/context/tutorial001.py!} -``` +{* docs_src/commands/context/tutorial001.py hl[17,21] *} Check it:
```console -$ python main.py create Camila +$ uv run python main.py create Camila // We get the message from the callback About to execute command: create Creating user: Camila -$ python main.py delete Camila +$ uv run python main.py delete Camila // We get the message from the callback, this time with delete About to execute command: delete @@ -44,22 +44,20 @@ And if no command is provided, the help message is shown. But we could make it run even without a subcommand with `invoke_without_command=True`: -```Python hl_lines="16" -{!../docs_src/commands/context/tutorial002.py!} -``` +{* docs_src/commands/context/tutorial002.py hl[16] *} Check it:
```console -$ python main.py +$ uv run python main.py // The callback is executed, we don't get the default help message Initializing database // Try with a command -$ python main.py create Camila +$ uv run python main.py create Camila // The callback is still executed Initializing database @@ -76,22 +74,20 @@ For that, we can get the `typer.Context` and check if there's an invoked command If it's `None`, it means that we are not calling a subcommand but the main program (the callback) directly: -```Python hl_lines="17 21" -{!../docs_src/commands/context/tutorial003.py!} -``` +{* docs_src/commands/context/tutorial003.py hl[17,21] *} Check it:
```console -$ python main.py +$ uv run python main.py // The callback is executed Initializing database // Check it with a subcommand -$ python main.py create Camila +$ uv run python main.py create Camila // This time the callback is not executed Creating user: Camila @@ -103,20 +99,16 @@ Creating user: Camila You can pass configurations for the context when creating a command or callback. -To read more about the available configurations check the docs for Click's `Context`. - For example, you could keep additional *CLI parameters* not declared in your CLI program with `ignore_unknown_options` and `allow_extra_args`. Then you can access those extra raw *CLI parameters* as a `list` of `str` in `ctx.args`: -```Python hl_lines="7 9 10" -{!../docs_src/commands/context/tutorial004.py!} -``` +{* docs_src/commands/context/tutorial004.py hl[7,9,10] *}
```console -$ python main.py --name Camila --city Berlin +$ uv run python main.py --name Camila --city Berlin Got extra arg: --name Got extra arg: Camila @@ -126,5 +118,8 @@ Got extra arg: Berlin
-!!! tip - Notice that it saves all the extra *CLI parameters* as a raw `list` of `str`, including the *CLI option* names and values, everything together. +/// tip + +Notice that it saves all the extra *CLI parameters* as a raw `list` of `str`, including the *CLI option* names and values, everything together. + +/// diff --git a/docs/tutorial/commands/help.md b/docs/tutorial/commands/help.md index 7e08cc29a9..197ea04434 100644 --- a/docs/tutorial/commands/help.md +++ b/docs/tutorial/commands/help.md @@ -1,10 +1,10 @@ +# Command Help + The same as before, you can add help for the commands in the docstrings and the *CLI options*. And the `typer.Typer()` application receives a parameter `help` that you can pass with the main help text for your CLI program: -```Python hl_lines="3 8 9 10 20 23 24 25 26 27 39 42 43 44 45 46 55 56 57" -{!../docs_src/commands/help/tutorial001.py!} -``` +{* docs_src/commands/help/tutorial001.py hl[5,10:12,23,27:31,44,48:52,61:63] *} Check it: @@ -12,7 +12,7 @@ Check it: ```console // Check the new help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -24,38 +24,44 @@ Options: --help Show this message and exit. Commands: - create Create a new user with USERNAME. - delete Delete a user with USERNAME. + create Create a new user with username. + delete Delete a user with username. delete-all Delete ALL users in the database. init Initialize the users database. // Now the commands have inline help 🎉 // Check the help for create -$ python main.py create --help +$ uv run python main.py create --help + +Usage: main.py create [OPTIONS] {username} -Usage: main.py create [OPTIONS] USERNAME + Create a new user with username. - Create a new user with USERNAME. +Arguments: + username [required] Options: --help Show this message and exit. // Check the help for delete -$ python main.py delete --help +$ uv run python main.py delete --help -Usage: main.py delete [OPTIONS] USERNAME +Usage: main.py delete [OPTIONS] {username} - Delete a user with USERNAME. + Delete a user with username. If --force is not used, will ask for confirmation. +Arguments: + username [required] + Options: --force / --no-force Force deletion without confirmation. [required] --help Show this message and exit. // Check the help for delete-all -$ python main.py delete-all --help +$ uv run python main.py delete-all --help Usage: main.py delete-all [OPTIONS] @@ -68,7 +74,7 @@ Options: --help Show this message and exit. // Check the help for init -$ python main.py init --help +$ uv run python main.py init --help Usage: main.py init [OPTIONS] @@ -80,18 +86,19 @@ Options:
-!!! tip - `typer.Typer()` receives several other parameters for other things, we'll see that later. +/// tip + +`typer.Typer()` receives several other parameters for other things, we'll see that later. - You will also see how to use "Callbacks" later, and those include a way to add this same help message in a function docstring. +You will also see how to use "Callbacks" later, and those include a way to add this same help message in a function docstring. + +/// ## Overwrite command help You will probably be better adding the help text as a docstring to your functions, but if for some reason you wanted to overwrite it, you can use the `help` function argument passed to `@app.command()`: -```Python hl_lines="6 14" -{!../docs_src/commands/help/tutorial002.py!} -``` +{* docs_src/commands/help/tutorial002.py hl[6,14] *} Check it: @@ -99,7 +106,7 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice it uses the help passed to @app.command() Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -111,10 +118,387 @@ Options: --help Show this message and exit. Commands: - create Create a new user with USERNAME. - delete Delete a user with USERNAME. + create Create a new user with username. + delete Delete a user with username. + +// It uses "Create a new user with username." instead of "Some internal utility function to create." +``` + +
+ +## Deprecate a Command + +There could be cases where you have a command in your app that you need to deprecate, so that your users stop using it, even while it's still supported for a while. + +You can mark it with the parameter `deprecated=True`: + +{* docs_src/commands/help/tutorial003.py hl[14] *} + +And when you show the `--help` option you will see it's marked as "`deprecated`": + +
+ +```console +$ uv run python main.py --help + + Usage: main.py [OPTIONS] COMMAND [ARGS]... + +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --install-completion Install completion for the current │ +│ shell. │ +│ --show-completion Show completion for the current │ +│ shell, to copy it or customize the │ +│ installation. │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Commands ────────────────────────────────────────────────────────╮ +│ create Create a user. │ +│ delete Delete a user. (deprecated) │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +And if you check the `--help` for the deprecated command (in this example, the command `delete`), it also shows it as deprecated: + +
+ +```console +$ uv run python main.py delete --help + + Usage: main.py delete [OPTIONS] {username} + + (deprecated) + Delete a user. + This is deprecated and will stop being supported soon. + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * username <str> [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +## Suggest Commands + +As of version 0.20.0, Typer added support for suggesting mistyped command names. This feature is **enabled by default**, but you can disable it with the parameter `suggest_commands=False`: + +{* docs_src/commands/index/tutorial005.py hl[3] *} + +If a user mistypes a command, they'll see a helpful suggestion: + +
+ +```console +$ uv run python main.py crate + +Usage: main.py [OPTIONS] COMMAND [ARGS]... +Try 'main.py --help' for help. +╭─ Error ───────────────────────────────────────────────────────────╮ +│ No such command 'crate'. Did you mean 'create'? │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +If there are multiple close matches, Typer will suggest them all. This feature uses Python's built-in `difflib.get_close_matches()` to find similar command names, making your CLI more user-friendly by helping users recover from typos. + +## Rich Markdown and Markup + +Typer installs **Rich** to allow for more formatting in the docstrings and the `help` parameter for *CLI arguments* and *CLI options*. You will see more about it below. 👇 + +/// note + +You can disable rich text formatting by setting `rich_markup_mode` to `None` for your specific app. +Alternatively, you can disable it globally using an environmental variable `TYPER_USE_RICH` set to `False` or `0`. + +/// + +### Rich Markup + +If you set `rich_markup_mode="rich"` when creating the `typer.Typer()` app (which is the default), you will be able to use [Rich Console Markup](https://rich.readthedocs.io/en/stable/markup.html) in the docstring, and even in the help for the *CLI arguments* and options: + +{* docs_src/commands/help/tutorial004.py hl[5,11,15:17,22,25,28] *} + +With that, you can use [Rich Console Markup](https://rich.readthedocs.io/en/stable/markup.html) to format the text in the docstring for the command `create`, make the word "`create`" bold and green, and even use an [emoji](https://rich.readthedocs.io/en/stable/markup.html#emoji). + +You can also use markup in the help for the `username` CLI Argument. + +And the same as before, the help text overwritten for the command `delete` can also use Rich Markup, the same in the CLI Argument and CLI Option. + +If you run the program and check the help, you will see that **Typer** uses **Rich** internally to format the help. + +Check the help for the `create` command: + +
+ +```console +$ uv run python main.py create --help + + Usage: main.py create [OPTIONS] {username} + + Create a new shiny user. ✨ + This requires a username. + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * username <str> The username to be created │ +│ [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +And check the help for the `delete` command: + +
+ +```console +$ uv run python main.py delete --help + + Usage: main.py delete [OPTIONS] {username} + + Delete a user with username. + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * username <str> The username to be deleted │ +│ [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --force --no-force Force the deletion 💥 │ +│ [default: no-force] │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +### Rich Markdown -// It uses "Create a new user with USERNAME." instead of "Some internal utility function to create." +If you set `rich_markup_mode="markdown"` when creating the `typer.Typer()` app, you will be able to use Markdown in the docstring: + +{* docs_src/commands/help/tutorial005.py hl[5,10,13:21,26,28:29] *} + +With that, you can use Markdown to format the text in the docstring for the command `create`, make the word "`create`" bold, show a list of items, and even use an [emoji](https://rich.readthedocs.io/en/stable/markup.html#emoji). + +And the same as before, the help text overwritten for the command `delete` can also use Markdown. + +Check the help for the `create` command: + +
+ +```console +$ uv run python main.py create --help + + Usage: main.py create [OPTIONS] {username} + + Create a new shiny user. ✨ + + • Create a username + • Show that the username is created + + ─────────────────────────────────────────────────────────────────── + Learn more at the Typer docs website + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * username <str> The username to be created │ +│ [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +And the same for the `delete` command: + +
+ +```console +$ uv run python main.py delete --help + + Usage: main.py delete [OPTIONS] {username} + + Delete a user with username. + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * username <str> The username to be deleted │ +│ [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --force --no-force Force the deletion 💥 │ +│ [default: no-force] │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +/// note + +Notice that in Markdown you cannot define colors. For colors you might prefer to use Rich markup. + +/// + +## Help Panels + +If you have many commands or CLI parameters, you might want to show their documentation in different panels when using the `--help` option. + +If you installed [Rich](https://rich.readthedocs.io/) as described in [Printing and Colors](../printing.md), you can configure the panel to use for each command or CLI parameter. + +### Help Panels for Commands + +To set the panel for a command you can pass the argument `rich_help_panel` with the name of the panel you want to use: + +{* docs_src/commands/help/tutorial006.py hl[22,30,38,46] *} + +Commands without a panel will be shown in the default panel `Commands`, and the rest will be shown in the next panels: + +
+ +```console +$ uv run python main.py --help + + Usage: main.py [OPTIONS] COMMAND [ARGS]... + +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --install-completion Install completion for the current │ +│ shell. │ +│ --show-completion Show completion for the current │ +│ shell, to copy it or customize the │ +│ installation. │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Commands ────────────────────────────────────────────────────────╮ +│ create Create a new user. ✨ │ +│ delete Delete a user. ❌ │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Utils and Configs ───────────────────────────────────────────────╮ +│ config Configure the system. ⚙ │ +│ sync Synchronize the system or something fancy like that. ♻ │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Help and Others ─────────────────────────────────────────────────╮ +│ help Get help with the system. ❓ │ +│ report Report an issue. ❗ │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +### Help Panels for CLI Parameters + +The same way, you can configure the panels for *CLI arguments* and *CLI options* with `rich_help_panel`. + +And of course, in the same application you can also set the `rich_help_panel` for commands. + +{* docs_src/commands/help/tutorial007.py hl[14,20,26,36] *} + +Then if you run the application you will see all the *CLI parameters* in their respective panels. + +* First the ***CLI arguments*** that don't have a panel name set in a **default** one named "`Arguments`". +* Next the ***CLI arguments*** with a **custom panel**. In this example named "`Secondary Arguments`". +* After that, the ***CLI options*** that don't have a panel in a **default** one named "`Options`". +* And finally, the ***CLI options*** with a **custom panel** set. In this example named "`Additional Data`". + +You can check the `--help` option for the command `create`: + +
+ +```console +$ uv run python main.py create --help + + Usage: main.py create [OPTIONS] {username} [lastname] + + Create a new user. ✨ + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * username <str> The username to create │ +│ [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Secondary Arguments ─────────────────────────────────────────────╮ +│ lastname lastname The last name of the new user │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --force --no-force Force the creation of the user │ +│ [default: no-force] │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Additional Data ─────────────────────────────────────────────────╮ +│ --age <int> The age of the new user │ +│ --favorite-color <str> The favorite color of the new │ +│ user │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +And of course, the `rich_help_panel` can be used in the same way for commands in the same application. + +And those panels will be shown when you use the main `--help` option. + +
+ +```console +$ uv run python main.py --help + + Usage: main.py [OPTIONS] COMMAND [ARGS]... + +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --install-completion Install completion for the current │ +│ shell. │ +│ --show-completion Show completion for the current │ +│ shell, to copy it or customize the │ +│ installation. │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Commands ────────────────────────────────────────────────────────╮ +│ create Create a new user. ✨ │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Utils and Configs ───────────────────────────────────────────────╮ +│ config Configure the system. ⚙ │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +You can see the custom panel for the commands for "`Utils and Configs`". + +## Epilog + +If you need, you can also add an epilog section to the help of your commands: + +{* docs_src/commands/help/tutorial008.py hl[6] *} + +And when you check the `--help` option it will look like: + +
+ +```console +$ uv run python main.py --help + + Usage: main.py [OPTIONS] {username} + + Create a new user. ✨ + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * username <str> [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --install-completion Install completion for the current │ +│ shell. │ +│ --show-completion Show completion for the current │ +│ shell, to copy it or customize the │ +│ installation. │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ + + Made with ❤ in Venus ```
diff --git a/docs/tutorial/commands/index.md b/docs/tutorial/commands/index.md index 0aaf8cd492..503d8ca8bb 100644 --- a/docs/tutorial/commands/index.md +++ b/docs/tutorial/commands/index.md @@ -1,3 +1,5 @@ +# Commands + We have seen how to create a CLI program with possibly several *CLI options* and *CLI arguments*. But **Typer** allows you to create CLI programs with several commands (also known as subcommands). @@ -28,8 +30,11 @@ Another command of `git` is `git pull`, it also has some *CLI parameters*. It's like if the same big program `git` had several small programs inside. -!!! tip - A command looks the same as a *CLI argument*, it's just some name without a preceding `--`. But commands have a predefined name, and are used to group different sets of functionalities into the same CLI application. +/// tip + +A command looks the same as a *CLI argument*, it's just some name without a preceding `--`. But commands have a predefined name, and are used to group different sets of functionalities into the same CLI application. + +/// ## Command or subcommand @@ -41,102 +46,107 @@ Have that in mind so you don't get confused. Here I'll use **CLI application** or **program** to refer to the program you are building in Python with Typer, and **command** to refer to one of these "subcommands" of your program. -## Explicit application +## A CLI application with multiple commands -Before creating CLI applications with multiple commands/subcommands we need to understand how to create an explicit `typer.Typer()` application. +**Typer** allows creating CLI applications with multiple commands/subcommands. -In the *CLI options* and *CLI argument* tutorials you have seen how to create a single function and then pass that function to `typer.run()`. +Now that you know how to create an explicit `typer.Typer()` application and add one command, let's see how to add multiple commands. -For example: +Let's say that we have a CLI application to manage users. -```Python hl_lines="9" -{!../docs_src/first_steps/tutorial002.py!} -``` +We'll have a command to `create` users and another command to `delete` them. -But that is actually a shortcut. Under the hood, **Typer** converts that to a CLI application with `typer.Typer()` and executes it. All that inside of `typer.run()`. +To begin, let's say it can only create and delete one single predefined user: -There's also a more explicit way to achieve the same: +{* docs_src/commands/index/tutorial002.py hl[6,11] *} -```Python hl_lines="3 6 12" -{!../docs_src/commands/index/tutorial001.py!} -``` +Now we have a CLI application with 2 commands, `create` and `delete`: -When you use `typer.run()`, **Typer** is doing more or less the same as above, it will: +
-* Create a new `typer.Typer()` "application". -* Create a new "`command`" with your function. -* Call the same "application" as if it was a function with "`app()`". +```console +// Check the help +$ uv run python main.py --help -!!! info "`@decorator` Info" - That `@something` syntax in Python is called a "decorator". +Usage: main.py [OPTIONS] COMMAND [ARGS]... - You put it on top of a function. Like a pretty decorative hat (I guess that's where the term came from). +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. + +Commands: + create + delete + +// Test them +$ uv run python main.py create + +Creating user: Hiro Hamada - A "decorator" takes the function below and does something with it. +$ uv run python main.py delete - In our case, this decorator tells **Typer** that the function below is a "`command`". +Deleting user: Hiro Hamada -Both ways, with `typer.run()` and creating the explicit application, achieve the same. +// Now we have 2 commands! 🎉 +``` -!!! tip - If your use case is solved with just `typer.run()`, that's fine, you don't have to create the explicit `app` and use `@app.command()`, etc. +
- You might want to do that later when your app needs the extra features, but if it doesn't need them yet, that's fine. +Notice that the help text now shows the 2 commands: `create` and `delete`. -If you run the second example, with the explicit `app`, it works exactly the same: +/// tip -
+By default, the names of the commands are generated from the function name. -```console -// Without a CLI argument -$ python main.py +/// -Usage: main.py [OPTIONS] NAME -Try "main.py --help" for help. +## Show the help message if no command is given -Error: Missing argument 'NAME'. +By default, we need to specify `--help` to get the command's help page. -// With the NAME CLI argument -$ python main.py Camila +However, by setting `no_args_is_help=True` when defining the `typer.Typer()` application, the help function will be shown whenever no argument is given: -Hello Camila +{* docs_src/commands/index/tutorial003.py hl[3] *} -// Asking for help -$ python main.py --help +Now we can run this: -Usage: main.py [OPTIONS] NAME +
+ +```console +// Check the help without having to type --help +$ uv run python main.py + +Usage: main.py [OPTIONS] COMMAND [ARGS]... Options: --install-completion Install completion for the current shell. --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. + +Commands: + create + delete ```
-## A CLI application with multiple commands - -Coming back to the CLI applications with multiple commands/subcommands, **Typer** allows creating CLI applications with multiple of them. -Now that you know how to create an explicit `typer.Typer()` application and add one command, let's see how to add multiple commands. +## Sorting of the commands -Let's say that we have a CLI application to manage users. +Note that by design, **Typer** shows the commands in the order they've been declared. -We'll have a command to `create` users and another command to `delete` them. +So, if we take our original example, with `create` and `delete` commands, and reverse the order in the Python file: -To begin, let's say it can only create and delete one single predefined user: +{* docs_src/commands/index/tutorial004.py hl[7,12] *} -```Python hl_lines="6 11" -{!../docs_src/commands/index/tutorial002.py!} -``` - -Now we have a CLI application with 2 commands, `create` and `delete`: +Then we will see the `delete` command first in the help output:
```console // Check the help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -146,37 +156,12 @@ Options: --help Show this message and exit. Commands: - create delete - -// Test them -$ python main.py create - -Creating user: Hiro Hamada - -$ python main.py delete - -Deleting user: Hiro Hamada - -// Now we have 2 commands! 🎉 + create ```
-Notice that the help text now shows the 2 commands: `create` and `delete`. - -!!! tip - By default, the names of the commands are generated from the function name. - -## Click Group - -If you come from Click, a `typer.Typer` app with subcommands is more or less the equivalent of a Click Group. - -!!! note "Technical Details" - A `typer.Typer` app is *not* a Click Group, but it provides the equivalent functionality. And it creates a Click Group when calling it. - - It is not directly a Group because **Typer** doesn't modify the functions in your code to convert them to another type of object, it only registers them. - ## Decorator Technical Details When you use `@app.command()` the function under the decorator is registered in the **Typer** application and is then used later by the application. @@ -184,8 +169,3 @@ When you use `@app.command()` the function under the decorator is registered in But Typer doesn't modify that function itself, the function is left as is. That means that if your function is simple enough that you could create it without using `typer.Option()` or `typer.Argument()`, you could use the same function for a **Typer** application and a **FastAPI** application putting both decorators on top, or similar tricks. - -!!! note "Click Technical Details" - This behavior is a design difference with Click. - - In Click, when you add a `@click.command()` decorator it actually modifies the function underneath and replaces it with an object. diff --git a/docs/tutorial/commands/name.md b/docs/tutorial/commands/name.md index 458f54101c..5ff29b43ea 100644 --- a/docs/tutorial/commands/name.md +++ b/docs/tutorial/commands/name.md @@ -1,10 +1,11 @@ +# Custom Command Name + By default, the command names are generated from the function name. So, if your function is something like: ```Python -def create(username: str): - ... +def create(username: str): ... ``` Then the command name will be `create`. @@ -15,16 +16,14 @@ And what if you wanted the command to still be named `create`? For this, you can set the name of the command in the first parameter for the `@app.command()` decorator: -```Python hl_lines="6 11" -{!../docs_src/commands/name/tutorial001.py!} -``` +{* docs_src/commands/name/tutorial001.py hl[6,11] *} Now, even though the functions are named `cli_create_user()` and `cli_delete_user()`, the commands will still be named `create` and `delete`:
```console -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -38,9 +37,18 @@ Commands: delete // Test it -$ python main.py create Camila +$ uv run python main.py create Camila Creating user: Camila ```
+ +Note that any underscores in the function name will be replaced with dashes. + +So if your function is something like: + +```Python +def create_user(username: str): ... +``` +Then the command name will be `create-user`. diff --git a/docs/tutorial/commands/one-or-multiple.md b/docs/tutorial/commands/one-or-multiple.md index cc72a6ba45..8985967cae 100644 --- a/docs/tutorial/commands/one-or-multiple.md +++ b/docs/tutorial/commands/one-or-multiple.md @@ -1,8 +1,8 @@ -You might have noticed that if you create a single command, as in the first example: +# One or Multiple Commands -```Python hl_lines="3 6 12" -{!../docs_src/commands/index/tutorial001.py!} -``` +You might have noticed that if you create a single command, as in the following example: + +{* docs_src/typer_app/tutorial001.py hl[3,6,12] *} **Typer** is smart enough to create a CLI application with that single function as the main CLI application, not as a command/subcommand: @@ -10,22 +10,22 @@ You might have noticed that if you create a single command, as in the first exam ```console // Without a CLI argument -$ python main.py +$ uv run python main.py -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} Try "main.py --help" for help. -Error: Missing argument 'NAME'. +Error: Missing argument 'name'. -// With the NAME CLI argument -$ python main.py Camila +// With the 'name' CLI argument +$ uv run python main.py Camila Hello Camila // Asking for help -$ python main.py +$ uv run python main.py --help -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} Options: --install-completion Install completion for the current shell. @@ -35,14 +35,15 @@ Options:
-!!! tip - Notice that it doesn't show a command `main`, even though the function name is `main`. +/// tip + +Notice that it doesn't show a command `main`, even though the function name is `main`. + +/// But if you add multiple commands, **Typer** will create one *CLI command* for each one of them: -```Python hl_lines="6 11" -{!../docs_src/commands/index/tutorial002.py!} -``` +{* docs_src/commands/index/tutorial002.py hl[6,11] *} Here we have 2 commands `create` and `delete`: @@ -50,7 +51,7 @@ Here we have 2 commands `create` and `delete`: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -64,11 +65,11 @@ Commands: delete // Test the commands -$ python main.py create +$ uv run python main.py create Creating user: Hiro Hamada -$ python main.py delete +$ uv run python main.py delete Deleting user: Hiro Hamada ``` @@ -79,9 +80,7 @@ Deleting user: Hiro Hamada If you want to create a CLI app with one single command but you still want it to be a command/subcommand you can just add a callback: -```Python hl_lines="11 12 13" -{!../docs_src/commands/one_or_multiple/tutorial001.py!} -``` +{* docs_src/commands/one_or_multiple/tutorial001.py hl[11,12,13] *} And now your CLI program will have a single command. @@ -91,7 +90,7 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the single command create Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -105,7 +104,7 @@ Commands: create // Try it -$ python main.py create +$ uv run python main.py create Creating user: Hiro Hamada ``` @@ -116,23 +115,21 @@ Creating user: Hiro Hamada Now that you are using a callback just to have a single command, you might as well use it to add documentation for your app: -```Python hl_lines="11 12 13 14 15 16 17" -{!../docs_src/commands/one_or_multiple/tutorial002.py!} -``` +{* docs_src/commands/one_or_multiple/tutorial002.py hl[11,12,13,14,15,16,17] *} And now the docstring from the callback will be used as the help text:
```console -$ python main.py --help +$ uv run python main.py --help // Notice the help text from the docstring Usage: main.py [OPTIONS] COMMAND [ARGS]... Creates a single user Hiro Hamada. - In the next version it will create 5 users more. + In the next version it will create 5 more users. Options: --install-completion Install completion for the current shell. @@ -143,7 +140,7 @@ Commands: create // And it still works the same, the callback does nothing -$ python main.py create +$ uv run python main.py create Creating user: Hiro Hamada ``` diff --git a/docs/tutorial/commands/options.md b/docs/tutorial/commands/options.md index 8e14386173..c8abf32ab0 100644 --- a/docs/tutorial/commands/options.md +++ b/docs/tutorial/commands/options.md @@ -1,10 +1,10 @@ +# Command CLI Options + Commands can also have their own *CLI options*. In fact, each command can have different *CLI arguments* and *CLI options*: -```Python hl_lines="7 13 14 24 33" -{!../docs_src/commands/options/tutorial001.py!} -``` +{* docs_src/commands/options/tutorial001.py hl[9,15:18,28:30,39] *} Here we have multiple commands, with different *CLI parameters*: @@ -35,13 +35,16 @@ Commands: create delete delete-all - info + init ```
-!!! tip - Check the command `delete-all`, by default command names are generated from the function name, replacing `_` with `-`. +/// tip + +Check the command `delete-all`, by default command names are generated from the function name, replacing `_` with `-`. + +/// Test it: @@ -49,46 +52,46 @@ Test it: ```console // Check the command create -$ python main.py create Camila +$ uv run python main.py create Camila Creating user: Camila // Now test the command delete -$ python main.py delete Camila +$ uv run python main.py delete Camila -# Are you sure you want to delete the user? [y/N]: $ y +# Are you sure you want to delete the user? [y/n]: $ y Deleting user: Camila -$ python main.py delete Wade +$ uv run python main.py delete Wade -# Are you sure you want to delete the user? [y/N]: $ n +# Are you sure you want to delete the user? [y/n]: $ n Operation cancelled // And finally, the command delete-all // Notice it doesn't have CLI arguments, only a CLI option -$ python main.py delete-all +$ uv run python main.py delete-all -# Are you sure you want to delete ALL users? [y/N]: $ y +# Are you sure you want to delete ALL users? [y/n]: $ y Deleting all users -$ python main.py delete-all +$ uv run python main.py delete-all -# Are you sure you want to delete ALL users? [y/N]: $ n +# Are you sure you want to delete ALL users? [y/n]: $ n Operation cancelled // And if you pass the --force CLI option, it doesn't need to confirm -$ python main.py delete-all --force +$ uv run python main.py delete-all --force Deleting all users // And init that doesn't take any CLI parameter -$ python main.py init +$ uv run python main.py init Initializing user database ``` diff --git a/docs/tutorial/exceptions.md b/docs/tutorial/exceptions.md new file mode 100644 index 0000000000..27b66944cd --- /dev/null +++ b/docs/tutorial/exceptions.md @@ -0,0 +1,269 @@ +# Exceptions and Errors + +When your code has errors and you run it, it will show the error and an exception. + +Typer does some tricks to help you detect those errors quickly. + +## Example Broken App + +Let's take this example broken app: + +{* docs_src/exceptions/tutorial001.py hl[8] *} + +This code is broken because you can't sum a string and a number (`name + 3`). + +## Exceptions with Rich + +**Typer** will automatically use Rich to automatically show you nicely printed errors. + +It will **omit** all the parts of the traceback (the chain of things that called your function) that come from the internal parts in Typer. + +So, the error you see will be **much clearer** and simpler, to help you detect the problem in your code quickly: + +
+ +```console +$ uv run python main.py + +╭──────────────── Traceback (most recent call last) ────────────────╮ +│ /home/user/code/superapp/main.py:8 in main │ +│ │ +│ 5 │ +│ 6 @app.command() │ +│ 7 def main(name: str = "morty"): │ +│ ❱ 8 │ print(name + 3) │ +│ 9 │ +│ 10 │ +│ 11 if __name__ == "__main__": │ +╰───────────────────────────────────────────────────────────────────╯ +TypeError: can only concatenate str (not "int") to str +``` + +
+ +## Exceptions without Rich + +You can disable Rich globally using the environmental variable `TYPER_USE_RICH`. + +In this case, Typer will still do some tricks to show you the information **as clearly as possible**: + +
+ +```console +$ uv run python main.py + +Traceback (most recent call last): + + File "main.py", line 12, in + app() + + File "main.py", line 8, in main + print(name + 3) + +TypeError: can only concatenate str (not "int") to str +``` + +
+ +## Show Local Variables for Detailed Debugging + +When using Rich, you can get more verbose output by printing the values of the local variables as part of the error message. + +By default, this setting is disabled (since Typer 0.23.0) to avoid showing **delicate information**, for example a **password**, a **key** or a **token**. + +In these cases, it could be problematic if the automatic errors show the value in those local variables. + +This would be relevant in particular if your CLI application is being run on some CI (continuous integration) system that is recording the logs. + +However, if you do want to enable the setting, you can set the parameter `pretty_exceptions_show_locals=True` when creating the `typer.Typer()` application: + +{* docs_src/exceptions/tutorial002.py hl[3] *} + +Now, when using Rich, you will see the error with the local variables: + +
+ +```console +$ uv run python main.py + +╭──────────────── Traceback (most recent call last) ────────────────╮ +│ /home/user/code/superapp/main.py:5 in main │ +│ │ +│ 2 │ +│ 3 │ +│ 4 def main(name: str = "morty"): │ +│ ❱ 5 │ print(name + 3) │ +│ 6 │ +│ 7 │ +│ 8 if __name__ == "__main__": │ +│ │ +│ ╭──── locals ────╮ │ +│ │ name = 'morty' │ │ +│ ╰────────────────╯ │ +╰───────────────────────────────────────────────────────────────────╯ +TypeError: can only concatenate str (not "int") to str +``` + +
+ +Being able to see the values of local variables is very **helpful** to diagnose, **debug**, and fix problems. + +But you should only enable it if you're not dealing with delicate information. + +## Disable Short Output + +If you want to show the full exception, including the internal parts in Typer, you can use the parameter `pretty_exceptions_short=False`: + +{* docs_src/exceptions/tutorial003.py hl[3] *} + +Now when you run it, you will see the whole output: + +
+ +```console +$ uv run python main.py + +╭──────────────── Traceback (most recent call last) ────────────────╮ +│ /home/user/code/superapp/main.py:12 in <module> │ +│ │ +│ 9 │ +│ 10 │ +│ 11 if __name__ == "__main__": │ +│ ❱ 12 │ app() │ +│ 13 │ +│ │ +│ ╭─────────────────────────── locals ────────────────────────────╮ │ +│ │ __annotations__ = {} │ │ +│ │ __builtins__ = <module 'builtins' (built-in)> │ │ +│ │ __cached__ = None │ │ +│ │ __doc__ = None │ │ +│ │ __file__ = 'main.py' │ │ +│ │ __loader__ = <_frozen_importlib_external.SourceFileLoad… │ │ +│ │ object at 0x7f047db1c050> │ │ +│ │ __name__ = '__main__' │ │ +│ │ __package__ = None │ │ +│ │ __spec__ = None │ │ +│ │ app = <typer.main.Typer object at 0x7f047db51d90> │ │ +│ │ main = <function main at 0x7f047db56830> │ │ +│ │ typer = <module 'typer' from │ │ +│ │ '/home/user/code/superapp/env/lib/python3.… │ │ +│ ╰───────────────────────────────────────────────────────────────╯ │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/ma │ +│ in.py:328 in __call__ │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/ma │ +│ in.py:311 in __call__ │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/click/co │ +│ re.py:1130 in __call__ │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/co │ +│ re.py:723 in main │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/co │ +│ re.py:216 in _main │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/click/co │ +│ re.py:1404 in invoke │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/click/co │ +│ re.py:760 in invoke │ +│ │ +│ /home/user/code/superapp/env/lib/python3.7/site-packages/typer/ma │ +│ in.py:683 in wrapper │ +│ │ +│ /home/user/code/superapp/main.py:8 in main │ +│ │ +│ 5 │ +│ 6 @app.command() │ +│ 7 def main(name: str = "morty"): │ +│ ❱ 8 │ print(name + 3) │ +│ 9 │ +│ 10 │ +│ 11 if __name__ == "__main__": │ +│ │ +│ ╭──── locals ────╮ │ +│ │ name = 'morty' │ │ +│ ╰────────────────╯ │ +╰───────────────────────────────────────────────────────────────────╯ +TypeError: can only concatenate str (not "int") to str +``` + +
+ +## Disable Pretty Exceptions + +You can also entirely disable pretty exceptions with the parameter `pretty_exceptions_enable=False`: + +{* docs_src/exceptions/tutorial004.py hl[3] *} + +And now you will see the full standard exception as with any other Python program: + +
+ +```console +$ uv run python main.py + +Traceback (most recent call last): + File "main.py", line 12, in + app() + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 328, in __call__ + raise e + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 311, in __call__ + return get_command(self)(*args, **kwargs) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1130, in __call__ + return self.main(*args, **kwargs) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 723, in main + **extra, + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 216, in _main + rv = self.invoke(ctx) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1404, in invoke + return ctx.invoke(self.callback, **ctx.params) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 760, in invoke + return __callback(*args, **kwargs) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 683, in wrapper + return callback(**use_params) # type: ignore + File "main.py", line 8, in main + print(name + 3) +TypeError: can only concatenate str (not "int") to str +``` + +
+ +You could also achieve the same with the environment variable `TYPER_STANDARD_TRACEBACK=1` (or by setting the deprecated variable `_TYPER_STANDARD_TRACEBACK=1`). + +This will work for any other Typer program too, in case you need to debug a problem in a Typer program made by someone else: + +
+ +```console +export TYPER_STANDARD_TRACEBACK=1 +$ uv run python main.py + + +Traceback (most recent call last): + File "main.py", line 12, in + app() + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 328, in __call__ + raise e + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 311, in __call__ + return get_command(self)(*args, **kwargs) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1130, in __call__ + return self.main(*args, **kwargs) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 723, in main + **extra, + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/core.py", line 216, in _main + rv = self.invoke(ctx) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 1404, in invoke + return ctx.invoke(self.callback, **ctx.params) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/click/core.py", line 760, in invoke + return __callback(*args, **kwargs) + File "/home/user/code/superapp/env/lib/python3.7/site-packages/typer/main.py", line 683, in wrapper + return callback(**use_params) # type: ignore + File "main.py", line 8, in main + print(name + 3) +TypeError: can only concatenate str (not "int") to str +``` + +
diff --git a/docs/tutorial/first-steps.md b/docs/tutorial/first-steps.md index b431715ae8..a26a1417e4 100644 --- a/docs/tutorial/first-steps.md +++ b/docs/tutorial/first-steps.md @@ -1,13 +1,10 @@ +# First Steps + ## The simplest example The simplest **Typer** file could look like this: -```Python -{!../docs_src/first_steps/tutorial001.py!} -``` - -!!! tip - You will learn more about `typer.echo()` later in the docs. +{* docs_src/first_steps/tutorial001.py *} Copy that to a file `main.py`. @@ -16,21 +13,21 @@ Test it:
```console -$ python main.py +$ uv run python main.py Hello World // It just prints "Hello World". // Now check the --help -$ python main.py --help - -Usage: main.py [OPTIONS] - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +$ uv run python main.py --help + + Usage: main.py [OPTIONS] + +╭─ Options ─────────────────────────────────────────╮ +│ --help Show this message │ +│ and exit. │ +╰───────────────────────────────────────────────────╯ ```
@@ -64,39 +61,41 @@ They are a bit different from **CLI options** that you will see later below. Update the previous example with an argument `name`: -```Python hl_lines="4 5" -{!../docs_src/first_steps/tutorial002.py!} -``` +{* docs_src/first_steps/tutorial002.py hl[4,5] *}
```console -$ python main.py +$ uv run python main.py // If you run it without the argument, it shows a nice error -Usage: main.py [OPTIONS] NAME -Try "main.py --help" for help. +Usage: main.py [OPTIONS] {name} +Try 'main.py --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'name'. │ +╰───────────────────────────────────────────────────╯ -Error: Missing argument 'NAME'. - -// Now pass that NAME CLI argument -$ python main.py Camila +// Now pass that 'name' CLI argument +$ uv run python main.py Camila Hello Camila // Here "Camila" is the CLI argument // To pass a name with spaces for the same CLI argument, use quotes -$ python main.py "Camila Gutiérrez" +$ uv run python main.py "Camila Gutiérrez" Hello Camila Gutiérrez ```
-!!! tip - If you need to pass a single value that contains spaces to a *CLI argument*, use quotes (`"`) around it. +/// tip + +If you need to pass a single value that contains spaces to a *CLI argument*, use quotes (`"`) around it. + +/// ## Two CLI arguments @@ -104,55 +103,57 @@ Now let's say we want to have the name and last name separated. So, extend that to have 2 arguments, `name` and `lastname`: -```Python hl_lines="4 5" -{!../docs_src/first_steps/tutorial003.py!} -``` +{* docs_src/first_steps/tutorial003.py hl[4,5] *}
```console // Check the main --help -$ python main.py --help - -Usage: main.py [OPTIONS] NAME LASTNAME - -Arguments: - NAME [required] - LASTNAME [required] - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +$ uv run python main.py --help + + Usage: main.py [OPTIONS] {name} {lastname} + +╭─ Arguments ─────────────────────────────────────────────────────╮ +│ * name <str> [required] │ +│ * lastname <str> [required] │ +╰─────────────────────────────────────────────────────────────────╯ +╭─ Options ───────────────────────────────────────────────────────╮ +│ --help Show this message and │ +│ exit. │ +╰─────────────────────────────────────────────────────────────────╯ // There are now 2 CLI arguments, name and lastname // Now pass a single name argument -$ python main.py Camila - -Usage: main.py [OPTIONS] NAME LASTNAME -Try "main.py --help" for help. +$ uv run python main.py Camila -Error: Missing argument 'LASTNAME'. +Usage: main.py [OPTIONS] {name} {lastname} +Try 'main.py --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'lastname'. │ +╰───────────────────────────────────────────────────╯ // These 2 arguments are required, so, pass both: -$ python main.py Camila Gutiérrez +$ uv run python main.py Camila Gutiérrez Hello Camila Gutiérrez ```
-!!! tip - Notice that the order is important. The last name has to go after the first name. +/// tip - If you called it with: +Notice that the order is important. The last name has to go after the first name. - ``` - $ python main.py Gutiérrez Camila - ``` +If you called it with: - your app wouldn't have a way to know which is the `name` and which the `lastname`. It expects the first *CLI argument* to be the `name` and the second *CLI argument* to be the `lastname`. +``` +$ uv run python main.py Gutiérrez Camila +``` + +your app wouldn't have a way to know which is the `name` and which the `lastname`. It expects the first *CLI argument* to be the `name` and the second *CLI argument* to be the `lastname`. + +/// ## What is a **CLI option** @@ -212,18 +213,19 @@ So, the main and **most important** difference is that: * *CLI options* **start with `--`** and don't depend on the order * *CLI arguments* depend on the **sequence order** -!!! tip - In this example above the *CLI option* `--size` is just a "flag" or "switch" that will contain a boolean value, `True` or `False`, depending on if it was added to the command or not. +/// tip + +In this example above the *CLI option* `--size` is just a "flag" or "switch" that will contain a boolean value, `True` or `False`, depending on if it was added to the command or not. - This one doesn't receive any values. But *CLI options* can also receive values like *CLI arguments*. You'll see how later. +This one doesn't receive any values. But *CLI options* can also receive values like *CLI arguments*. You'll see how later. + +/// ## Add one *CLI option* Now add a `--formal` *CLI option*: -```Python hl_lines="4 5" -{!../docs_src/first_steps/tutorial004.py!} -``` +{* docs_src/first_steps/tutorial004.py hl[4,5] *} Here `formal` is a `bool` that is `False` by default. @@ -231,46 +233,49 @@ Here `formal` is a `bool` that is `False` by default. ```console // Get the help -$ python main.py --help - -Usage: main.py [OPTIONS] NAME LASTNAME - -Arguments: - NAME [required] - LASTNAME [required] - -Options: - --formal / --no-formal [default: False] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +$ uv run python main.py --help + + Usage: main.py [OPTIONS] {name} {lastname} + +╭─ Arguments ─────────────────────────────────────────────────────╮ +│ * name <str> [required] │ +│ * lastname <str> [required] │ +╰─────────────────────────────────────────────────────────────────╯ +╭─ Options ───────────────────────────────────────────────────────╮ +│ --formal --no-formal [default: no-formal] │ +│ --help Show this message and │ +│ exit. │ +╰─────────────────────────────────────────────────────────────────╯ ```
-!!! tip - Notice that it automatically creates a `--formal` and a `--no-formal` because it detected that `formal` is a `bool`. +/// tip + +Notice that it automatically creates a `--formal` and a `--no-formal` because it detected that `formal` is a `bool`. + +/// Now call it normally:
```console -$ python main.py Camila Gutiérrez +$ uv run python main.py Camila Gutiérrez Hello Camila Gutiérrez // But if you pass --formal -$ python main.py Camila Gutiérrez --formal +$ uv run python main.py Camila Gutiérrez --formal Good day Ms. Camila Gutiérrez. // And as --formal is a CLI option you can put it anywhere in this command -$ python main.py Camila --formal Gutiérrez +$ uv run python main.py Camila --formal Gutiérrez Good day Ms. Camila Gutiérrez. -$ python main.py --formal Camila Gutiérrez +$ uv run python main.py --formal Camila Gutiérrez Good day Ms. Camila Gutiérrez. ``` @@ -281,62 +286,66 @@ Good day Ms. Camila Gutiérrez. To convert the `lastname` from a *CLI argument* to a *CLI option*, give it a default value of `""`: -```Python hl_lines="4" -{!../docs_src/first_steps/tutorial005.py!} -``` +{* docs_src/first_steps/tutorial005.py hl[4] *} As `lastname` now has a default value of `""` (an empty string) it is no longer required in the function, and **Typer** will now by default make it an optional *CLI option*.
```console -$ python main.py --help - -Usage: main.py [OPTIONS] NAME - -Arguments: - NAME [required] - -Options: - --lastname TEXT [default: ] - --formal / --no-formal [default: False] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +$ uv run python main.py --help + + Usage: main.py [OPTIONS] {name} + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * name <str> [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --lastname <str> │ +│ --formal --no-formal [default: no-formal] │ +│ --help Show this message │ +│ and exit. │ +╰───────────────────────────────────────────────────────────────────╯ ```
-!!! tip - Notice the `--lastname`, and notice that it takes a textual value. +/// tip + +Notice the `--lastname`, and notice that it takes a textual value. + +A *CLI option* with a value like `--lastname` (contrary to a *CLI option* without a value, a `bool` flag, like `--formal` or `--size`) takes as its value whatever is at the *right side* of the *CLI option*. - A *CLI option* with a value like `--lastname` (contrary to a *CLI option* without a value, a `bool` flag, like `--formal` or `--size`) takes as its value whatever is at the *right side* of the *CLI option*. +///
```console // Call it without a --lastname -$ python main.py Camila +$ uv run python main.py Camila Hello Camila // Pass the --lastname -$ python main.py Camila --lastname Gutiérrez +$ uv run python main.py Camila --lastname Gutiérrez Hello Camila Gutiérrez ```
-!!! tip - Notice that "`Gutiérrez`" is at the right side of `--lastname`. A *CLI option* with a value takes as its value whatever is at the *right side*. +/// tip + +Notice that "`Gutiérrez`" is at the right side of `--lastname`. A *CLI option* with a value takes as its value whatever is at the *right side*. + +/// And as `--lastname` is now a *CLI option* that doesn't depend on the order, you can pass it before the name:
```console -$ python main.py --lastname Gutiérrez Camila +$ uv run python main.py --lastname Gutiérrez Camila // and it will still work normally Hello Camila Gutiérrez @@ -348,38 +357,38 @@ Hello Camila Gutiérrez If you add a docstring to your function it will be used in the help text: -```Python hl_lines="5 6 7 8 9" -{!../docs_src/first_steps/tutorial006.py!} -``` +{* docs_src/first_steps/tutorial006.py hl[5,6,7,8,9] *} Now see it with the `--help` option:
```console -$ python main.py --help - -Usage: main.py [OPTIONS] NAME - - Say hi to NAME, optionally with a --lastname. - - If --formal is used, say hi very formally. - -Arguments: - NAME [required] - -Options: - --lastname TEXT [default: ] - --formal / --no-formal [default: False] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. +$ uv run python main.py --help + + Usage: main.py [OPTIONS] {name} + + Say hi to 'name', optionally with a --lastname. + If --formal is used, say hi very formally. + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * name <str> [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --lastname <str> │ +│ --formal --no-formal [default: no-formal] │ +│ --help Show this message │ +│ and exit. │ +╰───────────────────────────────────────────────────────────────────╯ ```
-!!! tip - There is another place to document the specific *CLI options* and *CLI arguments* that will show up next to them in the help text as with `--install-completion` or `--help`, you will learn that later in the tutorial. +/// tip + +There is another place to document the specific *CLI options* and *CLI arguments* that will show up next to them in the help text as with `--install-completion` or `--help`, you will learn that later in the tutorial. + +/// ## Arguments, options, parameters, optional, required @@ -396,25 +405,28 @@ def main(name: str, lastname: str = ""): are called "Python function parameters" or "Python function arguments". -!!! note "Technical Details" - There's actually a very small distinction in Python between "parameter" and "argument". +/// note | Technical Details - It's quite technical... and somewhat pedantic. +There's actually a very small distinction in Python between "parameter" and "argument". - One refers to the variable name in a function *declaration*. Like: +It's quite technical... and somewhat pedantic. - ``` - def bring_person(name: str, lastname: str = ""): - pass - ``` +*Parameter* refers to the variable name in a function *declaration*. Like: - The other refers to the value passed when *calling* a function. Like: +``` +def bring_person(name: str, lastname: str = ""): + pass +``` - ``` - person = bring_person("Camila", lastname="Gutiérrez") - ``` +*Argument* refers to the value passed when *calling* a function. Like: - ...but you will probably see them used interchangeably in most of the places (including here). +``` +person = bring_person("Camila", lastname="Gutiérrez") +``` + +...but you will probably see them used interchangeably in most of the places (including here). + +/// #### Python default values @@ -443,7 +455,7 @@ In reality, the parameters that require an order can be made *optional* too. And ### In **Typer** -To try and make it a bit easier, we'll normally use the words "parameter" or "argument" to refer to Python functions. +To try and make it a bit easier, we'll normally use the words "parameter" or "argument" to refer to "Python functions parameters" or "Python functions arguments". We'll use ***CLI argument*** to refer to those *CLI parameters* that depend on the specific order. That are **required** by default. @@ -451,25 +463,25 @@ And we'll use ***CLI option*** to refer to those *CLI parameters* that depend on We will use ***CLI parameter*** to refer to both, *CLI arguments* and *CLI options*. -## **Typer CLI** +## The `typer` Command -Now that you know the basics of **Typer**, you might want to install and use [Typer CLI](../typer-cli.md){.internal-link target=_blank}. +When you [activate the project's virtual environment](install.md#activate-the-virtual-environment), the `typer` command installed in that project becomes available directly in your shell. -**Typer CLI** is a tool to run your **Typer** scripts giving you ✨ auto completion ✨ in your terminal. +After you install completion for it, this `typer` command allows you to run your scripts with ✨ auto completion ✨ in your terminal. As an alternative to running with Python:
```console -$ python main.py +$ uv run python main.py Hello World ```
-You can run with **Typer CLI**: +You can run with the `typer` command:
@@ -485,7 +497,10 @@ Hello World So you can use it to have auto completion for your own scripts as you continue with the tutorial. -!!! tip - Your CLI application built with **Typer** won't need [Typer CLI](../typer-cli.md){.internal-link target=_blank} to have auto completion once you create a Python package. +/// tip + +Your CLI application built with **Typer** won't need the `typer` command to have auto completion once you create a Python package. + +But for short scripts and for learning, before creating a Python package, it might be useful. - But for short scripts and for learning, before creating a Python package, it might be useful. +/// diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 11fd55943d..6d896d149d 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -1,14 +1,23 @@ -## Python types +# Learn -If you need a refresher about how to use Python type hints, check the first part of FastAPI's Python types intro. +Learn how to use **Typer** in this step-by-step **Tutorial** - **User Guide**. -You can also check the mypy cheat sheet. +It covers everything you need to know from the **simplest scripts** to **complex CLI applications**. + +You could consider this a **book**, a **course**, the **official** and recommended way to learn **Typer**. 😎 + +## Python Types + +If you need a refresher about how to use Python type hints, check the first part of [FastAPI's Python types intro](https://fastapi.tiangolo.com/python-types/). + +You can also check the [mypy cheat sheet](https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html). In short (very short), you can declare a function with parameters like: ```Python from typing import Optional + def type_example(name: str, formal: bool = False, intro: Optional[str] = None): pass ``` @@ -23,17 +32,15 @@ These type hints are what give you autocomplete in your editor and several other **Typer** is based on these type hints. -## Intro +## About this Tutorial This tutorial shows you how to use **Typer** with all its features, step by step. Each section gradually builds on the previous ones, but it's structured to separate topics, so that you can go directly to any specific one to solve your specific CLI needs. -It is also built to work as a future reference. +It is also built to work as a future reference so you can come back and see exactly what you need. -So you can come back and see exactly what you need. - -## Run the code +## Run the Code All the code blocks can be copied and used directly (they are tested Python files). @@ -42,7 +49,7 @@ To run any of the examples, copy the code to a file `main.py`, and run it:
```console -$ python main.py +$ uv run python main.py ✨ The magic happens here ✨ ``` @@ -51,28 +58,8 @@ $ python main.py It is **HIGHLY encouraged** that you write or copy the code, edit it and run it locally. -Using it in your editor is what really shows you the benefits of **Typer**, seeing how little code you have to write, all the type checks, autocompletion, etc. - -And running the examples is what will really help you understand what is going on. - -You can learn a lot more by running some examples and playing around with them than by reading all the docs here. - ---- +Using it in your editor is what really shows you the benefits of **Typer**, seeing how little code you have to write, all the **inline errors**, **autocompletion**, etc. -## Install **Typer** - -The first step is to install **Typer**. - -For the tutorial, you might want to install it with all the optional dependencies and features: - -
- -```console -$ pip install typer[all] ----> 100% -Successfully installed typer click colorama shellingham -``` - -
+And running the examples is what will really help you **understand** what is going on. -...that also includes `colorama` and `shellingham`. +You can learn a lot more by **running some examples** and **playing around** with them than by reading all the docs here. diff --git a/docs/tutorial/install.md b/docs/tutorial/install.md new file mode 100644 index 0000000000..b1ed8e74cd --- /dev/null +++ b/docs/tutorial/install.md @@ -0,0 +1,118 @@ +# Install **Typer** + +The first step is to set up your project and add **Typer**. + +Install [`uv`](https://docs.astral.sh/uv/getting-started/installation/), then create a project and add Typer: + +
+ +```console +$ uv init awesome-project --bare +$ cd awesome-project +$ uv add typer +---> 100% +``` + +
+ +`uv add` creates the project's virtual environment in `.venv`, adds Typer to `pyproject.toml`, and creates `uv.lock` so the same package versions can be installed later. + +/// details | What these commands do + +* `uv init`: create a new Python project. +* `awesome-project`: create the project in a new directory with this name. +* `--bare`: create only the minimal `pyproject.toml` file, without generating a sample `main.py`, `README.md`, or other files. You will create the application files yourself in the next steps of this tutorial. + +Then `cd awesome-project` enters the new project directory before adding Typer. + +`uv` will use a compatible Python version already installed on your system, or download one if needed. + +When you run `uv add`, it selects compatible versions of Typer and all the packages Typer depends on. It records the exact versions in `uv.lock`, making it possible to install the same package versions later on another computer or when distributing the application. + +Creating or updating this file is called [**locking** the project dependencies](https://docs.astral.sh/uv/concepts/projects/sync/). `uv` does this automatically when you add a package. + +/// + +By default, `typer` comes with `rich`, `pydantic` and `shellingham`. + +## Activate the Virtual Environment + +`uv add typer` installs both the Typer library and the `typer` command in the project's `.venv`. + +You can normally run project commands with `uv run`. But to configure and use shell completion for the first-class `typer` command, activate the project's virtual environment so that `typer` is available directly in your shell. + +//// tab | Linux, macOS + +
+ +```console +$ source .venv/bin/activate +``` + +
+ +//// + +//// tab | Windows PowerShell + +
+ +```console +$ .venv\Scripts\Activate.ps1 +``` + +
+ +//// + +//// tab | Windows Bash + +
+ +```console +$ source .venv/Scripts/activate +``` + +
+ +//// + +Enable completion for your current shell with: + +
+ +```console +$ typer --install-completion +``` + +
+ +Restart your terminal for the new completion configuration to take effect. + +Activate the project's virtual environment again in each new terminal session where you want to use the `typer` command directly. + +/// details | Using `pip` instead + +If you prefer to manage a virtual environment and packages manually, create and activate a virtual environment and then install Typer with `pip install typer`. + +Read the [Virtual Environments guide](https://tiangolo.com/guides/virtual-environments/) for the detailed steps. + +/// + +## AI Agent Skills + +Typer includes an official skill for AI coding agents. It is bundled with the package, so its guidance stays aligned with the version of Typer installed in your project and updates when you update Typer. + +After installing Typer in your project, you can install the skill with Library Skills: + +```bash +uvx library-skills +``` + +/// note + +`uvx` is an alias for `uv tool run`. It runs Library Skills in a temporary, isolated environment while Library Skills scans the packages installed in your project. + +/// + +The skill is compatible with Codex, Claude Code, Cursor, GitHub Copilot, Gemini CLI, Pi, OpenCode, and most other coding agents. For Claude Code, select `.claude/skills` when asked where to install the skill. diff --git a/docs/tutorial/launch.md b/docs/tutorial/launch.md index 90b3767b25..46cf8f8d72 100644 --- a/docs/tutorial/launch.md +++ b/docs/tutorial/launch.md @@ -1,17 +1,17 @@ +# Launching Applications + You can launch applications from your CLI program with `typer.launch()`. It will launch the appropriate application depending on the URL or file type you pass it: -```Python hl_lines="6" -{!../docs_src/launch/tutorial001.py!} -``` +{* docs_src/launch/tutorial001.py hl[9] *} Check it:
```console -$ python main.py +$ uv run python main.py Opening Typer docs @@ -24,21 +24,22 @@ Opening Typer docs You can also make the operating system open the file browser indicating where a file is located with `locate=True`: -```Python hl_lines="17" -{!../docs_src/launch/tutorial002.py!} -``` +{* docs_src/launch/tutorial002.py hl[20] *} + +/// tip + +The rest of the code in this example is just making sure the app directory exists and creating the config file. -!!! tip - The rest of the code in this example is just making sure the app directory exists and creating the config file. +But the most important part is the `typer.launch(config_file_str, locate=True)` with the argument `locate=True`. - But the most important part is the `typer.launch(config_file_str, locate=True)` with the argument `locate=True`. +/// Check it:
```console -$ python main.py +$ uv run python main.py Opening config directory diff --git a/docs/tutorial/multiple-values/arguments-with-multiple-values.md b/docs/tutorial/multiple-values/arguments-with-multiple-values.md index 5783c8e83b..20af278684 100644 --- a/docs/tutorial/multiple-values/arguments-with-multiple-values.md +++ b/docs/tutorial/multiple-values/arguments-with-multiple-values.md @@ -1,17 +1,17 @@ +# CLI Arguments with Multiple Values + *CLI arguments* can also receive multiple values. -You can define the type of a *CLI argument* using `typing.List`. +You can define the type of a *CLI argument* using `list`. -```Python hl_lines="7" -{!../docs_src/multiple_values/arguments_with_multiple_values/tutorial001.py!} -``` +{* docs_src/multiple_values/arguments_with_multiple_values/tutorial001.py hl[9] *} And then you can pass it as many *CLI arguments* of that type as you want:
```console -$ python main.py ./index.md ./first-steps.md woohoo! +$ uv run python main.py ./index.md ./first-steps.md woohoo! This file exists: index.md woohoo! @@ -21,19 +21,23 @@ woohoo!
-!!! tip - We also declared a final *CLI argument* `celebration`, and it's correctly used even if we pass an arbitrary number of `files` first. +/// tip + +We also declared a final *CLI argument* `celebration`, and it's correctly used even if we pass an arbitrary number of `files` first. + +/// -!!! info - A `List` can only be used in the last command (if there are subcommands), as this will take anything to the right and assume it's part of the expected *CLI arguments*. +/// note + +A `list` can only be used in the last command (if there are subcommands), as this will take anything to the right and assume it's part of the expected *CLI arguments*. + +/// ## *CLI arguments* with tuples If you want a specific number of values and types, you can use a tuple, and it can even have default values: -```Python hl_lines="7 8" -{!../docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py!} -``` +{* docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py hl[10:12] *} Check it: @@ -41,32 +45,30 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] [NAMES]... +Usage: main.py [OPTIONS] [names]... Arguments: - [NAMES]... Select 3 characters to play with [default: Harry, Hermione, Ron] + names... Select 3 characters to play with [default: Harry, Hermione, Ron] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Use it with its defaults -$ python main.py +$ uv run python main.py Hello Harry Hello Hermione Hello Ron // If you pass an invalid number of arguments you will get an error -$ python main.py Draco Hagrid +$ uv run python main.py Draco Hagrid -Error: argument names takes 3 values +Error: Argument 'names' takes 3 values // And if you pass the exact number of values it will work correctly -$ python main.py Draco Hagrid Dobby +$ uv run python main.py Draco Hagrid Dobby Hello Draco Hello Hagrid diff --git a/docs/tutorial/multiple-values/index.md b/docs/tutorial/multiple-values/index.md index ee1bfa4da7..b511eef10a 100644 --- a/docs/tutorial/multiple-values/index.md +++ b/docs/tutorial/multiple-values/index.md @@ -1,3 +1,5 @@ +# Multiple Values + There are several ways to declare multiple values for *CLI options* and *CLI arguments*. We'll see them in the next short sections. diff --git a/docs/tutorial/multiple-values/multiple-options.md b/docs/tutorial/multiple-values/multiple-options.md index a6c2ca2e83..61b8e1e11c 100644 --- a/docs/tutorial/multiple-values/multiple-options.md +++ b/docs/tutorial/multiple-values/multiple-options.md @@ -1,12 +1,12 @@ +# Multiple CLI Options + You can declare a *CLI option* that can be used multiple times, and then get all the values. For example, let's say you want to accept several users in a single execution. -For this, use the standard Python `typing.List` to declare it as a `list` of `str`: +For this, use the standard Python `list` to declare it as a list of `str`: -```Python hl_lines="1 6" -{!../docs_src/multiple_values/multiple_options/tutorial001.py!} -``` +{* docs_src/multiple_values/multiple_options/tutorial001.py hl[9] *} You will receive the values as you declared them, as a `list` of `str`. @@ -15,18 +15,19 @@ Check it:
```console -$ python main.py +// The default value is 'None' +$ uv run python main.py -No provided users +No provided users (raw input = None) Aborted! // Now pass a user -$ python main.py --user Camila +$ uv run python main.py --user Camila Processing user: Camila // And now try with several users -$ python main.py --user Camila --user Rick --user Morty +$ uv run python main.py --user Camila --user Rick --user Morty Processing user: Camila Processing user: Rick @@ -39,26 +40,24 @@ Processing user: Morty The same way, you can use other types and they will be converted by **Typer** to their declared type: -```Python hl_lines="6" -{!../docs_src/multiple_values/multiple_options/tutorial002.py!} -``` +{* docs_src/multiple_values/multiple_options/tutorial002.py hl[9] *} Check it:
```console -$ python main.py +$ uv run python main.py The sum is 0 // Try with some numbers -$ python main.py --number 2 +$ uv run python main.py --number 2 The sum is 2.0 // Try with some numbers -$ python main.py --number 2 --number 3 --number 4.5 +$ uv run python main.py --number 2 --number 3 --number 4.5 The sum is 9.5 ``` diff --git a/docs/tutorial/multiple-values/options-with-multiple-values.md b/docs/tutorial/multiple-values/options-with-multiple-values.md index 03a9c816e8..b77d1a73ab 100644 --- a/docs/tutorial/multiple-values/options-with-multiple-values.md +++ b/docs/tutorial/multiple-values/options-with-multiple-values.md @@ -1,19 +1,19 @@ +# CLI Options with Multiple Values + You can also declare a *CLI option* that takes several values of different types. You can set the number of values and types to anything you want, but it has to be a fixed number of values. -For this, use the standard Python `typing.Tuple`: +For this, use the standard Python `tuple`: -```Python hl_lines="1 6" -{!../docs_src/multiple_values/options_with_multiple_values/tutorial001.py!} -``` +{* docs_src/multiple_values/options_with_multiple_values/tutorial001.py hl[9] *} Each of the internal types defines the type of each value in the tuple. So: ```Python -user: Tuple[str, int, bool] +user: tuple[str, int, bool] ``` means that the parameter `user` is a tuple of 3 values. @@ -48,11 +48,6 @@ coins = user[1] is_wizard = user[2] ``` -!!! tip - Notice that the default is a tuple with `(None, None, None)`. - - You cannot simply use `None` here as the default because Click doesn't support it. - ## Check it Now let's see how this works in the terminal: @@ -61,32 +56,30 @@ Now let's see how this works in the terminal: ```console // check the help -$ python main.py --help +$ uv run python main.py --help -// Notice the <TEXT INTEGER BOOLEAN> +// Notice the Usage: main.py [OPTIONS] Options: - --user <TEXT INTEGER BOOLEAN>... - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. + --user ... + --help Show this message and exit. // Now try it -$ python main.py --user Camila 50 yes +$ uv run python main.py --user Camila 50 yes The username Camila has 50 coins And this user is a wizard! // With other values -$ python main.py --user Morty 3 no +$ uv run python main.py --user Morty 3 no The username Morty has 3 coins // Try with invalid values (not enough) -$ python main.py --user Camila 50 +$ uv run python main.py --user Camila 50 -Error: --user option requires 3 arguments +Error: Option '--user' requires 3 arguments ```
diff --git a/docs/tutorial/one-file-per-command.md b/docs/tutorial/one-file-per-command.md new file mode 100644 index 0000000000..e2a48ec37c --- /dev/null +++ b/docs/tutorial/one-file-per-command.md @@ -0,0 +1,144 @@ +# One File Per Command + +When your CLI application grows, you can split it into multiple files and modules. This pattern helps maintain a clean and organized code structure. ✨ + +This tutorial will show you how to use `add_typer` to create sub commands and organize your commands in multiple files. + +We will create a simple CLI with the following commands: + +- `version` +- `users add 'name'` +- `users delete 'name'` + +## CLI structure + +Here is the structure we'll be working with: + +```text +mycli/ +├── __init__.py +├── main.py +├── users/ +│ ├── __init__.py +│ ├── add.py +│ └── delete.py +└── version.py +``` + +`mycli` will be our package, and it will contain the following modules: + +- `main.py`: The main module that will import the `version` and `users` modules. +- `version.py`: A module that will contain the `version` command. +- `users/`: A package (inside of our `mycli` package) that will contain the `add` and `delete` commands. + +## Implementation + +Let's start implementing our CLI! 🚀 + +We'll create the `version` module, the `main` module, and the `users` package. + +### Version Module (`version.py`) + +Let's start by creating the `version` module. This module will contain the `version` command. + +{* docs_src/one_file_per_command/app/version.py *} + +In this file we are creating a new Typer app instance for the `version` command. + +This is not required in single-file applications, but in the case of multi-file applications it will allow us to include this command in the main application using `app.add_typer()`. + +Let's see that next! + +### Main Module (`main.py`) + +The main module will be the entry point of the application. It will import the version module and the users module. + +/// tip + +We'll see how to implement the users module in the next section. + +/// + +{* docs_src/one_file_per_command/app/main.py hl[8,9] *} + +In this module, we import the `version` and `users` modules and add them to the main app using `app.add_typer()`. + +For the `users` module, we specify the name as `"users"` to group the commands under the `users` sub-command. + +Notice that we didn't add a name for the `version_app` Typer app. Because of this, Typer will add the commands (just one in this case) declared in the `version_app` directly at the top level. So, there will be a top-level `version` sub-command. + +But for `users`, we add a name `"users"`, this way those commands will be under the sub-command `users` instead of at the top level. So, there will be a `users add` and `users delete` sub-sub-commands. 😅 + +/// tip + +If you want a command to group the included commands in a sub-app, add a name. + +If you want to include the commands from a sub-app directly at the top level, don't add a name, or set it to `None`. 🤓 + +/// + +Let's now create the `users` module with the `add` and `delete` commands. + +### Users Add Command (`users/add.py`) + +{* docs_src/one_file_per_command/app/users/add.py *} + +Like the `version` module, we create a new Typer app instance for the `users/add` command. This allows us to include the `add` command in the users app. + +### Users Delete Command (`users/delete.py`) + +{* docs_src/one_file_per_command/app/users/delete.py *} + +And once again, we create a new Typer app instance for the `users/delete` command. This allows us to include the `delete` command in the users app. + +### Users' app (`users/__init__.py`) + +Finally, we need to create an `__init__.py` file in the `users` directory to define the `users` app. + +{* docs_src/one_file_per_command/app/users/__init__.py *} + +Similarly to the `version` module, we create a new `Typer` app instance for the `users` module. This allows us to include the `add` and `delete` commands in the users app. + +## Running the Application + +Now we are ready to run the application! 😎 + +To run the application, you can execute it as a Python module: + +
+ +```console +$ uv run python -m mycli.main version + +My CLI Version 1.0 + +$ uv run python -m mycli.main users add Camila + +Adding user: Camila +``` + +
+ +And if you built a package and installed your app, you can then use the `mycli` command: + +
+ +```console +$ mycli version + +My CLI Version 1.0 + +$ mycli users add Camila + +Adding user: Camila +``` + +
+ +## Callbacks + +Have in mind that if you include a sub-app with `app.add_typer()` **without a name**, the commands will be added to the top level, so **only the top level callback** (if there's any) will be used, the one declared in the main app. + +If you **want to use a callback** for a sub-app, you need to include the sub-app **with a name**, which creates a sub-command grouping the commands in that sub-app. 🤓 + +In the example above, if the `users` sub-app had a callback, it would be used. But if the `version` sub-app had a callback, it would not be used, because the `version` sub-app was included without a name. diff --git a/docs/tutorial/options/autocompletion.md b/docs/tutorial/options-autocompletion.md similarity index 62% rename from docs/tutorial/options/autocompletion.md rename to docs/tutorial/options-autocompletion.md index 136f661b7b..56b2751f6d 100644 --- a/docs/tutorial/options/autocompletion.md +++ b/docs/tutorial/options-autocompletion.md @@ -1,4 +1,6 @@ -As you have seen, apps built with **Typer** have completion in your shell that works when you create a Python package or using **Typer CLI**. +# CLI Option autocompletion + +As you have seen, apps built with **Typer** have completion in your shell that works when you create a Python package or using the `typer` command. It normally completes *CLI options*, *CLI arguments*, and subcommands (that you will learn about later). @@ -8,17 +10,15 @@ But you can also provide auto completion for the **values** of *CLI options* and Before checking how to provide custom completions, let's check again how it works. -After installing completion (for your own Python package or for **Typer CLI**), when you use your CLI program and start adding a *CLI option* with `--` an then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). +After installing completion for your own Python package (or using the `typer` command), when you use your CLI program and start adding a *CLI option* with `--` and then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). -To check it quickly without creating a new Python package, install [Typer CLI](../../typer-cli.md){.internal-link target=_blank}. +To check it quickly without creating a new Python package, use the `typer` command. -Then let's create small example script: +Then let's create a small example program: -```Python -{!../docs_src/options/autocompletion/tutorial001.py!} -``` +{* docs_src/options_autocompletion/tutorial001.py *} -And let's try it with **Typer CLI** to get completion: +And let's try it with the `typer` command to get completion:
@@ -48,11 +48,9 @@ Hello Camila Right now we get completion for the *CLI option* names, but not for the values. -We can provide completion for the values creating an `autocompletion` function, similar to the `callback` functions from [CLI Option Callback and Context](./callback-and-context.md){.internal-link target=_blank}: +We can provide completion for the values creating an `autocompletion` function, similar to the `callback` functions from [CLI Option Callback and Context](./options/callback-and-context.md): -```Python hl_lines="4 5 10" -{!../docs_src/options/autocompletion/tutorial002.py!} -``` +{* docs_src/options_autocompletion/tutorial002.py hl[6:7,16] *} We return a `list` of strings from the `complete_name()` function. @@ -81,9 +79,7 @@ Modify the `complete_name()` function to receive a parameter of type `str`, it w Then we can check and return only the values that start with the incomplete value from the command line: -```Python hl_lines="6 7 8 9 10 11" -{!../docs_src/options/autocompletion/tutorial003.py!} -``` +{* docs_src/options_autocompletion/tutorial003.py hl[8:13] *} Now let's try it: @@ -100,12 +96,15 @@ Camila Carlos Now we are only returning the valid values, that start with `Ca`, we are no longer returning `Sebastian` as a completion option. -!!! tip - You have to declare the incomplete value of type `str` and that's what you will receive in the function. +/// tip + +You have to declare the incomplete value of type `str` and that's what you will receive in the function. - No matter if the actual value will be an `int`, or something else, when doing completion, you will only get a `str` as the incomplete value. +No matter if the actual value will be an `int`, or something else, when doing completion, you will only get a `str` as the incomplete value. - And the same way, you can only return `str`, not `int`, etc. +And the same way, you can only return `str`, not `int`, etc. + +/// ## Add help to completions @@ -119,21 +118,23 @@ In the `complete_name()` function, instead of providing one `str` per completion So, in the end, we return a `list` of `tuples` of `str`: -```Python hl_lines="3 4 5 6 7 10 11 12 13 14 15 16" -{!../docs_src/options/autocompletion/tutorial004.py!} -``` +{* docs_src/options_autocompletion/tutorial004.py hl[5:9,12:18] *} + +/// tip + +If you want to have help text for each item, make sure each item in the list is a `tuple`. Not a `list`. -!!! tip - If you want to have help text for each item, make sure each item in the list is a `tuple`. Not a `list`. +In the end, the return will be a `list` (or other iterable) of `tuples` of 2 `str`. - Click checks specifically for a `tuple` when extracting the help text. +/// - So in the end, the return will be a `list` (or other iterable) of `tuples` of 2 `str`. +/// note -!!! info - The help text will be visible in Zsh, Fish, and PowerShell. +The help text will be visible in Zsh, Fish, and PowerShell. - Bash doesn't support showing the help text, but completion will still work the same. +Bash doesn't support showing the help text, but completion will still work the same. + +/// If you have a shell like Zsh, it would look like: @@ -154,23 +155,27 @@ Sebastian -- The type hints guy. Instead of creating and returning a list with values (`str` or `tuple`), we can use `yield` with each value that we want in the completion. -That way our function will be a generator that **Typer** (actually Click) can iterate: +That way our function will be a [generator](https://docs.python.org/3.8/glossary.html#index-19) that **Typer** can iterate: -```Python hl_lines="10 11 12 13" -{!../docs_src/options/autocompletion/tutorial005.py!} -``` +{* docs_src/options_autocompletion/tutorial005.py hl[12:15] *} That simplifies our code a bit and works the same. -!!! tip - If all the `yield` part seems complex for you, don't worry, you can just use the version with the `list` above. +/// tip + +If the `yield` part seems complex for you, don't worry, you can just use the version with the `list` above. + +In the end, that's just to save us a couple of lines of code. - In the end, that's just to save us a couple of lines of code. +/// -!!! info - The function can use `yield`, so it doesn't have to return strictly a `list`, it just has to be iterable. +/// note - But each of the elements for completion has to be a `str` or a `tuple` (when containing a help text). +The function can use `yield`, so it doesn't have to return strictly a `list`, it just has to be [iterable](https://docs.python.org/3.8/glossary.html#term-iterable). + +But each of the elements for completion has to be a `str` or a `tuple` (when containing a help text). + +/// ## Access other *CLI parameters* with the Context @@ -178,16 +183,17 @@ Let's say that now we want to modify the program to be able to "say hi" to multi So, we will allow multiple `--name` *CLI options*. -!!! tip - You will learn more about *CLI parameters* with multiple values later in the tutorial. +/// tip - So, for now, take this as a sneak peek 😉. +You will learn more about *CLI parameters* with multiple values later in the tutorial. -For this we use a `List` of `str`: +So, for now, take this as a sneak peek 😉. -```Python hl_lines="6 7 8" -{!../docs_src/options/autocompletion/tutorial006.py!} -``` +/// + +For this we use a `list` of `str`: + +{* docs_src/options_autocompletion/tutorial006.py hl[8:13] *} And then we can use it like: @@ -206,15 +212,13 @@ Hello Sebastian And the same way as before, we want to provide **completion** for those names. But we don't want to provide the **same names** for completion if they were already given in previous parameters. -For that, we will access and use the "Context". When you create a **Typer** application it uses Click underneath. And every Click application has a special object called a "Context" that is normally hidden. +For that, we will access and use the "Context". Every Typer application has a special object called a "Context" that is normally hidden. But you can access the context by declaring a function parameter of type `typer.Context`. And from that context you can get the current values for each parameter. -```Python hl_lines="12 13 15" -{!../docs_src/options/autocompletion/tutorial007.py!} -``` +{* docs_src/options_autocompletion/tutorial007.py hl[12:13,15] *} We are getting the `names` already provided with `--name` in the command line before this completion was triggered. @@ -252,8 +256,11 @@ Carlos -- The writer of scripts.
-!!! tip - It's quite possible that if there's only one option left, your shell will complete it right away instead of showing the option with the help text, to save you more typing. +/// tip + +It's quite possible that if there's only one option left, your shell will complete it right away instead of showing the option with the help text, to save you more typing. + +/// ## Getting the raw *CLI parameters* @@ -261,10 +268,13 @@ You can also get the raw *CLI parameters*, just a `list` of `str` with everythin For example, something like `["typer", "main.py", "run", "--name"]`. -!!! tip - This would be for advanced scenarios, in most use cases you would be better off using the context. +/// tip + +This would be for advanced scenarios, in most use cases you would be better off using the context. - But it's still possible if you need it. +But it's still possible if you need it. + +/// As a simple example, let's show it on the screen before completion. @@ -272,25 +282,35 @@ Because completion is based on the output printed by your program (handled inter ### Printing to "standard error" -!!! tip - If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](../printing.md#standard-output-and-standard-error){.internal-link target=_blank}. +/// tip + +If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](./printing.md#standard-output-and-standard-error). + +/// The completion system only reads from "standard output", so, printing to "standard error" won't break completion. 🚀 -You can print to "standard error" with `typer.echo("some text", err=True)`. +You can print to "standard error" with a **Rich** `Console(stderr=True)`. -Using `err=True` tells **Typer** (actually Click) that the output should be shown in "standard error". +Using `stderr=True` tells **Rich** that the output should be shown in "standard error". -```Python hl_lines="12 13" -{!../docs_src/options/autocompletion/tutorial008.py!} -``` +{* docs_src/options_autocompletion/tutorial008.py hl[12,15:16] *} -We get all the *CLI parameters* as a raw `list` of `str` by declaring a parameter with type `List[str]`, here it's named `args`. +/// note -!!! tip - Here we name the list of all the raw *CLI parameters* `args` because that's the convention with Click. +If you have disabled Rich, you can also use `print(lastname, file=sys.stderr)` or `typer.echo("some text", err=True)` instead. - But it doesn't contain only *CLI arguments*, it has everything, including *CLI options* and values, as a raw `list` of `str`. +/// + +We get all the *CLI parameters* as a raw `list` of `str` by declaring a parameter with type `list[str]`, here it's named `args`. + +/// tip + +Here we name the list of all the raw *CLI parameters* `args` because that's the usual convention. + +But it doesn't contain only *CLI arguments*, it has everything, including *CLI options* and values, as a raw `list` of `str`. + +/// And then we just print it to "standard error". @@ -310,18 +330,19 @@ Sebastian -- The type hints guy.
-!!! tip - This is a very simple (and quite useless) example, just so you know how it works and that you can use it. +/// tip + +This is a very simple (and quite useless) example, just so you know how it works and that you can use it. - But it's probably useful only in very advanced use cases. +But it's probably useful only in very advanced use cases. + +/// ## Getting the Context and the raw *CLI parameters* Of course, you can declare everything if you need it, the context, the raw *CLI parameters*, and the incomplete `str`: -```Python hl_lines="12" -{!../docs_src/options/autocompletion/tutorial009.py!} -``` +{* docs_src/options_autocompletion/tutorial009.py hl[15] *} Check it: @@ -358,6 +379,6 @@ You can declare function parameters of these types: * `str`: for the incomplete value. * `typer.Context`: for the current context. -* `List[str]`: for the raw *CLI parameters*. +* `list[str]`: for the raw *CLI parameters*. It doesn't matter how you name them, in which order, or which ones of the 3 options you declare. It will all "**just work**" ✨ diff --git a/docs/tutorial/options/callback-and-context.md b/docs/tutorial/options/callback-and-context.md index 2e924226a9..04e4536125 100644 --- a/docs/tutorial/options/callback-and-context.md +++ b/docs/tutorial/options/callback-and-context.md @@ -1,3 +1,5 @@ +# CLI Option Callback and Context + In some occasions you might want to have some custom logic for a specific *CLI parameter* (for a *CLI option* or *CLI argument*) that is executed with the value received from the terminal. In those cases you can use a *CLI parameter* callback function. @@ -6,9 +8,7 @@ In those cases you can use a *CLI parameter* callback function. For example, you could do some validation before the rest of the code is executed. -```Python hl_lines="4 5 6 7 10" -{!../docs_src/options/callback/tutorial001.py!} -``` +{* docs_src/options/callback/tutorial001.py hl[8:11,15] *} Here you pass a function to `typer.Option()` or `typer.Argument()` with the keyword argument `callback`. @@ -23,11 +23,11 @@ Check it:
```console -$ python main.py --name Camila +$ uv run python main.py --name Camila Hello Camila -$ python main.py --name Rick +$ uv run python main.py --name Rick Usage: main.py [OPTIONS] @@ -43,9 +43,9 @@ There's something to be aware of with callbacks and completion that requires som But first let's just use completion in your shell (Bash, Zsh, Fish, or PowerShell). -After installing completion (for your own Python package or for **Typer CLI**), when you use your CLI program and start adding a *CLI option* with `--` an then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). +After installing completion (for your own Python package), when you use your CLI program and start adding a *CLI option* with `--` and then hit TAB, your shell will show you the available *CLI options* (the same for *CLI arguments*, etc). -To check it quickly without creating a new Python package, install [Typer CLI](../../typer-cli.md){.internal-link target=_blank} and use it with the previous script: +To check it quickly with the previous script use the `typer` command:
@@ -61,12 +61,12 @@ utils -- Extra utility commands for Typer apps. $ typer ./main.py run --help // You get a help text with your CLI options as you normally would -Usage: typer run [OPTIONS] +Usage: typer [PATH_OR_MODULE] run [OPTIONS] Run the provided Typer app. Options: - --name TEXT [required] + --name --help Show this message and exit. // Then try completion with your program @@ -94,9 +94,7 @@ But the main **important point** is that it is all based on values printed by yo Let's say that when the callback is running, we want to show a message saying that it's validating the name: -```Python hl_lines="5" -{!../docs_src/options/callback/tutorial002.py!} -``` +{* docs_src/options/callback/tutorial002.py hl[9] *} And because the callback will be called when the shell calls your program asking for completion, that message `"Validating name"` will be printed and it will break completion. @@ -123,23 +121,19 @@ rutyper ./main.pyed Typer app. ### Fix completion - using the `Context` -When you create a **Typer** application it uses Click underneath. - -And every Click application has a special object called a "Context" that is normally hidden. +Every Typer application has a special object called a "Context" that is normally hidden. But you can access the context by declaring a function parameter of type `typer.Context`. The "context" has some additional data about the current execution of your program: -```Python hl_lines="4 5 6" -{!../docs_src/options/callback/tutorial003.py!} -``` +{* docs_src/options/callback/tutorial003.py hl[8:10] *} The `ctx.resilient_parsing` will be `True` when handling completion, so you can just return without printing anything else. But it will be `False` when calling the program normally. So you can continue the execution of your previous code. -That's all is needed to fix completion 🚀 +That's all is needed to fix completion. 🚀 Check it: @@ -163,11 +157,9 @@ Hello Camila ## Using the `CallbackParam` object -The same way you can access the `typer.Context` by declaring a function parameter with its value, you can declare another function parameter with type `typer.CallbackParam` to get the specific Click `Parameter` object. +The same way you can access the `typer.Context` by declaring a function parameter with its value, you can declare another function parameter with type `typer.CallbackParam` to get the specific `Parameter` object. -```Python hl_lines="4 7" -{!../docs_src/options/callback/tutorial004.py!} -``` +{* docs_src/options/callback/tutorial004.py hl[8,11] *} It's probably not very common, but you could do it if you need it. @@ -178,7 +170,7 @@ Check it:
```console -$ python main.py --name Camila +$ uv run python main.py --name Camila Validating param: name Hello Camila @@ -196,10 +188,6 @@ You don't have to worry about their names, their order, etc. As it's based on standard Python types, it "**just works**". ✨ -### Click's `Parameter` - -The `typer.CallbackParam` is actually just a sub-class of Click's `Parameter`, so you get all the right completion in your editor. - ### Callback with type annotations You can get the `typer.Context` and the `typer.CallbackParam` simply by declaring a function parameter of each type. diff --git a/docs/tutorial/options/help.md b/docs/tutorial/options/help.md index 0fa075c0aa..61f157b4fd 100644 --- a/docs/tutorial/options/help.md +++ b/docs/tutorial/options/help.md @@ -1,37 +1,27 @@ +# CLI Options with Help + You already saw how to add a help text for *CLI arguments* with the `help` parameter. Let's now do the same for *CLI options*: -```Python hl_lines="6 7" -{!../docs_src/options/help/tutorial001.py!} -``` - -We are replacing the default values we had before with `typer.Option()`. +{* docs_src/options/help/tutorial001.py hl[11:12] *} -As we no longer have a default value there, the first parameter to `typer.Option()` serves the same purpose of defining that default value. - -So, if we had: - -```Python -lastname: str = "" -``` +The same way as with `typer.Argument()`, we can put `typer.Option()` inside of `Annotated`. -now we write: +We can then pass the `help` keyword parameter: ```Python -lastname: str = typer.Option("") +lastname: Annotated[str, typer.Option(help="this option does this and that")] = "" ``` -And both forms achieve the same: a *CLI option* with a default value of an empty string (`""`). +...to create the help for that *CLI option*. -And then we can pass the `help` keyword parameter: +The same way as with `typer.Argument()`, **Typer** also supports the old style using the function parameter default value: ```Python -lastname: str = typer.Option("", help="this option does this and that") +lastname: str = typer.Option(default="", help="this option does this and that") ``` -to create the help for that *CLI option*. - Copy that example from above to a file `main.py`. Test it: @@ -39,22 +29,20 @@ Test it:
```console -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} - Say hi to NAME, optionally with a --lastname. + Say hi to 'name', optionally with a --lastname. If --formal is used, say hi very formally. Arguments: - NAME [required] + name [required] Options: - --lastname TEXT Last name of person to greet. [default: ] - --formal / --no-formal Say hi formally. [default: False] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + --lastname Last name of person to greet. + --formal / --no-formal Say hi formally. [default: no-formal] --help Show this message and exit. // Now you have a help text for the --lastname and --formal CLI options 🎉 @@ -62,32 +50,76 @@ Options:
+## *CLI Options* help panels + +The same as with *CLI arguments*, you can put the help for some *CLI options* in different panels to be shown with the `--help` option. + +Using Rich, you can set the `rich_help_panel` parameter to the name of the panel you want for each *CLI option*: + +{* docs_src/options/help/tutorial002.py hl[15,21] *} + +Now, when you check the `--help` option, you will see a default panel named "`Options`" for the *CLI options* that don't have a custom `rich_help_panel`. + +And below you will see other panels for the *CLI options* that have a custom panel set in the `rich_help_panel` parameter: + +
+ +```console +$ uv run python main.py --help + + Usage: main.py [OPTIONS] {name} + + Say hi to 'name', optionally with a --lastname. + If --formal is used, say hi very formally. + +╭─ Arguments ───────────────────────────────────────────────────────╮ +│ * name <str> [required] │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Options ─────────────────────────────────────────────────────────╮ +│ --lastname <str> Last name of person to greet. │ +│ --help Show this message and exit. │ +╰───────────────────────────────────────────────────────────────────╯ +╭─ Customization and Utils ─────────────────────────────────────────╮ +│ --formal --no-formal Say hi formally. │ +│ [default: no-formal] │ +│ --debug --no-debug Enable debugging. │ +│ [default: no-debug] │ +╰───────────────────────────────────────────────────────────────────╯ +``` + +
+ +Here we have a custom *CLI options* panel named "`Customization and Utils`". + +## Help with style using Rich + +In a future section you will see how to use custom markup in the `help` for *CLI options* when reading about [Commands - Command Help](../commands/help.md#rich-markdown-and-markup). + +If you are in a hurry you can jump there, but otherwise, it would be better to continue reading here and following the tutorial in order. + + ## Hide default from help You can tell Typer to not show the default value in the help text with `show_default=False`: -```Python hl_lines="4" -{!../docs_src/options/help/tutorial002.py!} -``` +{* docs_src/options/help/tutorial003.py hl[9] *} And it will no longer show the default value in the help text:
```console -$ python main.py +$ uv run python main.py Hello Wade Wilson // Show the help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] Options: - --fullname TEXT - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + --fullname --help Show this message and exit. // Notice there's no [default: Wade Wilson] 🔥 @@ -95,7 +127,31 @@ Options:
-!!! note "Technical Details" - In Click applications the default values are hidden by default. 🙈 +## Custom default string + +You can use the same `show_default` to pass a custom string (instead of a `bool`) to customize the default value to be shown in the help text: + +{* docs_src/options/help/tutorial004.py hl[11] *} - In **Typer** these default values are shown by default. 👀 +And it will be used in the help text: + +
+ +```console +$ uv run python main.py + +Hello Wade Wilson + +// Show the help +$ uv run python main.py --help + +Usage: main.py [OPTIONS] + +Options: + --fullname [default: (Deadpoolio the amazing's name)] + --help Show this message and exit. + +// Notice how it shows "(Deadpoolio the amazing's name)" instead of the actual default of "Wade Wilson" +``` + +
diff --git a/docs/tutorial/options/index.md b/docs/tutorial/options/index.md index 13c4505eb3..f71c2167e7 100644 --- a/docs/tutorial/options/index.md +++ b/docs/tutorial/options/index.md @@ -1,3 +1,5 @@ +# CLI Options + In the next short sections we will see how to modify *CLI options* using `typer.Option()`. `typer.Option()` works very similarly to `typer.Argument()`, but has some extra features that we'll see next. diff --git a/docs/tutorial/options/name.md b/docs/tutorial/options/name.md index 982e7e4d63..40fdf94d18 100644 --- a/docs/tutorial/options/name.md +++ b/docs/tutorial/options/name.md @@ -1,3 +1,5 @@ +# CLI Option Name + By default **Typer** will create a *CLI option* name from the function parameter. So, if you have a function with: @@ -10,7 +12,7 @@ def main(user_name: Optional[str] = None): or ```Python -def main(user_name: Optional[str] = typer.Option(None)): +def main(user_name: Annotated[Optional[str], typer.Option()] = None): pass ``` @@ -24,41 +26,38 @@ But you can customize it if you want to. Let's say the function parameter name is `user_name` as above, but you want the *CLI option* to be just `--name`. -You can pass the *CLI option* name that you want to have in the next positional argument passed to `typer.Option()`: +You can pass the *CLI option* name that you want to have in the following positional argument passed to `typer.Option()`: -```Python hl_lines="4" -{!../docs_src/options/name/tutorial001.py!} -``` +{* docs_src/options/name/tutorial001.py hl[9] *} + +/// note -Here you are passing the string `"--name"` as the second positional argument to `typer.Option()`. +"[Positional](https://docs.python.org/3.8/glossary.html#term-argument)" means that it's not a function argument with a keyword name. -!!! info - "Positional" means that it's not a function argument with a keyword name. +For example `show_default=True` is a keyword argument. "`show_default`" is the keyword. - For example `show_default=True` is a keyword argument. "`show_default`" is the keyword. +But in `"--name"` there's no `option_name="--name"` or something similar, it's just the string value `"--name"` that goes in `typer.Option()`. - But in `"--name"` there's no `option_name="--name"` or something similar, it's just the string value `"--name"` that goes in `typer.Option()` after the `...` value passed in the first position. +That's a "positional argument" in a function. - That's a "positional argument" in a function. +/// Check it:
```console -$ python main.py --help +$ uv run python main.py --help // Notice the --name instead of --user-name Usage: main.py [OPTIONS] Options: - --name TEXT [required] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + --name [required] --help Show this message and exit. // Try it -$ python --name Camila +$ uv run python main.py --name Camila Hello Camila ``` @@ -172,20 +171,21 @@ tar: You must specify one of the blah, blah, error, error In **Typer** you can also define *CLI option* short names the same way you can customize the long names. -`typer.Option()` receives as a first function argument the default value, e.g. `None`, and all the next *positional* values are to define the *CLI option* name(s). +You can pass *positional* arguments to `typer.Option()` to define the *CLI option* name(s). -!!! tip - Remember the *positional* function arguments are those that don't have a keyword. +/// tip - All the other function arguments/parameters you pass to `typer.Option()` like `prompt=True` and `help="This option blah, blah"` require the keyword. +Remember the *positional* function arguments are those that don't have a keyword. + +All the other function arguments/parameters you pass to `typer.Option()` like `prompt=True` and `help="This option blah, blah"` require the keyword. + +/// You can overwrite the *CLI option* name to use as in the previous example, but you can also declare extra alternatives, including short names. For example, extending the previous example, let's add a *CLI option* short name `-n`: -```Python hl_lines="4" -{!../docs_src/options/name/tutorial002.py!} -``` +{* docs_src/options/name/tutorial002.py hl[9] *} Here we are overwriting the *CLI option* name that by default would be `--user-name`, and we are defining it to be `--name`. And we are also declaring a *CLI option* short name of `-n`. @@ -195,19 +195,17 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the two CLI option names -n and --name Usage: main.py [OPTIONS] Options: - -n, --name TEXT [required] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + -n, --name [required] --help Show this message and exit. // Try the short version -$ python main.py -n Camila +$ uv run python main.py -n Camila Hello Camila ``` @@ -218,28 +216,24 @@ Hello Camila If you only declare a short name like `-n` then that will be the only *CLI option* name. And neither `--name` nor `--user-name` will be available. -```Python hl_lines="4" -{!../docs_src/options/name/tutorial003.py!} -``` +{* docs_src/options/name/tutorial003.py hl[9] *} Check it:
```console -$ python main.py --help +$ uv run python main.py --help // Notice there's no --name nor --user-name, only -n Usage: main.py [OPTIONS] Options: - -n TEXT [required] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + -n [required] --help Show this message and exit. // Try it -$ python main.py -n Camila +$ uv run python main.py -n Camila Hello Camila ``` @@ -250,34 +244,30 @@ Hello Camila Continuing with the example above, as **Typer** allows you to declare a *CLI option* as having only a short name, if you want to have the default long name plus a short name, you have to declare both explicitly: -```Python hl_lines="4" -{!../docs_src/options/name/tutorial004.py!} -``` +{* docs_src/options/name/tutorial004.py hl[9] *} Check it:
```console -$ python main.py --help +$ uv run python main.py --help // Notice that we have the long version --user-name back // and we also have the short version -n Usage: main.py [OPTIONS] Options: - -n, --user-name TEXT [required] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + -n, --user-name [required] --help Show this message and exit. // Try it -$ python main.py --user-name Camila +$ uv run python main.py --user-name Camila Hello Camila // And try the short version -$ python main.py -n Camila +$ uv run python main.py -n Camila ```
@@ -288,39 +278,38 @@ You can create multiple short names and use them together. You don't have to do anything special for it to work (apart from declaring those short versions): -```Python hl_lines="5 6" -{!../docs_src/options/name/tutorial005.py!} -``` +{* docs_src/options/name/tutorial005.py hl[10:11] *} + +/// tip + +Notice that, again, we are declaring the long and short version of the *CLI option* names. -!!! tip - Notice that, again, we are declaring the long and short version of the *CLI option* names. +/// Check it:
```console -$ python main.py --help +$ uv run python main.py --help // We now have short versions -n and -f // And also long versions --name and --formal Usage: main.py [OPTIONS] Options: - -n, --name TEXT [required] + -n, --name [required] -f, --formal - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Try the short versions -$ python main.py -n Camila -f +$ uv run python main.py -n Camila -f Good day Ms. Camila. // And try the 2 short versions together // See how -n has to go last, to be able to get the value -$ python main.py -fn Camila +$ uv run python main.py -fn Camila Good day Ms. Camila. ``` diff --git a/docs/tutorial/options/password.md b/docs/tutorial/options/password.md index 2d2840f4bf..a36103cd82 100644 --- a/docs/tutorial/options/password.md +++ b/docs/tutorial/options/password.md @@ -1,15 +1,15 @@ +# Password CLI Option and Confirmation Prompt + Apart from having a prompt, you can make a *CLI option* have a `confirmation_prompt=True`: -```Python hl_lines="5" -{!../docs_src/options/password/tutorial001.py!} -``` +{* docs_src/options/password/tutorial001.py hl[11] *} And the CLI program will ask for confirmation:
```console -$ python main.py Camila +$ uv run python main.py Camila // It prompts for the email # Email: $ camila@example.com @@ -30,16 +30,14 @@ You can achieve the same using `hide_input=True`. And if you combine it with `confirmation_prompt=True` you can easily receive a password with double confirmation: -```Python hl_lines="6 7 8" -{!../docs_src/options/password/tutorial002.py!} -``` +{* docs_src/options/password/tutorial002.py hl[12] *} Check it:
```console -$ python main.py Camila +$ uv run python main.py Camila // It prompts for the password, but doesn't show anything when you type # Password: $ diff --git a/docs/tutorial/options/prompt.md b/docs/tutorial/options/prompt.md index 92398dab7e..9940807c10 100644 --- a/docs/tutorial/options/prompt.md +++ b/docs/tutorial/options/prompt.md @@ -1,16 +1,16 @@ +# CLI Option Prompt + It's also possible to, instead of just showing an error, ask for the missing value with `prompt=True`: -```Python hl_lines="4" -{!../docs_src/options/prompt/tutorial001.py!} -``` +{* docs_src/options/prompt/tutorial001.py hl[9] *} And then your program will ask the user for it in the terminal:
```console -// Call it with the NAME CLI argument -$ python main.py Camila +// Call it with the 'name' CLI argument +$ uv run python main.py Camila // It asks for the missing CLI option --lastname # Lastname: $ Gutiérrez @@ -24,17 +24,15 @@ Hello Camila Gutiérrez You can also set a custom prompt, passing the string that you want to use instead of just `True`: -```Python hl_lines="5" -{!../docs_src/options/prompt/tutorial002.py!} -``` +{* docs_src/options/prompt/tutorial002.py hl[11] *} And then your program will ask for it using with your custom prompt:
```console -// Call it with the NAME CLI argument -$ python main.py Camila +// Call it with the 'name' CLI argument +$ uv run python main.py Camila // It uses the custom prompt # Please tell me your last name: $ Gutiérrez @@ -52,16 +50,14 @@ You can do it passing the parameter `confirmation_prompt=True`. Let's say it's a CLI app to delete a project: -```Python hl_lines="4" -{!../docs_src/options/prompt/tutorial003.py!} -``` +{* docs_src/options/prompt/tutorial003.py hl[10] *} And it will prompt the user for a value and then for the confirmation:
```console -$ python main.py +$ uv run python main.py // Your app will first prompt for the project name, and then for the confirmation # Project name: $ Old Project @@ -70,12 +66,12 @@ $ python main.py Deleting project Old Project // If the user doesn't type the same, receives an error and a new prompt -$ python main.py +$ uv run python main.py # Project name: $ Old Project # Repeat for confirmation: $ New Spice -Error: the two entered values do not match +Error: The two entered values do not match # Project name: $ Old Project # Repeat for confirmation: $ Old Project diff --git a/docs/tutorial/options/required.md b/docs/tutorial/options/required.md index c81f3ec173..73ca6dda6f 100644 --- a/docs/tutorial/options/required.md +++ b/docs/tutorial/options/required.md @@ -1,3 +1,5 @@ +# Required CLI Options + We said before that *by default*: * *CLI options* are **optional** @@ -7,47 +9,63 @@ Well, that's how they work *by default*, and that's the convention in many CLI p But if you really want, you can change that. -To make a *CLI option* required, pass `...` to `typer.Option()`. +To make a *CLI option* required, you can put `typer.Option()` inside of `Annotated` and leave the parameter without a default value. + +Let's make `--lastname` a required *CLI option*: + +{* docs_src/options/required/tutorial001.py hl[9] *} + +The same way as with `typer.Argument()`, the old style of using the function parameter default value is also supported, in that case you would just not pass anything to the `default` parameter. + +{* docs_src/options/required/tutorial001_non_an.py hl[7] *} + +Or you can explicitly pass `...` to `typer.Option(default=...)`: -!!! info - If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". +{* docs_src/options/required/tutorial002.py hl[7] *} + +/// note + +If you hadn't seen that `...` before: it is a special single value, it is [part of Python and is called "Ellipsis"](https://docs.python.org/3/library/constants.html#Ellipsis). + +/// That will tell **Typer** that it's still a *CLI option*, but it doesn't have a default value, and it's required. -Let's make `--lastname` a required *CLI option*: +/// tip -```Python hl_lines="4" -{!../docs_src/options/required/tutorial001.py!} -``` +Again, prefer to use the `Annotated` version if possible. That way your code will mean the same in standard Python and in **Typer**. + +/// And test it:
```console -// Pass the NAME CLI argument -$ python main.py Camila +// Pass the 'name' CLI argument +$ uv run python main.py Camila // We didn't pass the now required --lastname CLI option -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} Try "main.py --help" for help. Error: Missing option '--lastname'. // Now update it to pass the required --lastname CLI option -$ python main.py Camila --lastname Gutiérrez +$ uv run python main.py Camila --lastname Gutiérrez Hello Camila Gutiérrez // And if you check the help -$ python main.py --help +$ uv run python main.py --help + +Usage: main.py [OPTIONS] {name} -Usage: main.py [OPTIONS] NAME +Arguments: + name [required] Options: - --lastname TEXT [required] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + --lastname [required] --help Show this message and exit. // It now tells you that --lastname is required 🎉 diff --git a/docs/tutorial/options/version.md b/docs/tutorial/options/version.md index 40026692d9..bcf2b01e7f 100644 --- a/docs/tutorial/options/version.md +++ b/docs/tutorial/options/version.md @@ -1,3 +1,5 @@ +# Version CLI Option, `is_eager` + You could use a callback to implement a `--version` *CLI option*. It would show the version of your CLI program and then it would terminate it. Even before any other *CLI parameter* is processed. @@ -6,12 +8,13 @@ It would show the version of your CLI program and then it would terminate it. Ev Let's see a first version of how it could look like: -```Python hl_lines="8-11 16-18" -{!../docs_src/options/version/tutorial001.py!} -``` +{* docs_src/options/version/tutorial001.py hl[10:13,19:21] *} + +/// tip + +Notice that we don't have to get the `typer.Context` and check for `ctx.resilient_parsing` for completion to work, because we only print and modify the program when `--version` is passed, otherwise, nothing is printed or changed from the callback. -!!! tip - Notice that we don't have to get the `typer.Context` and check for `ctx.resilient_parsing` for completion to work, because we only print and modify the program when `--version` is passed, otherwise, nothing is printed or changed from the callback. +/// If the `--version` *CLI option* is passed, we get a value `True` in the callback. @@ -24,27 +27,24 @@ Check it:
```console -$ python main.py --help +$ uv run python main.py --help // We get a --version, and don't get an awkward --no-version 🎉 Usage: main.py [OPTIONS] Options: --version - --name TEXT - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - + --name [default: World] --help Show this message and exit. // We can call it normally -$ python main.py --name Camila +$ uv run python main.py --name Camila Hello Camila // And we can get the version -$ python main.py --version +$ uv run python main.py --version Awesome CLI Version: 0.1.0 @@ -57,16 +57,14 @@ Awesome CLI Version: 0.1.0 But now let's say that the `--name` *CLI option* that we declared before `--version` is required, and it has a callback that could exit the program: -```Python hl_lines="14-16 21-23" -{!../docs_src/options/version/tutorial002.py!} -``` +{* docs_src/options/version/tutorial002.py hl[16:19,25:27] *} Then our CLI program could not work as expected in some cases as it is *right now*, because if we use `--version` after `--name` then the callback for `--name` will be processed before and we can get its error:
```console -$ python main.py --name Rick --version +$ uv run python main.py --name Rick --version Only Camila is allowed Aborted! @@ -74,31 +72,38 @@ Aborted!
-!!! tip - We don't have to check for `ctx.resilient_parsing` in the `name_callback()` for completion to work, because we are not using `typer.echo()`, instead we are raising a `typer.BadParameter`. +/// tip + +We don't have to check for `ctx.resilient_parsing` in the `name_callback()` for completion to work, because we are not using `typer.echo()`, instead we are raising a `typer.BadParameter`. -!!! note "Technical Details" - `typer.BadParameter` prints the error to "standard error", not to "standard output", and because the completion system only reads from "standard output", it won't break completion. +/// -!!! info - If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](../printing.md#standard-output-and-standard-error){.internal-link target=_blank}. +/// note | Technical Details + +`typer.BadParameter` prints the error to "standard error", not to "standard output", and because the completion system only reads from "standard output", it won't break completion. + +/// + +/// note + +If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](../printing.md#standard-output-and-standard-error). + +/// ### Fix with `is_eager` For those cases, we can mark a *CLI parameter* (a *CLI option* or *CLI argument*) with `is_eager=True`. -That will tell **Typer** (actually Click) that it should process this *CLI parameter* before the others: +That will tell **Typer** that it should process this *CLI parameter* before the others: -```Python hl_lines="22-24" -{!../docs_src/options/version/tutorial003.py!} -``` +{* docs_src/options/version/tutorial003.py hl[25:28] *} Check it:
```console -$ python main.py --name Rick --version +$ uv run python main.py --name Rick --version // Now we only get the version, and the name is not used Awesome CLI Version: 0.1.0 diff --git a/docs/tutorial/package.md b/docs/tutorial/package.md index bdda580699..e8161d3ee0 100644 --- a/docs/tutorial/package.md +++ b/docs/tutorial/package.md @@ -1,23 +1,28 @@ +# Building a Package + When you create a CLI program with **Typer** you probably want to create your own Python package. That's what allows your users to install it and have it as an independent program that they can use in their terminal. -And that's also required for shell auto completion to work (unless you use your program through [Typer CLI](../typer-cli.md){.internal-link target=_blank}). +And that's also required for shell auto completion to work (unless you use your program through the `typer` command). -Nowadays, there are several ways and tools to create Python packages (what you install with `pip install something`). +Nowadays, there are several ways and tools to create Python packages (what you install with `pip install something` or `uv add something`). You might even have your favorite already. Here's a very opinionated, short guide, showing one of the alternative ways of creating a Python package with a **Typer** app, from scratch. -!!! tip - If you already have a favorite way of creating Python packages, feel free to skip this. +/// tip + +If you already have a favorite way of creating Python packages, feel free to skip this. + +/// ## Prerequisites -For this guide we'll use Poetry. +For this guide we'll use [uv](https://docs.astral.sh/uv/). -Poetry's docs are great, so go ahead, check them and install it. +uv's docs are great, so go ahead, check them and install it. ## Create a project @@ -27,14 +32,14 @@ To make sure your package doesn't collide with the package created by someone el So, if your name is Rick, we'll call it `rick-portal-gun`. -Create a project with Poetry: +Create a project with uv:
```console -$ poetry new rick-portal-gun +$ uv init --package rick-portal-gun -Created package rick_portal_gun in rick-portal-gun +Initialized project `rick-portal-gun` at `/home/rick-portal-gun` // Enter the new project directory cd ./rick-portal-gun @@ -44,46 +49,34 @@ cd ./rick-portal-gun ## Dependencies and environment -Add `typer[all]` to your dependencies: +Add `typer` to your dependencies:
```console -$ poetry add typer[all] +$ uv add typer // It creates a virtual environment for your project -Creating virtualenv rick-portal-gun-w31dJa0b-py3.6 in /home/rick/.cache/pypoetry/virtualenvs -Using version ^0.1.0 for typer - -Updating dependencies -Resolving dependencies... (1.2s) - -Writing lock file - ----> 100% +Using CPython 3.14.0 interpreter at: /location/of/python/ +Creating virtual environment at: .venv + +Resolved 9 packages in 21ms + Built rick-portal-gun @ file:/home/rick-portal-gun +Prepared 1 package in 19ms +Installed 9 packages in 34ms + + colorama==0.4.6 + + markdown-it-py==4.0.0 + + mdurl==0.1.2 + + pygments==2.19.2 + + rich==14.2.0 + + rick-portal-gun==0.1.0 (from file:/home/rick-portal-gun) + + shellingham==1.5.4 + + typer==0.21.0 + + typing-extensions==4.15.0 -Package operations: 15 installs, 0 updates, 0 removals - - - Installing zipp (3.1.0) - - Installing importlib-metadata (1.5.0) - - Installing pyparsing (2.4.6) - - Installing six (1.14.0) - - Installing attrs (19.3.0) - - Installing click (7.1.1) - - Installing colorama (0.4.3) - - Installing more-itertools (8.2.0) - - Installing packaging (20.3) - - Installing pluggy (0.13.1) - - Installing py (1.8.1) - - Installing shellingham (1.3.2) - - Installing wcwidth (0.1.8) - - Installing pytest (5.4.1) - - Installing typer (0.0.11) // Activate that new virtual environment -$ poetry shell - -Spawning shell within /home/rick/.cache/pypoetry/virtualenvs/rick-portal-gun-w31dJa0b-py3.6 +$ source .venv/bin/activate // Open an editor using this new environment, for example VS Code $ code ./ @@ -95,21 +88,19 @@ You can see that you have a generated project structure that looks like: ``` . -├── poetry.lock ├── pyproject.toml -├── README.rst -├── rick_portal_gun -│   └── __init__.py -└── tests - ├── __init__.py - └── test_rick_portal_gun.py +├── README.md +├── src +│   └── rick_portal_gun +│     └── __init__.py +└── uv.lock ``` ## Create your app Now let's create an extremely simple **Typer** app. -Create a file `rick_portal_gun/main.py` with: +Create a file `src/rick_portal_gun/main.py` with: ```Python import typer @@ -141,18 +132,15 @@ def load(): typer.echo("Loading portal gun") ``` -!!! tip - As we are creating an installable Python package, there's no need to add a section with `if __name__ == "__main__":`. - -## Modify the README +/// tip -Let's change the README. By default it's a file `README.rst`. +As we are creating an installable Python package, there's no need to add a section with `if __name__ == "__main__":`. -Let's change it to `README.md`. So, change the extension from `.rst` to `.md`. +/// -So that we can use Markdown instead of reStructuredText. +## Modify the README -And change the file to have something like: +Let's change the README to have something like: ```Markdown # Portal Gun @@ -160,84 +148,32 @@ And change the file to have something like: The awesome Portal Gun ``` -## Modify your project metadata - -Edit your file `pyproject.toml`. - -It would look something like: - -```TOML -[tool.poetry] -name = "rick-portal-gun" -version = "0.1.0" -description = "" -authors = ["Rick Sanchez "] - -[tool.poetry.dependencies] -python = "^3.6" -typer = {extras = ["all"], version = "^0.1.0"} - -[tool.poetry.dev-dependencies] -pytest = "^5.2" - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" -``` - -We changed the default README, so let's make it use the new `README.md`. - -Add the line: - -```TOML hl_lines="6" -[tool.poetry] -name = "rick-portal-gun" -version = "0.1.0" -description = "" -authors = ["Rick Sanchez "] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.6" -typer = {extras = ["all"], version = "^0.1.0"} - -[tool.poetry.dev-dependencies] -pytest = "^5.2" - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" -``` - ## Add a "script" -We are creating a Python package that can be installed with `pip install`. +We are creating a Python package that can be installed with `uv add` or `pip install`. But we want it to provide a CLI program that can be executed in the shell. -To do that, we add a configuration to the `pyproject.toml` in the section `[tool.poetry.scripts]`: +To do that, we add a configuration to the `pyproject.toml` in the section `[project.scripts]`: -```TOML hl_lines="8 9" -[tool.poetry] +```TOML hl_lines="12 13" +[project] name = "rick-portal-gun" version = "0.1.0" -description = "" -authors = ["Rick Sanchez "] +description = "Add your description here" readme = "README.md" +authors = ["Rick Sanchez "] +requires-python = ">=3.14" +dependencies = [ + "typer>=0.21.0", +] -[tool.poetry.scripts] +[project.scripts] rick-portal-gun = "rick_portal_gun.main:app" -[tool.poetry.dependencies] -python = "^3.6" -typer = {extras = ["all"], version = "^0.1.0"} - -[tool.poetry.dev-dependencies] -pytest = "^5.2" - [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["uv_build>=0.8.14,<0.9.0"] +build-backend = "uv_build" ``` Here's what that line means: @@ -264,10 +200,11 @@ The `app` in `"rick_portal_gun.main:app"` is the thing to import from the module ```Python from rick_portal_gun.main import app + app() ``` -That config section tells Poetry that when this package is installed we want it to create a command line program called `rick-portal-gun`. +That config section tells uv that when this package is installed, we want it to create a command line program called `rick-portal-gun`. And that the object to call (like a function) is the one in the variable `app` inside of the module `rick_portal_gun.main`. @@ -280,20 +217,22 @@ You can now install it:
```console -$ poetry install - -Installing dependencies from lock file +$ uv sync -No dependencies to install or update +Resolved 10 packages in 1ms + Built rick-portal-gun @ file:/home/rick-portal-gun +Prepared 1 package in 18ms +Uninstalled 1 package in 1ms +Installed 1 package in 13ms + ~ rick-portal-gun==0.1.0 (from file:/home/rick-portal-gun) - - Installing rick-portal-gun (0.1.0) ```
## Try your CLI program -Your package is installed in the environment created by Poetry, but you can already use it. +Your package is installed in the environment created by uv, but you can already use it.
@@ -302,10 +241,10 @@ Your package is installed in the environment created by Poetry, but you can alre $ which rick-portal-gun // You get the one from your environment -/home/rick/.cache/pypoetry/virtualenvs/rick-portal-gun-w31dJa0b-py3.6/bin/rick-portal-gun +/home/rick-portal-gun/.venv/bin/rick-portal-gun // Try it -$ rick-portal-gun +$ rick-portal-gun --help // You get all the standard help Usage: rick-portal-gun [OPTIONS] COMMAND [ARGS]... @@ -319,8 +258,8 @@ Options: --help Show this message and exit. Commands: - load Load the portal gun shoot Shoot the portal gun + load Load the portal gun ```
@@ -329,19 +268,17 @@ Commands: Python packages have a standard format called a "wheel". It's a file that ends in `.whl`. -You can create a wheel with Poetry: +You can create a wheel with uv:
```console -$ poetry build - -Building rick-portal-gun (0.1.0) - - Building sdist - - Built rick-portal-gun-0.1.0.tar.gz +$ uv build - - Building wheel - - Built rick_portal_gun-0.1.0-py3-none-any.whl +Building source distribution (uv build backend)... +Building wheel from source distribution (uv build backend)... +Successfully built dist/rick_portal_gun-0.1.0.tar.gz +Successfully built dist/rick_portal_gun-0.1.0-py3-none-any.whl ```
@@ -362,25 +299,23 @@ The `.whl` is the wheel file. You can send that wheel file to anyone and they ca ## Test your wheel package -Now you can open another terminal and install that package from the file for your own user with: +Now you can open another terminal and install that package as an isolated command-line tool with:
```console -$ pip install --user /home/rock/code/rick-portal-gun/dist/rick_portal_gun-0.1.0-py3-none-any.whl +$ uv tool install /home/rick/rick-portal-gun/dist/rick_portal_gun-0.1.0-py3-none-any.whl ---> 100% ```
-!!! warning - The `--user` is important, that ensures you install it in your user's directory and not in the global system. +/// tip - If you installed it in the global system (e.g. with `sudo`) you could install a version of a library (e.g. a sub-dependency) that is incompatible with your system. +`uv` installs the tool in its own isolated environment, avoiding conflicts with your projects and system Python. -!!! tip - Bonus points if you use `pipx` to install it while keeping an isolated environment for your Python CLI programs 🚀 +/// Now you have your CLI program installed. And you can use it freely: @@ -402,14 +337,17 @@ Having it installed globally (and not in a single environment), you can now inst ```console $ rick-portal-gun --install-completion -zsh completion installed in /home/user/.zshrc. +zsh completion installed in /home/rick/.zshrc. Completion will take effect once you restart the terminal. ```
-!!! tip - If you want to remove completion you can just delete the added line in that file. +/// tip + +If you want to remove completion you can just delete the added line in that file. + +/// And after you restart the terminal you will get completion for your new CLI program: @@ -453,8 +391,11 @@ Here we pass `pip` as the value for `-m`, so, Python will execute the module `pi These two are more or less equivalent, the `install fastapi` will be passed to `pip`. -!!! tip - In the case of `pip`, in many occasions it's actually recommended that you run it with `python -m`, because if you create a virtual environment with its own `python`, that will ensure that you use the `pip` from *that* environment. +/// tip + +In the case of `pip`, in many occasions it's actually recommended that you run it with `python -m`, because if you create a virtual environment with its own `python`, that will ensure that you use the `pip` from *that* environment. + +/// ### Add a `__main__.py` @@ -462,19 +403,18 @@ You can support that same style of calling the package/module for your own packa Python will look for that file and execute it. -The file would live right beside `__init__.py`: +The file would live right beside `__init__.py` and `main.py`: ``` hl_lines="7" . -├── poetry.lock ├── pyproject.toml -├── README.rst -├── rick_portal_gun -│ ├── __init__.py -│ └── __main__.py -└── tests - ├── __init__.py - └── test_rick_portal_gun.py +├── README.md +├── src +│   └── rick_portal_gun +│     ├── __init__.py +│     ├── __main__.py +│     └── main.py +└── uv.lock ``` No other file has to import it, you don't have to reference it in your `pyproject.toml` or anything else, it just works by default, as it is standard Python behavior. @@ -483,17 +423,18 @@ Then in that file you can execute your **Typer** program: ```Python from .main import app + app() ``` -Now, after installing your package, if you call it with `python -m` it will work (for the main part): +Now, after installing your package, if you call it with `python -m` it will work:
```console -$ python -m rick_portal_gun +$ python -m rick_portal_gun --help -Usage: __main__.py [OPTIONS] COMMAND [ARGS]... +Usage: python -m rick_portal_gun [OPTIONS] COMMAND [ARGS]... Awesome Portal Gun @@ -504,65 +445,19 @@ Options: --help Show this message and exit. Commands: - load Load the portal gun shoot Shoot the portal gun + load Load the portal gun ```
-!!! tip - Notice that you have to pass the importable version of the package name, so `rick_portal_gun` instead of `rick-portal-gun`. - -That works! 🚀 Sort of... 🤔 +/// tip -See the `__main__.py` in the help instead of `rick-portal-gun`? We'll fix that next. +Notice that you have to pass the importable version of the package name, so `rick_portal_gun` instead of `rick-portal-gun`. -### Set a program name in `__main__.py` +/// -We are setting the program name in the file `pyproject.toml` in the line like: - -```TOML -[tool.poetry.scripts] -rick-portal-gun = "rick_portal_gun.main:app" -``` - -But when Python runs our package as a script with `python -m`, it doesn't have the information of the program name. - -So, to fix the help text to use the correct program name when called with `python -m`, we can pass it to the app in `__main__.py`: - -```Python -from .main import app -app(prog_name="rick-portal-gun") -``` - -!!! tip - You can pass all the arguments and keyword arguments you could pass to a Click application, including `prog_name`. - -
- -```console -$ python -m rick_portal_gun - -Usage: rick-portal-gun [OPTIONS] COMMAND [ARGS]... - - Awesome Portal Gun - -Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - - --help Show this message and exit. - -Commands: - load Load the portal gun - shoot Shoot the portal gun -``` - -
- -Great! That works correctly! 🎉 ✅ - -Notice that now it uses `rick-portal-gun` instead of `__main__.py` in the help. +That works! 🚀 ### Autocompletion and `python -m` @@ -584,7 +479,7 @@ But you can still support `python -m` for the cases where it's useful. ## Publish to PyPI (optional) -You can publish that new package to PyPI to make it public, so others can install it easily. +You can publish that new package to [PyPI](https://pypi.org/) to make it public, so others can install it easily. So, go ahead and create an account there (it's free). @@ -592,9 +487,9 @@ So, go ahead and create an account there (it's free). To do it, you first need to configure a PyPI auth token. -Login to PyPI. +Login to [PyPI](https://pypi.org/). -And then go to https://pypi.org/manage/account/token/ to create a new token. +And then go to [https://pypi.org/manage/account/token/](https://pypi.org/manage/account/token/) to create a new token. Let's say your new API token is: @@ -602,12 +497,12 @@ Let's say your new API token is: pypi-wubalubadubdub-deadbeef1234 ``` -Now configure Poetry to use this token with the command `poetry config pypi-token.pypi`: +Now configure uv to use this token by setting an environment variable:
```console -$ poetry config pypi-token.pypi pypi-wubalubadubdub-deadbeef1234 +$ export UV_PUBLISH_TOKEN=pypi-wubalubadubdub-deadbeef1234 // It won't show any output, but it's already configured ``` @@ -615,74 +510,42 @@ $ poetry config pypi-token.pypi pypi-wubalubadubdub-deadbeef1234 ### Publish to PyPI -Now you can publish your package with Poetry. - -You could build the package (as we did above) and then publish later, or you could tell poetry to build it before publishing in one go: +Now you can publish your package.
```console -$ poetry publish --build - -# There are 2 files ready for publishing. Build anyway? (yes/no) [no] $ yes +$ uv publish ----> 100% - -Building rick-portal-gun (0.1.0) - - Building sdist - - Built rick-portal-gun-0.1.0.tar.gz - - - Building wheel - - Built rick_portal_gun-0.1.0-py3-none-any.whl - -Publishing rick-portal-gun (0.1.0) to PyPI - - Uploading rick-portal-gun-0.1.0.tar.gz 100% - - Uploading rick_portal_gun-0.1.0-py3-none-any.whl 100% +Publishing 2 files https://upload.pypi.org/legacy/ +Uploading rick_portal_gun-0.1.0-py3-none-any.whl (2.3KiB) +Uploading rick_portal_gun-0.1.0.tar.gz (841.0B) ```
-Now you can go to PyPI and check your projects at https://pypi.org/manage/projects/. +Now you can go to PyPI and check your projects at [https://pypi.org/manage/projects/](https://pypi.org/manage/projects/). You should now see your new "rick-portal-gun" package. ### Install from PyPI -Now to see that we can install it form PyPI, open another terminal, and uninstall the currently installed package. +Now to see that we can install it from PyPI, open another terminal, and uninstall the currently installed package.
```console -$ pip uninstall rick-portal-gun - -Found existing installation: rick-portal-gun 0.1.0 -Uninstalling rick-portal-gun-0.1.0: - Would remove: - /home/user/.local/bin/rick-portal-gun - /home/user/.local/lib/python3.6/site-packages/rick_portal_gun-0.1.0.dist-info/* - /home/user/.local/lib/python3.6/site-packages/rick_portal_gun/* -# Proceed (y/n)? $ y - Successfully uninstalled rick-portal-gun-0.1.0 +$ uv tool uninstall rick-portal-gun ```
-And now install it again, but this time using just the name, so that `pip` pulls it from PyPI: +And now install it again, but this time using just the name, so that `uv` pulls it from PyPI:
```console -$ pip install --user rick-portal-gun - -// Notice that it says "Downloading" 🚀 -Collecting rick-portal-gun - Downloading rick_portal_gun-0.1.0-py3-none-any.whl (1.8 kB) -Requirement already satisfied: typer[all]<0.0.12,>=0.0.11 in ./.local/lib/python3.6/site-packages (from rick-portal-gun) (0.0.11) -Requirement already satisfied: click<7.2.0,>=7.1.1 in ./anaconda3/lib/python3.6/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (7.1.1) -Requirement already satisfied: colorama; extra == "all" in ./anaconda3/lib/python3.6/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (0.4.3) -Requirement already satisfied: shellingham; extra == "all" in ./anaconda3/lib/python3.6/site-packages (from typer[all]<0.0.12,>=0.0.11->rick-portal-gun) (1.3.1) -Installing collected packages: rick-portal-gun -Successfully installed rick-portal-gun-0.1.0 +$ uv tool install rick-portal-gun ```
@@ -700,11 +563,9 @@ Loading portal gun
-## Generate docs with **Typer CLI** (optional) - -You can install and use [Typer CLI](../typer-cli.md){.internal-link target=_blank} to generate docs for your package. +## Generate docs -After installing it, you can use it to generate a new `README.md`: +You can use the `typer` command to generate docs for your package that you can put in your `README.md`:
@@ -727,32 +588,23 @@ Now you can publish a new version with the updated docs. For that you need to first increase the version in `pyproject.toml`: ```TOML hl_lines="3" -[tool.poetry] +[project] name = "rick-portal-gun" version = "0.2.0" -description = "" -authors = ["Rick Sanchez "] +description = "Add your description here" readme = "README.md" +authors = ["Rick Sanchez "] +requires-python = ">=3.14" +dependencies = [ + "typer>=0.21.0", +] -[tool.poetry.scripts] +[project.scripts] rick-portal-gun = "rick_portal_gun.main:app" -[tool.poetry.dependencies] -python = "^3.6" -typer = {extras = ["all"], version = "^0.1.0"} - -[tool.poetry.dev-dependencies] -pytest = "^5.2" - [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" -``` - -And in the file `rick_portal_gun/__init__.py`: - -```Python -__version__ = '0.2.0' +requires = ["uv_build>=0.8.14,<0.9.0"] +build-backend = "uv_build" ``` And then build and publish again: @@ -760,20 +612,12 @@ And then build and publish again:
```console -$ poetry publish --build - ----> 100% +$ uv build +$ uv publish -Building rick-portal-gun (0.2.0) - - Building sdist - - Built rick-portal-gun-0.2.0.tar.gz - - - Building wheel - - Built rick_portal_gun-0.2.0-py3-none-any.whl - -Publishing rick-portal-gun (0.2.0) to PyPI - - Uploading rick-portal-gun-0.2.0.tar.gz 100% - - Uploading rick_portal_gun-0.2.0-py3-none-any.whl 100% +Publishing 2 files https://upload.pypi.org/legacy/ +Uploading rick_portal_gun-0.2.0-py3-none-any.whl (2.3KiB) +Uploading rick_portal_gun-0.2.0.tar.gz (840.0B) ```
@@ -784,16 +628,14 @@ And now you can go to PyPI, to the project page, and reload it, and it will now This is a very simple guide. You could add many more steps. -For example, you should use Git, the version control system, to save your code. - -You can add a lot of extra metadata to your `pyproject.toml`, check the docs for Poetry: Libraries. +For example, you should use [Git](https://git-scm.com/), the version control system, to save your code. -You could use `pipx` to manage your installed CLI Python programs in isolated environments. +You could use [uv](https://docs.astral.sh/uv/) to manage your installed CLI Python programs in isolated environments. -Maybe use automatic formatting with Black. +Maybe use automatic formatting with [Ruff](https://docs.astral.sh/ruff/). -You'll probably want to publish your code as open source to GitHub. +You'll probably want to publish your code as open source to [GitHub](https://github.com/). And then you could integrate a CI tool to run your tests and deploy your package automatically. -And there's a long etc. But now you have the basics and you can continue on your own 🚀. +And there's a long etc. But now you have the basics and you can continue on your own. 🚀 diff --git a/docs/tutorial/parameter-types/bool.md b/docs/tutorial/parameter-types/bool.md index 1f493eca8d..fc3ff07af2 100644 --- a/docs/tutorial/parameter-types/bool.md +++ b/docs/tutorial/parameter-types/bool.md @@ -1,3 +1,5 @@ +# Boolean CLI Options + We have seen some examples of *CLI options* with `bool`, and how **Typer** creates `--something` and `--no-something` automatically. But we can customize those names. @@ -8,9 +10,7 @@ Let's say that we want a `--force` *CLI option* only, we want to discard `--no-f We can do that by specifying the exact name we want: -```Python hl_lines="4" -{!../docs_src/parameter_types/bool/tutorial001.py!} -``` +{* docs_src/parameter_types/bool/tutorial001.py hl[9] *} Now there's only a `--force` *CLI option*: @@ -18,34 +18,32 @@ Now there's only a `--force` *CLI option*: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice there's only --force, we no longer have --no-force Usage: main.py [OPTIONS] Options: - --force [default: False] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + --force --help Show this message and exit. // Try it: -$ python main.py +$ uv run python main.py Not forcing // Now add --force -$ python main.py --force +$ uv run python main.py --force Forcing operation // And --no-force no longer exists ⛔️ -$ python main.py --no-force +$ uv run python main.py --no-force Usage: main.py [OPTIONS] Try "main.py --help" for help. -Error: no such option: --no-force +Error: No such option: --no-force ```
@@ -60,9 +58,7 @@ We might want to instead have `--accept` and `--reject`. We can do that by passing a single `str` with the 2 names for the `bool` *CLI option* separated by `/`: -```Python hl_lines="6" -{!../docs_src/parameter_types/bool/tutorial002.py!} -``` +{* docs_src/parameter_types/bool/tutorial002.py hl[9] *} Check it: @@ -70,29 +66,27 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the --accept / --reject Usage: main.py [OPTIONS] Options: --accept / --reject - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Try it -$ python main.py +$ uv run python main.py I don't know what you want yet // Now pass --accept -$ python main.py --accept +$ uv run python main.py --accept Accepting! // And --reject -$ python main.py --reject +$ uv run python main.py --reject Rejecting! ``` @@ -105,9 +99,7 @@ The same way, you can declare short versions of the names for these *CLI options For example, let's say we want `-f` for `--force` and `-F` for `--no-force`: -```Python hl_lines="4" -{!../docs_src/parameter_types/bool/tutorial003.py!} -``` +{* docs_src/parameter_types/bool/tutorial003.py hl[9] *} Check it: @@ -115,24 +107,22 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the -f, --force / -F, --no-force Usage: main.py [OPTIONS] Options: - -f, --force / -F, --no-force [default: False] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + -f, --force / -F, --no-force [default: no-force] --help Show this message and exit. // Try with the short name -f -$ python main.py -f +$ uv run python main.py -f Forcing operation // Try with the short name -F -$ python main.py -F +$ uv run python main.py -F Not forcing ``` @@ -145,14 +135,15 @@ If you want to (although it might not be a good idea), you can declare only *CLI To do that, use a space and a single `/` and pass the negative name after: -```Python hl_lines="4" -{!../docs_src/parameter_types/bool/tutorial004.py!} -``` +{* docs_src/parameter_types/bool/tutorial004.py hl[9] *} + +/// tip + +Have in mind that it's a string with a preceding space and then a `/`. -!!! tip - Have in mind that it's a string with a preceding space and then a `/`. +So, it's `" /-S"` not `"/-S"`. - So, it's `" /-S"` not `"/-S"`. +/// Check it: @@ -160,29 +151,27 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the / -d, --demo Usage: main.py [OPTIONS] Options: / -d, --demo [default: True] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Try it -$ python main.py +$ uv run python main.py Running in production // Now pass --demo -$ python main.py --demo +$ uv run python main.py --demo Running demo // And the short version -$ python main.py -d +$ uv run python main.py -d Running demo ``` diff --git a/docs/tutorial/parameter-types/custom-types.md b/docs/tutorial/parameter-types/custom-types.md new file mode 100644 index 0000000000..c78da59837 --- /dev/null +++ b/docs/tutorial/parameter-types/custom-types.md @@ -0,0 +1,13 @@ +# Custom Types + +You can easily use your own custom types in your **Typer** applications. + +The way to do it is by providing a way to parse input into your own types. + +## Type Parser + +`typer.Argument` and `typer.Option` can create custom parameter types with a `parser` callable. + +{* docs_src/parameter_types/custom_types/tutorial001.py hl[14:15,23:24] *} + +The function (or callable) that you pass to the parameter `parser` will receive the input value as a string and should return the parsed value with your own custom type. diff --git a/docs/tutorial/parameter-types/datetime.md b/docs/tutorial/parameter-types/datetime.md index 0a392470a0..7ba68a0209 100644 --- a/docs/tutorial/parameter-types/datetime.md +++ b/docs/tutorial/parameter-types/datetime.md @@ -1,46 +1,47 @@ -You can specify a *CLI parameter* as a Python `datetime`. +# DateTime -Your function will receive a standard Python `datetime` object, and again, your editor will give you completion, etc. +You can specify a *CLI parameter* as a Python [`datetime`](https://docs.python.org/3/library/datetime.html). -```Python hl_lines="1 6 7 8" -{!../docs_src/parameter_types/datetime/tutorial001.py!} -``` +Your function will receive a standard Python `datetime` object, and again, your editor will give you completion, etc. -Typer will accept any string from the following formats: +{* docs_src/parameter_types/datetime/tutorial001.py hl[1,9,10,11] *} +Typer will accept any string from datetime formats [supported by Pydantic](https://pydantic.dev/docs/validation/latest/api/pydantic/standard_library_types/#validation-9), +including Unix timestamps as seconds or milliseconds since the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time), +and several 'standard' options such as * `%Y-%m-%d` * `%Y-%m-%dT%H:%M:%S` -* `%Y-%m%d %H:%M:%S` +* `%Y-%m-%d %H:%M:%S` +* ... Check it:
```console -$ python main.py --help +$ uv run python main.py --help -Usage: main.py [OPTIONS] BIRTH:[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S] +Usage: main.py [OPTIONS] {birth} Arguments: - BIRTH:[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S][required] + birth [required] Options: - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. --help Show this message and exit. // Pass a datetime -$ python main.py 1956-01-31T10:00:00 +$ uv run python main.py 1956-01-31T10:00:00 Interesting day to be born: 1956-01-31 10:00:00 Birth hour: 10 // An invalid date -$ python main.py july-19-1989 +$ uv run python main.py july-19-1989 -Usage: main.py [OPTIONS] [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d%H:%M:%S] +Usage: main.py [OPTIONS] {birth} +Try 'main.py --help' for help. -Error: Invalid value for '[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]': invalid datetime format: july-19-1989. (choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S) +Error: Invalid value for 'birth': Input should be a valid datetime or date, invalid character in year ```
@@ -49,7 +50,7 @@ Error: Invalid value for '[%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]': inval You can also customize the formats received for the `datetime` with the `formats` parameter. -`formats` receives a list of strings with the date formats that would be passed to datetime.strptime(). +`formats` receives a list of strings with the date formats that would be passed to [datetime.strptime()](https://docs.python.org/3/library/datetime.html#datetime.datetime.strptime). For example, let's imagine that you want to accept an ISO formatted datetime, but for some strange reason, you also want to accept a format with: @@ -60,27 +61,37 @@ For example, let's imagine that you want to accept an ISO formatted datetime, bu ...It's a crazy example, but let's say you also needed that strange format: -```Python hl_lines="8" -{!../docs_src/parameter_types/datetime/tutorial002.py!} -``` +{* docs_src/parameter_types/datetime/tutorial002.py hl[13] *} + +/// tip -!!! tip - Notice the last string in `formats`: `"%m/%d/%Y"`. +Notice the last string in `formats`: `"%m/%d/%Y"`. + +/// Check it:
```console -// ISO dates work -$ python main.py 1969-10-29 +// ISO dates work (first pattern) +$ uv run python main.py 1969-10-29 Launch will be at: 1969-10-29 00:00:00 -// But the strange custom format also works -$ python main.py 10/29/1969 +// The strange custom format also works (second pattern) +$ uv run python main.py 10/29/1969 Launch will be at: 1969-10-29 00:00:00 + +// But notice that a 'standard' date format now doesn't work anymore, as it's not included in 'formats': +$ uv run python main.py 1969-10-29T10:00:00 + +Usage: main.py [OPTIONS] {launch_date} +Try 'main.py --help' for help. + +Error: Invalid value for 'launch_date': Value error, '1969-10-29T10:00:00' does not match the formats '%Y-%m-%d', '%m/%d/%Y'. + ```
diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 6af834bf54..af43df4d3f 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -1,42 +1,51 @@ -To define a *CLI parameter* that can take a value from a predefined set of values you can use a standard Python `enum.Enum`: +# Enum - Choices -```Python hl_lines="1 6 7 8 9 12 13" -{!../docs_src/parameter_types/enum/tutorial001.py!} -``` +To define a *CLI parameter* that can take a value from a predefined set of values you can use a standard Python [`enum.Enum`](https://docs.python.org/3/library/enum.html): + +{* docs_src/parameter_types/enum/tutorial001.py hl[1,6:9,16:17] *} + +/// tip + +Notice that the function parameter `network` will be an `Enum`, not a `str`. -!!! tip - Notice that the function parameter `network` will be an `Enum`, not a `str`. +To get the `str` value in your function's code use `network.value`. - To get the `str` value in your function's code use `network.value`. +/// Check it:
```console -$ python main.py --help +$ uv run python main.py --help -// Notice the predefined values [simple|conv|lstm] +// Notice the predefined values Usage: main.py [OPTIONS] Options: - --network [simple|conv|lstm] [default: simple] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. + --network [default: simple] --help Show this message and exit. // Try it -$ python main.py --network conv +$ uv run python main.py --network conv Training neural network of type: conv // Invalid value -$ python main.py --network capsule +$ uv run python main.py --network capsule + +Usage: main.py [OPTIONS] +Try "main.py --help" for help. + +Error: Invalid value for '--network': 'capsule' is not one of 'simple', 'conv', 'lstm'. + +// Note that enums are case sensitive by default +$ uv run python main.py --network CONV Usage: main.py [OPTIONS] Try "main.py --help" for help. -Error: Invalid value for '--network': invalid choice: capsule. (choose from simple, conv, lstm) +Error: Invalid value for '--network': 'CONV' is not one of 'simple', 'conv', 'lstm'. ```
@@ -45,9 +54,7 @@ Error: Invalid value for '--network': invalid choice: capsule. (choose from simp You can make an `Enum` (choice) *CLI parameter* be case-insensitive with the `case_sensitive` parameter: -```Python hl_lines="13" -{!../docs_src/parameter_types/enum/tutorial002.py!} -``` +{* docs_src/parameter_types/enum/tutorial002.py hl[19] *} And then the values of the `Enum` will be checked no matter if lower case, upper case, or a mix: @@ -55,14 +62,86 @@ And then the values of the `Enum` will be checked no matter if lower case, upper ```console // Notice the upper case CONV -$ python main.py --network CONV +$ uv run python main.py --network CONV Training neural network of type: conv // A mix also works -$ python main.py --network LsTm +$ uv run python main.py --network LsTm Training neural network of type: lstm ```
+ +### List of Enum values + +A *CLI parameter* can also take a list of `Enum` values: + +{* docs_src/parameter_types/enum/tutorial003.py hl[17] *} + +This works just like any other parameter value taking a list of things: + +
+ +```console +$ uv run python main.py --help + +// Notice the default values being shown +Usage: main.py [OPTIONS] + +Options: + --groceries [default: Eggs, Cheese] + --help Show this message and exit. + +// Try it with the default values +$ uv run python main.py + +Buying groceries: Eggs, Cheese + +// Try it with a single value +$ uv run python main.py --groceries "Eggs" + +Buying groceries: Eggs + +// Try it with multiple values +$ uv run python main.py --groceries "Eggs" --groceries "Bacon" + +Buying groceries: Eggs, Bacon +``` + +
+ +### Literal choices + +You can also use `Literal` to represent a set of possible predefined choices, without having to use an `Enum`: + +{* docs_src/parameter_types/enum/tutorial004.py hl[10] *} + +
+ +```console +$ uv run python main.py --help + +// Notice the predefined values +Usage: main.py [OPTIONS] + +Options: + --network [default: simple] + --help Show this message and exit. + +// Try it +$ uv run python main.py --network conv + +Training neural network of type: conv + +// Invalid value +$ uv run python main.py --network capsule + +Usage: main.py [OPTIONS] +Try "main.py --help" for help. + +Error: Invalid value for '--network': 'capsule' is not one of 'simple', 'conv', 'lstm'. +``` + +
diff --git a/docs/tutorial/parameter-types/file.md b/docs/tutorial/parameter-types/file.md index 0afe92dbfc..98a960076d 100644 --- a/docs/tutorial/parameter-types/file.md +++ b/docs/tutorial/parameter-types/file.md @@ -1,16 +1,21 @@ +# File + Apart from `Path` *CLI parameters* you can also declare some types of "files". -!!! tip - In most of the cases you are probably fine just using `Path`. +/// tip + +In most of the cases you are probably fine just using `Path`. + +You can read and write data with `Path` the same way. - You can read and write data with `Path` the same way. +/// -The difference is that these types will give you a Python file-like object instead of a Python Path. +The difference is that these types will give you a Python [file-like object](https://docs.python.org/3/glossary.html#term-file-object) instead of a Python [Path](https://docs.python.org/3/library/pathlib.html#basic-use). A "file-like object" is the same type of object returned by `open()` as in: ```Python -with open('file.txt') as f: +with open("file.txt") as f: # Here f is the file-like object read_data = f.read() print(read_data) @@ -40,11 +45,9 @@ instead of having `bytes`, e.g.: content = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o" ``` -You will get all the correct editor support, attributes, methods, etc for the file-like object: +You will get all the correct editor support, attributes, methods, etc for the file-like object:` -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial001.py!} -``` +{* docs_src/parameter_types/file/tutorial001.py hl[9] *} Check it: @@ -58,7 +61,7 @@ $ echo "some settings" > config.txt $ echo "some more settings" >> config.txt // Now run your program -$ python main.py --config config.txt +$ uv run python main.py --config config.txt Config line: some settings @@ -71,9 +74,7 @@ Config line: some more settings For writing text, you can use `typer.FileTextWrite`: -```Python hl_lines="4 5" -{!../docs_src/parameter_types/file/tutorial002.py!} -``` +{* docs_src/parameter_types/file/tutorial002.py hl[9:10] *} This would be for writing human text, like: @@ -89,7 +90,7 @@ Check it:
```console -$ python main.py --config text.txt +$ uv run python main.py --config text.txt Config written @@ -101,10 +102,13 @@ Some config written by the app
-!!! info "Technical Details" - `typer.FileTextWrite` is a just a convenience class. +/// note | Technical Details - It's the same as using `typer.FileText` and setting `mode="w"`. You will learn about `mode` later below. +`typer.FileTextWrite` is a just a convenience class. + +It's the same as using `typer.FileText` and setting `mode="w"`. You will learn about `mode` later below. + +/// ## `FileBinaryRead` @@ -114,16 +118,14 @@ You will receive `bytes` from it. It's useful for reading binary files like images: -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial003.py!} -``` +{* docs_src/parameter_types/file/tutorial003.py hl[9] *} Check it:
```console -$ python main.py --file lena.jpg +$ uv run python main.py --file lena.jpg Processed bytes total: 512 Processed bytes total: 1024 @@ -145,14 +147,12 @@ Have in mind that you have to pass `bytes` to its `.write()` method, not `str`. If you have a `str`, you have to encode it first to get `bytes`. -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial004.py!} -``` +{* docs_src/parameter_types/file/tutorial004.py hl[9] *}
```console -$ python main.py --file binary.dat +$ uv run python main.py --file binary.dat Binary file written @@ -168,17 +168,17 @@ $ ls ./binary.dat You can use several configuration parameters for these types (classes) in `typer.Option()` and `typer.Argument()`: -* `mode`: controls the "mode" to open the file with. +* `mode`: controls the "[mode](https://docs.python.org/3/library/functions.html#open)" to open the file with. * It's automatically set for you by using the classes above. * Read more about it below. * `encoding`: to force a specific encoding, e.g. `"utf-8"`. * `lazy`: delay I/O operations. Automatic by default. - * By default, when writing files, Click will generate a file-like object that is not yet the actual file. Once you start writing, it will go, open the file and start writing to it, but not before. This is mainly useful to avoid creating the file until you start writing to it. It's normally safe to leave this automatic. But you can overwrite it setting `lazy=False`. By default, it's `lazy=True` for writing and `lazy=False` for reading. + * By default, when writing files, Typer will generate a file-like object that is not yet the actual file. Once you start writing, it will go, open the file and start writing to it, but not before. This is mainly useful to avoid creating the file until you start writing to it. It's normally safe to leave this automatic. But you can overwrite it setting `lazy=False`. By default, it's `lazy=True` for writing and `lazy=False` for reading. * `atomic`: if true, all writes will actually go to a temporal file and then moved to the final destination after completing. This is useful with files modified frequently by several users/programs. ## Advanced `mode` -By default, **Typer** will configure the `mode` for you: +By default, **Typer** will configure the [`mode`](https://docs.python.org/3/library/functions.html#open) for you: * `typer.FileText`: `mode="r"`, to read text. * `typer.FileTextWrite`: `mode="w"`, to write text. @@ -197,28 +197,29 @@ You can override the `mode` from the defaults above. For example, you could use `mode="a"` to write "appending" to the same file: -```Python hl_lines="4" -{!../docs_src/parameter_types/file/tutorial005.py!} -``` +{* docs_src/parameter_types/file/tutorial005.py hl[9] *} -!!! tip - As you are manually setting `mode="a"`, you can use `typer.FileText` or `typer.FileTextWrite`, both will work. +/// tip + +As you are manually setting `mode="a"`, you can use `typer.FileText` or `typer.FileTextWrite`, both will work. + +/// Check it:
```console -$ python main.py --config config.txt +$ uv run python main.py --config config.txt Config line written // Run your program a couple more times to see how it appends instead of overwriting -$ python main.py --config config.txt +$ uv run python main.py --config config.txt Config line written -$ python main.py --config config.txt +$ uv run python main.py --config config.txt Config line written @@ -231,18 +232,3 @@ This is a single line ```
- -## About the different types - -!!! info - These are technical details about why the different types/classes provided by **Typer**. - - But you don't need this information to be able to use them. You can skip it. - -**Typer** provides you these different types (classes) because they inherit directly from the actual Python implementation that will be provided underneath for each case. - -This way your editor will give you the right type checks and completion for each type. - -Even if you use `lazy`. When you use `lazy` Click creates a especial object to delay writes, and serves as a "proxy" to the actual file that will be written. But this especial proxy object doesn't expose the attributes and methods needed for type checks and completion in the editor. If you access those attributes or call the methods, the "proxy" lazy object will call them in the final object and it will all work. But you wouldn't get autocompletion for them. - -But because these **Typer** classes inherit from the actual implementation that will be provided underneath (not the lazy object), you will get all the autocompletion and type checks in the editor. diff --git a/docs/tutorial/parameter-types/index.md b/docs/tutorial/parameter-types/index.md index db78037bf9..f5da3e92fc 100644 --- a/docs/tutorial/parameter-types/index.md +++ b/docs/tutorial/parameter-types/index.md @@ -1,3 +1,5 @@ +# CLI Parameter Types + You can use several data types for the *CLI options* and *CLI arguments*, and you can add data validation requirements too. ## Data conversion @@ -6,11 +8,9 @@ When you declare a *CLI parameter* with some type **Typer** will convert the dat For example: -```Python hl_lines="4" -{!../docs_src/parameter_types/index/tutorial001.py!} -``` +{* docs_src/parameter_types/index/tutorial001.py hl[7] *} -In this example, the value received for the *CLI argument* `NAME` will be treated as `str`. +In this example, the value received for the *CLI argument* `name` will be treated as `str`. The value for the *CLI option* `--age` will be converted to an `int` and `--height-meters` will be converted to a `float`. @@ -21,40 +21,38 @@ And here's how it looks like:
```console -$ python main.py --help +$ uv run python main.py --help -// Notice how --age is an INTEGER and --height-meters is a FLOAT -Usage: main.py [OPTIONS] NAME +// Notice how --age is an 'int' (integer) and --height-meters is a 'float' +Usage: main.py [OPTIONS] {name} Arguments: - NAME [required] + name [required] Options: - --age INTEGER [default: 20] - --height-meters FLOAT [default: 1.89] - --female / --no-female [default: True] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. + --age [default: 20] + --height-meters [default: 1.89] + --female / --no-female [default: female] + --help Show this message and exit. // Call it with CLI parameters -$ python main.py Camila --age 15 --height-meters 1.70 --female +$ uv run python main.py Camila --age 15 --height-meters 1.70 --female // All the data has the correct Python type -NAME is Camila, of type: class 'str' ---age is 15, of type: class 'int' ---height-meters is 1.7, of type: class 'float' ---female is True, of type: class 'bool' +name is Camila, of type: +--age is 15, of type: +--height-meters is 1.7, of type: +--female is True, of type: // And if you pass an incorrect type -$ python main.py Camila --age 15.3 +$ uv run python main.py Camila --age 15.3 -Usage: main.py [OPTIONS] NAME +Usage: main.py [OPTIONS] {name} Try "main.py --help" for help. -Error: Invalid value for '--age': 15.3 is not a valid integer +Error: Invalid value for '--age': Input should be a valid integer, unable to parse string as an integer -// Because 15.3 is not an INTEGER (it's a float) +// Because 15.3 is not an integer. ```
@@ -62,7 +60,3 @@ Error: Invalid value for '--age': 15.3 is not a valid integer ## Watch next See more about specific types and validations in the next sections... - - -!!! info "Technical Details" - All the types you will see in the next sections are handled underneath by Click's Parameter Types. diff --git a/docs/tutorial/parameter-types/number.md b/docs/tutorial/parameter-types/number.md index d41322d6fe..2e922f2a6e 100644 --- a/docs/tutorial/parameter-types/number.md +++ b/docs/tutorial/parameter-types/number.md @@ -1,8 +1,8 @@ +# Number + You can define numeric validations with `max` and `min` values for `int` and `float` *CLI parameters*: -```Python hl_lines="5 6 7" -{!../docs_src/parameter_types/number/tutorial001.py!} -``` +{* docs_src/parameter_types/number/tutorial001.py hl[10:12] *} *CLI arguments* and *CLI options* can both use these validations. @@ -13,54 +13,52 @@ Check it:
```console -$ python main.py --help +$ uv run python main.py --help -// Notice the extra RANGE in the help text for --age and --score -Usage: main.py [OPTIONS] ID +// Notice the extra range information in the help text for --age and --score +Usage: main.py [OPTIONS] {ID} Arguments: - ID [required] + ID [0<=x<=1000; required] Options: - --age INTEGER RANGE [default: 20] - --score FLOAT RANGE [default: 0] - --install-completion Install completion for the current shell. - --show-completion Show completion for the current shell, to copy it or customize the installation. - --help Show this message and exit. + --age [default: 20; x>=18] + --score [default: 0; x<=100] + --help Show this message and exit. // Pass all the CLI parameters -$ python main.py 5 --age 20 --score 90 +$ uv run python main.py 5 --age 20 --score 90 ID is 5 --age is 20 --score is 90.0 // Pass an invalid ID -$ python main.py 1002 +$ uv run python main.py 1002 -Usage: main.py [OPTIONS] ID +Usage: main.py [OPTIONS] {ID} Try "main.py --help" for help. -Error: Invalid value for 'ID': 1002 is not in the valid range of 0 to 1000. +Error: Invalid value for 'ID': Input should be less than or equal to 1000 // Pass an invalid age -$ python main.py 5 --age 15 +$ uv run python main.py 5 --age 15 -Usage: main.py [OPTIONS] ID +Usage: main.py [OPTIONS] {ID} Try "main.py --help" for help. -Error: Invalid value for '--age': 15 is smaller than the minimum valid value 18. +Error: Invalid value for '--age': Input should be greater than or equal to 18 // Pass an invalid score -$ python main.py 5 --age 20 --score 100.5 +$ uv run python main.py 5 --age 20 --score 100.5 -Usage: main.py [OPTIONS] ID +Usage: main.py [OPTIONS] {ID} Try "main.py --help" for help. -Error: Invalid value for '--score': 100.5 is bigger than the maximum valid value 100. +Error: Invalid value for '--score': Input should be less than or equal to 100 // But as we didn't specify a minimum score, this is accepted -$ python main.py 5 --age 20 --score -5 +$ uv run python main.py 5 --age 20 --score -5 ID is 5 --age is 20 @@ -75,9 +73,7 @@ You might want to, instead of showing an error, use the closest minimum or maxim You can do it with the `clamp` parameter: -```Python hl_lines="5 6 7" -{!../docs_src/parameter_types/number/tutorial002.py!} -``` +{* docs_src/parameter_types/number/tutorial002.py hl[10:12] *} And then, when you pass data that is out of the valid range, it will be "clamped", the closest valid value will be used: @@ -85,30 +81,28 @@ And then, when you pass data that is out of the valid range, it will be "clamped ```console // ID doesn't have clamp, so it shows an error -$ python main.py 1002 +$ uv run python main.py 1002 -Usage: main.py [OPTIONS] ID +Usage: main.py [OPTIONS] {ID} Try "main.py --help" for help. -Error: Invalid value for 'ID': 1002 is not in the valid range of 0 to 1000. +Error: Invalid value for 'ID': Input should be less than or equal to 1000 // But --rank and --score use clamp -$ python main.py 5 --rank 11 --score -5 +$ uv run python main.py 5 --rank 11 --score -5 ID is 5 --rank is 10 ---score is 0 +--score is 0.0 ```
## Counter *CLI options* -You can make a *CLI option* work as a counter with the `counter` parameter: +You can make a *CLI option* work as a counter with the `count` parameter: -```Python hl_lines="4" -{!../docs_src/parameter_types/number/tutorial003.py!} -``` +{* docs_src/parameter_types/number/tutorial003.py hl[9] *} It means that the *CLI option* will be like a boolean flag, e.g. `--verbose`. @@ -118,32 +112,32 @@ And the value you receive in the function will be the amount of times that `--ve ```console // Check it -$ python main.py +$ uv run python main.py Verbose level is 0 // Now use one --verbose -$ python main.py --verbose +$ uv run python main.py --verbose Verbose level is 1 // Now 3 --verbose -$ python main.py --verbose --verbose --verbose +$ uv run python main.py --verbose --verbose --verbose Verbose level is 3 // And with the short name -$ python main.py -v +$ uv run python main.py -v Verbose level is 1 // And with the short name 3 times -$ python main.py -v -v -v +$ uv run python main.py -v -v -v Verbose level is 3 // As short names can be put together, this also works -$ python main.py -vvv +$ uv run python main.py -vvv Verbose level is 3 ``` diff --git a/docs/tutorial/parameter-types/path.md b/docs/tutorial/parameter-types/path.md index 887ab37b80..dea4df9703 100644 --- a/docs/tutorial/parameter-types/path.md +++ b/docs/tutorial/parameter-types/path.md @@ -1,10 +1,10 @@ -You can declare a *CLI parameter* to be a standard Python `pathlib.Path`. +# Path + +You can declare a *CLI parameter* to be a standard Python [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#basic-use). This is what you would do for directory paths, file paths, etc: -```Python hl_lines="1 7" -{!../docs_src/parameter_types/path/tutorial001.py!} -``` +{* docs_src/parameter_types/path/tutorial001.py hl[1,10] *} And again, as you receive a standard Python `Path` object the same as the type annotation, your editor will give you autocompletion for all its attributes and methods. @@ -14,13 +14,13 @@ Check it: ```console // No config -$ python main.py +$ uv run python main.py +Aborted. No config file -Aborted! // Pass a config that doesn't exist -$ python main.py --config config.txt +$ uv run python main.py --config config.txt The config doesn't exist @@ -28,12 +28,12 @@ The config doesn't exist $ echo "some settings" > config.txt // And try again -$ python main.py --config config.txt +$ uv run python main.py --config config.txt Config file contents: some settings // And with a directory -$ python main.py --config ./ +$ uv run python main.py --config ./ Config is a directory, will use all its config files ``` @@ -51,55 +51,56 @@ You can perform several validations for `Path` *CLI parameters*: * `readable`: if true, a readable check is performed. * `resolve_path`: if this is true, then the path is fully resolved before the value is passed onwards. This means that it’s absolute and symlinks are resolved. -!!! note "Technical Details" - It will not expand a tilde-prefix (something with `~`, like `~/Documents/`), as this is supposed to be done by the shell only. +/// note | Technical Details + +It will not expand a tilde-prefix (something with `~`, like `~/Documents/`), as this is supposed to be done by the shell only. -!!! tip - All these parameters come directly from Click. +/// For example: -```Python hl_lines="9 10 11 12 13 14" -{!../docs_src/parameter_types/path/tutorial002.py!} -``` +{* docs_src/parameter_types/path/tutorial002.py hl[14:19] *} Check it:
```console -$ python main.py --config config.txt +$ uv run python main.py --config config.txt Usage: main.py [OPTIONS] Try "main.py --help" for help. -Error: Invalid value for '--config': File 'config.txt' does not exist. +Error: Invalid value for '--config': file 'config.txt' does not exist. // Now create a quick config $ echo "some settings" > config.txt // And try again -$ python main.py --config config.txt +$ uv run python main.py --config config.txt Config file contents: some settings // And with a directory -$ python main.py --config ./ +$ uv run python main.py --config ./ Usage: main.py [OPTIONS] Try "main.py --help" for help. -Error: Invalid value for '--config': File './' is a directory. +Error: Invalid value for '--config': file './' is a directory. ```
### Advanced `Path` configurations -!!! warning "Advanced Details" - You probably won't need these configurations at first, you may want to skip it. +/// warning | Advanced Details + +You probably won't need these configurations at first, you may want to skip it. + +They are used for more advanced use cases. - They are used for more advanced use cases. +/// * `allow_dash`: If this is set to True, a single dash to indicate standard streams is permitted. -* `path_type`: optionally a string type that should be used to represent the path. The default is None which means the return value will be either bytes or unicode depending on what makes most sense given the input data Click deals with. +* `path_type`: optionally a string type that should be used to represent the path. The default is `None` which means the return value will be either bytes or unicode depending on what makes most sense given the input data. diff --git a/docs/tutorial/parameter-types/uuid.md b/docs/tutorial/parameter-types/uuid.md index 0042f47061..1f263a8918 100644 --- a/docs/tutorial/parameter-types/uuid.md +++ b/docs/tutorial/parameter-types/uuid.md @@ -1,29 +1,30 @@ -!!! info - A UUID is a "Universally Unique Identifier". +# UUID - It's a standard format for identifiers, like passport numbers, but for anything, not just people in countries. +/// note - They look like this: +A UUID is a ["Universally Unique Identifier"](https://en.wikipedia.org/wiki/Universally_unique_identifier). - ``` - d48edaa6-871a-4082-a196-4daab372d4a1 - ``` +It's a standard format for identifiers, like passport numbers, but for anything, not just people in countries. - The way they are generated makes them sufficiently long and random that you could assume that every UUID generated is unique. Even if it was generated by a different application, database, or system. +They look like this: - So, if your system uses UUIDs to identify your data, you could mix it with the data from some other system that also uses UUIDs with some confidence that their IDs (UUIDs) won't clash with yours. +``` +d48edaa6-871a-4082-a196-4daab372d4a1 +``` - This wouldn't be true if you just used `int`s as identifiers, as most databases do. +The way they are generated makes them sufficiently long and random that you could assume that every UUID generated is unique. Even if it was generated by a different application, database, or system. +So, if your system uses UUIDs to identify your data, you could mix it with the data from some other system that also uses UUIDs with some confidence that their IDs (UUIDs) won't clash with yours. +This wouldn't be true if you just used `int`s as identifiers, as most databases do. + +/// You can declare a *CLI parameter* as a UUID: -```Python hl_lines="1 6 7 8" -{!../docs_src/parameter_types/uuid/tutorial001.py!} -``` +{* docs_src/parameter_types/uuid/tutorial001.py hl[1,9:11] *} -Your Python code will receive a standard Python `UUID` object with all its attributes and methods, and as you are annotating your function parameter with that type, you will have type checks, autocompletion in your editor, etc. +Your Python code will receive a standard Python [`UUID`](https://docs.python.org/3.8/library/uuid.html) object with all its attributes and methods, and as you are annotating your function parameter with that type, you will have type checks, autocompletion in your editor, etc. Check it: @@ -31,18 +32,18 @@ Check it: ```console // Pass a valid UUID v4 -$ python main.py d48edaa6-871a-4082-a196-4daab372d4a1 +$ uv run python main.py d48edaa6-871a-4082-a196-4daab372d4a1 -USER_ID is d48edaa6-871a-4082-a196-4daab372d4a1 +User ID is d48edaa6-871a-4082-a196-4daab372d4a1 UUID version is: 4 // An invalid value -$ python main.py 7479706572-72756c6573 +$ uv run python main.py 7479706572-72756c6573 -Usage: main.py [OPTIONS] USER_ID +Usage: main.py [OPTIONS] {user_id} Try "main.py --help" for help. -Error: Invalid value for 'USER_ID': 7479706572-72756c6573 is not a valid UUID value +Error: Invalid value for 'user_id': Input should be a valid UUID, invalid group count: expected 5, found 2 ```
diff --git a/docs/tutorial/printing.md b/docs/tutorial/printing.md index 407408c670..fc189d4ada 100644 --- a/docs/tutorial/printing.md +++ b/docs/tutorial/printing.md @@ -1,89 +1,119 @@ -You can use `typer.echo()` to print to the screen: +# Printing and Colors -```Python hl_lines="5" -{!../docs_src/first_steps/tutorial001.py!} -``` - -The reason to use `typer.echo()` instead of just `print()` is that it applies some error corrections in case the terminal is misconfigured, and it will properly output color if it's supported. +You can use the normal `print()` to show information on the screen: -!!! info - `typer.echo()` comes directly from Click, you can read more about it in Click's docs. +{* docs_src/typer_app/tutorial001.py hl[8] *} -Check it: +It will show the output normally:
```console -$ python main.py +$ uv run python main.py World Hello World ```
-## Color +## Use Rich -!!! info - For colors to work correctly on Windows you need to also install `colorama`. +You can also display beautiful and more complex information using [Rich](https://rich.readthedocs.io/). It comes by default when you install `typer`. - You don't need to call `colorama.init()`. Typer (actually Click) will handle it underneath. +### Use Rich `print` - And make sure you use `typer.echo()` instead of `print()`. +For the simplest cases, you can just import `print` from `rich` and use it instead of the standard `print`: -!!! note "Technical Details" - The way color works in terminals is by using some codes (ASCII codes) as part of the text. +{* docs_src/printing/tutorial001.py hl[2,18] *} - So, a colored text is still just a `str`. +Just with that, **Rich** will be able to print your data with nice colors and structure: -You can create colored strings to output to the terminal with `typer.style()`, that gives you `str`s that you can then pass to `typer.echo()`: +
-```Python hl_lines="7 9" -{!../docs_src/printing/tutorial001.py!} +```console +$ uv run python main.py + +Here's the data +{ + 'name': 'Rick', + 'age': 42, + 'items': [ + {'name': 'Portal Gun'}, + {'name': 'Plumbus'} + ], + 'active': True, + 'affiliation': None +} ``` -!!! tip - The parameters `fg` and `bg` receive strings with the color names for the "**f**ore**g**round" and "**b**ack**g**round" colors. You could simply pass `fg="green"` and `bg="red"`. +
- But **Typer** provides them all as variables like `typer.colors.GREEN` just so you can use autocompletion while selecting them. +### Rich Markup -Check it: +Rich also supports a [custom markup syntax](https://rich.readthedocs.io/en/stable/markup.html) to set colors and styles, for example: + +{* docs_src/printing/tutorial002.py hl[9] *} + +
+ +```console +$ uv run python main.py + +Alert! Portal gun shooting! 💥 +``` -
-python main.py -everything is good -python main.py --no-good -everything is bad
-You can pass these function arguments to `typer.style()`: +In this example you can see how to use font styles, colors, and even emojis. -* `fg`: the foreground color. -* `bg`: the background color. -* `bold`: enable or disable bold mode. -* `dim`: enable or disable dim mode. This is badly supported. -* `underline`: enable or disable underline. -* `blink`: enable or disable blinking. -* `reverse`: enable or disable inverse rendering (foreground becomes background and the other way round). -* `reset`: by default a reset-all code is added at the end of the string which means that styles do not carry over. This can be disabled to compose styles. +To learn more check out the [Rich docs](https://rich.readthedocs.io/en/stable/markup.html). -!!! info - You can read more about it in Click's docs about `style()` +### Rich Tables -## `typer.secho()` - style and print +The way Rich works internally is that it uses a `Console` object to display the information. -There's a shorter form to style and print at the same time with `typer.secho()` it's like `typer.echo()` but also adds style like `typer.style()`: +When you call Rich's `print`, it automatically creates this object and uses it. -```Python hl_lines="5" -{!../docs_src/printing/tutorial002.py!} -``` +But for advanced use cases, you could create a `Console` yourself. -Check it: +{* docs_src/printing/tutorial003.py hl[2:3,5,12:15] *} + +In this example, we create a `Console`, and a `Table`. And then we can add some rows to the table, and print it. + +If you run it, you will see a nicely formatted table: + +
+ +```console +$ uv run python main.py + +┏━━━━━━━┳━━━━━━━━━━━━┓ +┃ Name ┃ Item ┃ +┡━━━━━━━╇━━━━━━━━━━━━┩ +│ Rick │ Portal Gun │ +│ Morty │ Plumbus │ +└───────┴────────────┘ +``` -
-python main.py Camila -Welcome here Camila
+Rich has many other features, as an example, you can check the docs for: + +* [Prompt](https://rich.readthedocs.io/en/stable/prompt.html) +* [Markdown](https://rich.readthedocs.io/en/stable/markdown.html) +* [Panel](https://rich.readthedocs.io/en/stable/panel.html) +* ...and more. + +### Typer and Rich + +If you are wondering what tool should be used for what, **Typer** is useful for structuring the command line application, with options, arguments, subcommands, data validation, etc. + +In general, **Typer** tends to be the entry point to your program, taking the first input from the user. + +**Rich** is useful for the parts that need to *display* information. Showing beautiful content on the screen. + +The best results for your command line application are achieved combining both **Typer** and **Rich**. + ## "Standard Output" and "Standard Error" The way printing works underneath is that the **operating system** (Linux, Windows, macOS) treats what we print as if our CLI program was **writing text** to a "**virtual file**" called "**standard output**". @@ -100,29 +130,36 @@ And there's another "**virtual file**" called "**standard error**" that is norma But we can also "print" to "standard error". And both are shown on the terminal to the users. -!!! info - If you use PowerShell it's quite possible that what you print to "standard error" won't be shown in the terminal. +/// note - In PowerShell, to see "standard error" you would have to check the variable `$Error`. +If you use PowerShell it's quite possible that what you print to "standard error" won't be shown in the terminal. - But it will work normally in Bash, Zsh, and Fish. +In PowerShell, to see "standard error" you would have to check the variable `$Error`. + +But it will work normally in Bash, Zsh, and Fish. + +/// ### Printing to "standard error" -You can print to "standard error" with `typer.echo("some text", err=True)`. +You can print to "standard error" creating a Rich `Console` with `stderr=True`. -Using `err=True` tells **Typer** (actually Click) that the output should be shown in "standard error". +/// tip -```Python hl_lines="5" -{!../docs_src/printing/tutorial003.py!} -``` +`stderr` is short for "standard error". + +/// + +Using `stderr=True` tells **Rich** that the output should be shown in "standard error". + +{* docs_src/printing/tutorial004.py hl[4,11] *} When you try it in the terminal, it will probably just look the same:
```console -$ python main.py +$ uv run python main.py Here is something written to standard error ``` @@ -140,3 +177,102 @@ This virtual file is called "**standard input**". Right now this probably seems quite useless 🤷‍♂. But understanding that will come handy in the future, for example for autocompletion and testing. + +## Typer Echo + +/// warning + +In most of the cases, for displaying advanced information, it is recommended to use [Rich](https://rich.readthedocs.io/). + +You can probably skip the rest of this section. 🎉😎 + +/// + +**Typer** also has a small utility `typer.echo()` to print information on the screen. But normally you shouldn't need it. + +For the simplest cases, you can use the standard Python `print()`. + +And for the cases where you want to display data more beautifully, or more advanced content, you should use **Rich** instead. + +### Why `typer.echo` + +`typer.echo()` applies some checks to try and convert binary data to strings, and other similar things. + +But in most of the cases you wouldn't need it, as in modern Python strings (`str`) already support and use Unicode, and you would rarely deal with pure `bytes` that you want to print on the screen. + +If you have some `bytes` objects, you would probably want to decode them intentionally and directly before trying to print them. + +And if you want to print data with colors and other features, you are much better off with the more advanced tools in **Rich**. + +### Untrusted Text + +`typer.echo()` preserves terminal control sequences, including those used for colors. When displaying text from an untrusted source, escape control characters first. For a diagnostic representation of a string, you can use `typer.echo(repr(value))`. + +Setting `color=False` disables styling, but does not sanitize all terminal control sequences. + +### Color + +/// note | Technical Details + +The way color works in terminals is by using some codes (ANSI escape sequences) as part of the text. + +So, a colored text is still just a `str`. + +/// + +/// tip + +Again, you are much better off using [Rich](https://rich.readthedocs.io/) for this. 😎 + +/// + +You can create colored strings to output to the terminal with `typer.style()`, that gives you `str`s that you can then pass to `typer.echo()`: + +{* docs_src/printing/tutorial005.py hl[10,12] *} + +/// tip + +The parameters `fg` and `bg` receive strings with the color names for the "foreground" and "background" colors. You could simply pass `fg="green"` and `bg="red"`. + +But **Typer** provides them all as variables like `typer.colors.GREEN` just so you can use autocompletion while selecting them. + +/// + +Check it: + +
+python main.py +everything is good +python main.py --no-good +everything is bad +
+ +You can pass these function arguments to `typer.style()`: + +* `fg`: the foreground color. +* `bg`: the background color. +* `bold`: enable or disable bold mode. +* `dim`: enable or disable dim mode. This is badly supported. +* `underline`: enable or disable underline. +* `blink`: enable or disable blinking. +* `reverse`: enable or disable inverse rendering (foreground becomes background and the other way round). +* `reset`: by default a reset-all code is added at the end of the string which means that styles do not carry over. This can be disabled to compose styles. + +### `typer.secho()` - style and print + +/// tip + +In case you didn't see above, you are much better off using [Rich](https://rich.readthedocs.io/) for this. 😎 + +/// + +There's a shorter form to style and print at the same time with `typer.secho()` it's like `typer.echo()` but also adds style like `typer.style()`: + +{* docs_src/printing/tutorial006.py hl[8] *} + +Check it: + +
+python main.py Camila +Welcome here Camila +
diff --git a/docs/tutorial/progressbar.md b/docs/tutorial/progressbar.md index 3e2416404c..0d22b9a9ab 100644 --- a/docs/tutorial/progressbar.md +++ b/docs/tutorial/progressbar.md @@ -1,12 +1,91 @@ -If you are executing an operation that can take some time, you can inform it to the user with a progress bar. +# Progress Bar -For this, you can use `typer.progressbar()`: +If you are executing an operation that can take some time, you can inform it to the user. 🤓 -```Python hl_lines="8" -{!../docs_src/progressbar/tutorial001.py!} +## Progress Bar + +You can use [Rich's Progress Display](https://rich.readthedocs.io/en/stable/progress.html) to show a progress bar, for example: + +{* docs_src/progressbar/tutorial001.py hl[4,12] *} + +You put the thing that you want to iterate over inside of Rich's `track()`, and then iterate over that. + +Check it: + +
+ +```console +$ uv run python main.py + +---> 100% + +Processed 100 things. +``` + +
+ +...actually, it will look a lot prettier. ✨ But I can't show you the animation here in the docs. 😅 + +The colors and information will look something like this: + +
+ +```console +$ uv run python main.py + +Processing... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━ 74% 0:00:01 +``` + +
+ +## Spinner + +When you don't know how long the operation will take, you can use a spinner instead. + +Rich allows you to display many things in complex and advanced ways. + +For example, this will show two spinners: + +{* docs_src/progressbar/tutorial002.py hl[4,11:18] *} + +I can't show you the beautiful animation here in the docs. 😅 + +But at some point in time it will look like this (imagine it's spinning). 🤓 + +
+ +```console +$ uv run python main.py + +⠹ Processing... +⠹ Preparing... ``` -You use `typer.progressbar()` with a `with` statement, as in: +
+ +You can learn more about it in the [Rich docs for Progress Display](https://rich.readthedocs.io/en/stable/progress.html). + +## Typer `progressbar` + +If you can, you should use **Rich** as explained above, it has more features, it's more advanced, and can display information more beautifully. ✨ + +/// tip + +If you can use Rich, use the information above, the Rich docs, and skip the rest of this page. 😎 + +/// + +But if you can't use Rich and have it disabled, Typer comes with a simple utility to show progress bars. + +### Use `typer.progressbar` + +/// tip + +Remember, you are much better off using [Rich](https://rich.readthedocs.io/) for this. 😎 + +/// + +You can use `typer.progressbar()` with a `with` statement, as in: ```Python with typer.progressbar(something) as progress: @@ -15,6 +94,8 @@ with typer.progressbar(something) as progress: And you pass as function argument to `typer.progressbar()` the thing that you would normally iterate over. +{* docs_src/progressbar/tutorial003.py hl[11] *} + So, if you have a list of users, this could be: ```Python @@ -26,7 +107,7 @@ with typer.progressbar(users) as progress: And the `with` statement using `typer.progressbar()` gives you an object that you can iterate over, just like if it was the same thing that you would iterate over normally. -But by iterating over this object **Typer** (actually Click) will know to update the progress bar: +But by iterating over this object **Typer** will know to update the progress bar: ```Python users = ["Camila", "Rick", "Morty"] @@ -36,20 +117,26 @@ with typer.progressbar(users) as progress: typer.echo(user) ``` -!!! tip - Notice that there are 2 levels of code blocks. One for the `with` statement and one for the `for` statement. +/// tip + +Notice that there are 2 levels of code blocks. One for the `with` statement and one for the `for` statement. + +/// -!!! info - This is mostly useful for operations that take some time. +/// note - In the example above we are faking it with `time.sleep()`. +This is mostly useful for operations that take some time. + +In the example above we are faking it with `time.sleep()`. + +/// Check it:
```console -$ python main.py +$ uv run python main.py ---> 100% @@ -58,22 +145,26 @@ Processed 100 things.
-## Setting a Progress Bar `length` +### Setting a Progress Bar `length` + +/// tip + +Remember, you are much better off using [Rich](https://rich.readthedocs.io/) for this. 😎 + +/// The progress bar is generated from the length of the iterable (e.g. the list of users). But if the length is not available (for example, with something that fetches a new user from a web API each time) you can pass an explicit `length` to `typer.progressbar()`. -```Python hl_lines="14" -{!../docs_src/progressbar/tutorial002.py!} -``` +{* docs_src/progressbar/tutorial004.py hl[18] *} Check it:
```console -$ python main.py +$ uv run python main.py ---> 100% @@ -82,9 +173,9 @@ Processed 100 user IDs.
-### About the function with `yield` +#### About the function with `yield` -If you hadn't seen something like that `yield` above, that's a "generator". +If you hadn't seen something like that `yield` above, that's a "[generator](https://docs.python.org/3/glossary.html#term-generator)". You can iterate over that function with a `for` and at each iteration it will give you the value at `yield`. @@ -98,19 +189,24 @@ def iterate_user_ids(): for i in range(100): yield i + for i in iterate_user_ids(): print(i) ``` would print each of the "user IDs" (here it's just the numbers from `0` to `99`). -## Add a `label` +### Add a `label` + +/// tip + +Remember, you are much better off using [Rich](https://rich.readthedocs.io/) for this. 😎 + +/// You can also set a `label`: -```Python hl_lines="8" -{!../docs_src/progressbar/tutorial003.py!} -``` +{* docs_src/progressbar/tutorial005.py hl[11] *} Check it: @@ -126,14 +222,12 @@ If you need to manually iterate over something and update the progress bar irreg And then calling the `.update()` method in the object from the `with` statement: -```Python hl_lines="8 12" -{!../docs_src/progressbar/tutorial004.py!} -``` +{* docs_src/progressbar/tutorial006.py hl[11,17] *} Check it:
python main.py -Processed 100 things in batches. +Processed 1000 things in batches.
diff --git a/docs/tutorial/prompt.md b/docs/tutorial/prompt.md index 6928019cb3..53b13877fc 100644 --- a/docs/tutorial/prompt.md +++ b/docs/tutorial/prompt.md @@ -1,17 +1,17 @@ -When you need to ask the user for info interactively you should normally use [*CLI Option*s with Prompt](options/prompt.md){.internal-link target=_blank}, because they allow using the CLI program in a non-interactive way (for example, a Bash script could use it). +# Ask with Prompt + +When you need to ask the user for info interactively you should normally use [*CLI Option*s with Prompt](options/prompt.md), because they allow using the CLI program in a non-interactive way (for example, a Bash script could use it). But if you absolutely need to ask for interactive information without using a *CLI option*, you can use `typer.prompt()`: -```Python hl_lines="5" -{!../docs_src/prompt/tutorial001.py!} -``` +{* docs_src/prompt/tutorial001.py hl[8] *} Check it:
```console -$ python main.py +$ uv run python main.py # What's your name?:$ Camila @@ -22,25 +22,23 @@ Hello Camila ## Confirm -There's also an alternative to ask for confirmation. Again, if possible, you should use a [*CLI Option* with a confirmation prompt](options/prompt.md){.internal-link target=_blank}: +There's also an alternative to ask for confirmation. Again, if possible, you should use a [*CLI Option* with a confirmation prompt](options/prompt.md): -```Python hl_lines="5" -{!../docs_src/prompt/tutorial002.py!} -``` +{* docs_src/prompt/tutorial002.py hl[8] *} Check it:
```console -$ python main.py +$ uv run python main.py # Are you sure you want to delete it? [y/N]:$ y Deleting it! // This time cancel it -$ python main.py +$ uv run python main.py # Are you sure you want to delete it? [y/N]:$ n @@ -54,21 +52,19 @@ Aborted! As it's very common to abort if the user doesn't confirm, there's an integrated parameter `abort` that does it automatically: -```Python hl_lines="5" -{!../docs_src/prompt/tutorial003.py!} -``` +{* docs_src/prompt/tutorial003.py hl[8] *}
```console -$ python main.py +$ uv run python main.py # Are you sure you want to delete it? [y/N]:$ y Deleting it! // This time cancel it -$ python main.py +$ uv run python main.py # Are you sure you want to delete it? [y/N]:$ n @@ -76,3 +72,23 @@ Aborted! ```
+ +## Prompt with Rich + +You can use Rich to prompt the user for input: + +{* docs_src/prompt/tutorial004.py hl[2,9] *} + +And when you run it, it will look like: + +
+ +```console +$ uv run python main.py + +# Enter your name 😎:$ Morty + +Hello Morty +``` + +
diff --git a/docs/tutorial/subcommands/add-typer.md b/docs/tutorial/subcommands/add-typer.md index 0e867558cc..6c4f6207d6 100644 --- a/docs/tutorial/subcommands/add-typer.md +++ b/docs/tutorial/subcommands/add-typer.md @@ -1,3 +1,5 @@ +# Add Typer + We'll start with the core idea. To add a `typer.Typer()` app inside of another. @@ -8,16 +10,14 @@ Let's imagine that you are creating a *CLI program* to manage items in some dist It could be in an `items.py` file with this: -```Python -{!../docs_src/subcommands/tutorial001/items.py!} -``` +{* docs_src/subcommands/tutorial001/items.py *} And you would use it like:
```console -$ python items.py create Wand +$ uv run python items.py create Wand Creating item: Wand ``` @@ -30,16 +30,14 @@ But then you realize that you also have to manage users from your *CLI app*. It could be a file `users.py` with something like: -```Python -{!../docs_src/subcommands/tutorial001/users.py!} -``` +{* docs_src/subcommands/tutorial001/users.py *} And you would use it like:
```console -$ python users.py create Camila +$ uv run python users.py create Camila Creating user: Camila ``` @@ -56,9 +54,7 @@ In this case, as with `git remote`, we can put them together as subcommands in a Now create a `main.py` with: -```Python hl_lines="3 4 7 8" -{!../docs_src/subcommands/tutorial001/main.py!} -``` +{* docs_src/subcommands/tutorial001/main.py hl[3,4,7,8] *} Here's what we do in `main.py`: @@ -78,7 +74,7 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -88,8 +84,8 @@ Options: --help Show this message and exit. Commands: - items users + items ```
@@ -102,7 +98,7 @@ Let's check the `items` command: ```console // Check the help for items -$ python main.py items --help +$ uv run python main.py items --help // It shows its own commands (subcommands): create, delete, sell Usage: main.py items [OPTIONS] COMMAND [ARGS]... @@ -116,26 +112,29 @@ Commands: sell // Try it -$ python main.py items create Wand +$ uv run python main.py items create Wand Creating item: Wand -$ python main.py items sell Vase +$ uv run python main.py items sell Vase Selling item: Vase ```
-!!! tip - Notice that we are still calling `$ python main.py` but now we are using the command `items`. +/// tip + +Notice that we are still calling `$ python main.py` but now we are using the command `items`. + +/// And now check the command `users`, with all its subcommands:
```console -$ python main.py users --help +$ uv run python main.py users --help Usage: main.py users [OPTIONS] COMMAND [ARGS]... @@ -147,7 +146,7 @@ Commands: delete // Try it -$ python main.py users create Camila +$ uv run python main.py users create Camila Creating user: Camila ``` @@ -166,4 +165,4 @@ Do you need sub-sub-sub-subcommands? Go ahead, create all the `typer.Typer()`s y In the next sections we'll update this with more features, but you already have the core idea. -This way, in the same spirit of Click, **Typer** applications are composable, each `typer.Typer()` can be a *CLI app* by itself, but it can also be added as a command group to another Typer app. +This way, **Typer** applications are composable, each `typer.Typer()` can be a *CLI app* by itself, but it can also be added as a command group to another Typer app. diff --git a/docs/tutorial/subcommands/callback-override.md b/docs/tutorial/subcommands/callback-override.md index 6a2683e9e3..39c70a9405 100644 --- a/docs/tutorial/subcommands/callback-override.md +++ b/docs/tutorial/subcommands/callback-override.md @@ -1,12 +1,12 @@ +# Sub-Typer Callback Override + When creating a **Typer** app you can define a callback function, it always executes and defines the *CLI arguments* and *CLI options* that go before a command. When adding a Typer app inside of another, the sub-Typer can also have its own callback. It can handle any *CLI parameters* that go before its own commands and execute any extra code: -```Python hl_lines="9 10 11" -{!../docs_src/subcommands/callback_override/tutorial001.py!} -``` +{* docs_src/subcommands/callback_override/tutorial001.py hl[9,10,11] *} In this case it doesn't define any *CLI parameters*, it just writes a message. @@ -15,7 +15,7 @@ Check it:
```console -$ python main.py users create Camila +$ uv run python main.py users create Camila // Notice the first message is not created by the command function but by the callback Running a users command @@ -28,9 +28,7 @@ Creating user: Camila It's also possible to add a callback when creating the `typer.Typer()` app that will be added to another Typer app: -```Python hl_lines="6 7 10" -{!../docs_src/subcommands/callback_override/tutorial002.py!} -``` +{* docs_src/subcommands/callback_override/tutorial002.py hl[6,7,10] *} This achieves exactly the same as above, it's just another place to add the callback. @@ -39,7 +37,7 @@ Check it:
```console -$ python main.py users create Camila +$ uv run python main.py users create Camila Running a users command Creating user: Camila @@ -51,11 +49,9 @@ Creating user: Camila If a callback was added when creating the `typer.Typer()` app, it's possible to override it with a new one using `@app.callback()`. -This is the same information you saw on the section about [Commands - Typer Callback](../commands/callback.md){.internal-link target=_blank}, and it applies the same for sub-Typer apps: +This is the same information you saw on the section about [Commands - Typer Callback](../commands/callback.md), and it applies the same for sub-Typer apps: -```Python hl_lines="6 7 10 14 15 16" -{!../docs_src/subcommands/callback_override/tutorial003.py!} -``` +{* docs_src/subcommands/callback_override/tutorial003.py hl[6,7,10,14,15,16] *} Here we had defined a callback when creating the `typer.Typer()` sub-app, but then we override it with a new callback with the function `user_callback()`. @@ -66,7 +62,7 @@ Check it:
```console -$ python main.py users create Camila +$ uv run python main.py users create Camila // Notice the message from the new callback Callback override, running users command @@ -81,9 +77,7 @@ Lastly, you can override the callback defined anywhere else when adding a sub-Ty This has the highest priority: -```Python hl_lines="13 14 17" -{!../docs_src/subcommands/callback_override/tutorial004.py!} -``` +{* docs_src/subcommands/callback_override/tutorial004.py hl[13,14,17] *} Notice that the precedence goes to `app.add_typer()` and is not affected by the order of execution. There's another callback defined below, but the one from `app.add_typer()` wins. @@ -94,7 +88,7 @@ Check it:
```console -$ python users create Camila +$ uv run python users create Camila // Notice the message from the callback added in add_typer() I have the high land! Running users command diff --git a/docs/tutorial/subcommands/index.md b/docs/tutorial/subcommands/index.md index 5a8df35958..5494247fc1 100644 --- a/docs/tutorial/subcommands/index.md +++ b/docs/tutorial/subcommands/index.md @@ -1,4 +1,6 @@ -You read before how to create a program with [Commands](../commands/index.md){.internal-link target=_blank}. +# SubCommands - Command Groups + +You read before how to create a program with [Commands](../commands/index.md). Now we'll see how to create a *CLI program* with commands that have their own subcommands. Also known as command groups. @@ -21,7 +23,7 @@ origin git@github.com:yourusername/typer.git (fetch) origin git@github.com:yourusername/typer.git (push) // git remote add takes 2 CLI arguments, a name and URL -$ git remote add upstream https://github.com/tiangolo/typer.git +$ git remote add upstream https://github.com/fastapi/typer.git // Doesn't output anything, but now you have another remote repository called upstream @@ -30,8 +32,8 @@ $ git remote -v origin git@github.com:yourusername/typer.git (fetch) origin git@github.com:yourusername/typer.git (push) -upstream https://github.com/tiangolo/typer.git (fetch) -upstream https://github.com/tiangolo/typer.git (push) +upstream https://github.com/fastapi/typer.git (fetch) +upstream https://github.com/fastapi/typer.git (push) ```
diff --git a/docs/tutorial/subcommands/name-and-help.md b/docs/tutorial/subcommands/name-and-help.md index 474dc91ae9..9b3ba3e951 100644 --- a/docs/tutorial/subcommands/name-and-help.md +++ b/docs/tutorial/subcommands/name-and-help.md @@ -1,3 +1,5 @@ +# SubCommand Name and Help + When adding a Typer app to another we have seen how to set the `name` to use for the command. For example to set the command to `users`: @@ -8,11 +10,9 @@ app.add_typer(users.app, name="users") ## Add a help text -We can also set the `help` while adding a Typer: +We can also set the `help` text while adding a Typer: -```Python hl_lines="6" -{!../docs_src/subcommands/name_help/tutorial001.py!} -``` +{* docs_src/subcommands/name_help/tutorial001.py hl[6] *} And then we get that help text for that command in the *CLI program*: @@ -20,7 +20,7 @@ And then we get that help text for that command in the *CLI program*: ```console // Check the main help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -33,7 +33,7 @@ Commands: users Manage users in the app. // Check the help for the users command -$ python main.py users --help +$ uv run python main.py users --help Usage: main.py users [OPTIONS] COMMAND [ARGS]... @@ -48,18 +48,21 @@ Commands:
-We can set the `name` and `help` in several places, each one taking precedence over the other, overriding the previous value. +We can set the `help` in several places, each one taking precedence over the other, overriding the previous value. Let's see those locations. -!!! tip - There are other attributes that can be set in that same way in the same places we'll see next. +/// tip + +There are other attributes that can be set in that same way in the same places we'll see next. + +But those are documented later in another section. - But those are documented later in another section. +/// -## Inferring name and help from callback +## Inferring help text from callback -### Inferring a command's name and help +### Inferring a command's help text When you create a command with `@app.command()`, by default, it generates the name from the function name. @@ -78,25 +81,15 @@ def create(item: str): ...will create a command `create` with a help text of `Create an item`. -### Inferring name and help from `@app.callback()` +### Inferring the help text from `@app.callback()` The same way, if you define a callback in a `typer.Typer()`, the help text is extracted from the callback function's docstring. -And if that Typer app is added to another Typer app, the default name of the command is generated from the name of the callback function. - Here's an example: -```Python hl_lines="6 9 10 11 12 13" -{!../docs_src/subcommands/name_help/tutorial002.py!} -``` - -Notice that now we added the sub-Typer without specifying a `name` nor a `help`. +{* docs_src/subcommands/name_help/tutorial002.py hl[9,10,11,12,13] *} -They are now inferred from the callback function. - -The command name will be the same callback function's name: `users`. - -And the help text for that `users` command will be the callback function's docstring: `Manage users in the app.`. +The help text for that command will be the callback function's docstring: `Manage users in the app.`. Check it: @@ -104,9 +97,9 @@ Check it: ```console // Check the main help -$ python main.py --help +$ uv run python main.py --help -// Notice the command name "users" and the help text "Manage users in the app." +// Notice the help text "Manage users in the app." Usage: main.py [OPTIONS] COMMAND [ARGS]... Options: @@ -118,7 +111,7 @@ Commands: users Manage users in the app. // Check the help for the users command -$ python main.py users --help +$ uv run python main.py users --help // Notice the main description: "Manage users in the app." Usage: main.py users [OPTIONS] COMMAND [ARGS]... @@ -134,17 +127,21 @@ Commands:
-### Name and help from callback parameter in `typer.Typer()` +/// note + +Before Typer 0.14.0, in addition to the help text, the command name was also inferred from the callback function name, this is no longer the case. -If you pass a `callback` parameter while creating a `typer.Typer(callback=some_function)` it will be used to infer the name and help text. +/// + +### Help from callback parameter in `typer.Typer()` + +If you pass a `callback` parameter while creating a `typer.Typer(callback=some_function)` it will be used to infer the help text. This has the lowest priority, we'll see later what has a higher priority and can override it. Check the code: -```Python hl_lines="6 7 8 9 12" -{!../docs_src/subcommands/name_help/tutorial003.py!} -``` +{* docs_src/subcommands/name_help/tutorial003.py hl[6,7,8,9,12] *} This achieves exactly the same as the previous example. @@ -154,9 +151,9 @@ Check it: ```console // Check the main help -$ python main.py --help +$ uv run python main.py --help -// Notice the command name "users" and the help text "Manage users in the app." +// Notice the help text "Manage users in the app." Usage: main.py [OPTIONS] COMMAND [ARGS]... Options: @@ -168,7 +165,7 @@ Commands: users Manage users in the app. // Check the help for the users command -$ python main.py users --help +$ uv run python main.py users --help // Notice the main description: "Manage users in the app." Usage: main.py users [OPTIONS] COMMAND [ARGS]... @@ -186,13 +183,11 @@ Commands: ### Override a callback set in `typer.Typer()` with `@app.callback()` -The same as with normal **Typer** apps, if you pass a `callback` to `typer.Typer(callback=some_function)` and then override it with `@app.callback()`, the name and help text will be inferred from the new callback: +The same as with normal **Typer** apps, if you pass a `callback` to `typer.Typer(callback=some_function)` and then override it with `@app.callback()`, the help text will be inferred from the new callback: -```Python hl_lines="16 17 18 19 20" -{!../docs_src/subcommands/name_help/tutorial004.py!} -``` +{* docs_src/subcommands/name_help/tutorial004.py hl[16,17,18,19,20] *} -Now the name of the command will be `users` instead of `old-callback`, and the help text will be `Manage users in the app.` instead of `Old callback help.`. +Now the help text will be `Manage users in the app.` instead of `Old callback help.`. Check it: @@ -200,9 +195,9 @@ Check it: ```console // Check the main help -$ python main.py --help +$ uv run python main.py --help -// Notice the command name "users" and the help text "Manage users in the app." +// Notice the help text "Manage users in the app." Usage: main.py [OPTIONS] COMMAND [ARGS]... Options: @@ -214,7 +209,7 @@ Commands: users Manage users in the app. // Check the help for the users command -$ python main.py users --help +$ uv run python main.py users --help // Notice the main description: "Manage users in the app." Usage: main.py users [OPTIONS] COMMAND [ARGS]... @@ -230,19 +225,17 @@ Commands:
-### Infer name and help from callback in `app.add_typer()` +### Help from callback in `app.add_typer()` -If you override the callback in `app.add_typer()` when including a sub-app, the name and help will be inferred from this callback function. +If you override the callback in `app.add_typer()` when including a sub-app, the help will be inferred from this callback function. -This takes precedence over inferring the name and help from a callback set in `@sub_app.callback()` and `typer.Typer(callback=sub_app_callback)`. +This takes precedence over inferring the help from a callback set in `@sub_app.callback()` and `typer.Typer(callback=sub_app_callback)`. Check the code: -```Python hl_lines="15 16 17 18 21" -{!../docs_src/subcommands/name_help/tutorial005.py!} -``` +{* docs_src/subcommands/name_help/tutorial005.py hl[15,16,17,18,21] *} -Now the command will be `new-users` instead of `users`. And the help text will be `I have the highland! Create some users.` instead of the previous ones. +The help text will be `I have the highland! Create some users.` instead of the previous ones. Check it: @@ -250,7 +243,7 @@ Check it: ```console // Check the main help -$ python main.py --help +$ uv run python main.py --help // Check the command new-users and its help text Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -264,7 +257,7 @@ Commands: new-users I have the highland! Create some users. // Now check the help for the new-users command -$ python main.py new-users --help +$ uv run python main.py new-users --help // Notice the help text Usage: main.py new-users [OPTIONS] COMMAND [ARGS]... @@ -282,39 +275,43 @@ Commands: ### Enough inferring -So, when inferring a name and help text, the precedence order from lowest priority to highest is: +So, when inferring help text, the precedence order from lowest priority to highest is: * `sub_app = typer.Typer(callback=some_function)` * `@sub_app.callback()` * `app.add_typer(sub_app, callback=new_function)` -That's for inferring the name and help text from functions. +That's for inferring the help text from functions. -But if you set the name and help text explicitly, that has a higher priority than these. +But if you set the help text explicitly, that has a higher priority than these. ## Set the name and help Let's now see the places where you can set the command name and help text, from lowest priority to highest. -!!! tip - Setting the name and help text explicitly always has a higher precedence than inferring from a callback function. +/// tip + +Setting the help text explicitly always has a higher precedence than inferring from a callback function. + +/// ### Name and help in `typer.Typer()` -You could have all the callbacks and overrides we defined before, but the name and help text was inferred from the function name and docstring. +You could have all the callbacks and overrides we defined before, but the help text was inferred from the function docstring. If you set it explicitly, that takes precedence over inferring. You can set it when creating a new `typer.Typer()`: -```Python hl_lines="12" -{!../docs_src/subcommands/name_help/tutorial006.py!} -``` +{* docs_src/subcommands/name_help/tutorial006.py hl[12] *} + +/// note -!!! info - The rest of the callbacks and overrides are there only to show you that they don't affect the name and help text when you set it explicitly. +The rest of the callbacks and overrides are there only to show you that they don't affect the name and help text when you set it explicitly. -We set an explicit name `exp-users`, and an explicit help `Explicit help.`. +/// + +We set an explicit help `Explicit help.`. So that will take precedence now. @@ -324,7 +321,7 @@ Check it: ```console // Check the main help -$ python main.py --help +$ uv run python main.py --help // Notice the command name is exp-users and the help text is "Explicit help." Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -338,7 +335,7 @@ Commands: exp-users Explicit help. // Check the help for the exp-users command -$ python main.py exp-users --help +$ uv run python main.py exp-users --help // Notice the main help text Usage: main.py exp-users [OPTIONS] COMMAND [ARGS]... @@ -354,17 +351,15 @@ Commands:
-### Name and help in `@app.callback()` +### Help text in `@app.callback()` -Any parameter that you use when creating a `typer.Typer()` app can be overridden in the parameters of `@app.callback()`. +Many parameters that you use when creating a `typer.Typer()` app can be overridden in the parameters of `@app.callback()`. -Continuing with the previous example, we now override the values in `@user_app.callback()`: +Continuing with the previous example, we now override the `help` in `@user_app.callback()`: -```Python hl_lines="24" -{!../docs_src/subcommands/name_help/tutorial007.py!} -``` +{* docs_src/subcommands/name_help/tutorial007.py hl[24] *} -And now the command name will be `call-users` and the help text will be `Help from callback for users.`. +And now the help text will be `Help from callback for users.`. Check it: @@ -372,9 +367,9 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help -// The command name now is call-users and the help text is "Help from callback for users.". +// The help text is now "Help from callback for users.". Usage: main.py [OPTIONS] COMMAND [ARGS]... Options: @@ -383,13 +378,13 @@ Options: --help Show this message and exit. Commands: - call-users Help from callback for users. + users Help from callback for users. -// Check the call-users command help -$ python main.py call-users --help +// Check the users command help +$ uv run python main.py users --help // Notice the main help text -Usage: main.py call-users [OPTIONS] COMMAND [ARGS]... +Usage: main.py users [OPTIONS] COMMAND [ARGS]... Help from callback for users. @@ -406,9 +401,7 @@ Commands: And finally, with the highest priority, you can override all that by explicitly setting the `name` and `help` in `app.add_typer()`, just like we did on the first example above: -```Python hl_lines="21" -{!../docs_src/subcommands/name_help/tutorial008.py!} -``` +{* docs_src/subcommands/name_help/tutorial008.py hl[21] *} And now, with the highest priorities of them all, the command name will now be `cake-sith-users` and the help text will be `Unlimited powder! Eh, users.`. @@ -418,7 +411,7 @@ Check it: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help // Notice the command name cake-sith-users and the new help text "Unlimited powder! Eh, users." Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -432,7 +425,7 @@ Commands: cake-sith-users Unlimited powder! Eh, users. // And check the help for the command cake-sith-users -$ python main.py cake-sith-users --help +$ uv run python main.py cake-sith-users --help // Notice the main help text Usage: main.py cake-sith-users [OPTIONS] COMMAND [ARGS]... @@ -450,13 +443,17 @@ Commands: ## Recap -The precedence to generate a command's name and help, from lowest priority to highest, is: +The precedence to generate a command's **help**, from lowest priority to highest, is: * Implicitly inferred from `sub_app = typer.Typer(callback=some_function)` * Implicitly inferred from the callback function under `@sub_app.callback()` * Implicitly inferred from `app.add_typer(sub_app, callback=some_function)` -* Explicitly set on `sub_app = typer.Typer(name="some-name", help="Some help.")` -* Explicitly set on `@sub_app.callback("some-name", help="Some help.")` -* Explicitly set on `app.add_typer(sub_app, name="some-name", help="Some help.")` +* Explicitly set on `sub_app = typer.Typer(help="Some help.")` +* Explicitly set on `app.add_typer(sub_app, help="Some help.")` + +And the priority to set the command's **name**, from lowest priority to highest, is: + +* Explicitly set on `sub_app = typer.Typer(name="some-name")` +* Explicitly set on `app.add_typer(sub_app, name="some-name")` So, `app.add_typer(sub_app, name="some-name", help="Some help.")` always wins. diff --git a/docs/tutorial/subcommands/nested-subcommands.md b/docs/tutorial/subcommands/nested-subcommands.md index a3fbe6c827..06e8f6e9b5 100644 --- a/docs/tutorial/subcommands/nested-subcommands.md +++ b/docs/tutorial/subcommands/nested-subcommands.md @@ -1,3 +1,5 @@ +# Nested SubCommands + We'll now see how these same ideas can be extended for deeply nested commands. Let's imagine that the same *CLI program* from the previous examples now needs to handle `lands`. @@ -10,9 +12,7 @@ And each of those could have their own commands, like `create` and `delete`. Let's start with a file `reigns.py`: -```Python -{!../docs_src/subcommands/tutorial003/reigns.py!} -``` +{* docs_src/subcommands/tutorial003/reigns.py *} This is already a simple *CLI program* to manage reigns: @@ -20,7 +20,7 @@ This is already a simple *CLI program* to manage reigns: ```console // Check the help -$ python reigns.py --help +$ uv run python reigns.py --help Usage: reigns.py [OPTIONS] COMMAND [ARGS]... @@ -34,11 +34,11 @@ Commands: destroy // Try it -$ python reigns.py conquer Cintra +$ uv run python reigns.py conquer Cintra Conquering reign: Cintra -$ python reigns.py destroy Mordor +$ uv run python reigns.py destroy Mordor Destroying reign: Mordor ``` @@ -49,9 +49,7 @@ Destroying reign: Mordor And now the equivalent for managing towns in `towns.py`: -```Python -{!../docs_src/subcommands/tutorial003/towns.py!} -``` +{* docs_src/subcommands/tutorial003/towns.py *} With it, you can manage towns: @@ -59,7 +57,7 @@ With it, you can manage towns: ```console // Check the help -$ python towns.py --help +$ uv run python towns.py --help Usage: towns.py [OPTIONS] COMMAND [ARGS]... @@ -69,15 +67,15 @@ Options: --help Show this message and exit. Commands: - burn found + burn // Try it -$ python towns.py found "New Asgard" +$ uv run python towns.py found "New Asgard" Founding town: New Asgard -$ python towns.py burn Vizima +$ uv run python towns.py burn Vizima Burning town: Vizima ``` @@ -88,9 +86,7 @@ Burning town: Vizima Now let's put the `reigns` and `towns` together in the same *CLI program* in `lands.py`: -```Python -{!../docs_src/subcommands/tutorial003/lands.py!} -``` +{* docs_src/subcommands/tutorial003/lands.py *} And now we have a single *CLI program* with a command (or command group) `reigns` that has its own commands. And another command `towns` with its own subcommands. @@ -100,7 +96,7 @@ Check it: ```console // Check the help -$ python lands.py --help +$ uv run python lands.py --help Usage: lands.py [OPTIONS] COMMAND [ARGS]... @@ -114,7 +110,7 @@ Commands: towns // We still have the help for reigns -$ python lands.py reigns --help +$ uv run python lands.py reigns --help Usage: lands.py reigns [OPTIONS] COMMAND [ARGS]... @@ -126,7 +122,7 @@ Commands: destroy // And the help for towns -$ python lands.py towns --help +$ uv run python lands.py towns --help Usage: lands.py towns [OPTIONS] COMMAND [ARGS]... @@ -134,8 +130,8 @@ Options: --help Show this message and exit. Commands: - burn found + burn ```
@@ -146,20 +142,20 @@ Now try it, manage the lands through the CLI: ```console // Try the reigns command -$ python lands.py reigns conquer Gondor +$ uv run python lands.py reigns conquer Gondor Conquering reign: Gondor -$ python lands.py reigns destroy Nilfgaard +$ uv run python lands.py reigns destroy Nilfgaard Destroying reign: Nilfgaard // Try the towns command -$ python lands.py towns found Springfield +$ uv run python lands.py towns found Springfield Founding town: Springfield -$ python lands.py towns burn Atlantis +$ uv run python lands.py towns burn Atlantis Burning town: Atlantis ``` @@ -191,9 +187,7 @@ This already is a quite deeply nested "tree" of commands/command groups. But to achieve that, we just have to add the `lands` **Typer** app to the same `main.py` file we already had: -```Python hl_lines="4 10" -{!../docs_src/subcommands/tutorial003/main.py!} -``` +{* docs_src/subcommands/tutorial003/main.py hl[4,10] *} And now we have everything in a single *CLI program*: @@ -201,7 +195,7 @@ And now we have everything in a single *CLI program*: ```console // Check the main help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -211,27 +205,27 @@ Options: --help Show this message and exit. Commands: + users items lands - users // Try some users commands -$ python main.py users create Camila +$ uv run python main.py users create Camila Creating user: Camila // Now try some items commands -$ python main.py items create Sword +$ uv run python main.py items create Sword Creating item: Sword // And now some lands commands for reigns -$ python main.py lands reigns conquer Gondor +$ uv run python main.py lands reigns conquer Gondor Conquering reign: Gondor // And for towns -$ python main.py lands towns found Cartagena +$ uv run python main.py lands towns found Cartagena Founding town: Cartagena ``` @@ -244,44 +238,35 @@ Here are all the files if you want to review/copy them: `reigns.py`: -```Python -{!../docs_src/subcommands/tutorial003/reigns.py!} -``` +{* docs_src/subcommands/tutorial003/reigns.py *} `towns.py`: -```Python -{!../docs_src/subcommands/tutorial003/towns.py!} -``` +{* docs_src/subcommands/tutorial003/towns.py *} `lands.py`: -```Python -{!../docs_src/subcommands/tutorial003/lands.py!} -``` +{* docs_src/subcommands/tutorial003/lands.py *} `users.py`: -```Python -{!../docs_src/subcommands/tutorial003/users.py!} -``` +{* docs_src/subcommands/tutorial003/users.py *} `items.py`: -```Python -{!../docs_src/subcommands/tutorial003/items.py!} -``` +{* docs_src/subcommands/tutorial003/items.py *} `main.py`: -```Python -{!../docs_src/subcommands/tutorial003/main.py!} -``` +{* docs_src/subcommands/tutorial003/main.py *} + +/// tip + +All these files have an `if __name__ == "__main__"` block just to demonstrate how each of them can also be an independent *CLI app*. -!!! tip - All these files have an `if __name__ == "__main__"` block just to demonstrate how each of them can also be an independent *CLI app*. +But for your final application, only `main.py` would need it. - But for your final application, only `main.py` would need it. +/// ## Recap @@ -289,7 +274,10 @@ That's it, you can just add **Typer** applications one inside another as much as You can probably achieve a simpler *CLI program* design that's easier to use than the example here. But if your requirements are complex, **Typer** helps you build your *CLI app* easily. -!!! tip - Auto completion helps a lot, specially with complex programs. +/// tip + +Auto completion helps a lot, specially with complex programs. + +Check the docs about adding auto completion to your *CLI apps*. - Check the docs about adding auto completion to your *CLI apps*. +/// diff --git a/docs/tutorial/subcommands/single-file.md b/docs/tutorial/subcommands/single-file.md index 88abd9578c..3c35d262fb 100644 --- a/docs/tutorial/subcommands/single-file.md +++ b/docs/tutorial/subcommands/single-file.md @@ -1,10 +1,10 @@ +# SubCommands in a Single File + In some cases, it's possible that your application code needs to live on a single file. You can still use the same ideas: -```Python -{!../docs_src/subcommands/tutorial002/main.py!} -``` +{* docs_src/subcommands/tutorial002/main.py *} There are several things to notice here... @@ -14,9 +14,7 @@ First, you can create `typer.Typer()` objects and add them to another one at the It doesn't have to be done after creating the subcommands: -```Python hl_lines="4 5 6 7" -{!../docs_src/subcommands/tutorial002/main.py!} -``` +{* docs_src/subcommands/tutorial002/main.py hl[4,5,6,7] *} You can add the commands (subcommands) to each `typer.Typer()` app later and it will still work. @@ -26,9 +24,7 @@ As you now have subcommands like `create` for `users` and for `items`, you can n So we use longer names: -```Python hl_lines="11 16 21 26 31" -{!../docs_src/subcommands/tutorial002/main.py!} -``` +{* docs_src/subcommands/tutorial002/main.py hl[11,16,21,26,31] *} ## Command name @@ -42,7 +38,7 @@ To call them like: ```console // We want this ✔️ -$ python main.py items create +$ uv run python main.py items create ```
@@ -53,16 +49,14 @@ instead of: ```console // We don't want this ⛔️ -$ python main.py items items-create +$ uv run python main.py items items-create ```
So we pass the name we want to use for each subcommand as the function argument to the decorator: -```Python hl_lines="10 15 20 25 30" -{!../docs_src/subcommands/tutorial002/main.py!} -``` +{* docs_src/subcommands/tutorial002/main.py hl[10,15,20,25,30] *} ## Check it @@ -73,7 +67,7 @@ It still works the same: ```console // Check the help -$ python main.py --help +$ uv run python main.py --help Usage: main.py [OPTIONS] COMMAND [ARGS]... @@ -97,7 +91,7 @@ Check the `items` command: ```console // Check the help for items -$ python main.py items --help +$ uv run python main.py items --help // It shows its own commands (subcommands): create, delete, sell Usage: main.py items [OPTIONS] COMMAND [ARGS]... @@ -111,11 +105,11 @@ Commands: sell // Try it -$ python main.py items create Wand +$ uv run python main.py items create Wand Creating item: Wand -$ python main.py items sell Vase +$ uv run python main.py items sell Vase Selling item: Vase ``` @@ -128,7 +122,7 @@ And the same for the `users` command:
```console -$ python main.py users --help +$ uv run python main.py users --help Usage: main.py users [OPTIONS] COMMAND [ARGS]... @@ -140,7 +134,7 @@ Commands: delete // Try it -$ python main.py users create Camila +$ uv run python main.py users create Camila Creating user: Camila ``` diff --git a/docs/tutorial/terminating.md b/docs/tutorial/terminating.md index 94f6d239e0..bbc918e71a 100644 --- a/docs/tutorial/terminating.md +++ b/docs/tutorial/terminating.md @@ -1,3 +1,5 @@ +# Terminating + There are some cases where you might want to terminate a command at some point, and stop all subsequent execution. It could be that your code determined that the program completed successfully, or it could be an operation aborted. @@ -10,9 +12,7 @@ This doesn't have to mean that there's an error, just that nothing else needs to In that case, you can raise a `typer.Exit()` exception: -```Python hl_lines="9" -{!../docs_src/terminating/tutorial001.py!} -``` +{* docs_src/terminating/tutorial001.py hl[9] *} There are several things to see in this example. @@ -25,13 +25,13 @@ Check it:
```console -$ python main.py Camila +$ uv run python main.py Camila User created: Camila Notification sent for new user: Camila // Try with an existing user -$ python main.py rick +$ uv run python main.py rick The user already exists @@ -40,12 +40,15 @@ The user already exists
-!!! tip - Even though you are raising an exception, it doesn't necessarily mean there's an error. +/// tip + +Even though you are raising an exception, it doesn't necessarily mean there's an error. + +This is done with an exception because it works as an "error" and stops all execution. - This is done with an exception because it works as an "error" and stops all execution. +But then **Typer** catches it and just terminates the program normally. - But then **Typer** (actually Click) catches it and just terminates the program normally. +/// ## Exit with an error @@ -53,16 +56,14 @@ The user already exists You can pass a `code` with a number other than `0` to tell the terminal that there was an error in the execution of the program: -```Python hl_lines="7" -{!../docs_src/terminating/tutorial002.py!} -``` +{* docs_src/terminating/tutorial002.py hl[10] *} Check it:
```console -$ python main.py Camila +$ uv run python main.py Camila New user created: Camila @@ -72,7 +73,7 @@ $ echo $? 0 // Now make it exit with an error -$ python main.py root +$ uv run python main.py root The root user is reserved @@ -86,8 +87,11 @@ $ echo $?
-!!! tip - The error code might be used by other programs (for example a Bash script) that execute your CLI program. +/// tip + +The error code might be used by other programs (for example a Bash script) that execute your CLI program. + +/// ## Abort @@ -95,21 +99,19 @@ There's a special exception that you can use to "abort" a program. It works more or less the same as `typer.Exit()` but will print `"Aborted!"` to the screen and can be useful in certain cases later to make it explicit that the execution was aborted: -```Python hl_lines="7" -{!../docs_src/terminating/tutorial003.py!} -``` +{* docs_src/terminating/tutorial003.py hl[10] *} Check it:
```console -$ python main.py Camila +$ uv run python main.py Camila New user created: Camila // Now make it exit with an error -$ python main.py root +$ uv run python main.py root The root user is reserved Aborted! diff --git a/docs/tutorial/testing.md b/docs/tutorial/testing.md index 108ffc344f..e57f5960eb 100644 --- a/docs/tutorial/testing.md +++ b/docs/tutorial/testing.md @@ -1,17 +1,17 @@ -Testing **Typer** applications is very easy with pytest. +# Testing + +Testing **Typer** applications is very easy with [pytest](https://docs.pytest.org/en/latest/). Let's say you have an application `app/main.py` with: -```Python -{!../docs_src/testing/app01/main.py!} -``` +{* docs_src/testing/app01/main.py *} So, you would use it like:
```console -$ python main.py Camila --city Berlin +$ uv run python main.py Camila --city Berlin Hello Camila Let's have a coffee in Berlin @@ -33,12 +33,13 @@ Import `CliRunner` and create a `runner` object. This runner is what will "invoke" or "call" your command line application. -```Python hl_lines="1 5" -{!../docs_src/testing/app01/test_main.py!} -``` +{* docs_src/testing/app01/test_main.py hl[1,5] *} + +/// tip -!!! tip - It's important that the name of the file starts with `test_`, that way pytest will be able to detect it and use it automatically. +It's important that the name of the file starts with `test_`, that way pytest will be able to detect it and use it automatically. + +/// ### Call the app @@ -50,30 +51,35 @@ The first parameter to `runner.invoke()` is a `Typer` app. The second parameter is a `list` of `str`, with all the text you would pass in the command line, right as you would pass it: -```Python hl_lines="8 9" -{!../docs_src/testing/app01/test_main.py!} -``` +{* docs_src/testing/app01/test_main.py hl[8,9] *} + +/// tip -!!! tip - The name of the function has to start with `test_`, that way pytest can detect it and use it automatically. +The name of the function has to start with `test_`, that way pytest can detect it and use it automatically. + +/// ### Check the result Then, inside of the test function, add `assert` statements to ensure that everything in the result of the call is as it should be. -```Python hl_lines="10 11 12" -{!../docs_src/testing/app01/test_main.py!} -``` +{* docs_src/testing/app01/test_main.py hl[10,11,12] *} Here we are checking that the exit code is 0, as it is for programs that exit without errors. Then we check that the text printed to "standard output" contains the text that our CLI program prints. -!!! tip - You could also check `result.stderr` for "standard error". +/// tip -!!! info - If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error){.internal-link target=_blank}. +You could also check the output sent to "standard error" (`stderr`) or "standard output" (`stdout`) independently by accessing `result.stdout` and `result.stderr` in your tests. + +/// + +/// note + +If you need a refresher about what is "standard output" and "standard error" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error). + +/// ### Call `pytest` @@ -82,10 +88,10 @@ Then you can call `pytest` in your directory and it will run your tests:
```console -$ pytest +$ uv run pytest ================ test session starts ================ -platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +platform linux -- Python 3.10, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 rootdir: /home/user/code/superawesome-cli/app plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 collected 1 item @@ -103,16 +109,14 @@ test_main.py . If you have a CLI with prompts, like: -```Python hl_lines="7" -{!../docs_src/testing/app02/main.py!} -``` +{* docs_src/testing/app02/main.py hl[9] *} That you would use like:
```console -$ python main.py Camila +$ uv run python main.py Camila # Email: $ camila@example.com @@ -125,30 +129,27 @@ You can test the input typed in the terminal using `input="camila@example.com\n" This is because what you type in the terminal goes to "**standard input**" and is handled by the operating system as if it was a "virtual file". -!!! info - If you need a refresher about what is "standard output", "standard error", and "standard input" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error){.internal-link target=_blank}. +/// note + +If you need a refresher about what is "standard output", "standard error", and "standard input" check the section in [Printing and Colors: "Standard Output" and "Standard Error"](printing.md#standard-output-and-standard-error). + +/// When you hit the ENTER key after typing the email, that is just a "new line character". And in Python that is represented with `"\n"`. So, if you use `input="camila@example.com\n"` it means: "type `camila@example.com` in the terminal, then hit the ENTER key": -```Python hl_lines="9" -{!../docs_src/testing/app02/test_main.py!} -``` +{* docs_src/testing/app02/test_main.py hl[9] *} ## Test a function If you have a script and you never created an explicit `typer.Typer` app, like: -```Python hl_lines="9" -{!../docs_src/testing/app03/main.py!} -``` +{* docs_src/testing/app03/main.py hl[9] *} ...you can still test it, by creating an app during testing: -```Python hl_lines="6 7 13" -{!../docs_src/testing/app03/test_main.py!} -``` +{* docs_src/testing/app03/test_main.py hl[6,7,13] *} Of course, if you are testing that script, it's probably easier/cleaner to just create the explicit `typer.Typer` app in `main.py` instead of creating it just during the test. diff --git a/docs/tutorial/typer-app.md b/docs/tutorial/typer-app.md new file mode 100644 index 0000000000..d7345c33c2 --- /dev/null +++ b/docs/tutorial/typer-app.md @@ -0,0 +1,121 @@ +# Typer App + +## Explicit application + +So far, you have seen how to create a single function and then pass that function to `typer.run()`. + +For example: + +{* docs_src/first_steps/tutorial002.py hl[9] *} + +But that is actually a shortcut. Under the hood, **Typer** converts that to a CLI application with `typer.Typer()` and executes it. All that inside of `typer.run()`. + +There's also a more explicit way to achieve the same: + +{* docs_src/typer_app/tutorial001.py hl[3,6,12] *} + +When you use `typer.run()`, **Typer** is doing more or less the same as above, it will: + +* Create a new `typer.Typer()` "application". +* Create a new "`command`" with your function. +* Call the same "application" as if it was a function with "`app()`". + +/// note | `@decorator` Info + +That `@something` syntax in Python is called a "decorator". + +You put it on top of a function. Like a pretty decorative hat (I guess that's where the term came from). + +A "decorator" takes the function below and does something with it. + +In our case, this decorator tells **Typer** that the function below is a "`command`". +You will learn more about commands later in the section [commands](./commands/index.md). + +/// + +Both ways, with `typer.run()` and creating the explicit application, achieve almost the same. + +/// tip + +If your use case is solved with just `typer.run()`, that's fine, you don't have to create the explicit `app` and use `@app.command()`, etc. + +You might want to do that later when your app needs extra features, but if it doesn't need them yet, that's fine. + +/// + +If you run the second example, with the explicit `app`, it works exactly the same: + +
+ +```console +// Without a CLI argument +$ uv run python main.py + +Usage: main.py [OPTIONS] {name} +Try "main.py --help" for help. + +Error: Missing argument 'name'. + +// With the 'name' CLI argument +$ uv run python main.py Camila + +Hello Camila + +// Asking for help +$ uv run python main.py --help + +Usage: main.py [OPTIONS] {name} + +Arguments: + name [required] + +Options: + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell, to copy it or customize the installation. + --help Show this message and exit. +``` + +
+ +## CLI application completion + +There's a little detail that is worth noting here. + +Now the help shows two new *CLI options*: + +* `--install-completion` +* `--show-completion` + +To get shell/tab completion, it's necessary to build a package that you and your users can install and **call directly**. + +So instead of running a Python script like: + +
+ +```console +$ uv run python main.py + +✨ Some magic here ✨ +``` + +
+ +...It would be called like: + +
+ +```console +$ magic-app + +✨ Some magic here ✨ +``` + +
+ +Having a standalone program like that allows setting up shell/tab completion. + +The first step to be able to create an installable package like that is to use an explicit `typer.Typer()` app. + +Later you can learn all the process to create a standalone CLI application and [Build a Package](./package.md). + +But for now, it's just good to know that you are on that path. 😎 diff --git a/docs/typer-cli.md b/docs/tutorial/typer-command.md similarity index 53% rename from docs/typer-cli.md rename to docs/tutorial/typer-command.md index 35f17e4d77..21bc12f098 100644 --- a/docs/typer-cli.md +++ b/docs/tutorial/typer-command.md @@ -1,100 +1,67 @@ -# Typer CLI +# `typer` command -

- Run Typer scripts with completion, without having to create a package, using Typer CLI. -

-

- - Build Status - - - Coverage - - - Package version - -

+The `typer` command provides ✨ completion ✨ in the Terminal for your own small scripts. Even if they don't use Typer internally. Of course, it works better if you use **Typer** in your script. -There is an optional utility tool called **Typer CLI**, additional to **Typer** itself. +It's probably most useful if you have a small custom Python script using **Typer** (maybe as part of some project), for some small tasks, and it's not complex/important enough to create a whole installable Python package. -It's main feature is to provide ✨ completion ✨ in the Terminal for your own small programs built with **Typer**. +In that case, you can run your program with the `typer` command in your Terminal, and it will provide completion for your script. -...without you having to create a complete installable Python package. +The `typer` command also has functionality to generate Markdown documentation for your own **Typer** programs 📝. -It's probably most useful if you have a small custom Python script using **Typer** (maybe as part of some project), for some small tasks, and it's not complex/important enough to create a whole installable Python package for it (something to be installed with `pip`). +## Install -In that case, you can install **Typer CLI**, and run your program with the `typer` command in your Terminal, and it will provide completion for your script. +Add Typer to your project: -You can also use **Typer CLI** to generate Markdown documentation for your own **Typer** programs 📝. - ---- - -**Documentation**: https://typer.tiangolo.com/typer-cli/ - -**Source Code for Typer CLI**: https://github.com/tiangolo/typer-cli - ---- - -## **Typer** or **Typer CLI** - -**Typer** is a library for building CLIs (Command Line Interface applications). - -You use **Typer** in your Python scripts. Like in: - -```Python -import typer +```console +$ uv add typer +``` +This installs both the Typer library and the `typer` command in the project's `.venv`. -def main(): - typer.echo("Hello World") +Activate that environment to make `typer` available as a first-class command in your terminal: +//// tab | Linux, macOS -if __name__ == "__main__": - typer.run(main) +```console +$ source .venv/bin/activate ``` -**Typer CLI** is a command line application to run simple programs created with **Typer**, with completion in your terminal 🚀. - -You use **Typer CLI** in your terminal, to run your scripts (as an alternative to calling `python` directly). Like in: +//// -
+//// tab | Windows PowerShell ```console -$ typer my_script.py run - -Hello World +$ .venv\Scripts\Activate.ps1 ``` -
+//// -But you never import anything from **Typer CLI** in your own scripts. +//// tab | Windows Bash -## Usage +```console +$ source .venv/Scripts/activate +``` -### Install +//// -Install **Typer CLI**: +Using the `typer` command from the project environment ensures it uses the same Typer version and dependencies as the project. -
+If you don't want to use the `typer` command, you can call the Typer library as a module with: ```console -$ python -m pip install typer-cli ----> 100% -Successfully installed typer-cli +$ uv run python -m typer ``` -
- -That creates a `typer` command you can call in your terminal, much like `python`, `git`, or `echo`. +## Install completion -You can then install completion for it: +You can then install completion for the `typer` command with:
```console $ typer --install-completion -zsh completion installed in /home/user/.bashrc. +bash completion installed in /home/user/.bashrc. Completion will take effect once you restart the terminal. ``` @@ -137,9 +104,8 @@ For it to work, you would also install **Typer**:
```console -$ python -m pip install typer +$ uv add typer ---> 100% -Successfully installed typer ```
@@ -151,15 +117,15 @@ Then you could run your script with normal Python:
```console -$ python my_custom_script.py hello +$ uv run python my_custom_script.py hello Hello World! -$ python my_custom_script.py hello --name Camila +$ uv run python my_custom_script.py hello --name Camila Hello Camila! -$ python my_custom_script.py bye --name Camila +$ uv run python my_custom_script.py bye --name Camila Bye Camila ``` @@ -170,11 +136,9 @@ There's nothing wrong with using Python directly to run it. And, in fact, if som ⛔️ But in your terminal, you won't get completion when hitting TAB for any of the subcommands or options, like `hello`, `bye`, and `--name`. -### Run with **Typer CLI** - -Here's where **Typer CLI** is useful. +### Run with the `typer` command. -You can also run the same script with the `typer` command you get after installing `typer-cli`: +You can also run the same script with the `typer` command:
@@ -197,24 +161,24 @@ Bye Camila * Instead of using `python` directly you use the `typer` command. * After the name of the file, add the subcommand `run`. -✔️ If you installed completion for **Typer CLI** (for the `typer` command) as described above, when you hit TAB you will have ✨ completion for everything ✨, including all the subcommands and options of your script, like `hello`, `bye`, and `--name` 🚀. +✔️ If you installed completion for the `typer` command as described above, when you hit TAB you will have ✨ completion for everything ✨, including all the subcommands and options of your script, like `hello`, `bye`, and `--name` 🚀. ## If main -Because **Typer CLI** won't use the block with: +Because the `typer` command won't use the block with: ```Python if __name__ == "__main__": app() ``` -...you can also remove it if you are calling that script only with **Typer CLI** (using the `typer` command). +...you can also remove it if you are calling that script only with the `typer` command. ## Run other files -**Typer CLI** can run any script with **Typer**, but the script doesn't even have to use **Typer** at all. +The `typer` command can run any script with **Typer**, but the script doesn't even have to use **Typer** at all. -**Typer CLI** could even run a file with a function that could be used with `typer.run()`, even if the script doesn't use `typer.run()` or anything else. +You could even run a file with a function that could be used with `typer.run()`, even if the script doesn't use `typer.run()` or anything else. For example, a file `main.py` like this will still work: @@ -232,13 +196,13 @@ Then you can call it with: ```console $ typer main.py run --help -Usage: typer run [OPTIONS] +Usage: typer [PATH_OR_MODULE] run [OPTIONS] Say hi to someone, by default to the World. Options: - --name TEXT - --help Show this message and exit. + --name [default: World] + --help Show this message and exit. $ typer main.py run --name Camila @@ -259,10 +223,10 @@ For example: ```console $ typer my_package.main run --help -Usage: typer run [OPTIONS] +Usage: typer [PATH_OR_MODULE] run [OPTIONS] Options: - --name TEXT + --name --help Show this message and exit. $ typer my_package.main run --name Camila @@ -281,7 +245,7 @@ You can specify one of the following **CLI options**: ### Defaults -When your run a script with the **Typer CLI** (the `typer` command) it will use the app from the following priority: +When your run a script with the `typer` command it will use the app from the following priority: * An app object from the `--app` *CLI Option*. * A function to convert to a **Typer** app from `--func` *CLI Option* (like when using `typer.run()`). @@ -292,19 +256,17 @@ When your run a script with the **Typer CLI** (the `typer` command) it will use ## Generate docs -**Typer CLI** can also generate Markdown documentation for your **Typer** application. +You can also use the `typer` command to generate Markdown documentation for your **Typer** application. ### Sample script with docs For example, you could have a script like: -```Python -{!../docs_src/commands/help/tutorial001.py!} -``` +{* docs_src/commands/help/tutorial001.py *} -### Generate docs with Typer CLI +### Generate docs with the `typer` command -Then you could generate docs for it with **Typer CLI**. +Then you could generate docs for it with the `typer` command. You can use the subcommand `utils`. @@ -318,10 +280,21 @@ $ typer some_script.py utils docs
+/// tip + +If you don't want to use the `typer` command, you can still generate docs with: + +```console +$ uv run python -m typer some_script.py utils docs +``` + +/// + **Options**: -* `--name TEXT`: The name of the CLI program to use in docs. -* `--output FILE`: An output file to write docs to, like README.md. +* `--name `: The name of the CLI program to use in docs. +* `--output `: An output file to write docs to, like README.md. +* `--title `: A title to use in the docs, by default the name of the command. For example: @@ -359,19 +332,19 @@ $ awesome-cli [OPTIONS] COMMAND [ARGS]... **Commands**: -* `create`: Create a new user with USERNAME. -* `delete`: Delete a user with USERNAME. +* `create`: Create a new user with username. +* `delete`: Delete a user with username. * `delete-all`: Delete ALL users in the database. * `init`: Initialize the users database. ## `awesome-cli create` -Create a new user with USERNAME. +Create a new user with username. **Usage**: ```console -$ awesome-cli create [OPTIONS] USERNAME +$ awesome-cli create [OPTIONS] {username} ``` **Options**: @@ -380,14 +353,14 @@ $ awesome-cli create [OPTIONS] USERNAME ## `awesome-cli delete` -Delete a user with USERNAME. +Delete a user with 'username'. If --force is not used, will ask for confirmation. **Usage**: ```console -$ awesome-cli delete [OPTIONS] USERNAME +$ awesome-cli delete [OPTIONS] {username} ``` **Options**: @@ -425,9 +398,3 @@ $ awesome-cli init [OPTIONS] **Options**: * `--help`: Show this message and exit. - ---- - -## License - -**Typer CLI**, the same as **Typer**, is licensed under the terms of the MIT license. diff --git a/docs/tutorial/using-click.md b/docs/tutorial/using-click.md deleted file mode 100644 index 36a25d4c43..0000000000 --- a/docs/tutorial/using-click.md +++ /dev/null @@ -1,180 +0,0 @@ -!!! warning - This is a more advanced topic, if you are starting with **Typer**, feel free to skip it. - - It will be mostly useful for people that already work with Click and have questions around it. - -**Typer** is powered by Click. It does all the work underneath. - -Here is some more information related to using both together. - -## A single app with both Click and **Typer** - -If you already have a Click application and want to migrate to **Typer**, or to add some Typer components, you can get a Click `Command` from your Typer application and then use Click directly. - -### How Click works - -Before knowing how to combine Click and **Typer**, let's first check a little about how Click works. - -#### Click `Command` - -Any Click application has an object of class `Command`. That's, more or less, the most basic Click object. - -A `Command` can have its own *CLI arguments* and *CLI options*, and it has a function that it calls. - -For example, in this Click app: - -```Python hl_lines="7 14" -{!../docs_src/using_click/tutorial001.py!} -``` - -The original `hello` variable is converted by Click from a function to a `Command` object. And the original `hello` function is used by that `Command` internally, but it is no longer named `hello` (as `hello` is now a Click `Command`). - -#### Click `Group` - -Then Click also has a `Group` class, it **inherits from `Command`**. So, a `Group` object is *also* a `Command`. - -A `Group` can also have its own *CLI arguments* and *CLI options*. - -A `Group` can have subcommands of class `Command` or sub groups of class `Group` as well. - -And a `Group` can also have a function that it calls, right before calling the function for any specific subcommand. - -For example: - -```Python hl_lines="5 19 20" -{!../docs_src/using_click/tutorial002.py!} -``` - -The `cli` variable is converted by Click from a function to a `Group` object. And the original `cli` function is used by that `Group` internally. - -!!! tip - The original `cli` function would be the equivalent of a [Typer Callback](./commands/callback.md){.internal-link target=_blank}. - -Then the `cli` variable, that now is a `Group` object, is used to add sub-commands. - -### How **Typer** works - -Typer doesn't modify the functions. You create an explicit variable of class `typer.Typer` and use it to *register* those functions. - -And then, when you call the app, Typer goes and creates a Click `Command` (or `Group`), and then calls it. - -If your app only has one command, then when you call it, **Typer** creates a single Click `Command` object and calls it. - -But **Typer** creates a Click `Group` object if your app has any of: - -* More than one command. -* A callback. -* Sub-Typer apps (sub commands). - -!!! tip - If you want to learn more about this check the section [One or Multiple Commands](./commands/one-or-multiple.md){.internal-link target=_blank}. - -### Combine Click and **Typer** - -**Typer** uses an internal function `typer.main.get_command()` to generate a Click `Command` (or `Group`) from a `typer.Typer` object. - -You can use it directly, and use the Click object with other Click applications. - -### Including a Click app in a **Typer** app - -For example, you could have a **Typer** app, generate a Click `Group` from it, and then include other Click apps in it: - -```Python hl_lines="15 16 29 31 34" -{!../docs_src/using_click/tutorial003.py!} -``` - -Notice that we add a callback that does nothing (only document the CLI program), to make sure **Typer** creates a Click `Group`. That way we can add sub-commands to that Click `Group`. - -Then we generate a Click object from our `typer.Typer` app (`typer_click_object`), and then we can include another Click object (`hello`) in this Click `Group`. - -And that way, our **Typer** app will have a subcommand `top` built with Typer, and a subcommand `hello` built with Click. - -Check it: - -
- -```console -$ python main.py - -// Notice we have both subcommands, top and hello -Usage: main.py [OPTIONS] COMMAND [ARGS]... - -Error: Missing command. - -// Call the Typer part -$ python main.py top - -The Typer app is at the top level - -// Call the Click part -$ python main.py hello --name Camila - -Hello Camila! -``` - -
- -### Including a **Typer** app in a Click app - -The same way, you can do the contrary and include a **Typer** sub app in a bigger Click app: - -```Python hl_lines="31 33 36" -{!../docs_src/using_click/tutorial004.py!} -``` - -Notice that we don't have to add a callback or more commands, we can just create a **Typer** app that generates a single Click `Command`, as we don't need to include anything under the Typer app. - -Then we generate a Click object from our `typer.Typer` app (`typer_click_object`), and then we use **the Click `cli` to include** our Click object from our Typer app. - -In this case, the original Click app includes the **Typer** app. - -And then we call the *original Click* app, not the Typer app. - -Check it: - -
- -```console -$ python main.py - -// We get our Typer app down there in the sub command -Usage: main.py [OPTIONS] COMMAND [ARGS]... - -Options: - --help Show this message and exit. - -Commands: - dropdb - initdb - sub A single-command Typer sub app - - -// Use the Click part -$ python main.py initdb - -Initialized the database - -// And use the Typer part -$ python main.py sub - -Typer is now below Click, the Click app is the top level -``` - -
- -## About Click decorators - -Typer apps don't work with Click decorators directly. - -This is because **Typer** doesn't modify functions to add metadata or to convert them to another object like Click does. - -So, things like `@click.pass_context` won't work. - -Most of the functionality provided by decorators in Click has an alternative way of doing it in **Typer**. - -For example, to access the context, you can just declare a function parameter of type `typer.Context`. - -!!! tip - You can read more about using the context in the docs: [Commands: Using the Context](commands/context.md){.internal-link target=_blank} - -But if you need to use something based on Click decorators, you can always generate a Click object using the methods described above, and use it as you would normally use Click. diff --git a/docs/virtual-environments.md b/docs/virtual-environments.md new file mode 100644 index 0000000000..ef960399d8 --- /dev/null +++ b/docs/virtual-environments.md @@ -0,0 +1,35 @@ +# Virtual Environments + +When you work with Python projects, you should use a **virtual environment** to isolate the packages installed for each project. + +For Typer projects, I recommend using [uv](https://docs.astral.sh/uv/) to manage the project, its dependencies, and its virtual environment. + +## Create a Project { #create-a-project } + +Install `uv` using the [official installation guide](https://docs.astral.sh/uv/getting-started/installation/), and then create a project: + +
+ +```console +$ uv init awesome-project --bare +$ cd awesome-project +$ uv add typer +``` + +
+ +`uv` creates a virtual environment for the project automatically. You don't need to create or activate one yourself. + +Run commands inside the project environment with `uv run`, for example: + +
+ +```console +$ uv run python main.py +``` + +
+ +## Learn More { #learn-more } + +Read the [Virtual Environments guide](https://tiangolo.com/guides/virtual-environments/) to learn how virtual environments work underneath, including activation and the alternative `python -m venv` and `pip` workflow. diff --git a/tests/test_compat/__init__.py b/docs_src/app_dir/__init__.py similarity index 100% rename from tests/test_compat/__init__.py rename to docs_src/app_dir/__init__.py diff --git a/docs_src/app_dir/tutorial001.py b/docs_src/app_dir/tutorial001.py index b288b5fa84..a0fd4b70d5 100644 --- a/docs_src/app_dir/tutorial001.py +++ b/docs_src/app_dir/tutorial001.py @@ -4,13 +4,16 @@ APP_NAME = "my-super-cli-app" +app = typer.Typer() + +@app.command() def main(): app_dir = typer.get_app_dir(APP_NAME) config_path: Path = Path(app_dir) / "config.json" if not config_path.is_file(): - typer.echo("Config file doesn't exist yet") + print("Config file doesn't exist yet") if __name__ == "__main__": - typer.run(main) + app() diff --git a/tests/test_tutorial/test_options/test_completion/__init__.py b/docs_src/arguments/__init__.py similarity index 100% rename from tests/test_tutorial/test_options/test_completion/__init__.py rename to docs_src/arguments/__init__.py diff --git a/tests/test_tutorial/test_using_click/__init__.py b/docs_src/arguments/default/__init__.py similarity index 100% rename from tests/test_tutorial/test_using_click/__init__.py rename to docs_src/arguments/default/__init__.py diff --git a/docs_src/arguments/default/tutorial001.py b/docs_src/arguments/default/tutorial001.py index 538397c960..7ac5ac1669 100644 --- a/docs_src/arguments/default/tutorial001.py +++ b/docs_src/arguments/default/tutorial001.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("Wade Wilson")): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str, typer.Argument()] = "Wade Wilson"): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/default/tutorial001_non_an.py b/docs_src/arguments/default/tutorial001_non_an.py new file mode 100644 index 0000000000..6873fbeab4 --- /dev/null +++ b/docs_src/arguments/default/tutorial001_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("Wade Wilson")): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/default/tutorial002.py b/docs_src/arguments/default/tutorial002.py index 4f7b900bed..aab843f3db 100644 --- a/docs_src/arguments/default/tutorial002.py +++ b/docs_src/arguments/default/tutorial002.py @@ -1,15 +1,19 @@ import random +from typing import Annotated import typer +app = typer.Typer() + def get_name(): return random.choice(["Deadpool", "Rick", "Morty", "Hiro"]) -def main(name: str = typer.Argument(get_name)): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str, typer.Argument(default_factory=get_name)]): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/default/tutorial002_non_an.py b/docs_src/arguments/default/tutorial002_non_an.py new file mode 100644 index 0000000000..87398e84c4 --- /dev/null +++ b/docs_src/arguments/default/tutorial002_non_an.py @@ -0,0 +1,18 @@ +import random + +import typer + +app = typer.Typer() + + +def get_name(): + return random.choice(["Deadpool", "Rick", "Morty", "Hiro"]) + + +@app.command() +def main(name: str = typer.Argument(default_factory=get_name)): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/envvar/__init__.py b/docs_src/arguments/envvar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/arguments/envvar/tutorial001.py b/docs_src/arguments/envvar/tutorial001.py index 568ac99779..26d0007236 100644 --- a/docs_src/arguments/envvar/tutorial001.py +++ b/docs_src/arguments/envvar/tutorial001.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("World", envvar="AWESOME_NAME")): - typer.echo(f"Hello Mr. {name}") +@app.command() +def main(name: Annotated[str, typer.Argument(envvar="AWESOME_NAME")] = "World"): + print(f"Hello Mr. {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/envvar/tutorial001_non_an.py b/docs_src/arguments/envvar/tutorial001_non_an.py new file mode 100644 index 0000000000..25666e2907 --- /dev/null +++ b/docs_src/arguments/envvar/tutorial001_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("World", envvar="AWESOME_NAME")): + print(f"Hello Mr. {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/envvar/tutorial002.py b/docs_src/arguments/envvar/tutorial002.py index 93f35ab9ff..edcc42bb56 100644 --- a/docs_src/arguments/envvar/tutorial002.py +++ b/docs_src/arguments/envvar/tutorial002.py @@ -1,9 +1,16 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("World", envvar=["AWESOME_NAME", "GOD_NAME"])): - typer.echo(f"Hello Mr. {name}") +@app.command() +def main( + name: Annotated[str, typer.Argument(envvar=["AWESOME_NAME", "GOD_NAME"])] = "World", +): + print(f"Hello Mr. {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/envvar/tutorial002_non_an.py b/docs_src/arguments/envvar/tutorial002_non_an.py new file mode 100644 index 0000000000..cfb14a2427 --- /dev/null +++ b/docs_src/arguments/envvar/tutorial002_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("World", envvar=["AWESOME_NAME", "GOD_NAME"])): + print(f"Hello Mr. {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/envvar/tutorial003.py b/docs_src/arguments/envvar/tutorial003.py index 2b64059ce3..0b6da988a6 100644 --- a/docs_src/arguments/envvar/tutorial003.py +++ b/docs_src/arguments/envvar/tutorial003.py @@ -1,9 +1,18 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("World", envvar="AWESOME_NAME", show_envvar=False)): - typer.echo(f"Hello Mr. {name}") +@app.command() +def main( + name: Annotated[ + str, typer.Argument(envvar="AWESOME_NAME", show_envvar=False) + ] = "World", +): + print(f"Hello Mr. {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/envvar/tutorial003_non_an.py b/docs_src/arguments/envvar/tutorial003_non_an.py new file mode 100644 index 0000000000..3cbeaaba49 --- /dev/null +++ b/docs_src/arguments/envvar/tutorial003_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("World", envvar="AWESOME_NAME", show_envvar=False)): + print(f"Hello Mr. {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/__init__.py b/docs_src/arguments/help/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/arguments/help/tutorial001.py b/docs_src/arguments/help/tutorial001.py index e6774df4b1..dc04a1f1f0 100644 --- a/docs_src/arguments/help/tutorial001.py +++ b/docs_src/arguments/help/tutorial001.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument(..., help="The name of the user to greet")): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str, typer.Argument(help="The name of the user to greet")]): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/help/tutorial001_non_an.py b/docs_src/arguments/help/tutorial001_non_an.py new file mode 100644 index 0000000000..c0928136ea --- /dev/null +++ b/docs_src/arguments/help/tutorial001_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument(..., help="The name of the user to greet")): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial002.py b/docs_src/arguments/help/tutorial002.py index 25868287fb..7fd2b70dbc 100644 --- a/docs_src/arguments/help/tutorial002.py +++ b/docs_src/arguments/help/tutorial002.py @@ -1,12 +1,17 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument(..., help="The name of the user to greet")): +@app.command() +def main(name: Annotated[str, typer.Argument(help="The name of the user to greet")]): """ - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. """ - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/help/tutorial002_non_an.py b/docs_src/arguments/help/tutorial002_non_an.py new file mode 100644 index 0000000000..11098b1085 --- /dev/null +++ b/docs_src/arguments/help/tutorial002_non_an.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument(..., help="The name of the user to greet")): + """ + Say hi to 'name' very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial003.py b/docs_src/arguments/help/tutorial003.py index 4cc9407c71..ef8441f2f5 100644 --- a/docs_src/arguments/help/tutorial003.py +++ b/docs_src/arguments/help/tutorial003.py @@ -1,12 +1,17 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("World", help="Who to greet")): +@app.command() +def main(name: Annotated[str, typer.Argument(help="Who to greet")] = "World"): """ - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. """ - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/help/tutorial003_non_an.py b/docs_src/arguments/help/tutorial003_non_an.py new file mode 100644 index 0000000000..183cf318da --- /dev/null +++ b/docs_src/arguments/help/tutorial003_non_an.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("World", help="Who to greet")): + """ + Say hi to 'name' very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial004.py b/docs_src/arguments/help/tutorial004.py index c0fcdb9586..b926ccadd7 100644 --- a/docs_src/arguments/help/tutorial004.py +++ b/docs_src/arguments/help/tutorial004.py @@ -1,12 +1,21 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("World", help="Who to greet", show_default=False)): +@app.command() +def main( + name: Annotated[ + str, typer.Argument(help="Who to greet", show_default=False) + ] = "World", +): """ - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. """ - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/help/tutorial004_non_an.py b/docs_src/arguments/help/tutorial004_non_an.py new file mode 100644 index 0000000000..ef4bcfee9b --- /dev/null +++ b/docs_src/arguments/help/tutorial004_non_an.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("World", help="Who to greet", show_default=False)): + """ + Say hi to 'name' very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial005.py b/docs_src/arguments/help/tutorial005.py index 407e3ab45e..5d9aa7d1f4 100644 --- a/docs_src/arguments/help/tutorial005.py +++ b/docs_src/arguments/help/tutorial005.py @@ -1,13 +1,21 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( - name: str = typer.Argument( - "Wade Wilson", help="Who to greet", show_default="Deadpoolio the amazing's name" - ) + name: Annotated[ + str, + typer.Argument( + help="Who to greet", show_default="Deadpoolio the amazing's name" + ), + ] = "Wade Wilson", ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/help/tutorial005_non_an.py b/docs_src/arguments/help/tutorial005_non_an.py new file mode 100644 index 0000000000..cf5b95e946 --- /dev/null +++ b/docs_src/arguments/help/tutorial005_non_an.py @@ -0,0 +1,16 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str = typer.Argument( + "Wade Wilson", help="Who to greet", show_default="Deadpoolio the amazing's name" + ), +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial006.py b/docs_src/arguments/help/tutorial006.py index 139292465d..bcdd0d17a0 100644 --- a/docs_src/arguments/help/tutorial006.py +++ b/docs_src/arguments/help/tutorial006.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("World", metavar="✨username✨")): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str, typer.Argument(metavar="✨username✨")] = "World"): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/help/tutorial006_non_an.py b/docs_src/arguments/help/tutorial006_non_an.py new file mode 100644 index 0000000000..1aa65971e7 --- /dev/null +++ b/docs_src/arguments/help/tutorial006_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("World", metavar="✨username✨")): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial007.py b/docs_src/arguments/help/tutorial007.py index 440f2eb481..a0585678f8 100644 --- a/docs_src/arguments/help/tutorial007.py +++ b/docs_src/arguments/help/tutorial007.py @@ -1,12 +1,26 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument("World", hidden=True)): +@app.command() +def main( + name: Annotated[str, typer.Argument(help="Who to greet")], + lastname: Annotated[ + str, typer.Argument(help="The last name", rich_help_panel="Secondary Arguments") + ] = "", + age: Annotated[ + str, + typer.Argument(help="The user's age", rich_help_panel="Secondary Arguments"), + ] = "", +): """ - Say hi to NAME very gently, like Dirk. + Say hi to 'name' very gently, like Dirk. """ - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/help/tutorial007_non_an.py b/docs_src/arguments/help/tutorial007_non_an.py new file mode 100644 index 0000000000..a6924b2363 --- /dev/null +++ b/docs_src/arguments/help/tutorial007_non_an.py @@ -0,0 +1,23 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str = typer.Argument(..., help="Who to greet"), + lastname: str = typer.Argument( + "", help="The last name", rich_help_panel="Secondary Arguments" + ), + age: str = typer.Argument( + "", help="The user's age", rich_help_panel="Secondary Arguments" + ), +): + """ + Say hi to 'name' very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial008.py b/docs_src/arguments/help/tutorial008.py new file mode 100644 index 0000000000..0db231345f --- /dev/null +++ b/docs_src/arguments/help/tutorial008.py @@ -0,0 +1,17 @@ +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main(name: Annotated[str, typer.Argument(hidden=True)] = "World"): + """ + Say hi to 'name' very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/help/tutorial008_non_an.py b/docs_src/arguments/help/tutorial008_non_an.py new file mode 100644 index 0000000000..4a30cd8c44 --- /dev/null +++ b/docs_src/arguments/help/tutorial008_non_an.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument("World", hidden=True)): + """ + Say hi to 'name' very gently, like Dirk. + """ + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/optional/__init__.py b/docs_src/arguments/optional/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/arguments/optional/tutorial000.py b/docs_src/arguments/optional/tutorial000.py new file mode 100644 index 0000000000..65904062cd --- /dev/null +++ b/docs_src/arguments/optional/tutorial000.py @@ -0,0 +1,11 @@ +from typing import Annotated + +import typer + + +def main(name: Annotated[str, typer.Argument()]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/optional/tutorial000_non_an.py b/docs_src/arguments/optional/tutorial000_non_an.py new file mode 100644 index 0000000000..9c3a32b506 --- /dev/null +++ b/docs_src/arguments/optional/tutorial000_non_an.py @@ -0,0 +1,9 @@ +import typer + + +def main(name: str = typer.Argument()): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/arguments/optional/tutorial001.py b/docs_src/arguments/optional/tutorial001.py index e0412c8335..8c73522820 100644 --- a/docs_src/arguments/optional/tutorial001.py +++ b/docs_src/arguments/optional/tutorial001.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str = typer.Argument(...)): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str, typer.Argument()]): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/optional/tutorial001_non_an.py b/docs_src/arguments/optional/tutorial001_non_an.py new file mode 100644 index 0000000000..e5d2d0788f --- /dev/null +++ b/docs_src/arguments/optional/tutorial001_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument()): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/optional/tutorial002.py b/docs_src/arguments/optional/tutorial002.py index db1a660897..a16c8f1982 100644 --- a/docs_src/arguments/optional/tutorial002.py +++ b/docs_src/arguments/optional/tutorial002.py @@ -1,14 +1,14 @@ -from typing import Optional +from typing import Annotated import typer +app = typer.Typer() -def main(name: Optional[str] = typer.Argument(None)): - if name is None: - typer.echo("Hello World!") - else: - typer.echo(f"Hello {name}") + +@app.command() +def main(name: Annotated[str, typer.Argument()] = "World"): + print(f"Hello {name}!") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/arguments/optional/tutorial002_non_an.py b/docs_src/arguments/optional/tutorial002_non_an.py new file mode 100644 index 0000000000..b2085bd38d --- /dev/null +++ b/docs_src/arguments/optional/tutorial002_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument(default="World")): + print(f"Hello {name}!") + + +if __name__ == "__main__": + app() diff --git a/docs_src/arguments/optional/tutorial003.py b/docs_src/arguments/optional/tutorial003.py new file mode 100644 index 0000000000..058bf1e469 --- /dev/null +++ b/docs_src/arguments/optional/tutorial003.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = typer.Argument(default=...)): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/__init__.py b/docs_src/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/arguments/__init__.py b/docs_src/commands/arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/arguments/tutorial001.py b/docs_src/commands/arguments/tutorial001.py index 32f8eb53fa..8178b2dfa7 100644 --- a/docs_src/commands/arguments/tutorial001.py +++ b/docs_src/commands/arguments/tutorial001.py @@ -5,12 +5,12 @@ @app.command() def create(username: str): - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") @app.command() def delete(username: str): - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") if __name__ == "__main__": diff --git a/docs_src/commands/callback/__init__.py b/docs_src/commands/callback/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/callback/tutorial001.py b/docs_src/commands/callback/tutorial001.py index 26eff8bccb..5dd5784f6e 100644 --- a/docs_src/commands/callback/tutorial001.py +++ b/docs_src/commands/callback/tutorial001.py @@ -7,19 +7,19 @@ @app.command() def create(username: str): if state["verbose"]: - typer.echo("About to create a user") - typer.echo(f"Creating user: {username}") + print("About to create a user") + print(f"Creating user: {username}") if state["verbose"]: - typer.echo("Just created a user") + print("Just created a user") @app.command() def delete(username: str): if state["verbose"]: - typer.echo("About to delete a user") - typer.echo(f"Deleting user: {username}") + print("About to delete a user") + print(f"Deleting user: {username}") if state["verbose"]: - typer.echo("Just deleted a user") + print("Just deleted a user") @app.callback() @@ -28,7 +28,7 @@ def main(verbose: bool = False): Manage users in the awesome CLI app. """ if verbose: - typer.echo("Will write verbose output") + print("Will write verbose output") state["verbose"] = True diff --git a/docs_src/commands/callback/tutorial002.py b/docs_src/commands/callback/tutorial002.py index 4ab4d5342b..cb20ace884 100644 --- a/docs_src/commands/callback/tutorial002.py +++ b/docs_src/commands/callback/tutorial002.py @@ -2,7 +2,7 @@ def callback(): - typer.echo("Running a command") + print("Running a command") app = typer.Typer(callback=callback) @@ -10,7 +10,7 @@ def callback(): @app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/commands/callback/tutorial003.py b/docs_src/commands/callback/tutorial003.py index 72975deb0c..75c5bc1fa1 100644 --- a/docs_src/commands/callback/tutorial003.py +++ b/docs_src/commands/callback/tutorial003.py @@ -2,7 +2,7 @@ def callback(): - typer.echo("Running a command") + print("Running a command") app = typer.Typer(callback=callback) @@ -10,12 +10,12 @@ def callback(): @app.callback() def new_callback(): - typer.echo("Override callback, running a command") + print("Override callback, running a command") @app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/commands/callback/tutorial004.py b/docs_src/commands/callback/tutorial004.py index 11fba5d50f..fcc9dca3cc 100644 --- a/docs_src/commands/callback/tutorial004.py +++ b/docs_src/commands/callback/tutorial004.py @@ -16,7 +16,7 @@ def callback(): @app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/commands/context/__init__.py b/docs_src/commands/context/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/context/tutorial001.py b/docs_src/commands/context/tutorial001.py index 2a9bb682ab..5262df9146 100644 --- a/docs_src/commands/context/tutorial001.py +++ b/docs_src/commands/context/tutorial001.py @@ -5,12 +5,12 @@ @app.command() def create(username: str): - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") @app.command() def delete(username: str): - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") @app.callback() @@ -18,7 +18,7 @@ def main(ctx: typer.Context): """ Manage users in the awesome CLI app. """ - typer.echo(f"About to execute command: {ctx.invoked_subcommand}") + print(f"About to execute command: {ctx.invoked_subcommand}") if __name__ == "__main__": diff --git a/docs_src/commands/context/tutorial002.py b/docs_src/commands/context/tutorial002.py index 096058dd95..e396485242 100644 --- a/docs_src/commands/context/tutorial002.py +++ b/docs_src/commands/context/tutorial002.py @@ -5,12 +5,12 @@ @app.command() def create(username: str): - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") @app.command() def delete(username: str): - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") @app.callback(invoke_without_command=True) @@ -18,7 +18,7 @@ def main(): """ Manage users in the awesome CLI app. """ - typer.echo("Initializing database") + print("Initializing database") if __name__ == "__main__": diff --git a/docs_src/commands/context/tutorial003.py b/docs_src/commands/context/tutorial003.py index e4470e0b26..4782d47e18 100644 --- a/docs_src/commands/context/tutorial003.py +++ b/docs_src/commands/context/tutorial003.py @@ -5,12 +5,12 @@ @app.command() def create(username: str): - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") @app.command() def delete(username: str): - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") @app.callback(invoke_without_command=True) @@ -19,7 +19,7 @@ def main(ctx: typer.Context): Manage users in the awesome CLI app. """ if ctx.invoked_subcommand is None: - typer.echo("Initializing database") + print("Initializing database") if __name__ == "__main__": diff --git a/docs_src/commands/context/tutorial004.py b/docs_src/commands/context/tutorial004.py index 9280aed636..d040103457 100644 --- a/docs_src/commands/context/tutorial004.py +++ b/docs_src/commands/context/tutorial004.py @@ -8,7 +8,7 @@ ) def main(ctx: typer.Context): for extra_arg in ctx.args: - typer.echo(f"Got extra arg: {extra_arg}") + print(f"Got extra arg: {extra_arg}") if __name__ == "__main__": diff --git a/docs_src/commands/help/__init__.py b/docs_src/commands/help/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/help/tutorial001.py b/docs_src/commands/help/tutorial001.py index 3ac6bf7e41..5ed82b831b 100644 --- a/docs_src/commands/help/tutorial001.py +++ b/docs_src/commands/help/tutorial001.py @@ -1,3 +1,5 @@ +from typing import Annotated + import typer app = typer.Typer(help="Awesome CLI user manager.") @@ -6,38 +8,42 @@ @app.command() def create(username: str): """ - Create a new user with USERNAME. + Create a new user with username. """ - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") @app.command() def delete( username: str, - force: bool = typer.Option( - ..., - prompt="Are you sure you want to delete the user?", - help="Force deletion without confirmation.", - ), + force: Annotated[ + bool, + typer.Option( + prompt="Are you sure you want to delete the user?", + help="Force deletion without confirmation.", + ), + ], ): """ - Delete a user with USERNAME. + Delete a user with username. If --force is not used, will ask for confirmation. """ if force: - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") else: - typer.echo("Operation cancelled") + print("Operation cancelled") @app.command() def delete_all( - force: bool = typer.Option( - ..., - prompt="Are you sure you want to delete ALL users?", - help="Force deletion without confirmation.", - ) + force: Annotated[ + bool, + typer.Option( + prompt="Are you sure you want to delete ALL users?", + help="Force deletion without confirmation.", + ), + ], ): """ Delete ALL users in the database. @@ -45,9 +51,9 @@ def delete_all( If --force is not used, will ask for confirmation. """ if force: - typer.echo("Deleting all users") + print("Deleting all users") else: - typer.echo("Operation cancelled") + print("Operation cancelled") @app.command() @@ -55,7 +61,7 @@ def init(): """ Initialize the users database. """ - typer.echo("Initializing user database") + print("Initializing user database") if __name__ == "__main__": diff --git a/docs_src/commands/help/tutorial001_non_an.py b/docs_src/commands/help/tutorial001_non_an.py new file mode 100644 index 0000000000..cc767ce2cc --- /dev/null +++ b/docs_src/commands/help/tutorial001_non_an.py @@ -0,0 +1,62 @@ +import typer + +app = typer.Typer(help="Awesome CLI user manager.") + + +@app.command() +def create(username: str): + """ + Create a new user with username. + """ + print(f"Creating user: {username}") + + +@app.command() +def delete( + username: str, + force: bool = typer.Option( + ..., + prompt="Are you sure you want to delete the user?", + help="Force deletion without confirmation.", + ), +): + """ + Delete a user with username. + + If --force is not used, will ask for confirmation. + """ + if force: + print(f"Deleting user: {username}") + else: + print("Operation cancelled") + + +@app.command() +def delete_all( + force: bool = typer.Option( + ..., + prompt="Are you sure you want to delete ALL users?", + help="Force deletion without confirmation.", + ), +): + """ + Delete ALL users in the database. + + If --force is not used, will ask for confirmation. + """ + if force: + print("Deleting all users") + else: + print("Operation cancelled") + + +@app.command() +def init(): + """ + Initialize the users database. + """ + print("Initializing user database") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial002.py b/docs_src/commands/help/tutorial002.py index d098d23518..4a6b42281c 100644 --- a/docs_src/commands/help/tutorial002.py +++ b/docs_src/commands/help/tutorial002.py @@ -3,20 +3,20 @@ app = typer.Typer() -@app.command(help="Create a new user with USERNAME.") +@app.command(help="Create a new user with username.") def create(username: str): """ Some internal utility function to create. """ - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") -@app.command(help="Delete a user with USERNAME.") +@app.command(help="Delete a user with username.") def delete(username: str): """ Some internal utility function to delete. """ - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") if __name__ == "__main__": diff --git a/docs_src/commands/help/tutorial003.py b/docs_src/commands/help/tutorial003.py new file mode 100644 index 0000000000..6ea50b081b --- /dev/null +++ b/docs_src/commands/help/tutorial003.py @@ -0,0 +1,25 @@ +import typer + +app = typer.Typer() + + +@app.command() +def create(username: str): + """ + Create a user. + """ + print(f"Creating user: {username}") + + +@app.command(deprecated=True) +def delete(username: str): + """ + Delete a user. + + This is deprecated and will stop being supported soon. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial004.py b/docs_src/commands/help/tutorial004.py new file mode 100644 index 0000000000..da850be178 --- /dev/null +++ b/docs_src/commands/help/tutorial004.py @@ -0,0 +1,38 @@ +from typing import Annotated + +import typer + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create( + username: Annotated[ + str, typer.Argument(help="The username to be [green]created[/green]") + ], +): + """ + [bold green]Create[/bold green] a new [italic]shiny[/italic] user. :sparkles: + + This requires a [underline]username[/underline]. + """ + print(f"Creating user: {username}") + + +@app.command(help="[bold red]Delete[/bold red] a user with [italic]username[/italic].") +def delete( + username: Annotated[ + str, typer.Argument(help="The username to be [red]deleted[/red]") + ], + force: Annotated[ + bool, typer.Option(help="Force the [bold red]deletion[/bold red] :boom:") + ] = False, +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial004_non_an.py b/docs_src/commands/help/tutorial004_non_an.py new file mode 100644 index 0000000000..dacfa5e903 --- /dev/null +++ b/docs_src/commands/help/tutorial004_non_an.py @@ -0,0 +1,34 @@ +import typer + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create( + username: str = typer.Argument( + ..., help="The username to be [green]created[/green]" + ), +): + """ + [bold green]Create[/bold green] a new [italic]shiny[/italic] user. :sparkles: + + This requires a [underline]username[/underline]. + """ + print(f"Creating user: {username}") + + +@app.command(help="[bold red]Delete[/bold red] a user with [italic]username[/italic].") +def delete( + username: str = typer.Argument(..., help="The username to be [red]deleted[/red]"), + force: bool = typer.Option( + False, help="Force the [bold red]deletion[/bold red] :boom:" + ), +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial005.py b/docs_src/commands/help/tutorial005.py new file mode 100644 index 0000000000..19dc48acda --- /dev/null +++ b/docs_src/commands/help/tutorial005.py @@ -0,0 +1,38 @@ +from typing import Annotated + +import typer + +app = typer.Typer(rich_markup_mode="markdown") + + +@app.command() +def create( + username: Annotated[str, typer.Argument(help="The username to be **created**")], +): + """ + **Create** a new *shiny* user. :sparkles: + + * Create a username + + * Show that the username is created + + --- + + Learn more at the [Typer docs website](https://typer.tiangolo.com) + """ + print(f"Creating user: {username}") + + +@app.command(help="**Delete** a user with *username*.") +def delete( + username: Annotated[str, typer.Argument(help="The username to be **deleted**")], + force: Annotated[bool, typer.Option(help="Force the **deletion** :boom:")] = False, +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial005_non_an.py b/docs_src/commands/help/tutorial005_non_an.py new file mode 100644 index 0000000000..a2cc14cf23 --- /dev/null +++ b/docs_src/commands/help/tutorial005_non_an.py @@ -0,0 +1,34 @@ +import typer + +app = typer.Typer(rich_markup_mode="markdown") + + +@app.command() +def create(username: str = typer.Argument(..., help="The username to be **created**")): + """ + **Create** a new *shiny* user. :sparkles: + + * Create a username + + * Show that the username is created + + --- + + Learn more at the [Typer docs website](https://typer.tiangolo.com) + """ + print(f"Creating user: {username}") + + +@app.command(help="**Delete** a user with *username*.") +def delete( + username: str = typer.Argument(..., help="The username to be **deleted**"), + force: bool = typer.Option(False, help="Force the **deletion** :boom:"), +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial006.py b/docs_src/commands/help/tutorial006.py new file mode 100644 index 0000000000..ca959998b0 --- /dev/null +++ b/docs_src/commands/help/tutorial006.py @@ -0,0 +1,55 @@ +import typer + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create(username: str): + """ + [green]Create[/green] a new user. :sparkles: + """ + print(f"Creating user: {username}") + + +@app.command() +def delete(username: str): + """ + [red]Delete[/red] a user. :x: + """ + print(f"Deleting user: {username}") + + +@app.command(rich_help_panel="Utils and Configs") +def config(configuration: str): + """ + [blue]Configure[/blue] the system. :gear: + """ + print(f"Configuring the system with: {configuration}") + + +@app.command(rich_help_panel="Utils and Configs") +def sync(): + """ + [blue]Synchronize[/blue] the system or something fancy like that. :recycle: + """ + print("Syncing the system") + + +@app.command(rich_help_panel="Help and Others") +def help(): + """ + Get [yellow]help[/yellow] with the system. :question: + """ + print("Opening help portal...") + + +@app.command(rich_help_panel="Help and Others") +def report(): + """ + [yellow]Report[/yellow] an issue. :exclamation: + """ + print("Please open a new issue online, not a direct message") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial007.py b/docs_src/commands/help/tutorial007.py new file mode 100644 index 0000000000..0c5d120e5f --- /dev/null +++ b/docs_src/commands/help/tutorial007.py @@ -0,0 +1,45 @@ +from typing import Annotated + +import typer + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create( + username: Annotated[str, typer.Argument(help="The username to create")], + lastname: Annotated[ + str, + typer.Argument( + help="The last name of the new user", rich_help_panel="Secondary Arguments" + ), + ] = "", + force: Annotated[bool, typer.Option(help="Force the creation of the user")] = False, + age: Annotated[ + int | None, + typer.Option(help="The age of the new user", rich_help_panel="Additional Data"), + ] = None, + favorite_color: Annotated[ + str | None, + typer.Option( + help="The favorite color of the new user", + rich_help_panel="Additional Data", + ), + ] = None, +): + """ + [green]Create[/green] a new user. :sparkles: + """ + print(f"Creating user: {username}") + + +@app.command(rich_help_panel="Utils and Configs") +def config(configuration: str): + """ + [blue]Configure[/blue] the system. :gear: + """ + print(f"Configuring the system with: {configuration}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial007_non_an.py b/docs_src/commands/help/tutorial007_non_an.py new file mode 100644 index 0000000000..02269c7108 --- /dev/null +++ b/docs_src/commands/help/tutorial007_non_an.py @@ -0,0 +1,37 @@ +import typer + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create( + username: str = typer.Argument(..., help="The username to create"), + lastname: str = typer.Argument( + "", help="The last name of the new user", rich_help_panel="Secondary Arguments" + ), + force: bool = typer.Option(False, help="Force the creation of the user"), + age: int | None = typer.Option( + None, help="The age of the new user", rich_help_panel="Additional Data" + ), + favorite_color: str | None = typer.Option( + None, + help="The favorite color of the new user", + rich_help_panel="Additional Data", + ), +): + """ + [green]Create[/green] a new user. :sparkles: + """ + print(f"Creating user: {username}") + + +@app.command(rich_help_panel="Utils and Configs") +def config(configuration: str): + """ + [blue]Configure[/blue] the system. :gear: + """ + print(f"Configuring the system with: {configuration}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/help/tutorial008.py b/docs_src/commands/help/tutorial008.py new file mode 100644 index 0000000000..a4f8330e93 --- /dev/null +++ b/docs_src/commands/help/tutorial008.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command(epilog="Made with :heart: in [blue]Venus[/blue]") +def create(username: str): + """ + [green]Create[/green] a new user. :sparkles: + """ + print(f"Creating user: {username}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/index/__init__.py b/docs_src/commands/index/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/index/tutorial002.py b/docs_src/commands/index/tutorial002.py index 6afab4aaae..1ed8610db8 100644 --- a/docs_src/commands/index/tutorial002.py +++ b/docs_src/commands/index/tutorial002.py @@ -5,12 +5,12 @@ @app.command() def create(): - typer.echo("Creating user: Hiro Hamada") + print("Creating user: Hiro Hamada") @app.command() def delete(): - typer.echo("Deleting user: Hiro Hamada") + print("Deleting user: Hiro Hamada") if __name__ == "__main__": diff --git a/docs_src/commands/index/tutorial003.py b/docs_src/commands/index/tutorial003.py new file mode 100644 index 0000000000..4460db155e --- /dev/null +++ b/docs_src/commands/index/tutorial003.py @@ -0,0 +1,17 @@ +import typer + +app = typer.Typer(no_args_is_help=True) + + +@app.command() +def create(): + print("Creating user: Hiro Hamada") + + +@app.command() +def delete(): + print("Deleting user: Hiro Hamada") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/index/tutorial004.py b/docs_src/commands/index/tutorial004.py new file mode 100644 index 0000000000..83419b695f --- /dev/null +++ b/docs_src/commands/index/tutorial004.py @@ -0,0 +1,17 @@ +import typer + +app = typer.Typer() + + +@app.command() +def delete(): + print("Deleting user: Hiro Hamada") + + +@app.command() +def create(): + print("Creating user: Hiro Hamada") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/index/tutorial005.py b/docs_src/commands/index/tutorial005.py new file mode 100644 index 0000000000..ef9e5ded7b --- /dev/null +++ b/docs_src/commands/index/tutorial005.py @@ -0,0 +1,17 @@ +import typer + +app = typer.Typer(suggest_commands=True) + + +@app.command() +def create(): + typer.echo("Creating...") + + +@app.command() +def delete(): + typer.echo("Deleting...") + + +if __name__ == "__main__": + app() diff --git a/docs_src/commands/name/__init__.py b/docs_src/commands/name/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/name/tutorial001.py b/docs_src/commands/name/tutorial001.py index 4f6aea4ebe..cda0ef09e6 100644 --- a/docs_src/commands/name/tutorial001.py +++ b/docs_src/commands/name/tutorial001.py @@ -5,12 +5,12 @@ @app.command("create") def cli_create_user(username: str): - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") @app.command("delete") def cli_delete_user(username: str): - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") if __name__ == "__main__": diff --git a/docs_src/commands/one_or_multiple/__init__.py b/docs_src/commands/one_or_multiple/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/one_or_multiple/tutorial001.py b/docs_src/commands/one_or_multiple/tutorial001.py index d0ad1155ae..cf6bdb7ee6 100644 --- a/docs_src/commands/one_or_multiple/tutorial001.py +++ b/docs_src/commands/one_or_multiple/tutorial001.py @@ -5,7 +5,7 @@ @app.command() def create(): - typer.echo("Creating user: Hiro Hamada") + print("Creating user: Hiro Hamada") @app.callback() diff --git a/docs_src/commands/one_or_multiple/tutorial002.py b/docs_src/commands/one_or_multiple/tutorial002.py index 27ac47f814..351f3df4f3 100644 --- a/docs_src/commands/one_or_multiple/tutorial002.py +++ b/docs_src/commands/one_or_multiple/tutorial002.py @@ -5,7 +5,7 @@ @app.command() def create(): - typer.echo("Creating user: Hiro Hamada") + print("Creating user: Hiro Hamada") @app.callback() @@ -13,7 +13,7 @@ def callback(): """ Creates a single user Hiro Hamada. - In the next version it will create 5 users more. + In the next version it will create 5 more users. """ diff --git a/docs_src/commands/options/__init__.py b/docs_src/commands/options/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/options/tutorial001.py b/docs_src/commands/options/tutorial001.py index 0b694cd5c0..565830e6af 100644 --- a/docs_src/commands/options/tutorial001.py +++ b/docs_src/commands/options/tutorial001.py @@ -1,3 +1,5 @@ +from typing import Annotated + import typer app = typer.Typer() @@ -5,33 +7,37 @@ @app.command() def create(username: str): - typer.echo(f"Creating user: {username}") + print(f"Creating user: {username}") @app.command() def delete( username: str, - force: bool = typer.Option(..., prompt="Are you sure you want to delete the user?"), + force: Annotated[ + bool, typer.Option(prompt="Are you sure you want to delete the user?") + ], ): if force: - typer.echo(f"Deleting user: {username}") + print(f"Deleting user: {username}") else: - typer.echo("Operation cancelled") + print("Operation cancelled") @app.command() def delete_all( - force: bool = typer.Option(..., prompt="Are you sure you want to delete ALL users?") + force: Annotated[ + bool, typer.Option(prompt="Are you sure you want to delete ALL users?") + ], ): if force: - typer.echo("Deleting all users") + print("Deleting all users") else: - typer.echo("Operation cancelled") + print("Operation cancelled") @app.command() def init(): - typer.echo("Initializing user database") + print("Initializing user database") if __name__ == "__main__": diff --git a/docs_src/commands/options/tutorial001_non_an.py b/docs_src/commands/options/tutorial001_non_an.py new file mode 100644 index 0000000000..b8dccd5e71 --- /dev/null +++ b/docs_src/commands/options/tutorial001_non_an.py @@ -0,0 +1,40 @@ +import typer + +app = typer.Typer() + + +@app.command() +def create(username: str): + print(f"Creating user: {username}") + + +@app.command() +def delete( + username: str, + force: bool = typer.Option(..., prompt="Are you sure you want to delete the user?"), +): + if force: + print(f"Deleting user: {username}") + else: + print("Operation cancelled") + + +@app.command() +def delete_all( + force: bool = typer.Option( + ..., prompt="Are you sure you want to delete ALL users?" + ), +): + if force: + print("Deleting all users") + else: + print("Operation cancelled") + + +@app.command() +def init(): + print("Initializing user database") + + +if __name__ == "__main__": + app() diff --git a/docs_src/exceptions/__init__.py b/docs_src/exceptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/exceptions/tutorial001.py b/docs_src/exceptions/tutorial001.py new file mode 100644 index 0000000000..25cd6df27b --- /dev/null +++ b/docs_src/exceptions/tutorial001.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str = "morty"): + print(name + 3) + + +if __name__ == "__main__": + app() diff --git a/docs_src/exceptions/tutorial002.py b/docs_src/exceptions/tutorial002.py new file mode 100644 index 0000000000..937883775f --- /dev/null +++ b/docs_src/exceptions/tutorial002.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer(pretty_exceptions_show_locals=True) + + +@app.command() +def main(name: str = "morty"): + print(name + 3) + + +if __name__ == "__main__": + app() diff --git a/docs_src/exceptions/tutorial003.py b/docs_src/exceptions/tutorial003.py new file mode 100644 index 0000000000..656203c566 --- /dev/null +++ b/docs_src/exceptions/tutorial003.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer(pretty_exceptions_short=False) + + +@app.command() +def main(name: str = "morty"): + print(name + 3) + + +if __name__ == "__main__": + app() diff --git a/docs_src/exceptions/tutorial004.py b/docs_src/exceptions/tutorial004.py new file mode 100644 index 0000000000..660c58ecc3 --- /dev/null +++ b/docs_src/exceptions/tutorial004.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer(pretty_exceptions_enable=False) + + +@app.command() +def main(name: str = "morty"): + print(name + 3) + + +if __name__ == "__main__": + app() diff --git a/docs_src/first_steps/__init__.py b/docs_src/first_steps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/first_steps/tutorial001.py b/docs_src/first_steps/tutorial001.py index 4c5d803239..94897dafef 100644 --- a/docs_src/first_steps/tutorial001.py +++ b/docs_src/first_steps/tutorial001.py @@ -2,7 +2,7 @@ def main(): - typer.echo("Hello World") + print("Hello World") if __name__ == "__main__": diff --git a/docs_src/first_steps/tutorial002.py b/docs_src/first_steps/tutorial002.py index da2fac3e99..bb1e5c7b5f 100644 --- a/docs_src/first_steps/tutorial002.py +++ b/docs_src/first_steps/tutorial002.py @@ -2,7 +2,7 @@ def main(name: str): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": diff --git a/docs_src/first_steps/tutorial003.py b/docs_src/first_steps/tutorial003.py index daa11ca889..377b045fa1 100644 --- a/docs_src/first_steps/tutorial003.py +++ b/docs_src/first_steps/tutorial003.py @@ -2,7 +2,7 @@ def main(name: str, lastname: str): - typer.echo(f"Hello {name} {lastname}") + print(f"Hello {name} {lastname}") if __name__ == "__main__": diff --git a/docs_src/first_steps/tutorial004.py b/docs_src/first_steps/tutorial004.py index 1e5b964424..4a11dc020f 100644 --- a/docs_src/first_steps/tutorial004.py +++ b/docs_src/first_steps/tutorial004.py @@ -3,9 +3,9 @@ def main(name: str, lastname: str, formal: bool = False): if formal: - typer.echo(f"Good day Ms. {name} {lastname}.") + print(f"Good day Ms. {name} {lastname}.") else: - typer.echo(f"Hello {name} {lastname}") + print(f"Hello {name} {lastname}") if __name__ == "__main__": diff --git a/docs_src/first_steps/tutorial005.py b/docs_src/first_steps/tutorial005.py index 3c4036296b..25bc963a90 100644 --- a/docs_src/first_steps/tutorial005.py +++ b/docs_src/first_steps/tutorial005.py @@ -3,9 +3,9 @@ def main(name: str, lastname: str = "", formal: bool = False): if formal: - typer.echo(f"Good day Ms. {name} {lastname}.") + print(f"Good day Ms. {name} {lastname}.") else: - typer.echo(f"Hello {name} {lastname}") + print(f"Hello {name} {lastname}") if __name__ == "__main__": diff --git a/docs_src/first_steps/tutorial006.py b/docs_src/first_steps/tutorial006.py index d373bbcb5b..4970833ded 100644 --- a/docs_src/first_steps/tutorial006.py +++ b/docs_src/first_steps/tutorial006.py @@ -3,14 +3,14 @@ def main(name: str, lastname: str = "", formal: bool = False): """ - Say hi to NAME, optionally with a --lastname. + Say hi to 'name', optionally with a --lastname. If --formal is used, say hi very formally. """ if formal: - typer.echo(f"Good day Ms. {name} {lastname}.") + print(f"Good day Ms. {name} {lastname}.") else: - typer.echo(f"Hello {name} {lastname}") + print(f"Hello {name} {lastname}") if __name__ == "__main__": diff --git a/docs_src/launch/__init__.py b/docs_src/launch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/launch/tutorial001.py b/docs_src/launch/tutorial001.py index 892faa2091..983c8dbc8c 100644 --- a/docs_src/launch/tutorial001.py +++ b/docs_src/launch/tutorial001.py @@ -1,10 +1,13 @@ import typer +app = typer.Typer() + +@app.command() def main(): - typer.echo("Opening Typer's docs") + print("Opening Typer's docs") typer.launch("https://typer.tiangolo.com") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/launch/tutorial002.py b/docs_src/launch/tutorial002.py index 5ef13ec941..e64f492d87 100644 --- a/docs_src/launch/tutorial002.py +++ b/docs_src/launch/tutorial002.py @@ -4,7 +4,10 @@ APP_NAME = "my-super-cli-app" +app = typer.Typer() + +@app.command() def main(): app_dir = typer.get_app_dir(APP_NAME) app_dir_path = Path(app_dir) @@ -13,9 +16,9 @@ def main(): if not config_path.is_file(): config_path.write_text('{"version": "1.0.0"}') config_file_str = str(config_path) - typer.echo("Opening config directory") + print("Opening config directory") typer.launch(config_file_str, locate=True) if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/multiple_values/__init__.py b/docs_src/multiple_values/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/multiple_values/arguments_with_multiple_values/__init__.py b/docs_src/multiple_values/arguments_with_multiple_values/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial001.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial001.py index 5e04072142..041bc0831a 100644 --- a/docs_src/multiple_values/arguments_with_multiple_values/tutorial001.py +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial001.py @@ -1,15 +1,17 @@ from pathlib import Path -from typing import List import typer +app = typer.Typer() -def main(files: List[Path], celebration: str): + +@app.command() +def main(files: list[Path], celebration: str): for path in files: if path.is_file(): - typer.echo(f"This file exists: {path.name}") - typer.echo(celebration) + print(f"This file exists: {path.name}") + print(celebration) if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py index 9d032d4355..a4654255b3 100644 --- a/docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial002.py @@ -1,16 +1,19 @@ -from typing import Tuple +from typing import Annotated import typer +app = typer.Typer() + +@app.command() def main( - names: Tuple[str, str, str] = typer.Argument( - ("Harry", "Hermione", "Ron"), help="Select 3 characters to play with" - ) + names: Annotated[ + tuple[str, str, str], typer.Argument(help="Select 3 characters to play with") + ] = ("Harry", "Hermione", "Ron"), ): for name in names: - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial002_non_an.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial002_non_an.py new file mode 100644 index 0000000000..c9c08b79d4 --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial002_non_an.py @@ -0,0 +1,17 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + names: tuple[str, str, str] = typer.Argument( + ("Harry", "Hermione", "Ron"), help="Select 3 characters to play with" + ), +): + for name in names: + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/multiple_values/multiple_options/__init__.py b/docs_src/multiple_values/multiple_options/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/multiple_values/multiple_options/tutorial001.py b/docs_src/multiple_values/multiple_options/tutorial001.py index 5c601ffae6..3d0d1e80b3 100644 --- a/docs_src/multiple_values/multiple_options/tutorial001.py +++ b/docs_src/multiple_values/multiple_options/tutorial001.py @@ -1,15 +1,18 @@ -from typing import List, Optional +from typing import Annotated import typer +app = typer.Typer() -def main(user: Optional[List[str]] = typer.Option(None)): + +@app.command() +def main(user: Annotated[list[str] | None, typer.Option()] = None): if not user: - typer.echo("No provided users") + print(f"No provided users (raw input = {user})") raise typer.Abort() for u in user: - typer.echo(f"Processing user: {u}") + print(f"Processing user: {u}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/multiple_values/multiple_options/tutorial001_non_an.py b/docs_src/multiple_values/multiple_options/tutorial001_non_an.py new file mode 100644 index 0000000000..9ced2d9fa3 --- /dev/null +++ b/docs_src/multiple_values/multiple_options/tutorial001_non_an.py @@ -0,0 +1,16 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(user: list[str] | None = typer.Option(None)): + if not user: + print(f"No provided users (raw input = {user})") + raise typer.Abort() + for u in user: + print(f"Processing user: {u}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/multiple_values/multiple_options/tutorial002.py b/docs_src/multiple_values/multiple_options/tutorial002.py index db373e7527..3de7044aae 100644 --- a/docs_src/multiple_values/multiple_options/tutorial002.py +++ b/docs_src/multiple_values/multiple_options/tutorial002.py @@ -1,11 +1,14 @@ -from typing import List +from typing import Annotated import typer +app = typer.Typer() -def main(number: List[float] = typer.Option([])): - typer.echo(f"The sum is {sum(number)}") + +@app.command() +def main(number: Annotated[list[float], typer.Option()] = []): + print(f"The sum is {sum(number)}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/multiple_values/multiple_options/tutorial002_non_an.py b/docs_src/multiple_values/multiple_options/tutorial002_non_an.py new file mode 100644 index 0000000000..d951be87e2 --- /dev/null +++ b/docs_src/multiple_values/multiple_options/tutorial002_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(number: list[float] = typer.Option([])): + print(f"The sum is {sum(number)}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/multiple_values/options_with_multiple_values/__init__.py b/docs_src/multiple_values/options_with_multiple_values/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial001.py b/docs_src/multiple_values/options_with_multiple_values/tutorial001.py index 475b50bc1c..3419cb54ee 100644 --- a/docs_src/multiple_values/options_with_multiple_values/tutorial001.py +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial001.py @@ -1,17 +1,20 @@ -from typing import Tuple +from typing import Annotated import typer +app = typer.Typer() -def main(user: Tuple[str, int, bool] = typer.Option((None, None, None))): - username, coins, is_wizard = user - if not username: - typer.echo("No user provided") + +@app.command() +def main(user: Annotated[tuple[str, int, bool] | None, typer.Option()] = None): + if not user: + print("No user provided") raise typer.Abort() - typer.echo(f"The username {username} has {coins} coins") + username, coins, is_wizard = user + print(f"The username {username} has {coins} coins") if is_wizard: - typer.echo("And this user is a wizard!") + print("And this user is a wizard!") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial001_non_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial001_non_an.py new file mode 100644 index 0000000000..cb24714f3c --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial001_non_an.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(user: tuple[str, int, bool] | None = typer.Option(None)): + if not user: + print("No user provided") + raise typer.Abort() + username, coins, is_wizard = user + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + + +if __name__ == "__main__": + app() diff --git a/docs_src/one_file_per_command/__init__.py b/docs_src/one_file_per_command/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/one_file_per_command/app/__init__.py b/docs_src/one_file_per_command/app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/one_file_per_command/app/main.py b/docs_src/one_file_per_command/app/main.py new file mode 100644 index 0000000000..9df47f4f79 --- /dev/null +++ b/docs_src/one_file_per_command/app/main.py @@ -0,0 +1,13 @@ +import typer + +from .users import app as users_app +from .version import app as version_app + +app = typer.Typer() + +app.add_typer(version_app) +app.add_typer(users_app, name="users") + + +if __name__ == "__main__": + app() diff --git a/docs_src/one_file_per_command/app/users/__init__.py b/docs_src/one_file_per_command/app/users/__init__.py new file mode 100644 index 0000000000..ccfbb0775d --- /dev/null +++ b/docs_src/one_file_per_command/app/users/__init__.py @@ -0,0 +1,9 @@ +import typer + +from .add import app as add_app +from .delete import app as delete_app + +app = typer.Typer() + +app.add_typer(add_app) +app.add_typer(delete_app) diff --git a/docs_src/one_file_per_command/app/users/add.py b/docs_src/one_file_per_command/app/users/add.py new file mode 100644 index 0000000000..507267b80b --- /dev/null +++ b/docs_src/one_file_per_command/app/users/add.py @@ -0,0 +1,8 @@ +import typer + +app = typer.Typer() + + +@app.command() +def add(name: str): + print(f"Adding user: {name}") diff --git a/docs_src/one_file_per_command/app/users/delete.py b/docs_src/one_file_per_command/app/users/delete.py new file mode 100644 index 0000000000..cc39f892bd --- /dev/null +++ b/docs_src/one_file_per_command/app/users/delete.py @@ -0,0 +1,8 @@ +import typer + +app = typer.Typer() + + +@app.command() +def delete(name: str): + print(f"Deleting user: {name}") diff --git a/docs_src/one_file_per_command/app/version.py b/docs_src/one_file_per_command/app/version.py new file mode 100644 index 0000000000..3785df1a0c --- /dev/null +++ b/docs_src/one_file_per_command/app/version.py @@ -0,0 +1,8 @@ +import typer + +app = typer.Typer() + + +@app.command() +def version(): + print("My CLI Version 1.0") diff --git a/docs_src/options/__init__.py b/docs_src/options/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/autocompletion/tutorial006.py b/docs_src/options/autocompletion/tutorial006.py deleted file mode 100644 index ea64c676fc..0000000000 --- a/docs_src/options/autocompletion/tutorial006.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import List - -import typer - - -def main(name: List[str] = typer.Option(["World"], help="The name to say hi to.")): - for each_name in name: - typer.echo(f"Hello {each_name}") - - -if __name__ == "__main__": - typer.run(main) diff --git a/docs_src/options/callback/__init__.py b/docs_src/options/callback/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/callback/tutorial001.py b/docs_src/options/callback/tutorial001.py index c717ca8a8d..7806f614f8 100644 --- a/docs_src/options/callback/tutorial001.py +++ b/docs_src/options/callback/tutorial001.py @@ -1,5 +1,9 @@ +from typing import Annotated + import typer +app = typer.Typer() + def name_callback(value: str): if value != "Camila": @@ -7,9 +11,10 @@ def name_callback(value: str): return value -def main(name: str = typer.Option(..., callback=name_callback)): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/callback/tutorial001_non_an.py b/docs_src/options/callback/tutorial001_non_an.py new file mode 100644 index 0000000000..503acc1ad5 --- /dev/null +++ b/docs_src/options/callback/tutorial001_non_an.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +def name_callback(value: str): + if value != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return value + + +@app.command() +def main(name: str | None = typer.Option(default=None, callback=name_callback)): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/callback/tutorial002.py b/docs_src/options/callback/tutorial002.py index d579e56b96..f23bee2bff 100644 --- a/docs_src/options/callback/tutorial002.py +++ b/docs_src/options/callback/tutorial002.py @@ -1,16 +1,21 @@ +from typing import Annotated + import typer +app = typer.Typer() + def name_callback(value: str): - typer.echo("Validating name") + print("Validating name") if value != "Camila": raise typer.BadParameter("Only Camila is allowed") return value -def main(name: str = typer.Option(..., callback=name_callback)): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/callback/tutorial002_non_an.py b/docs_src/options/callback/tutorial002_non_an.py new file mode 100644 index 0000000000..edeeefff46 --- /dev/null +++ b/docs_src/options/callback/tutorial002_non_an.py @@ -0,0 +1,19 @@ +import typer + +app = typer.Typer() + + +def name_callback(value: str): + print("Validating name") + if value != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return value + + +@app.command() +def main(name: str | None = typer.Option(default=None, callback=name_callback)): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/callback/tutorial003.py b/docs_src/options/callback/tutorial003.py index f93ac566d5..2158474cd2 100644 --- a/docs_src/options/callback/tutorial003.py +++ b/docs_src/options/callback/tutorial003.py @@ -1,18 +1,23 @@ +from typing import Annotated + import typer +app = typer.Typer() + def name_callback(ctx: typer.Context, value: str): if ctx.resilient_parsing: return - typer.echo("Validating name") + print("Validating name") if value != "Camila": raise typer.BadParameter("Only Camila is allowed") return value -def main(name: str = typer.Option(..., callback=name_callback)): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/callback/tutorial003_non_an.py b/docs_src/options/callback/tutorial003_non_an.py new file mode 100644 index 0000000000..faf4f33f94 --- /dev/null +++ b/docs_src/options/callback/tutorial003_non_an.py @@ -0,0 +1,21 @@ +import typer + +app = typer.Typer() + + +def name_callback(ctx: typer.Context, value: str): + if ctx.resilient_parsing: + return + print("Validating name") + if value != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return value + + +@app.command() +def main(name: str | None = typer.Option(default=None, callback=name_callback)): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/callback/tutorial004.py b/docs_src/options/callback/tutorial004.py index 27d13e9ff5..89493678ac 100644 --- a/docs_src/options/callback/tutorial004.py +++ b/docs_src/options/callback/tutorial004.py @@ -1,18 +1,23 @@ +from typing import Annotated + import typer +app = typer.Typer() + def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): if ctx.resilient_parsing: return - typer.echo(f"Validating param: {param.name}") + print(f"Validating param: {param.name}") if value != "Camila": raise typer.BadParameter("Only Camila is allowed") return value -def main(name: str = typer.Option(..., callback=name_callback)): - typer.echo(f"Hello {name}") +@app.command() +def main(name: Annotated[str | None, typer.Option(callback=name_callback)] = None): + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/callback/tutorial004_non_an.py b/docs_src/options/callback/tutorial004_non_an.py new file mode 100644 index 0000000000..a02e138b5c --- /dev/null +++ b/docs_src/options/callback/tutorial004_non_an.py @@ -0,0 +1,21 @@ +import typer + +app = typer.Typer() + + +def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): + if ctx.resilient_parsing: + return + print(f"Validating param: {param.name}") + if value != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return value + + +@app.command() +def main(name: str | None = typer.Option(default=None, callback=name_callback)): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/help/__init__.py b/docs_src/options/help/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/help/tutorial001.py b/docs_src/options/help/tutorial001.py index 1e3bddcd30..0a7f9d0429 100644 --- a/docs_src/options/help/tutorial001.py +++ b/docs_src/options/help/tutorial001.py @@ -1,21 +1,26 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( name: str, - lastname: str = typer.Option("", help="Last name of person to greet."), - formal: bool = typer.Option(False, help="Say hi formally."), + lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "", + formal: Annotated[bool, typer.Option(help="Say hi formally.")] = False, ): """ - Say hi to NAME, optionally with a --lastname. + Say hi to 'name', optionally with a --lastname. If --formal is used, say hi very formally. """ if formal: - typer.echo(f"Good day Ms. {name} {lastname}.") + print(f"Good day Ms. {name} {lastname}.") else: - typer.echo(f"Hello {name} {lastname}") + print(f"Hello {name} {lastname}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/help/tutorial001_non_an.py b/docs_src/options/help/tutorial001_non_an.py new file mode 100644 index 0000000000..1b4c0f1178 --- /dev/null +++ b/docs_src/options/help/tutorial001_non_an.py @@ -0,0 +1,24 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str, + lastname: str = typer.Option("", help="Last name of person to greet."), + formal: bool = typer.Option(False, help="Say hi formally."), +): + """ + Say hi to 'name', optionally with a --lastname. + + If --formal is used, say hi very formally. + """ + if formal: + print(f"Good day Ms. {name} {lastname}.") + else: + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/help/tutorial002.py b/docs_src/options/help/tutorial002.py index c422c0b6dd..31c5857c15 100644 --- a/docs_src/options/help/tutorial002.py +++ b/docs_src/options/help/tutorial002.py @@ -1,9 +1,37 @@ +from typing import Annotated + import typer +app = typer.Typer() + + +@app.command() +def main( + name: str, + lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "", + formal: Annotated[ + bool, + typer.Option( + help="Say hi formally.", rich_help_panel="Customization and Utils" + ), + ] = False, + debug: Annotated[ + bool, + typer.Option( + help="Enable debugging.", rich_help_panel="Customization and Utils" + ), + ] = False, +): + """ + Say hi to 'name', optionally with a --lastname. -def main(fullname: str = typer.Option("Wade Wilson", show_default=False)): - typer.echo(f"Hello {fullname}") + If --formal is used, say hi very formally. + """ + if formal: + print(f"Good day Ms. {name} {lastname}.") + else: + print(f"Hello {name} {lastname}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/help/tutorial002_non_an.py b/docs_src/options/help/tutorial002_non_an.py new file mode 100644 index 0000000000..43d54ef812 --- /dev/null +++ b/docs_src/options/help/tutorial002_non_an.py @@ -0,0 +1,29 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str, + lastname: str = typer.Option("", help="Last name of person to greet."), + formal: bool = typer.Option( + False, help="Say hi formally.", rich_help_panel="Customization and Utils" + ), + debug: bool = typer.Option( + False, help="Enable debugging.", rich_help_panel="Customization and Utils" + ), +): + """ + Say hi to 'name', optionally with a --lastname. + + If --formal is used, say hi very formally. + """ + if formal: + print(f"Good day Ms. {name} {lastname}.") + else: + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/help/tutorial003.py b/docs_src/options/help/tutorial003.py new file mode 100644 index 0000000000..0ced4090b3 --- /dev/null +++ b/docs_src/options/help/tutorial003.py @@ -0,0 +1,14 @@ +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main(fullname: Annotated[str, typer.Option(show_default=False)] = "Wade Wilson"): + print(f"Hello {fullname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/help/tutorial003_non_an.py b/docs_src/options/help/tutorial003_non_an.py new file mode 100644 index 0000000000..a1f0fe7095 --- /dev/null +++ b/docs_src/options/help/tutorial003_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(fullname: str = typer.Option("Wade Wilson", show_default=False)): + print(f"Hello {fullname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/help/tutorial004.py b/docs_src/options/help/tutorial004.py new file mode 100644 index 0000000000..e4f29afd47 --- /dev/null +++ b/docs_src/options/help/tutorial004.py @@ -0,0 +1,18 @@ +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main( + fullname: Annotated[ + str, typer.Option(show_default="Deadpoolio the amazing's name") + ] = "Wade Wilson", +): + print(f"Hello {fullname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/help/tutorial004_non_an.py b/docs_src/options/help/tutorial004_non_an.py new file mode 100644 index 0000000000..c599ca62dd --- /dev/null +++ b/docs_src/options/help/tutorial004_non_an.py @@ -0,0 +1,16 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + fullname: str = typer.Option( + "Wade Wilson", show_default="Deadpoolio the amazing's name" + ), +): + print(f"Hello {fullname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/name/__init__.py b/docs_src/options/name/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/name/tutorial001.py b/docs_src/options/name/tutorial001.py index 7bc212f424..66d3669e0f 100644 --- a/docs_src/options/name/tutorial001.py +++ b/docs_src/options/name/tutorial001.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(user_name: str = typer.Option(..., "--name")): - typer.echo(f"Hello {user_name}") +@app.command() +def main(user_name: Annotated[str, typer.Option("--name")]): + print(f"Hello {user_name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/name/tutorial001_non_an.py b/docs_src/options/name/tutorial001_non_an.py new file mode 100644 index 0000000000..1f78ea069c --- /dev/null +++ b/docs_src/options/name/tutorial001_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(user_name: str = typer.Option(..., "--name")): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/name/tutorial002.py b/docs_src/options/name/tutorial002.py index 10eba64c17..b1e3913320 100644 --- a/docs_src/options/name/tutorial002.py +++ b/docs_src/options/name/tutorial002.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(user_name: str = typer.Option(..., "--name", "-n")): - typer.echo(f"Hello {user_name}") +@app.command() +def main(user_name: Annotated[str, typer.Option("--name", "-n")]): + print(f"Hello {user_name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/name/tutorial002_non_an.py b/docs_src/options/name/tutorial002_non_an.py new file mode 100644 index 0000000000..b4defb9a57 --- /dev/null +++ b/docs_src/options/name/tutorial002_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(user_name: str = typer.Option(..., "--name", "-n")): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/name/tutorial003.py b/docs_src/options/name/tutorial003.py index 5a8325aa6f..422a7cf032 100644 --- a/docs_src/options/name/tutorial003.py +++ b/docs_src/options/name/tutorial003.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(user_name: str = typer.Option(..., "-n")): - typer.echo(f"Hello {user_name}") +@app.command() +def main(user_name: Annotated[str, typer.Option("-n")]): + print(f"Hello {user_name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/name/tutorial003_non_an.py b/docs_src/options/name/tutorial003_non_an.py new file mode 100644 index 0000000000..c3cb9e4326 --- /dev/null +++ b/docs_src/options/name/tutorial003_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(user_name: str = typer.Option(..., "-n")): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/name/tutorial004.py b/docs_src/options/name/tutorial004.py index fa31904a6f..c5d6206206 100644 --- a/docs_src/options/name/tutorial004.py +++ b/docs_src/options/name/tutorial004.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(user_name: str = typer.Option(..., "--user-name", "-n")): - typer.echo(f"Hello {user_name}") +@app.command() +def main(user_name: Annotated[str, typer.Option("--user-name", "-n")]): + print(f"Hello {user_name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/name/tutorial004_non_an.py b/docs_src/options/name/tutorial004_non_an.py new file mode 100644 index 0000000000..53eeeabe2c --- /dev/null +++ b/docs_src/options/name/tutorial004_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(user_name: str = typer.Option(..., "--user-name", "-n")): + print(f"Hello {user_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/name/tutorial005.py b/docs_src/options/name/tutorial005.py index d0a094d8c3..05cf245b26 100644 --- a/docs_src/options/name/tutorial005.py +++ b/docs_src/options/name/tutorial005.py @@ -1,15 +1,20 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( - name: str = typer.Option(..., "--name", "-n"), - formal: bool = typer.Option(False, "--formal", "-f"), + name: Annotated[str, typer.Option("--name", "-n")], + formal: Annotated[bool, typer.Option("--formal", "-f")] = False, ): if formal: - typer.echo(f"Good day Ms. {name}.") + print(f"Good day Ms. {name}.") else: - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/name/tutorial005_non_an.py b/docs_src/options/name/tutorial005_non_an.py new file mode 100644 index 0000000000..3c05ab929d --- /dev/null +++ b/docs_src/options/name/tutorial005_non_an.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str = typer.Option(..., "--name", "-n"), + formal: bool = typer.Option(False, "--formal", "-f"), +): + if formal: + print(f"Good day Ms. {name}.") + else: + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/password/__init__.py b/docs_src/options/password/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/password/tutorial001.py b/docs_src/options/password/tutorial001.py index f9346b2e9d..26f2d26334 100644 --- a/docs_src/options/password/tutorial001.py +++ b/docs_src/options/password/tutorial001.py @@ -1,11 +1,17 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( - name: str, email: str = typer.Option(..., prompt=True, confirmation_prompt=True) + name: str, + email: Annotated[str, typer.Option(prompt=True, confirmation_prompt=True)], ): - typer.echo(f"Hello {name}, your email is {email}") + print(f"Hello {name}, your email is {email}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/password/tutorial001_non_an.py b/docs_src/options/password/tutorial001_non_an.py new file mode 100644 index 0000000000..579010a791 --- /dev/null +++ b/docs_src/options/password/tutorial001_non_an.py @@ -0,0 +1,14 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str, email: str = typer.Option(..., prompt=True, confirmation_prompt=True) +): + print(f"Hello {name}, your email is {email}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/password/tutorial002.py b/docs_src/options/password/tutorial002.py index 3eb3935375..efb895e2d8 100644 --- a/docs_src/options/password/tutorial002.py +++ b/docs_src/options/password/tutorial002.py @@ -1,15 +1,20 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( name: str, - password: str = typer.Option( - ..., prompt=True, confirmation_prompt=True, hide_input=True - ), + password: Annotated[ + str, typer.Option(prompt=True, confirmation_prompt=True, hide_input=True) + ], ): - typer.echo(f"Hello {name}. Doing something very secure with password.") - typer.echo(f"...just kidding, here it is, very insecure: {password}") + print(f"Hello {name}. Doing something very secure with password.") + print(f"...just kidding, here it is, very insecure: {password}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/password/tutorial002_non_an.py b/docs_src/options/password/tutorial002_non_an.py new file mode 100644 index 0000000000..ccf7559bb5 --- /dev/null +++ b/docs_src/options/password/tutorial002_non_an.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str, + password: str = typer.Option( + ..., prompt=True, confirmation_prompt=True, hide_input=True + ), +): + print(f"Hello {name}. Doing something very secure with password.") + print(f"...just kidding, here it is, very insecure: {password}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/prompt/__init__.py b/docs_src/options/prompt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/prompt/tutorial001.py b/docs_src/options/prompt/tutorial001.py index 2da2592455..d3f2c066d0 100644 --- a/docs_src/options/prompt/tutorial001.py +++ b/docs_src/options/prompt/tutorial001.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str, lastname: str = typer.Option(..., prompt=True)): - typer.echo(f"Hello {name} {lastname}") +@app.command() +def main(name: str, lastname: Annotated[str, typer.Option(prompt=True)]): + print(f"Hello {name} {lastname}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/prompt/tutorial001_non_an.py b/docs_src/options/prompt/tutorial001_non_an.py new file mode 100644 index 0000000000..ae27d29cbe --- /dev/null +++ b/docs_src/options/prompt/tutorial001_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str, lastname: str = typer.Option(..., prompt=True)): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/prompt/tutorial002.py b/docs_src/options/prompt/tutorial002.py index ec0f1c7722..b2f38f6152 100644 --- a/docs_src/options/prompt/tutorial002.py +++ b/docs_src/options/prompt/tutorial002.py @@ -1,11 +1,17 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( - name: str, lastname: str = typer.Option(..., prompt="Please tell me your last name") + name: str, + lastname: Annotated[str, typer.Option(prompt="Please tell me your last name")], ): - typer.echo(f"Hello {name} {lastname}") + print(f"Hello {name} {lastname}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/prompt/tutorial002_non_an.py b/docs_src/options/prompt/tutorial002_non_an.py new file mode 100644 index 0000000000..31c5c952e0 --- /dev/null +++ b/docs_src/options/prompt/tutorial002_non_an.py @@ -0,0 +1,14 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: str, lastname: str = typer.Option(..., prompt="Please tell me your last name") +): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/prompt/tutorial003.py b/docs_src/options/prompt/tutorial003.py index 11471d7a64..3ce2f21582 100644 --- a/docs_src/options/prompt/tutorial003.py +++ b/docs_src/options/prompt/tutorial003.py @@ -1,9 +1,16 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(project_name: str = typer.Option(..., prompt=True, confirmation_prompt=True)): - typer.echo(f"Deleting project {project_name}") +@app.command() +def main( + project_name: Annotated[str, typer.Option(prompt=True, confirmation_prompt=True)], +): + print(f"Deleting project {project_name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/prompt/tutorial003_non_an.py b/docs_src/options/prompt/tutorial003_non_an.py new file mode 100644 index 0000000000..96e0134b49 --- /dev/null +++ b/docs_src/options/prompt/tutorial003_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(project_name: str = typer.Option(..., prompt=True, confirmation_prompt=True)): + print(f"Deleting project {project_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/required/__init__.py b/docs_src/options/required/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/required/tutorial001.py b/docs_src/options/required/tutorial001.py index f62221b75b..d05c98b11f 100644 --- a/docs_src/options/required/tutorial001.py +++ b/docs_src/options/required/tutorial001.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(name: str, lastname: str = typer.Option(...)): - typer.echo(f"Hello {name} {lastname}") +@app.command() +def main(name: str, lastname: Annotated[str, typer.Option()]): + print(f"Hello {name} {lastname}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/required/tutorial001_non_an.py b/docs_src/options/required/tutorial001_non_an.py new file mode 100644 index 0000000000..b63b540aa6 --- /dev/null +++ b/docs_src/options/required/tutorial001_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str, lastname: str = typer.Option()): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/required/tutorial002.py b/docs_src/options/required/tutorial002.py new file mode 100644 index 0000000000..d2db9647c6 --- /dev/null +++ b/docs_src/options/required/tutorial002.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str, lastname: str = typer.Option(default=...)): + print(f"Hello {name} {lastname}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/version/__init__.py b/docs_src/options/version/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options/version/tutorial001.py b/docs_src/options/version/tutorial001.py index e09ae6a657..f5d65b2386 100644 --- a/docs_src/options/version/tutorial001.py +++ b/docs_src/options/version/tutorial001.py @@ -1,24 +1,27 @@ -from typing import Optional +from typing import Annotated import typer __version__ = "0.1.0" +app = typer.Typer() + def version_callback(value: bool): if value: - typer.echo(f"Awesome CLI Version: {__version__}") + print(f"Awesome CLI Version: {__version__}") raise typer.Exit() +@app.command() def main( - name: str = typer.Option("World"), - version: Optional[bool] = typer.Option( - None, "--version", callback=version_callback - ), + name: Annotated[str, typer.Option()] = "World", + version: Annotated[ + bool | None, typer.Option("--version", callback=version_callback) + ] = None, ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/version/tutorial001_non_an.py b/docs_src/options/version/tutorial001_non_an.py new file mode 100644 index 0000000000..6ba719836d --- /dev/null +++ b/docs_src/options/version/tutorial001_non_an.py @@ -0,0 +1,23 @@ +import typer + +__version__ = "0.1.0" + +app = typer.Typer() + + +def version_callback(value: bool): + if value: + print(f"Awesome CLI Version: {__version__}") + raise typer.Exit() + + +@app.command() +def main( + name: str = typer.Option("World"), + version: bool | None = typer.Option(None, "--version", callback=version_callback), +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/version/tutorial002.py b/docs_src/options/version/tutorial002.py index 9b4f9dcb97..9104195dff 100644 --- a/docs_src/options/version/tutorial002.py +++ b/docs_src/options/version/tutorial002.py @@ -1,29 +1,33 @@ -from typing import Optional +from typing import Annotated import typer __version__ = "0.1.0" +app = typer.Typer() + def version_callback(value: bool): if value: - typer.echo(f"Awesome CLI Version: {__version__}") + print(f"Awesome CLI Version: {__version__}") raise typer.Exit() def name_callback(name: str): if name != "Camila": raise typer.BadParameter("Only Camila is allowed") + return name +@app.command() def main( - name: str = typer.Option(..., callback=name_callback), - version: Optional[bool] = typer.Option( - None, "--version", callback=version_callback - ), + name: Annotated[str, typer.Option(callback=name_callback)], + version: Annotated[ + bool | None, typer.Option("--version", callback=version_callback) + ] = None, ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/version/tutorial002_non_an.py b/docs_src/options/version/tutorial002_non_an.py new file mode 100644 index 0000000000..9e881619c0 --- /dev/null +++ b/docs_src/options/version/tutorial002_non_an.py @@ -0,0 +1,29 @@ +import typer + +__version__ = "0.1.0" + +app = typer.Typer() + + +def version_callback(value: bool): + if value: + print(f"Awesome CLI Version: {__version__}") + raise typer.Exit() + + +def name_callback(name: str): + if name != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return name + + +@app.command() +def main( + name: str = typer.Option(..., callback=name_callback), + version: bool | None = typer.Option(None, "--version", callback=version_callback), +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/version/tutorial003.py b/docs_src/options/version/tutorial003.py index 886487dfa5..18740beec3 100644 --- a/docs_src/options/version/tutorial003.py +++ b/docs_src/options/version/tutorial003.py @@ -1,13 +1,15 @@ -from typing import Optional +from typing import Annotated import typer __version__ = "0.1.0" +app = typer.Typer() + def version_callback(value: bool): if value: - typer.echo(f"Awesome CLI Version: {__version__}") + print(f"Awesome CLI Version: {__version__}") raise typer.Exit() @@ -17,14 +19,16 @@ def name_callback(name: str): return name +@app.command() def main( - name: str = typer.Option(..., callback=name_callback), - version: Optional[bool] = typer.Option( - None, "--version", callback=version_callback, is_eager=True - ), + name: Annotated[str, typer.Option(callback=name_callback)], + version: Annotated[ + bool | None, + typer.Option("--version", callback=version_callback, is_eager=True), + ] = None, ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options/version/tutorial003_non_an.py b/docs_src/options/version/tutorial003_non_an.py new file mode 100644 index 0000000000..9d04c9045f --- /dev/null +++ b/docs_src/options/version/tutorial003_non_an.py @@ -0,0 +1,31 @@ +import typer + +__version__ = "0.1.0" + +app = typer.Typer() + + +def version_callback(value: bool): + if value: + print(f"Awesome CLI Version: {__version__}") + raise typer.Exit() + + +def name_callback(name: str): + if name != "Camila": + raise typer.BadParameter("Only Camila is allowed") + return name + + +@app.command() +def main( + name: str = typer.Option(..., callback=name_callback), + version: bool | None = typer.Option( + None, "--version", callback=version_callback, is_eager=True + ), +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/__init__.py b/docs_src/options_autocompletion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/options_autocompletion/tutorial001.py b/docs_src/options_autocompletion/tutorial001.py new file mode 100644 index 0000000000..908aebe3aa --- /dev/null +++ b/docs_src/options_autocompletion/tutorial001.py @@ -0,0 +1,14 @@ +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main(name: Annotated[str, typer.Option(help="The name to say hi to.")] = "World"): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial001.py b/docs_src/options_autocompletion/tutorial001_non_an.py similarity index 62% rename from docs_src/options/autocompletion/tutorial001.py rename to docs_src/options_autocompletion/tutorial001_non_an.py index a5ad1dc56d..1cfc18cc20 100644 --- a/docs_src/options/autocompletion/tutorial001.py +++ b/docs_src/options_autocompletion/tutorial001_non_an.py @@ -1,9 +1,12 @@ import typer +app = typer.Typer() + +@app.command() def main(name: str = typer.Option("World", help="The name to say hi to.")): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options_autocompletion/tutorial002.py b/docs_src/options_autocompletion/tutorial002.py new file mode 100644 index 0000000000..837800e822 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial002.py @@ -0,0 +1,23 @@ +from typing import Annotated + +import typer + + +def complete_name(): + return ["Camila", "Carlos", "Sebastian"] + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + str, typer.Option(help="The name to say hi to.", autocompletion=complete_name) + ] = "World", +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial002.py b/docs_src/options_autocompletion/tutorial002_non_an.py similarity index 74% rename from docs_src/options/autocompletion/tutorial002.py rename to docs_src/options_autocompletion/tutorial002_non_an.py index 87e17aab57..6c14a97ce4 100644 --- a/docs_src/options/autocompletion/tutorial002.py +++ b/docs_src/options_autocompletion/tutorial002_non_an.py @@ -5,13 +5,17 @@ def complete_name(): return ["Camila", "Carlos", "Sebastian"] +app = typer.Typer() + + +@app.command() def main( name: str = typer.Option( "World", help="The name to say hi to.", autocompletion=complete_name - ) + ), ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options_autocompletion/tutorial003.py b/docs_src/options_autocompletion/tutorial003.py new file mode 100644 index 0000000000..a57cd38005 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial003.py @@ -0,0 +1,29 @@ +from typing import Annotated + +import typer + +valid_names = ["Camila", "Carlos", "Sebastian"] + + +def complete_name(incomplete: str): + completion = [] + for name in valid_names: + if name.startswith(incomplete): + completion.append(name) + return completion + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + str, typer.Option(help="The name to say hi to.", autocompletion=complete_name) + ] = "World", +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial003.py b/docs_src/options_autocompletion/tutorial003_non_an.py similarity index 83% rename from docs_src/options/autocompletion/tutorial003.py rename to docs_src/options_autocompletion/tutorial003_non_an.py index 7ab6d7ad30..9af41f23b5 100644 --- a/docs_src/options/autocompletion/tutorial003.py +++ b/docs_src/options_autocompletion/tutorial003_non_an.py @@ -11,13 +11,17 @@ def complete_name(incomplete: str): return completion +app = typer.Typer() + + +@app.command() def main( name: str = typer.Option( "World", help="The name to say hi to.", autocompletion=complete_name - ) + ), ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options_autocompletion/tutorial004.py b/docs_src/options_autocompletion/tutorial004.py new file mode 100644 index 0000000000..d07927e1e5 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial004.py @@ -0,0 +1,34 @@ +from typing import Annotated + +import typer + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + + +def complete_name(incomplete: str): + completion = [] + for name, help_text in valid_completion_items: + if name.startswith(incomplete): + completion_item = (name, help_text) + completion.append(completion_item) + return completion + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + str, typer.Option(help="The name to say hi to.", autocompletion=complete_name) + ] = "World", +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial004.py b/docs_src/options_autocompletion/tutorial004_non_an.py similarity index 87% rename from docs_src/options/autocompletion/tutorial004.py rename to docs_src/options_autocompletion/tutorial004_non_an.py index 9352194751..3be0cf35db 100644 --- a/docs_src/options/autocompletion/tutorial004.py +++ b/docs_src/options_autocompletion/tutorial004_non_an.py @@ -16,13 +16,17 @@ def complete_name(incomplete: str): return completion +app = typer.Typer() + + +@app.command() def main( name: str = typer.Option( "World", help="The name to say hi to.", autocompletion=complete_name - ) + ), ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options_autocompletion/tutorial005.py b/docs_src/options_autocompletion/tutorial005.py new file mode 100644 index 0000000000..c7d7bf53ad --- /dev/null +++ b/docs_src/options_autocompletion/tutorial005.py @@ -0,0 +1,31 @@ +from typing import Annotated + +import typer + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + + +def complete_name(incomplete: str): + for name, help_text in valid_completion_items: + if name.startswith(incomplete): + yield (name, help_text) + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + str, typer.Option(help="The name to say hi to.", autocompletion=complete_name) + ] = "World", +): + print(f"Hello {name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial005.py b/docs_src/options_autocompletion/tutorial005_non_an.py similarity index 85% rename from docs_src/options/autocompletion/tutorial005.py rename to docs_src/options_autocompletion/tutorial005_non_an.py index 04d517d4f5..10ac532ad2 100644 --- a/docs_src/options/autocompletion/tutorial005.py +++ b/docs_src/options_autocompletion/tutorial005_non_an.py @@ -13,13 +13,17 @@ def complete_name(incomplete: str): yield (name, help_text) +app = typer.Typer() + + +@app.command() def main( name: str = typer.Option( "World", help="The name to say hi to.", autocompletion=complete_name - ) + ), ): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options_autocompletion/tutorial006.py b/docs_src/options_autocompletion/tutorial006.py new file mode 100644 index 0000000000..57e2ee285d --- /dev/null +++ b/docs_src/options_autocompletion/tutorial006.py @@ -0,0 +1,17 @@ +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[list[str], typer.Option(help="The name to say hi to.")] = ["World"], +): + for each_name in name: + print(f"Hello {each_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial006_non_an.py b/docs_src/options_autocompletion/tutorial006_non_an.py new file mode 100644 index 0000000000..ef23bf6468 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial006_non_an.py @@ -0,0 +1,13 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: list[str] = typer.Option(["World"], help="The name to say hi to.")): + for each_name in name: + print(f"Hello {each_name}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options_autocompletion/tutorial007.py b/docs_src/options_autocompletion/tutorial007.py new file mode 100644 index 0000000000..2b25a1075c --- /dev/null +++ b/docs_src/options_autocompletion/tutorial007.py @@ -0,0 +1,34 @@ +from typing import Annotated + +import typer + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + + +def complete_name(ctx: typer.Context, incomplete: str): + names = ctx.params.get("name") or [] + for name, help_text in valid_completion_items: + if name.startswith(incomplete) and name not in names: + yield (name, help_text) + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + list[str], + typer.Option(help="The name to say hi to.", autocompletion=complete_name), + ] = ["World"], +): + for n in name: + print(f"Hello {n}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial007.py b/docs_src/options_autocompletion/tutorial007_non_an.py similarity index 82% rename from docs_src/options/autocompletion/tutorial007.py rename to docs_src/options_autocompletion/tutorial007_non_an.py index 7d7b1b46e2..17bb7ead37 100644 --- a/docs_src/options/autocompletion/tutorial007.py +++ b/docs_src/options_autocompletion/tutorial007_non_an.py @@ -1,5 +1,3 @@ -from typing import List - import typer valid_completion_items = [ @@ -16,14 +14,18 @@ def complete_name(ctx: typer.Context, incomplete: str): yield (name, help_text) +app = typer.Typer() + + +@app.command() def main( - name: List[str] = typer.Option( + name: list[str] = typer.Option( ["World"], help="The name to say hi to.", autocompletion=complete_name - ) + ), ): for n in name: - typer.echo(f"Hello {n}") + print(f"Hello {n}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options_autocompletion/tutorial008.py b/docs_src/options_autocompletion/tutorial008.py new file mode 100644 index 0000000000..c4ae20ed23 --- /dev/null +++ b/docs_src/options_autocompletion/tutorial008.py @@ -0,0 +1,37 @@ +from typing import Annotated + +import typer +from rich.console import Console + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + +err_console = Console(stderr=True) + + +def complete_name(args: list[str], incomplete: str): + err_console.print(f"{args}") + for name, help_text in valid_completion_items: + if name.startswith(incomplete): + yield (name, help_text) + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + list[str], + typer.Option(help="The name to say hi to.", autocompletion=complete_name), + ] = ["World"], +): + for n in name: + print(f"Hello {n}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial008.py b/docs_src/options_autocompletion/tutorial008_non_an.py similarity index 61% rename from docs_src/options/autocompletion/tutorial008.py rename to docs_src/options_autocompletion/tutorial008_non_an.py index 92daa4ea01..7a71c3f1b2 100644 --- a/docs_src/options/autocompletion/tutorial008.py +++ b/docs_src/options_autocompletion/tutorial008_non_an.py @@ -1,6 +1,5 @@ -from typing import List - import typer +from rich.console import Console valid_completion_items = [ ("Camila", "The reader of books."), @@ -8,22 +7,28 @@ ("Sebastian", "The type hints guy."), ] +err_console = Console(stderr=True) + -def complete_name(args: List[str], incomplete: str): - typer.echo(f"{args}", err=True) +def complete_name(args: list[str], incomplete: str): + err_console.print(f"{args}") for name, help_text in valid_completion_items: if name.startswith(incomplete): yield (name, help_text) +app = typer.Typer() + + +@app.command() def main( - name: List[str] = typer.Option( + name: list[str] = typer.Option( ["World"], help="The name to say hi to.", autocompletion=complete_name - ) + ), ): for n in name: - typer.echo(f"Hello {n}") + print(f"Hello {n}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/options_autocompletion/tutorial009.py b/docs_src/options_autocompletion/tutorial009.py new file mode 100644 index 0000000000..5b86ff718d --- /dev/null +++ b/docs_src/options_autocompletion/tutorial009.py @@ -0,0 +1,38 @@ +from typing import Annotated + +import typer +from rich.console import Console + +valid_completion_items = [ + ("Camila", "The reader of books."), + ("Carlos", "The writer of scripts."), + ("Sebastian", "The type hints guy."), +] + +err_console = Console(stderr=True) + + +def complete_name(ctx: typer.Context, args: list[str], incomplete: str): + err_console.print(f"{args}") + names = ctx.params.get("name") or [] + for name, help_text in valid_completion_items: + if name.startswith(incomplete) and name not in names: + yield (name, help_text) + + +app = typer.Typer() + + +@app.command() +def main( + name: Annotated[ + list[str], + typer.Option(help="The name to say hi to.", autocompletion=complete_name), + ] = ["World"], +): + for n in name: + print(f"Hello {n}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/options/autocompletion/tutorial009.py b/docs_src/options_autocompletion/tutorial009_non_an.py similarity index 64% rename from docs_src/options/autocompletion/tutorial009.py rename to docs_src/options_autocompletion/tutorial009_non_an.py index 04b0c2df72..6f00da3758 100644 --- a/docs_src/options/autocompletion/tutorial009.py +++ b/docs_src/options_autocompletion/tutorial009_non_an.py @@ -1,6 +1,5 @@ -from typing import List - import typer +from rich.console import Console valid_completion_items = [ ("Camila", "The reader of books."), @@ -8,23 +7,29 @@ ("Sebastian", "The type hints guy."), ] +err_console = Console(stderr=True) + -def complete_name(ctx: typer.Context, args: List[str], incomplete: str): - typer.echo(f"{args}", err=True) +def complete_name(ctx: typer.Context, args: list[str], incomplete: str): + err_console.print(f"{args}") names = ctx.params.get("name") or [] for name, help_text in valid_completion_items: if name.startswith(incomplete) and name not in names: yield (name, help_text) +app = typer.Typer() + + +@app.command() def main( - name: List[str] = typer.Option( + name: list[str] = typer.Option( ["World"], help="The name to say hi to.", autocompletion=complete_name - ) + ), ): for n in name: - typer.echo(f"Hello {n}") + print(f"Hello {n}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/__init__.py b/docs_src/parameter_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/parameter_types/bool/tutorial001.py b/docs_src/parameter_types/bool/tutorial001.py index 46f6335cee..6a6ccb154b 100644 --- a/docs_src/parameter_types/bool/tutorial001.py +++ b/docs_src/parameter_types/bool/tutorial001.py @@ -1,12 +1,17 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(force: bool = typer.Option(False, "--force")): +@app.command() +def main(force: Annotated[bool, typer.Option("--force")] = False): if force: - typer.echo("Forcing operation") + print("Forcing operation") else: - typer.echo("Not forcing") + print("Not forcing") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/bool/tutorial001_non_an.py b/docs_src/parameter_types/bool/tutorial001_non_an.py new file mode 100644 index 0000000000..874be6353a --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial001_non_an.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(force: bool = typer.Option(False, "--force")): + if force: + print("Forcing operation") + else: + print("Not forcing") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/bool/tutorial002.py b/docs_src/parameter_types/bool/tutorial002.py index 8e88857b26..805b8b04f2 100644 --- a/docs_src/parameter_types/bool/tutorial002.py +++ b/docs_src/parameter_types/bool/tutorial002.py @@ -1,16 +1,19 @@ -from typing import Optional +from typing import Annotated import typer +app = typer.Typer() -def main(accept: Optional[bool] = typer.Option(None, "--accept/--reject")): + +@app.command() +def main(accept: Annotated[bool | None, typer.Option("--accept/--reject")] = None): if accept is None: - typer.echo("I don't know what you want yet") + print("I don't know what you want yet") elif accept: - typer.echo("Accepting!") + print("Accepting!") else: - typer.echo("Rejecting!") + print("Rejecting!") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/bool/tutorial002_non_an.py b/docs_src/parameter_types/bool/tutorial002_non_an.py new file mode 100644 index 0000000000..71d32c3780 --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial002_non_an.py @@ -0,0 +1,17 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(accept: bool | None = typer.Option(None, "--accept/--reject")): + if accept is None: + print("I don't know what you want yet") + elif accept: + print("Accepting!") + else: + print("Rejecting!") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/bool/tutorial003.py b/docs_src/parameter_types/bool/tutorial003.py index fda717e28c..a390336c6a 100644 --- a/docs_src/parameter_types/bool/tutorial003.py +++ b/docs_src/parameter_types/bool/tutorial003.py @@ -1,12 +1,17 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(force: bool = typer.Option(False, "--force/--no-force", "-f/-F")): +@app.command() +def main(force: Annotated[bool, typer.Option("--force/--no-force", "-f/-F")] = False): if force: - typer.echo("Forcing operation") + print("Forcing operation") else: - typer.echo("Not forcing") + print("Not forcing") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/bool/tutorial003_non_an.py b/docs_src/parameter_types/bool/tutorial003_non_an.py new file mode 100644 index 0000000000..7561d5a113 --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial003_non_an.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(force: bool = typer.Option(False, "--force/--no-force", "-f/-F")): + if force: + print("Forcing operation") + else: + print("Not forcing") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/bool/tutorial004.py b/docs_src/parameter_types/bool/tutorial004.py index 8c7f47c81b..f9baf65668 100644 --- a/docs_src/parameter_types/bool/tutorial004.py +++ b/docs_src/parameter_types/bool/tutorial004.py @@ -1,12 +1,17 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(in_prod: bool = typer.Option(True, " /--demo", " /-d")): +@app.command() +def main(in_prod: Annotated[bool, typer.Option(" /--demo", " /-d")] = True): if in_prod: - typer.echo("Running in production") + print("Running in production") else: - typer.echo("Running demo") + print("Running demo") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/bool/tutorial004_non_an.py b/docs_src/parameter_types/bool/tutorial004_non_an.py new file mode 100644 index 0000000000..0e3daf04c4 --- /dev/null +++ b/docs_src/parameter_types/bool/tutorial004_non_an.py @@ -0,0 +1,15 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(in_prod: bool = typer.Option(True, " /--demo", " /-d")): + if in_prod: + print("Running in production") + else: + print("Running demo") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/custom_types/__init__.py b/docs_src/parameter_types/custom_types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/parameter_types/custom_types/tutorial001.py b/docs_src/parameter_types/custom_types/tutorial001.py new file mode 100644 index 0000000000..528c4754bb --- /dev/null +++ b/docs_src/parameter_types/custom_types/tutorial001.py @@ -0,0 +1,31 @@ +from typing import Annotated + +import typer + + +class CustomClass: + def __init__(self, value: str): + self.value = value + + def __str__(self): + return f"" + + +def parse_custom_class(value: str): + return CustomClass(value * 2) + + +app = typer.Typer() + + +@app.command() +def main( + custom_arg: Annotated[CustomClass, typer.Argument(parser=parse_custom_class)], + custom_opt: Annotated[CustomClass, typer.Option(parser=parse_custom_class)] = "Foo", +): + print(f"custom_arg is {custom_arg}") + print(f"--custom-opt is {custom_opt}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/custom_types/tutorial001_non_an.py b/docs_src/parameter_types/custom_types/tutorial001_non_an.py new file mode 100644 index 0000000000..dde17a94cd --- /dev/null +++ b/docs_src/parameter_types/custom_types/tutorial001_non_an.py @@ -0,0 +1,29 @@ +import typer + + +class CustomClass: + def __init__(self, value: str): + self.value = value + + def __str__(self): + return f"" + + +def parse_custom_class(value: str): + return CustomClass(value * 2) + + +app = typer.Typer() + + +@app.command() +def main( + custom_arg: CustomClass = typer.Argument(parser=parse_custom_class), + custom_opt: CustomClass = typer.Option("Foo", parser=parse_custom_class), +): + print(f"custom_arg is {custom_arg}") + print(f"--custom-opt is {custom_opt}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/datetime/tutorial001.py b/docs_src/parameter_types/datetime/tutorial001.py index d32192817b..d559406f99 100644 --- a/docs_src/parameter_types/datetime/tutorial001.py +++ b/docs_src/parameter_types/datetime/tutorial001.py @@ -2,11 +2,14 @@ import typer +app = typer.Typer() + +@app.command() def main(birth: datetime): - typer.echo(f"Interesting day to be born: {birth}") - typer.echo(f"Birth hour: {birth.hour}") + print(f"Interesting day to be born: {birth}") + print(f"Birth hour: {birth.hour}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/datetime/tutorial002.py b/docs_src/parameter_types/datetime/tutorial002.py index 5cdba294d0..868bfbdb79 100644 --- a/docs_src/parameter_types/datetime/tutorial002.py +++ b/docs_src/parameter_types/datetime/tutorial002.py @@ -1,15 +1,20 @@ from datetime import datetime +from typing import Annotated import typer +app = typer.Typer() + +@app.command() def main( - launch_date: datetime = typer.Argument( - ..., formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"] - ) + launch_date: Annotated[ + datetime, + typer.Argument(formats=["%Y-%m-%d", "%m/%d/%Y"]), + ], ): - typer.echo(f"Launch will be at: {launch_date}") + print(f"Launch will be at: {launch_date}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/datetime/tutorial002_non_an.py b/docs_src/parameter_types/datetime/tutorial002_non_an.py new file mode 100644 index 0000000000..38e3cc4e64 --- /dev/null +++ b/docs_src/parameter_types/datetime/tutorial002_non_an.py @@ -0,0 +1,16 @@ +from datetime import datetime + +import typer + +app = typer.Typer() + + +@app.command() +def main( + launch_date: datetime = typer.Argument(..., formats=["%Y-%m-%d", "%m/%d/%Y"]), +): + print(f"Launch will be at: {launch_date}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/enum/tutorial001.py b/docs_src/parameter_types/enum/tutorial001.py index 6a31a84019..d212bb0cb3 100644 --- a/docs_src/parameter_types/enum/tutorial001.py +++ b/docs_src/parameter_types/enum/tutorial001.py @@ -9,9 +9,13 @@ class NeuralNetwork(str, Enum): lstm = "lstm" +app = typer.Typer() + + +@app.command() def main(network: NeuralNetwork = NeuralNetwork.simple): - typer.echo(f"Training neural network of type: {network.value}") + print(f"Training neural network of type: {network.value}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/enum/tutorial002.py b/docs_src/parameter_types/enum/tutorial002.py index cacde611fd..5825e8f086 100644 --- a/docs_src/parameter_types/enum/tutorial002.py +++ b/docs_src/parameter_types/enum/tutorial002.py @@ -1,4 +1,5 @@ from enum import Enum +from typing import Annotated import typer @@ -9,11 +10,17 @@ class NeuralNetwork(str, Enum): lstm = "lstm" +app = typer.Typer() + + +@app.command() def main( - network: NeuralNetwork = typer.Option(NeuralNetwork.simple, case_sensitive=False) + network: Annotated[ + NeuralNetwork, typer.Option(case_sensitive=False) + ] = NeuralNetwork.simple, ): - typer.echo(f"Training neural network of type: {network.value}") + print(f"Training neural network of type: {network.value}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/enum/tutorial002_non_an.py b/docs_src/parameter_types/enum/tutorial002_non_an.py new file mode 100644 index 0000000000..afdf3780e4 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial002_non_an.py @@ -0,0 +1,23 @@ +from enum import Enum + +import typer + + +class NeuralNetwork(str, Enum): + simple = "simple" + conv = "conv" + lstm = "lstm" + + +app = typer.Typer() + + +@app.command() +def main( + network: NeuralNetwork = typer.Option(NeuralNetwork.simple, case_sensitive=False), +): + print(f"Training neural network of type: {network.value}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/enum/tutorial003.py b/docs_src/parameter_types/enum/tutorial003.py new file mode 100644 index 0000000000..2f7f9b695d --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial003.py @@ -0,0 +1,22 @@ +from enum import Enum +from typing import Annotated + +import typer + + +class Food(str, Enum): + food_1 = "Eggs" + food_2 = "Bacon" + food_3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +def main(groceries: Annotated[list[Food], typer.Option()] = [Food.food_1, Food.food_3]): + print(f"Buying groceries: {', '.join([f.value for f in groceries])}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/enum/tutorial003_non_an.py b/docs_src/parameter_types/enum/tutorial003_non_an.py new file mode 100644 index 0000000000..a57205ab35 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial003_non_an.py @@ -0,0 +1,21 @@ +from enum import Enum + +import typer + + +class Food(str, Enum): + food_1 = "Eggs" + food_2 = "Bacon" + food_3 = "Cheese" + + +app = typer.Typer() + + +@app.command() +def main(groceries: list[Food] = typer.Option([Food.food_1, Food.food_3])): + print(f"Buying groceries: {', '.join([f.value for f in groceries])}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial004.py new file mode 100644 index 0000000000..cf0b29faa8 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial004.py @@ -0,0 +1,16 @@ +from typing import Annotated, Literal + +import typer + +app = typer.Typer() + + +@app.command() +def main( + network: Annotated[Literal["simple", "conv", "lstm"], typer.Option()] = "simple", +): + print(f"Training neural network of type: {network}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/enum/tutorial004_non_an.py b/docs_src/parameter_types/enum/tutorial004_non_an.py new file mode 100644 index 0000000000..015262728d --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial004_non_an.py @@ -0,0 +1,14 @@ +from typing import Literal + +import typer + +app = typer.Typer() + + +@app.command() +def main(network: Literal["simple", "conv", "lstm"] = typer.Option("simple")): + print(f"Training neural network of type: {network}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/file/tutorial001.py b/docs_src/parameter_types/file/tutorial001.py index 68de8d9d6f..304911b934 100644 --- a/docs_src/parameter_types/file/tutorial001.py +++ b/docs_src/parameter_types/file/tutorial001.py @@ -1,10 +1,15 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(config: typer.FileText = typer.Option(...)): +@app.command() +def main(config: Annotated[typer.FileText, typer.Option()]): for line in config: - typer.echo(f"Config line: {line}") + print(f"Config line: {line}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/file/tutorial001_non_an.py b/docs_src/parameter_types/file/tutorial001_non_an.py new file mode 100644 index 0000000000..78f6a3a892 --- /dev/null +++ b/docs_src/parameter_types/file/tutorial001_non_an.py @@ -0,0 +1,13 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(config: typer.FileText = typer.Option(...)): + for line in config: + print(f"Config line: {line}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/file/tutorial002.py b/docs_src/parameter_types/file/tutorial002.py index 183e24d022..ee5e17f64b 100644 --- a/docs_src/parameter_types/file/tutorial002.py +++ b/docs_src/parameter_types/file/tutorial002.py @@ -1,10 +1,15 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(config: typer.FileTextWrite = typer.Option(...)): +@app.command() +def main(config: Annotated[typer.FileTextWrite, typer.Option()]): config.write("Some config written by the app") - typer.echo("Config written") + print("Config written") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/file/tutorial002_non_an.py b/docs_src/parameter_types/file/tutorial002_non_an.py new file mode 100644 index 0000000000..ae8ff799fb --- /dev/null +++ b/docs_src/parameter_types/file/tutorial002_non_an.py @@ -0,0 +1,13 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(config: typer.FileTextWrite = typer.Option(...)): + config.write("Some config written by the app") + print("Config written") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/file/tutorial003.py b/docs_src/parameter_types/file/tutorial003.py index 092a6aadd6..e13d9b1277 100644 --- a/docs_src/parameter_types/file/tutorial003.py +++ b/docs_src/parameter_types/file/tutorial003.py @@ -1,13 +1,18 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(file: typer.FileBinaryRead = typer.Option(...)): +@app.command() +def main(file: Annotated[typer.FileBinaryRead, typer.Option()]): processed_total = 0 for bytes_chunk in file: # Process the bytes in bytes_chunk processed_total += len(bytes_chunk) - typer.echo(f"Processed bytes total: {processed_total}") + print(f"Processed bytes total: {processed_total}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/file/tutorial003_non_an.py b/docs_src/parameter_types/file/tutorial003_non_an.py new file mode 100644 index 0000000000..d33e7b359e --- /dev/null +++ b/docs_src/parameter_types/file/tutorial003_non_an.py @@ -0,0 +1,16 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(file: typer.FileBinaryRead = typer.Option(...)): + processed_total = 0 + for bytes_chunk in file: + # Process the bytes in bytes_chunk + processed_total += len(bytes_chunk) + print(f"Processed bytes total: {processed_total}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/file/tutorial004.py b/docs_src/parameter_types/file/tutorial004.py index 732974f055..1868d01a76 100644 --- a/docs_src/parameter_types/file/tutorial004.py +++ b/docs_src/parameter_types/file/tutorial004.py @@ -1,7 +1,12 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(file: typer.FileBinaryWrite = typer.Option(...)): +@app.command() +def main(file: Annotated[typer.FileBinaryWrite, typer.Option()]): first_line_str = "some settings\n" # You cannot write str directly to a binary file, you have to encode it to get bytes first_line_bytes = first_line_str.encode("utf-8") @@ -10,8 +15,8 @@ def main(file: typer.FileBinaryWrite = typer.Option(...)): # This is already bytes, it starts with b" second_line = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o" file.write(second_line) - typer.echo("Binary file written") + print("Binary file written") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/file/tutorial004_non_an.py b/docs_src/parameter_types/file/tutorial004_non_an.py new file mode 100644 index 0000000000..99ac6f6007 --- /dev/null +++ b/docs_src/parameter_types/file/tutorial004_non_an.py @@ -0,0 +1,20 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(file: typer.FileBinaryWrite = typer.Option(...)): + first_line_str = "some settings\n" + # You cannot write str directly to a binary file, you have to encode it to get bytes + first_line_bytes = first_line_str.encode("utf-8") + # Then you can write the bytes + file.write(first_line_bytes) + # This is already bytes, it starts with b" + second_line = b"la cig\xc3\xbce\xc3\xb1a trae al ni\xc3\xb1o" + file.write(second_line) + print("Binary file written") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/file/tutorial005.py b/docs_src/parameter_types/file/tutorial005.py index 310b0e3060..6686b9b917 100644 --- a/docs_src/parameter_types/file/tutorial005.py +++ b/docs_src/parameter_types/file/tutorial005.py @@ -1,10 +1,15 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(config: typer.FileText = typer.Option(..., mode="a")): +@app.command() +def main(config: Annotated[typer.FileText, typer.Option(mode="a")]): config.write("This is a single line\n") - typer.echo("Config line written") + print("Config line written") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/file/tutorial005_non_an.py b/docs_src/parameter_types/file/tutorial005_non_an.py new file mode 100644 index 0000000000..982cea6d59 --- /dev/null +++ b/docs_src/parameter_types/file/tutorial005_non_an.py @@ -0,0 +1,13 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(config: typer.FileText = typer.Option(..., mode="a")): + config.write("This is a single line\n") + print("Config line written") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/index/tutorial001.py b/docs_src/parameter_types/index/tutorial001.py index c394ecc955..fab855b088 100644 --- a/docs_src/parameter_types/index/tutorial001.py +++ b/docs_src/parameter_types/index/tutorial001.py @@ -1,12 +1,15 @@ import typer +app = typer.Typer() + +@app.command() def main(name: str, age: int = 20, height_meters: float = 1.89, female: bool = True): - typer.echo(f"NAME is {name}, of type: {type(name)}") - typer.echo(f"--age is {age}, of type: {type(age)}") - typer.echo(f"--height-meters is {height_meters}, of type: {type(height_meters)}") - typer.echo(f"--female is {female}, of type: {type(female)}") + print(f"NAME is {name}, of type: {type(name)}") + print(f"--age is {age}, of type: {type(age)}") + print(f"--height-meters is {height_meters}, of type: {type(height_meters)}") + print(f"--female is {female}, of type: {type(female)}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/number/tutorial001.py b/docs_src/parameter_types/number/tutorial001.py index a804db7b48..995f64b67f 100644 --- a/docs_src/parameter_types/number/tutorial001.py +++ b/docs_src/parameter_types/number/tutorial001.py @@ -1,15 +1,20 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( - id: int = typer.Argument(..., min=0, max=1000), - age: int = typer.Option(20, min=18), - score: float = typer.Option(0, max=100), + ID: Annotated[int, typer.Argument(min=0, max=1000)], + age: Annotated[int, typer.Option(min=18)] = 20, + score: Annotated[float, typer.Option(max=100)] = 0, ): - typer.echo(f"ID is {id}") - typer.echo(f"--age is {age}") - typer.echo(f"--score is {score}") + print(f"ID is {ID}") + print(f"--age is {age}") + print(f"--score is {score}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/number/tutorial001_non_an.py b/docs_src/parameter_types/number/tutorial001_non_an.py new file mode 100644 index 0000000000..37e9e39ff8 --- /dev/null +++ b/docs_src/parameter_types/number/tutorial001_non_an.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + ID: int = typer.Argument(..., min=0, max=1000), + age: int = typer.Option(20, min=18), + score: float = typer.Option(0, max=100), +): + print(f"ID is {ID}") + print(f"--age is {age}") + print(f"--score is {score}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/number/tutorial002.py b/docs_src/parameter_types/number/tutorial002.py index 7285088529..9df5e4838e 100644 --- a/docs_src/parameter_types/number/tutorial002.py +++ b/docs_src/parameter_types/number/tutorial002.py @@ -1,15 +1,20 @@ +from typing import Annotated + import typer +app = typer.Typer() + +@app.command() def main( - id: int = typer.Argument(..., min=0, max=1000), - rank: int = typer.Option(0, max=10, clamp=True), - score: float = typer.Option(0, min=0, max=100, clamp=True), + ID: Annotated[int, typer.Argument(min=0, max=1000)], + rank: Annotated[int, typer.Option(max=10, clamp=True)] = 0, + score: Annotated[float, typer.Option(min=0, max=100, clamp=True)] = 0, ): - typer.echo(f"ID is {id}") - typer.echo(f"--rank is {rank}") - typer.echo(f"--score is {score}") + print(f"ID is {ID}") + print(f"--rank is {rank}") + print(f"--score is {score}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/number/tutorial002_non_an.py b/docs_src/parameter_types/number/tutorial002_non_an.py new file mode 100644 index 0000000000..f4a624619f --- /dev/null +++ b/docs_src/parameter_types/number/tutorial002_non_an.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main( + ID: int = typer.Argument(..., min=0, max=1000), + rank: int = typer.Option(0, max=10, clamp=True), + score: float = typer.Option(0, min=0, max=100, clamp=True), +): + print(f"ID is {ID}") + print(f"--rank is {rank}") + print(f"--score is {score}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/number/tutorial003.py b/docs_src/parameter_types/number/tutorial003.py index 95737d02c9..3bf07d67ac 100644 --- a/docs_src/parameter_types/number/tutorial003.py +++ b/docs_src/parameter_types/number/tutorial003.py @@ -1,9 +1,14 @@ +from typing import Annotated + import typer +app = typer.Typer() + -def main(verbose: int = typer.Option(0, "--verbose", "-v", count=True)): - typer.echo(f"Verbose level is {verbose}") +@app.command() +def main(verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0): + print(f"Verbose level is {verbose}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/number/tutorial003_non_an.py b/docs_src/parameter_types/number/tutorial003_non_an.py new file mode 100644 index 0000000000..0ae2c594c3 --- /dev/null +++ b/docs_src/parameter_types/number/tutorial003_non_an.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(verbose: int = typer.Option(0, "--verbose", "-v", count=True)): + print(f"Verbose level is {verbose}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/path/tutorial001.py b/docs_src/parameter_types/path/tutorial001.py index c6a938bca2..8d2ae1d930 100644 --- a/docs_src/parameter_types/path/tutorial001.py +++ b/docs_src/parameter_types/path/tutorial001.py @@ -1,21 +1,24 @@ from pathlib import Path -from typing import Optional +from typing import Annotated import typer +app = typer.Typer() -def main(config: Optional[Path] = typer.Option(None)): + +@app.command() +def main(config: Annotated[Path | None, typer.Option()] = None): if config is None: - typer.echo("No config file") + print("No config file") raise typer.Abort() if config.is_file(): text = config.read_text() - typer.echo(f"Config file contents: {text}") + print(f"Config file contents: {text}") elif config.is_dir(): - typer.echo("Config is a directory, will use all its config files") + print("Config is a directory, will use all its config files") elif not config.exists(): - typer.echo("The config doesn't exist") + print("The config doesn't exist") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/path/tutorial001_non_an.py b/docs_src/parameter_types/path/tutorial001_non_an.py new file mode 100644 index 0000000000..e8edf2bf85 --- /dev/null +++ b/docs_src/parameter_types/path/tutorial001_non_an.py @@ -0,0 +1,23 @@ +from pathlib import Path + +import typer + +app = typer.Typer() + + +@app.command() +def main(config: Path | None = typer.Option(None)): + if config is None: + print("No config file") + raise typer.Abort() + if config.is_file(): + text = config.read_text() + print(f"Config file contents: {text}") + elif config.is_dir(): + print("Config is a directory, will use all its config files") + elif not config.exists(): + print("The config doesn't exist") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/path/tutorial002.py b/docs_src/parameter_types/path/tutorial002.py index 16ff0bcac0..a970a40736 100644 --- a/docs_src/parameter_types/path/tutorial002.py +++ b/docs_src/parameter_types/path/tutorial002.py @@ -1,22 +1,28 @@ from pathlib import Path +from typing import Annotated import typer +app = typer.Typer() + +@app.command() def main( - config: Path = typer.Option( - ..., - exists=True, - file_okay=True, - dir_okay=False, - writable=False, - readable=True, - resolve_path=True, - ) + config: Annotated[ + Path, + typer.Option( + exists=True, + file_okay=True, + dir_okay=False, + writable=False, + readable=True, + resolve_path=True, + ), + ], ): text = config.read_text() - typer.echo(f"Config file contents: {text}") + print(f"Config file contents: {text}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/parameter_types/path/tutorial002_non_an.py b/docs_src/parameter_types/path/tutorial002_non_an.py new file mode 100644 index 0000000000..d41c6a6d42 --- /dev/null +++ b/docs_src/parameter_types/path/tutorial002_non_an.py @@ -0,0 +1,25 @@ +from pathlib import Path + +import typer + +app = typer.Typer() + + +@app.command() +def main( + config: Path = typer.Option( + ..., + exists=True, + file_okay=True, + dir_okay=False, + writable=False, + readable=True, + resolve_path=True, + ), +): + text = config.read_text() + print(f"Config file contents: {text}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/parameter_types/uuid/tutorial001.py b/docs_src/parameter_types/uuid/tutorial001.py index 72cecc5509..cc9fb94ec9 100644 --- a/docs_src/parameter_types/uuid/tutorial001.py +++ b/docs_src/parameter_types/uuid/tutorial001.py @@ -2,11 +2,14 @@ import typer +app = typer.Typer() + +@app.command() def main(user_id: UUID): - typer.echo(f"USER_ID is {user_id}") - typer.echo(f"UUID version is: {user_id.version}") + print(f"User ID is {user_id}") + print(f"UUID version is: {user_id.version}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/printing/__init__.py b/docs_src/printing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/printing/tutorial001.py b/docs_src/printing/tutorial001.py index a9d41c9219..17850f6007 100644 --- a/docs_src/printing/tutorial001.py +++ b/docs_src/printing/tutorial001.py @@ -1,15 +1,22 @@ import typer +from rich import print +data = { + "name": "Rick", + "age": 42, + "items": [{"name": "Portal Gun"}, {"name": "Plumbus"}], + "active": True, + "affiliation": None, +} -def main(good: bool = True): - message_start = "everything is " - if good: - ending = typer.style("good", fg=typer.colors.GREEN, bold=True) - else: - ending = typer.style("bad", fg=typer.colors.WHITE, bg=typer.colors.RED) - message = message_start + ending - typer.echo(message) +app = typer.Typer() + + +@app.command() +def main(): + print("Here's the data") + print(data) if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/printing/tutorial002.py b/docs_src/printing/tutorial002.py index 6400555890..4a947db9b6 100644 --- a/docs_src/printing/tutorial002.py +++ b/docs_src/printing/tutorial002.py @@ -1,9 +1,13 @@ import typer +from rich import print +app = typer.Typer() -def main(name: str): - typer.secho(f"Welcome here {name}", fg=typer.colors.MAGENTA) + +@app.command() +def main(): + print("[bold red]Alert![/bold red] [green]Portal gun[/green] shooting! :boom:") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/printing/tutorial003.py b/docs_src/printing/tutorial003.py index 220a5e1feb..df3534e279 100644 --- a/docs_src/printing/tutorial003.py +++ b/docs_src/printing/tutorial003.py @@ -1,9 +1,19 @@ import typer +from rich.console import Console +from rich.table import Table +console = Console() +app = typer.Typer() + + +@app.command() def main(): - typer.echo(f"Here is something written to standard error", err=True) + table = Table("Name", "Item") + table.add_row("Rick", "Portal Gun") + table.add_row("Morty", "Plumbus") + console.print(table) if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/printing/tutorial004.py b/docs_src/printing/tutorial004.py new file mode 100644 index 0000000000..e5eea8aeae --- /dev/null +++ b/docs_src/printing/tutorial004.py @@ -0,0 +1,15 @@ +import typer +from rich.console import Console + +err_console = Console(stderr=True) + +app = typer.Typer() + + +@app.command() +def main(): + err_console.print("Here is something written to standard error") + + +if __name__ == "__main__": + app() diff --git a/docs_src/printing/tutorial005.py b/docs_src/printing/tutorial005.py new file mode 100644 index 0000000000..8c984cda0f --- /dev/null +++ b/docs_src/printing/tutorial005.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(good: bool = True): + message_start = "everything is " + if good: + ending = typer.style("good", fg=typer.colors.GREEN, bold=True) + else: + ending = typer.style("bad", fg=typer.colors.WHITE, bg=typer.colors.RED) + message = message_start + ending + typer.echo(message) + + +if __name__ == "__main__": + app() diff --git a/docs_src/printing/tutorial006.py b/docs_src/printing/tutorial006.py new file mode 100644 index 0000000000..cb5b02694d --- /dev/null +++ b/docs_src/printing/tutorial006.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str): + typer.secho(f"Welcome here {name}", fg=typer.colors.MAGENTA) + + +if __name__ == "__main__": + app() diff --git a/docs_src/progressbar/__init__.py b/docs_src/progressbar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/progressbar/tutorial001.py b/docs_src/progressbar/tutorial001.py index b609a6e428..cb334ece4f 100644 --- a/docs_src/progressbar/tutorial001.py +++ b/docs_src/progressbar/tutorial001.py @@ -1,17 +1,20 @@ import time import typer +from rich.progress import track +app = typer.Typer() + +@app.command() def main(): total = 0 - with typer.progressbar(range(100)) as progress: - for value in progress: - # Fake processing time - time.sleep(0.01) - total += 1 - typer.echo(f"Processed {total} things.") + for value in track(range(100), description="Processing..."): + # Fake processing time + time.sleep(0.01) + total += 1 + print(f"Processed {total} things.") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/progressbar/tutorial002.py b/docs_src/progressbar/tutorial002.py index 45f40d9d73..a8da31737a 100644 --- a/docs_src/progressbar/tutorial002.py +++ b/docs_src/progressbar/tutorial002.py @@ -1,23 +1,23 @@ import time import typer +from rich.progress import Progress, SpinnerColumn, TextColumn - -def iterate_user_ids(): - # Let's imagine this is a web API, not a range() - for i in range(100): - yield i +app = typer.Typer() +@app.command() def main(): - total = 0 - with typer.progressbar(iterate_user_ids(), length=100) as progress: - for value in progress: - # Fake processing time - time.sleep(0.01) - total += 1 - typer.echo(f"Processed {total} user IDs.") + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + transient=True, + ) as progress: + progress.add_task(description="Processing...", total=None) + progress.add_task(description="Preparing...", total=None) + time.sleep(5) + print("Done!") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/progressbar/tutorial003.py b/docs_src/progressbar/tutorial003.py index 579a28db47..01533e71cf 100644 --- a/docs_src/progressbar/tutorial003.py +++ b/docs_src/progressbar/tutorial003.py @@ -2,16 +2,19 @@ import typer +app = typer.Typer() + +@app.command() def main(): total = 0 - with typer.progressbar(range(100), label="Processing") as progress: + with typer.progressbar(range(100)) as progress: for value in progress: # Fake processing time time.sleep(0.01) total += 1 - typer.echo(f"Processed {total} things.") + print(f"Processed {total} things.") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/progressbar/tutorial004.py b/docs_src/progressbar/tutorial004.py index 3c08120739..a7f15d10b4 100644 --- a/docs_src/progressbar/tutorial004.py +++ b/docs_src/progressbar/tutorial004.py @@ -3,15 +3,25 @@ import typer +def iterate_user_ids(): + # Let's imagine this is a web API, not a range() + for i in range(100): + yield i + + +app = typer.Typer() + + +@app.command() def main(): - total = 1000 - with typer.progressbar(length=total) as progress: - for batch in range(4): + total = 0 + with typer.progressbar(iterate_user_ids(), length=100) as progress: + for value in progress: # Fake processing time - time.sleep(1) - progress.update(250) - typer.echo(f"Processed {total} things in batches.") + time.sleep(0.01) + total += 1 + print(f"Processed {total} user IDs.") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/progressbar/tutorial005.py b/docs_src/progressbar/tutorial005.py new file mode 100644 index 0000000000..c3a4d107c4 --- /dev/null +++ b/docs_src/progressbar/tutorial005.py @@ -0,0 +1,20 @@ +import time + +import typer + +app = typer.Typer() + + +@app.command() +def main(): + total = 0 + with typer.progressbar(range(100), label="Processing") as progress: + for value in progress: + # Fake processing time + time.sleep(0.01) + total += 1 + print(f"Processed {total} things.") + + +if __name__ == "__main__": + app() diff --git a/docs_src/progressbar/tutorial006.py b/docs_src/progressbar/tutorial006.py new file mode 100644 index 0000000000..d3da016a60 --- /dev/null +++ b/docs_src/progressbar/tutorial006.py @@ -0,0 +1,22 @@ +import time + +import typer + +app = typer.Typer() + + +@app.command() +def main(): + total = 1000 + with typer.progressbar(length=total) as progress: + for batch in range(4): + # Fake processing time + time.sleep(1) + # Increment by 250 on each loop iteration + # (it will take 4 seconds to reach 1000) + progress.update(250) + print(f"Processed {total} things in batches.") + + +if __name__ == "__main__": + app() diff --git a/docs_src/prompt/__init__.py b/docs_src/prompt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/prompt/tutorial001.py b/docs_src/prompt/tutorial001.py index 689d43bdfc..45e48d6ec5 100644 --- a/docs_src/prompt/tutorial001.py +++ b/docs_src/prompt/tutorial001.py @@ -1,10 +1,13 @@ import typer +app = typer.Typer() + +@app.command() def main(): person_name = typer.prompt("What's your name?") - typer.echo(f"Hello {person_name}") + print(f"Hello {person_name}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/prompt/tutorial002.py b/docs_src/prompt/tutorial002.py index 94e3d0fa1e..903862e594 100644 --- a/docs_src/prompt/tutorial002.py +++ b/docs_src/prompt/tutorial002.py @@ -1,13 +1,16 @@ import typer +app = typer.Typer() + +@app.command() def main(): delete = typer.confirm("Are you sure you want to delete it?") if not delete: - typer.echo("Not deleting") + print("Not deleting") raise typer.Abort() - typer.echo("Deleting it!") + print("Deleting it!") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/prompt/tutorial003.py b/docs_src/prompt/tutorial003.py index 4e0b816547..05fcd61cf4 100644 --- a/docs_src/prompt/tutorial003.py +++ b/docs_src/prompt/tutorial003.py @@ -1,10 +1,13 @@ import typer +app = typer.Typer() + +@app.command() def main(): delete = typer.confirm("Are you sure you want to delete it?", abort=True) - typer.echo("Deleting it!") + print("Deleting it!") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/prompt/tutorial004.py b/docs_src/prompt/tutorial004.py new file mode 100644 index 0000000000..66539dcdfa --- /dev/null +++ b/docs_src/prompt/tutorial004.py @@ -0,0 +1,14 @@ +import typer +from rich.prompt import Prompt + +app = typer.Typer() + + +@app.command() +def main(): + name = Prompt.ask("Enter your name :sunglasses:") + print(f"Hey there {name}!") + + +if __name__ == "__main__": + app() diff --git a/docs_src/subcommands/__init__.py b/docs_src/subcommands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/subcommands/callback_override/__init__.py b/docs_src/subcommands/callback_override/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/subcommands/callback_override/tutorial001.py b/docs_src/subcommands/callback_override/tutorial001.py index e9dfabd535..6442efa782 100644 --- a/docs_src/subcommands/callback_override/tutorial001.py +++ b/docs_src/subcommands/callback_override/tutorial001.py @@ -8,12 +8,12 @@ @users_app.callback() def users_callback(): - typer.echo("Running a users command") + print("Running a users command") @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/callback_override/tutorial002.py b/docs_src/subcommands/callback_override/tutorial002.py index d07ec7bc02..d75042f53e 100644 --- a/docs_src/subcommands/callback_override/tutorial002.py +++ b/docs_src/subcommands/callback_override/tutorial002.py @@ -4,7 +4,7 @@ def users_callback(): - typer.echo("Running a users command") + print("Running a users command") users_app = typer.Typer(callback=users_callback) @@ -13,7 +13,7 @@ def users_callback(): @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/callback_override/tutorial003.py b/docs_src/subcommands/callback_override/tutorial003.py index 6bebbfb464..26dc5676fc 100644 --- a/docs_src/subcommands/callback_override/tutorial003.py +++ b/docs_src/subcommands/callback_override/tutorial003.py @@ -4,7 +4,7 @@ def default_callback(): - typer.echo("Running a users command") + print("Running a users command") users_app = typer.Typer(callback=default_callback) @@ -13,12 +13,12 @@ def default_callback(): @users_app.callback() def user_callback(): - typer.echo("Callback override, running users command") + print("Callback override, running users command") @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/callback_override/tutorial004.py b/docs_src/subcommands/callback_override/tutorial004.py index b76b3707c5..bad099fcb4 100644 --- a/docs_src/subcommands/callback_override/tutorial004.py +++ b/docs_src/subcommands/callback_override/tutorial004.py @@ -4,14 +4,14 @@ def default_callback(): - typer.echo("Running a users command") + print("Running a users command") users_app = typer.Typer(callback=default_callback) def callback_for_add_typer(): - typer.echo("I have the high land! Running users command") + print("I have the high land! Running users command") app.add_typer(users_app, name="users", callback=callback_for_add_typer) @@ -19,12 +19,12 @@ def callback_for_add_typer(): @users_app.callback() def user_callback(): - typer.echo("Callback override, running users command") + print("Callback override, running users command") @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/__init__.py b/docs_src/subcommands/name_help/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/subcommands/name_help/tutorial001.py b/docs_src/subcommands/name_help/tutorial001.py index 51bcd6dcf6..b49091f428 100644 --- a/docs_src/subcommands/name_help/tutorial001.py +++ b/docs_src/subcommands/name_help/tutorial001.py @@ -8,7 +8,7 @@ @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/tutorial002.py b/docs_src/subcommands/name_help/tutorial002.py index 42d014082c..ae9f1036b5 100644 --- a/docs_src/subcommands/name_help/tutorial002.py +++ b/docs_src/subcommands/name_help/tutorial002.py @@ -3,7 +3,7 @@ app = typer.Typer() users_app = typer.Typer() -app.add_typer(users_app) +app.add_typer(users_app, name="users") @users_app.callback() @@ -15,7 +15,7 @@ def users(): @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/tutorial003.py b/docs_src/subcommands/name_help/tutorial003.py index c5af45245e..b6c10857dc 100644 --- a/docs_src/subcommands/name_help/tutorial003.py +++ b/docs_src/subcommands/name_help/tutorial003.py @@ -9,13 +9,13 @@ def users(): """ -users_app = typer.Typer(callback=users) +users_app = typer.Typer(callback=users, name="users") app.add_typer(users_app) @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/tutorial004.py b/docs_src/subcommands/name_help/tutorial004.py index 411b65e1e4..f1f5aed5c0 100644 --- a/docs_src/subcommands/name_help/tutorial004.py +++ b/docs_src/subcommands/name_help/tutorial004.py @@ -10,7 +10,7 @@ def old_callback(): users_app = typer.Typer(callback=old_callback) -app.add_typer(users_app) +app.add_typer(users_app, name="users") @users_app.callback() @@ -22,7 +22,7 @@ def users(): @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/tutorial005.py b/docs_src/subcommands/name_help/tutorial005.py index 65449829cc..50730171d7 100644 --- a/docs_src/subcommands/name_help/tutorial005.py +++ b/docs_src/subcommands/name_help/tutorial005.py @@ -9,7 +9,7 @@ def old_callback(): """ -users_app = typer.Typer(callback=old_callback) +users_app = typer.Typer(callback=old_callback, name="users") def new_users(): @@ -18,7 +18,7 @@ def new_users(): """ -app.add_typer(users_app, callback=new_users) +app.add_typer(users_app, callback=new_users, name="new-users") @users_app.callback() @@ -30,7 +30,7 @@ def users(): @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/tutorial006.py b/docs_src/subcommands/name_help/tutorial006.py index 0f53a46258..6395f693cc 100644 --- a/docs_src/subcommands/name_help/tutorial006.py +++ b/docs_src/subcommands/name_help/tutorial006.py @@ -30,7 +30,7 @@ def users(): @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/tutorial007.py b/docs_src/subcommands/name_help/tutorial007.py index 5a542e68d5..aaf7aec9fc 100644 --- a/docs_src/subcommands/name_help/tutorial007.py +++ b/docs_src/subcommands/name_help/tutorial007.py @@ -9,7 +9,7 @@ def old_callback(): """ -users_app = typer.Typer(callback=old_callback, name="exp-users", help="Explicit help.") +users_app = typer.Typer(callback=old_callback, name="users", help="Explicit help.") def new_users(): @@ -21,7 +21,7 @@ def new_users(): app.add_typer(users_app, callback=new_users) -@users_app.callback("call-users", help="Help from callback for users.") +@users_app.callback(help="Help from callback for users.") def users(): """ Manage users in the app. @@ -30,7 +30,7 @@ def users(): @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/name_help/tutorial008.py b/docs_src/subcommands/name_help/tutorial008.py index 3fd97add04..ddfc265051 100644 --- a/docs_src/subcommands/name_help/tutorial008.py +++ b/docs_src/subcommands/name_help/tutorial008.py @@ -26,7 +26,7 @@ def new_users(): ) -@users_app.callback("call-users", help="Help from callback for users.") +@users_app.callback(help="Help from callback for users.") def users(): """ Manage users in the app. @@ -35,7 +35,7 @@ def users(): @users_app.command() def create(name: str): - typer.echo(f"Creating user: {name}") + print(f"Creating user: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/tutorial001/__init__.py b/docs_src/subcommands/tutorial001/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/subcommands/tutorial001/items.py b/docs_src/subcommands/tutorial001/items.py index 608880bfa2..4dc2cfaba6 100644 --- a/docs_src/subcommands/tutorial001/items.py +++ b/docs_src/subcommands/tutorial001/items.py @@ -5,17 +5,17 @@ @app.command() def create(item: str): - typer.echo(f"Creating item: {item}") + print(f"Creating item: {item}") @app.command() def delete(item: str): - typer.echo(f"Deleting item: {item}") + print(f"Deleting item: {item}") @app.command() def sell(item: str): - typer.echo(f"Selling item: {item}") + print(f"Selling item: {item}") if __name__ == "__main__": diff --git a/docs_src/subcommands/tutorial001/users.py b/docs_src/subcommands/tutorial001/users.py index 49315e17e8..88cc4575e1 100644 --- a/docs_src/subcommands/tutorial001/users.py +++ b/docs_src/subcommands/tutorial001/users.py @@ -5,12 +5,12 @@ @app.command() def create(user_name: str): - typer.echo(f"Creating user: {user_name}") + print(f"Creating user: {user_name}") @app.command() def delete(user_name: str): - typer.echo(f"Deleting user: {user_name}") + print(f"Deleting user: {user_name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/tutorial002/__init__.py b/docs_src/subcommands/tutorial002/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/subcommands/tutorial002/main.py b/docs_src/subcommands/tutorial002/main.py index 8387386747..12c32214ea 100644 --- a/docs_src/subcommands/tutorial002/main.py +++ b/docs_src/subcommands/tutorial002/main.py @@ -9,27 +9,27 @@ @items_app.command("create") def items_create(item: str): - typer.echo(f"Creating item: {item}") + print(f"Creating item: {item}") @items_app.command("delete") def items_delete(item: str): - typer.echo(f"Deleting item: {item}") + print(f"Deleting item: {item}") @items_app.command("sell") def items_sell(item: str): - typer.echo(f"Selling item: {item}") + print(f"Selling item: {item}") @users_app.command("create") def users_create(user_name: str): - typer.echo(f"Creating user: {user_name}") + print(f"Creating user: {user_name}") @users_app.command("delete") def users_delete(user_name: str): - typer.echo(f"Deleting user: {user_name}") + print(f"Deleting user: {user_name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/tutorial003/__init__.py b/docs_src/subcommands/tutorial003/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/subcommands/tutorial003/items.py b/docs_src/subcommands/tutorial003/items.py index 608880bfa2..4dc2cfaba6 100644 --- a/docs_src/subcommands/tutorial003/items.py +++ b/docs_src/subcommands/tutorial003/items.py @@ -5,17 +5,17 @@ @app.command() def create(item: str): - typer.echo(f"Creating item: {item}") + print(f"Creating item: {item}") @app.command() def delete(item: str): - typer.echo(f"Deleting item: {item}") + print(f"Deleting item: {item}") @app.command() def sell(item: str): - typer.echo(f"Selling item: {item}") + print(f"Selling item: {item}") if __name__ == "__main__": diff --git a/docs_src/subcommands/tutorial003/reigns.py b/docs_src/subcommands/tutorial003/reigns.py index f50e081213..5bc8d7a314 100644 --- a/docs_src/subcommands/tutorial003/reigns.py +++ b/docs_src/subcommands/tutorial003/reigns.py @@ -5,12 +5,12 @@ @app.command() def conquer(name: str): - typer.echo(f"Conquering reign: {name}") + print(f"Conquering reign: {name}") @app.command() def destroy(name: str): - typer.echo(f"Destroying reign: {name}") + print(f"Destroying reign: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/tutorial003/towns.py b/docs_src/subcommands/tutorial003/towns.py index 3b020cb944..66d01b5e9d 100644 --- a/docs_src/subcommands/tutorial003/towns.py +++ b/docs_src/subcommands/tutorial003/towns.py @@ -5,12 +5,12 @@ @app.command() def found(name: str): - typer.echo(f"Founding town: {name}") + print(f"Founding town: {name}") @app.command() def burn(name: str): - typer.echo(f"Burning town: {name}") + print(f"Burning town: {name}") if __name__ == "__main__": diff --git a/docs_src/subcommands/tutorial003/users.py b/docs_src/subcommands/tutorial003/users.py index 49315e17e8..88cc4575e1 100644 --- a/docs_src/subcommands/tutorial003/users.py +++ b/docs_src/subcommands/tutorial003/users.py @@ -5,12 +5,12 @@ @app.command() def create(user_name: str): - typer.echo(f"Creating user: {user_name}") + print(f"Creating user: {user_name}") @app.command() def delete(user_name: str): - typer.echo(f"Deleting user: {user_name}") + print(f"Deleting user: {user_name}") if __name__ == "__main__": diff --git a/docs_src/terminating/__init__.py b/docs_src/terminating/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/terminating/tutorial001.py b/docs_src/terminating/tutorial001.py index c77a09c999..d567d81f96 100644 --- a/docs_src/terminating/tutorial001.py +++ b/docs_src/terminating/tutorial001.py @@ -5,21 +5,25 @@ def maybe_create_user(username: str): if username in existing_usernames: - typer.echo("The user already exists") + print("The user already exists") raise typer.Exit() else: - typer.echo(f"User created: {username}") + print(f"User created: {username}") def send_new_user_notification(username: str): # Somehow send a notification here for the new user, maybe an email - typer.echo(f"Notification sent for new user: {username}") + print(f"Notification sent for new user: {username}") +app = typer.Typer() + + +@app.command() def main(username: str): maybe_create_user(username=username) send_new_user_notification(username=username) if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/terminating/tutorial002.py b/docs_src/terminating/tutorial002.py index f7af10175e..408e531009 100644 --- a/docs_src/terminating/tutorial002.py +++ b/docs_src/terminating/tutorial002.py @@ -1,12 +1,15 @@ import typer +app = typer.Typer() + +@app.command() def main(username: str): if username == "root": - typer.echo("The root user is reserved") + print("The root user is reserved") raise typer.Exit(code=1) - typer.echo(f"New user created: {username}") + print(f"New user created: {username}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/terminating/tutorial003.py b/docs_src/terminating/tutorial003.py index 56e14063ee..37a2441d85 100644 --- a/docs_src/terminating/tutorial003.py +++ b/docs_src/terminating/tutorial003.py @@ -1,12 +1,15 @@ import typer +app = typer.Typer() + +@app.command() def main(username: str): if username == "root": - typer.echo("The root user is reserved") + print("The root user is reserved") raise typer.Abort() - typer.echo(f"New user created: {username}") + print(f"New user created: {username}") if __name__ == "__main__": - typer.run(main) + app() diff --git a/docs_src/testing/__init__.py b/docs_src/testing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/testing/app01/main.py b/docs_src/testing/app01/main.py index dc4743da2b..a456d190e9 100644 --- a/docs_src/testing/app01/main.py +++ b/docs_src/testing/app01/main.py @@ -1,15 +1,13 @@ -from typing import Optional - import typer app = typer.Typer() @app.command() -def main(name: str, city: Optional[str] = None): - typer.echo(f"Hello {name}") +def main(name: str, city: str | None = None): + print(f"Hello {name}") if city: - typer.echo(f"Let's have a coffee in {city}") + print(f"Let's have a coffee in {city}") if __name__ == "__main__": diff --git a/docs_src/testing/app01/test_main.py b/docs_src/testing/app01/test_main.py index 95716239c3..81d6a9cd3b 100644 --- a/docs_src/testing/app01/test_main.py +++ b/docs_src/testing/app01/test_main.py @@ -8,5 +8,5 @@ def test_app(): result = runner.invoke(app, ["Camila", "--city", "Berlin"]) assert result.exit_code == 0 - assert "Hello Camila" in result.stdout - assert "Let's have a coffee in Berlin" in result.stdout + assert "Hello Camila" in result.output + assert "Let's have a coffee in Berlin" in result.output diff --git a/docs_src/testing/app02/main.py b/docs_src/testing/app02/main.py index 1b3d6c7c56..ea76372885 100644 --- a/docs_src/testing/app02/main.py +++ b/docs_src/testing/app02/main.py @@ -1,11 +1,13 @@ +from typing import Annotated + import typer app = typer.Typer() @app.command() -def main(name: str, email: str = typer.Option(..., prompt=True)): - typer.echo(f"Hello {name}, your email is: {email}") +def main(name: str, email: Annotated[str, typer.Option(prompt=True)]): + print(f"Hello {name}, your email is: {email}") if __name__ == "__main__": diff --git a/docs_src/testing/app02/test_main.py b/docs_src/testing/app02/test_main.py index ffe01743ee..f28708ba5c 100644 --- a/docs_src/testing/app02/test_main.py +++ b/docs_src/testing/app02/test_main.py @@ -8,4 +8,4 @@ def test_app(): result = runner.invoke(app, ["Camila"], input="camila@example.com\n") assert result.exit_code == 0 - assert "Hello Camila, your email is: camila@example.com" in result.stdout + assert "Hello Camila, your email is: camila@example.com" in result.output diff --git a/docs_src/testing/app02_non_an/__init__.py b/docs_src/testing/app02_non_an/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/testing/app02_non_an/main.py b/docs_src/testing/app02_non_an/main.py new file mode 100644 index 0000000000..c3b141c626 --- /dev/null +++ b/docs_src/testing/app02_non_an/main.py @@ -0,0 +1,12 @@ +import typer + +app = typer.Typer() + + +@app.command() +def main(name: str, email: str = typer.Option(..., prompt=True)): + print(f"Hello {name}, your email is: {email}") + + +if __name__ == "__main__": + app() diff --git a/docs_src/testing/app02_non_an/test_main.py b/docs_src/testing/app02_non_an/test_main.py new file mode 100644 index 0000000000..f28708ba5c --- /dev/null +++ b/docs_src/testing/app02_non_an/test_main.py @@ -0,0 +1,11 @@ +from typer.testing import CliRunner + +from .main import app + +runner = CliRunner() + + +def test_app(): + result = runner.invoke(app, ["Camila"], input="camila@example.com\n") + assert result.exit_code == 0 + assert "Hello Camila, your email is: camila@example.com" in result.output diff --git a/docs_src/testing/app03/main.py b/docs_src/testing/app03/main.py index 4dfefdeafa..97325110d2 100644 --- a/docs_src/testing/app03/main.py +++ b/docs_src/testing/app03/main.py @@ -2,7 +2,7 @@ def main(name: str = "World"): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": diff --git a/docs_src/testing/app03/test_main.py b/docs_src/testing/app03/test_main.py index 425c491f11..f98fb0a734 100644 --- a/docs_src/testing/app03/test_main.py +++ b/docs_src/testing/app03/test_main.py @@ -12,4 +12,4 @@ def test_app(): result = runner.invoke(app, ["--name", "Camila"]) assert result.exit_code == 0 - assert "Hello Camila" in result.stdout + assert "Hello Camila" in result.output diff --git a/docs_src/typer_app/__init__.py b/docs_src/typer_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/commands/index/tutorial001.py b/docs_src/typer_app/tutorial001.py similarity index 77% rename from docs_src/commands/index/tutorial001.py rename to docs_src/typer_app/tutorial001.py index 3c37104fa1..2377d549ed 100644 --- a/docs_src/commands/index/tutorial001.py +++ b/docs_src/typer_app/tutorial001.py @@ -5,7 +5,7 @@ @app.command() def main(name: str): - typer.echo(f"Hello {name}") + print(f"Hello {name}") if __name__ == "__main__": diff --git a/docs_src/using_click/tutorial001.py b/docs_src/using_click/tutorial001.py deleted file mode 100644 index 6fbf00649a..0000000000 --- a/docs_src/using_click/tutorial001.py +++ /dev/null @@ -1,14 +0,0 @@ -import click - - -@click.command() -@click.option("--count", default=1, help="Number of greetings.") -@click.option("--name", prompt="Your name", help="The person to greet.") -def hello(count, name): - """Simple program that greets NAME for a total of COUNT times.""" - for x in range(count): - click.echo("Hello %s!" % name) - - -if __name__ == "__main__": - hello() diff --git a/docs_src/using_click/tutorial002.py b/docs_src/using_click/tutorial002.py deleted file mode 100644 index 44120e6fc6..0000000000 --- a/docs_src/using_click/tutorial002.py +++ /dev/null @@ -1,24 +0,0 @@ -import click - - -@click.group() -def cli(): - pass - - -@click.command() -def initdb(): - click.echo("Initialized the database") - - -@click.command() -def dropdb(): - click.echo("Dropped the database") - - -cli.add_command(initdb) -cli.add_command(dropdb) - - -if __name__ == "__main__": - cli() diff --git a/docs_src/using_click/tutorial003.py b/docs_src/using_click/tutorial003.py deleted file mode 100644 index b2b2d42049..0000000000 --- a/docs_src/using_click/tutorial003.py +++ /dev/null @@ -1,34 +0,0 @@ -import click -import typer - -app = typer.Typer() - - -@app.command() -def top(): - """ - Top level command, form Typer - """ - typer.echo("The Typer app is at the top level") - - -@app.callback() -def callback(): - """ - Typer app, including Click subapp - """ - - -@click.command() -@click.option("--name", prompt="Your name", help="The person to greet.") -def hello(name): - """Simple program that greets NAME for a total of COUNT times.""" - click.echo("Hello %s!" % name) - - -typer_click_object = typer.main.get_command(app) - -typer_click_object.add_command(hello, "hello") - -if __name__ == "__main__": - typer_click_object() diff --git a/docs_src/using_click/tutorial004.py b/docs_src/using_click/tutorial004.py deleted file mode 100644 index df43d1273c..0000000000 --- a/docs_src/using_click/tutorial004.py +++ /dev/null @@ -1,36 +0,0 @@ -import click -import typer - - -@click.group() -def cli(): - pass - - -@cli.command() -def initdb(): - click.echo("Initialized the database") - - -@cli.command() -def dropdb(): - click.echo("Dropped the database") - - -app = typer.Typer() - - -@app.command() -def sub(): - """ - A single-command Typer sub app - """ - typer.echo("Typer is now below Click, the Click app is the top level") - - -typer_click_object = typer.main.get_command(app) - -cli.add_command(typer_click_object, "sub") - -if __name__ == "__main__": - cli() diff --git a/includes/agent-banner.md b/includes/agent-banner.md new file mode 100644 index 0000000000..056c5a1d1f --- /dev/null +++ b/includes/agent-banner.md @@ -0,0 +1,11 @@ +/// details | AI coding agents + +Use the official Typer skill for your installed version. Install it from your project directory: + +```bash +uvx library-skills +``` + +See [Develop with AI](ai-development.md). + +/// diff --git a/mkdocs.env.yml b/mkdocs.env.yml new file mode 100644 index 0000000000..545d30a18c --- /dev/null +++ b/mkdocs.env.yml @@ -0,0 +1,5 @@ +# Define this here and not in the main mkdocs.yml file because that one could be auto +# updated and written, and the script would remove the env var +markdown_extensions: + pymdownx.highlight: + linenums: !ENV [LINENUMS, false] diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml deleted file mode 100644 index 9f2775ff97..0000000000 --- a/mkdocs.insiders.yml +++ /dev/null @@ -1,4 +0,0 @@ -INHERIT: mkdocs.yml -plugins: - - search - - social diff --git a/mkdocs.yml b/mkdocs.yml index aa185ea5d5..e2996f34d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,123 +1,226 @@ +INHERIT: ./mkdocs.env.yml site_name: Typer site_description: Typer, build great CLIs. Easy to code. Based on Python type hints. site_url: https://typer.tiangolo.com/ theme: + variant: modern name: material custom_dir: docs/overrides palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/lightbulb-auto + name: Switch to light mode + - media: '(prefers-color-scheme: light)' + scheme: default primary: black accent: teal + toggle: + icon: material/lightbulb + name: Switch to dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: black + accent: teal + toggle: + icon: material/lightbulb-outline + name: Switch to system preference + features: + - content.code.annotate + - content.code.copy + # - content.code.select + - content.footnote.tooltips + - content.tabs.link + - content.tooltips + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + # - navigation.instant.preview + - navigation.instant.progress + - navigation.path + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + icon: - repo: fontawesome/brands/github-alt - logo: img/icon-white.svg + repo: octicons/mark-github-24 + logo: img/icon.svg favicon: img/favicon.png - -repo_name: tiangolo/typer -repo_url: https://github.com/tiangolo/typer -edit_uri: "" + language: en +repo_name: fastapi/typer +repo_url: https://github.com/fastapi/typer +plugins: + mkdocstrings: + handlers: + python: + options: + extensions: + - griffe_typingdoc + show_root_heading: true + show_if_no_docstring: true + inherited_members: true + members_order: source + separate_signature: true + unwrap_annotated: true + filters: + - '!^_' + merge_init_into_class: true + docstring_section_style: spacy + signature_crossrefs: true + show_symbol_type_heading: true + show_symbol_type_toc: true nav: - Typer: index.md - - Features: features.md - - Tutorial - User Guide: - - Tutorial - User Guide - Intro: tutorial/index.md - - First Steps: tutorial/first-steps.md - - Printing and Colors: tutorial/printing.md - - Terminating: tutorial/terminating.md - - CLI Arguments: - - CLI Arguments Intro: tutorial/arguments/index.md - - Optional CLI Arguments: tutorial/arguments/optional.md - - CLI Arguments with Default: tutorial/arguments/default.md - - CLI Arguments with Help: tutorial/arguments/help.md - - CLI Arguments with Environment Variables: tutorial/arguments/envvar.md - - Other uses: tutorial/arguments/other-uses.md - - CLI Options: - - CLI Options Intro: tutorial/options/index.md - - CLI Options with Help: tutorial/options/help.md - - Required CLI Options: tutorial/options/required.md - - CLI Option Prompt: tutorial/options/prompt.md - - Password CLI Option and Confirmation Prompt: tutorial/options/password.md - - CLI Option Name: tutorial/options/name.md - - CLI Option Callback and Context: tutorial/options/callback-and-context.md - - Version CLI Option, is_eager: tutorial/options/version.md - - CLI Option autocompletion: tutorial/options/autocompletion.md - - Commands: - - Commands Intro: tutorial/commands/index.md - - Command CLI Arguments: tutorial/commands/arguments.md - - Command CLI Options: tutorial/commands/options.md - - Command Help: tutorial/commands/help.md - - Custom Command Name: tutorial/commands/name.md - - Typer Callback: tutorial/commands/callback.md - - One or Multiple Commands: tutorial/commands/one-or-multiple.md - - Using the Context: tutorial/commands/context.md - - CLI Parameter Types: - - CLI Parameter Types Intro: tutorial/parameter-types/index.md - - Number: tutorial/parameter-types/number.md - - Boolean CLI Options: tutorial/parameter-types/bool.md - - UUID: tutorial/parameter-types/uuid.md - - DateTime: tutorial/parameter-types/datetime.md - - Enum - Choices: tutorial/parameter-types/enum.md - - Path: tutorial/parameter-types/path.md - - File: tutorial/parameter-types/file.md - - SubCommands - Command Groups: - - SubCommands - Command Groups - Intro: tutorial/subcommands/index.md - - Add Typer: tutorial/subcommands/add-typer.md - - SubCommands in a Single File: tutorial/subcommands/single-file.md - - Nested SubCommands: tutorial/subcommands/nested-subcommands.md - - Sub-Typer Callback Override: tutorial/subcommands/callback-override.md - - SubCommand Name and Help: tutorial/subcommands/name-and-help.md - - Multiple Values: - - Multiple Values Intro: tutorial/multiple-values/index.md - - Multiple CLI Options: tutorial/multiple-values/multiple-options.md - - CLI Options with Multiple Values: tutorial/multiple-values/options-with-multiple-values.md - - CLI Arguments with Multiple Values: tutorial/multiple-values/arguments-with-multiple-values.md - - Ask with Prompt: tutorial/prompt.md - - Progress Bar: tutorial/progressbar.md - - CLI Application Directory: tutorial/app-dir.md - - Launching Applications: tutorial/launch.md - - Testing: tutorial/testing.md - - Using Click: tutorial/using-click.md - - Building a Package: tutorial/package.md - - Typer CLI - completion for small scripts: typer-cli.md - - Alternatives, Inspiration and Comparisons: alternatives.md - - Help Typer - Get Help: help-typer.md - - Development - Contributing: contributing.md - - Release Notes: release-notes.md + - features.md + - "": + - tutorial/index.md + - tutorial/install.md + - ai-development.md + - tutorial/first-steps.md + - tutorial/typer-app.md + - tutorial/printing.md + - tutorial/terminating.md + - "": + - tutorial/arguments/index.md + - tutorial/arguments/optional.md + - tutorial/arguments/default.md + - tutorial/arguments/help.md + - tutorial/arguments/envvar.md + - tutorial/arguments/other-uses.md + - "": + - tutorial/options/index.md + - tutorial/options/help.md + - tutorial/options/required.md + - tutorial/options/prompt.md + - tutorial/options/password.md + - tutorial/options/name.md + - tutorial/options/callback-and-context.md + - tutorial/options/version.md + - "": + - tutorial/commands/index.md + - tutorial/commands/arguments.md + - tutorial/commands/options.md + - tutorial/commands/help.md + - tutorial/commands/name.md + - tutorial/commands/callback.md + - tutorial/commands/one-or-multiple.md + - tutorial/commands/context.md + - tutorial/options-autocompletion.md + - "": + - tutorial/parameter-types/index.md + - tutorial/parameter-types/number.md + - tutorial/parameter-types/bool.md + - tutorial/parameter-types/uuid.md + - tutorial/parameter-types/datetime.md + - tutorial/parameter-types/enum.md + - tutorial/parameter-types/path.md + - tutorial/parameter-types/file.md + - tutorial/parameter-types/custom-types.md + - "": + - tutorial/subcommands/index.md + - tutorial/subcommands/add-typer.md + - tutorial/subcommands/single-file.md + - tutorial/subcommands/nested-subcommands.md + - tutorial/subcommands/callback-override.md + - tutorial/subcommands/name-and-help.md + - "": + - tutorial/multiple-values/index.md + - tutorial/multiple-values/multiple-options.md + - tutorial/multiple-values/options-with-multiple-values.md + - tutorial/multiple-values/arguments-with-multiple-values.md + - tutorial/prompt.md + - tutorial/progressbar.md + - tutorial/app-dir.md + - tutorial/launch.md + - tutorial/testing.md + - tutorial/package.md + - tutorial/exceptions.md + - tutorial/one-file-per-command.md + - tutorial/typer-command.md + - tutorial/click.md + - "": + - reference/index.md + - reference/typer.md + - reference/run_launch.md + - reference/parameters.md + - reference/file_objects.md + - reference/context.md + - "": + - resources/index.md + - help-typer.md + - contributing.md + - "": + - about/index.md + - alternatives.md + - management.md + - release-notes.md + +watch: + - scripts/agent_banner.py + - includes/agent-banner.md markdown_extensions: - - toc: - permalink: true - - markdown.extensions.codehilite: - guess_lang: false - - admonition - - codehilite - - extra - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' - - pymdownx.tabbed: - alternate_style: true - - mdx_include: - base_path: docs + scripts.agent_banner: + # Material for MkDocs Extensions + material.extensions.preview: + targets: + include: + - "*" + zensical.extensions.macros: + # Python Markdown + abbr: + attr_list: + footnotes: + md_in_html: + tables: + toc: + permalink: true + + # Python Markdown Extensions + pymdownx.betterem: + smart_enable: all + pymdownx.caret: + pymdownx.highlight: + line_spans: __span + linenums_style: pymdownx-inline + pymdownx.inlinehilite: + pymdownx.keys: + pymdownx.mark: + pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + pymdownx.tilde: + + # pymdownx blocks + pymdownx.blocks.admonition: + pymdownx.blocks.details: + pymdownx.blocks.tab: + alternate_style: True + + # Other extensions + mdx_include: + markdown_include_variants: extra: - analytics: - provider: google - property: UA-133183413-1 social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/typer - - icon: fontawesome/brands/twitter - link: https://twitter.com/tiangolo + - icon: octicons/mark-github-24 + link: https://github.com/fastapi/typer + - icon: fontawesome/brands/x-twitter + link: https://x.com/tiangolo + - icon: fontawesome/brands/bluesky + link: https://bsky.app/profile/tiangolo.com - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - icon: fontawesome/solid/globe link: https://tiangolo.com @@ -126,6 +229,8 @@ extra_css: - css/custom.css extra_javascript: - - https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js - js/termynal.js - js/custom.js + +validation: + unresolved_references: false diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 45d34ea136..0000000000 --- a/mypy.ini +++ /dev/null @@ -1,6 +0,0 @@ -[mypy] -ignore_missing_imports = True - -[mypy-typer.*] -disallow_untyped_defs = True -strict = True diff --git a/pyproject.toml b/pyproject.toml index 224a4d790d..82bc6336da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,17 @@ [build-system] -requires = ["flit_core >=2,<3"] -build-backend = "flit_core.buildapi" - -[tool.flit.metadata] -module = "typer" -author = "Sebastián Ramírez" -author-email = "tiangolo@gmail.com" -home-page = "https://github.com/tiangolo/typer" +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[project] +name = "typer" +license = "MIT" +license-files = ["LICENSE"] +dynamic = ["version"] +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +authors = [ + {name = "Sebastián Ramírez", email = "tiangolo@gmail.com"}, +] +requires-python = ">=3.10" classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", @@ -21,52 +26,210 @@ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "License :: OSI Approved :: MIT License" - ] -requires = [ - "click >= 7.1.1, <9.0.0" -] -description-file = "README.md" -requires-python = ">=3.6" - -[tool.flit.metadata.urls] -Documentation = "https://typer.tiangolo.com/" - -[tool.flit.metadata.requires-extra] -test = [ - "shellingham >=1.3.0,<2.0.0", - "pytest >=4.4.0,<5.4.0", - "pytest-cov >=2.10.0,<3.0.0", - "coverage >=5.2,<6.0", - "pytest-xdist >=1.32.0,<2.0.0", - "pytest-sugar >=0.9.4,<0.10.0", - "mypy ==0.910", - "black >=22.3.0,<23.0.0", - "isort >=5.0.6,<6.0.0" -] -doc = [ - "mkdocs >=1.1.2,<2.0.0", - "mkdocs-material >=8.1.4,<9.0.0", - "mdx-include >=1.4.1,<2.0.0", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", +] +dependencies = [ + "pydantic >=2.5.3", + "shellingham >=1.3.0", + "rich >=13.8.0", + "annotated-doc >=0.0.2", + "colorama; platform_system == 'Windows'", ] +readme = "README.md" + +[project.urls] +Homepage = "https://github.com/fastapi/typer" +Documentation = "https://typer.tiangolo.com" +Repository = "https://github.com/fastapi/typer" +Issues = "https://github.com/fastapi/typer/issues" +Changelog = "https://typer.tiangolo.com/release-notes/" + +[project.scripts] +typer = "typer.cli:main" + +[dependency-groups] dev = [ - "autoflake >=1.3.1,<2.0.0", - "flake8 >=3.8.3,<4.0.0", - "pre-commit >=2.17.0,<3.0.0", -] -all = [ - "colorama >=0.4.3,<0.5.0", - "shellingham >=1.3.0,<2.0.0" -] - -[tool.isort] -profile = "black" -known_third_party = ["typer", "click"] -skip_glob = [ - "docs_src/subcommands/tutorial001/main.py", - "docs_src/subcommands/tutorial003/lands.py", - "docs_src/subcommands/tutorial003/main.py", - ] + { include-group = "tests" }, + { include-group = "docs" }, + "prek >=0.3.2", + "zizmor >=1.23.1", +] +docs = [ + "cairosvg >=2.8.2", + "griffe-typingdoc >=0.3.0", + "griffe-warnings-deprecated >=1.1.0", + "markdown-include-variants >=0.0.10", + "mdx-include >=1.4.1", + "mkdocstrings[python] >=1.0.3", + "pillow >=11.3.0", + "pyyaml >=5.3.1", + "zensical>=0.0.42", +] +github-actions = [ + "httpx >=0.27.0", + "pydantic >=2.5.3", + "pydantic-settings >=2.1.0", + "pygithub >=2.3.0", + "smokeshow >=0.5.0", +] +tests = [ + "coverage[toml] >=7.13", + "mypy >=1.19.1", + "ty >=0.0.25", + "pytest >=9.0.0", + "pytest-cov >=7.0.0", + "pytest-sugar >=1.0.0", + "pytest-xdist >=3.6.1", + "rich >=12.3.0", + "ruff >=0.16.0", + "shellingham >=1.3.0", +] + +[tool.pdm] +version = { source = "file", path = "typer/__init__.py" } +distribution = true + +[tool.pdm.build] +source-includes = [ + "tests/", + "docs_src/", + "scripts/", +] + +[tool.pytest] +minversion = "9.0" +addopts = [ + "--strict-config", + "--strict-markers", +] +strict_xfail = true +filterwarnings = [ + "error", + # For pytest-xdist + 'ignore::DeprecationWarning:xdist', +] + +[tool.coverage.run] +parallel = true +data_file = "coverage/.coverage" +source = [ + "docs_src", + "tests", + "typer" +] +omit = [ + "typer/_typing.py", +] +context = '${CONTEXT}' +relative_files = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "@overload", + 'if __name__ == "__main__":', + "if TYPE_CHECKING:", +] + +[tool.mypy] +strict = true + +[[tool.mypy.overrides]] +module = "docs_src.*" +disallow_incomplete_defs = false +disallow_untyped_defs = false +disallow_untyped_calls = false + +[[tool.mypy.overrides]] +module = "shellingham" +ignore_missing_imports = true + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "TID", # flake8-tidy-imports +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "TID252", # relative imports okay +] + +[tool.ruff.lint.per-file-ignores] +# "__init__.py" = ["F401"] +# rich_utils is allowed to use rich imports +"typer/rich_utils.py" = ["TID251"] +# adapters is allowed to import pydantic at module level +"typer/_adapters.py" = ["TID253"] +# This file is more readable without yield from +"docs_src/progressbar/tutorial004.py" = ["UP028", "B007"] +# Default mutable data structure +"docs_src/options_autocompletion/tutorial006.py" = ["B006"] +"docs_src/multiple_values/multiple_options/tutorial002.py" = ["B006"] +"docs_src/options_autocompletion/tutorial007.py" = ["B006"] +"docs_src/options_autocompletion/tutorial008.py" = ["B006"] +"docs_src/options_autocompletion/tutorial009.py" = ["B006"] +"docs_src/parameter_types/enum/tutorial003.py" = ["B006"] +# Loop control variable `value` not used within loop body +"docs_src/progressbar/tutorial001.py" = ["B007"] +"docs_src/progressbar/tutorial003.py" = ["B007"] +"docs_src/progressbar/tutorial005.py" = ["B007"] +"docs_src/progressbar/tutorial006.py" = ["B007"] +# Local variable `delete` is assigned to but never used +"docs_src/prompt/tutorial003.py" = ["F841"] +# No need to worry about lazy imports in docs or scripts +"docs_src/*" = ["TID"] +"scripts/*" = ["TID253"] + +[tool.ruff.lint.isort] +known-third-party = ["typer"] +# For docs_src/subcommands/tutorial003/ +known-first-party = ["reigns", "towns", "lands", "items", "users"] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true + +[tool.ruff.lint.flake8-tidy-imports] +# Import these from within functions (lazy), not at the module level (TID253) +banned-module-level-imports = ["typer.rich_utils", "typer._adapters", "pydantic"] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"rich".msg = "Use 'typer.rich_utils' instead of importing from 'rich' directly." +"shellingham.detect_shell".msg = """\ +Use 'typer._completion_shared._get_shell_name' instead of using \ +'shellingham.detect_shell' directly. +""" + +[tool.ty.terminal] +error-on-warning = true + +[tool.typos.files] +extend-exclude = [ + "coverage/", + "dist/", + "docs/img/", + "docs/release-notes.md", + "htmlcov/", + "site/", + "site_build/", + "uv.lock", +] + +[tool.typos.default.extend-identifiers] +alls = "alls" + +[tool.typos.default.extend-words] +Ines = "Ines" diff --git a/scripts/add_latest_release_date.py b/scripts/add_latest_release_date.py new file mode 100644 index 0000000000..ecd1abdaa7 --- /dev/null +++ b/scripts/add_latest_release_date.py @@ -0,0 +1,40 @@ +"""Check release-notes.md and add today's date to the latest release header if missing.""" + +import re +import sys +from datetime import date + +RELEASE_NOTES_FILE = "docs/release-notes.md" +RELEASE_HEADER_PATTERN = re.compile(r"^## (\d+\.\d+\.\d+)\s*(\(.*\))?\s*$") + + +def main() -> None: + with open(RELEASE_NOTES_FILE) as f: + lines = f.readlines() + + for i, line in enumerate(lines): + match = RELEASE_HEADER_PATTERN.match(line) + if not match: + continue + + version = match.group(1) + date_part = match.group(2) + + if date_part: + print(f"Latest release {version} already has a date: {date_part}") + sys.exit(0) + + today = date.today().isoformat() + lines[i] = f"## {version} ({today})\n" + print(f"Added date: {version} ({today})") + + with open(RELEASE_NOTES_FILE, "w") as f: + f.writelines(lines) + sys.exit(0) + + print("No release header found") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/agent_banner.py b/scripts/agent_banner.py new file mode 100644 index 0000000000..78838a8d2e --- /dev/null +++ b/scripts/agent_banner.py @@ -0,0 +1,50 @@ +from pathlib import Path +from posixpath import relpath + +from markdown import Markdown +from markdown.extensions import Extension +from markdown.preprocessors import Preprocessor +from mkdocstrings import MkdocstringsInnerExtension +from zensical.extensions.context import ContextPreprocessor + + +class AgentBannerPreprocessor(Preprocessor): + def run(self, lines: list[str]) -> list[str]: + # Docstrings render their own Markdown inside an API reference page. + if any( + isinstance(extension, MkdocstringsInnerExtension) + for extension in self.md.registeredExtensions + ): + return lines + + context = ContextPreprocessor.from_markdown(self.md) + if context is None or context.page.path == "ai-development.md": + return lines + + banner_path = ( + Path(__file__).resolve().parent.parent / "includes/agent-banner.md" + ) + banner = banner_path.read_text(encoding="utf-8").strip() + guide_path = relpath( + "ai-development.md", Path(context.page.path).parent.as_posix() + ) + banner = banner.replace("(ai-development.md)", f"({guide_path})") + content = "\n".join(lines) + marker = "" + if marker in content: + return content.replace(marker, banner).splitlines() + + header, _, body = content.lstrip().partition("\n") + if header == "#" or header.startswith("# "): + return f"{header}\n\n{banner}\n\n{body.lstrip()}".splitlines() + return f"{banner}\n\n{content}".splitlines() + + +class AgentBannerExtension(Extension): + def extendMarkdown(self, md: Markdown) -> None: + # Insert the notice before macros and fenced code blocks are processed. + md.preprocessors.register(AgentBannerPreprocessor(md), "agent_banner", 36) + + +def makeExtension() -> AgentBannerExtension: + return AgentBannerExtension() diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh deleted file mode 100755 index 4f4ae2f74e..0000000000 --- a/scripts/build-docs.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -python -m mkdocs build - -cp ./docs/index.md ./README.md diff --git a/scripts/clean.sh b/scripts/clean.sh deleted file mode 100755 index d5a4b790ae..0000000000 --- a/scripts/clean.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -e - -if [ -d 'dist' ] ; then - rm -r dist -fi -if [ -d 'site' ] ; then - rm -r site -fi diff --git a/scripts/deploy_docs_status.py b/scripts/deploy_docs_status.py new file mode 100644 index 0000000000..e959c15d10 --- /dev/null +++ b/scripts/deploy_docs_status.py @@ -0,0 +1,127 @@ +import logging +import re +from typing import Literal + +from github import Auth, Github +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings + +site_domain = "typer.tiangolo.com" + + +class Settings(BaseSettings): + github_repository: str + github_token: SecretStr + deploy_url: str | None = None + commit_sha: str + run_id: int + state: Literal["pending", "success", "error"] = "pending" + + +class LinkData(BaseModel): + previous_link: str + preview_link: str + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + settings = Settings() + + logging.info(f"Using config: {settings.model_dump_json()}") + g = Github(auth=Auth.Token(settings.github_token.get_secret_value())) + repo = g.get_repo(settings.github_repository) + use_pr = next( + (pr for pr in repo.get_pulls() if pr.head.sha == settings.commit_sha), None + ) + if not use_pr: + logging.error(f"No PR found for hash: {settings.commit_sha}") + return + commits = list(use_pr.get_commits()) + current_commit = [c for c in commits if c.sha == settings.commit_sha][0] + run_url = f"https://github.com/{settings.github_repository}/actions/runs/{settings.run_id}" + if settings.state == "pending": + current_commit.create_status( + state="pending", + description="Deploying Docs", + context="deploy-docs", + target_url=run_url, + ) + logging.info("No deploy URL available yet") + return + if settings.state == "error": + current_commit.create_status( + state="error", + description="Error Deploying Docs", + context="deploy-docs", + target_url=run_url, + ) + logging.info("Error deploying docs") + return + assert settings.state == "success" + if not settings.deploy_url: + current_commit.create_status( + state="success", + description="No Docs Changes", + context="deploy-docs", + target_url=run_url, + ) + logging.info("No docs changes found") + return + assert settings.deploy_url + current_commit.create_status( + state="success", + description="Docs Deployed", + context="deploy-docs", + target_url=run_url, + ) + + files = list(use_pr.get_files()) + docs_files = [f for f in files if f.filename.startswith("docs/")] + + deploy_url = settings.deploy_url.rstrip("/") + links: list[LinkData] = [] + for f in docs_files: + match = re.match(r"docs/(.*)", f.filename) + if not match: + continue + path = match.group(1) + if path.endswith("index.md"): + use_path = path.replace("index.md", "") + else: + use_path = path.replace(".md", "/") + link = LinkData( + previous_link=f"https://{site_domain}/{use_path}", + preview_link=f"{deploy_url}/{use_path}", + ) + links.append(link) + links.sort(key=lambda x: x.preview_link) + + header = "## 📝 Docs preview" + message = header + message += f"\n\nLast commit {settings.commit_sha} at: {deploy_url}" + + if links: + message += "\n\n### Modified Pages\n\n" + for link in links: + message += f"* {link.preview_link}" + message += f" - ([before]({link.previous_link}))" + message += "\n" + + print(message) + issue = use_pr.as_issue() + comments = list(issue.get_comments()) + for comment in comments: + if ( + comment.body.startswith(header) + and comment.user.login == "github-actions[bot]" + ): + comment.edit(message) + break + else: + issue.create_comment(message) + + logging.info("Finished") + + +if __name__ == "__main__": + main() diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile new file mode 100644 index 0000000000..b158e2db04 --- /dev/null +++ b/scripts/docker/Dockerfile @@ -0,0 +1,28 @@ +FROM python:latest + +# Add Fish +RUN echo 'deb http://download.opensuse.org/repositories/shells:/fish:/release:/3/Debian_12/ /' | tee /etc/apt/sources.list.d/shells:fish:release:3.list +RUN curl -fsSL https://download.opensuse.org/repositories/shells:fish:release:3/Debian_12/Release.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/shells_fish_release_3.gpg > /dev/null + +# Install packages including Fish, Zsh, PowerShell +RUN apt-get update && apt-get install -y \ + wget \ + apt-transport-https \ + software-properties-common \ + nano \ + vim \ + fish \ + zsh \ + && wget https://github.com/PowerShell/PowerShell/releases/download/v7.4.4/powershell_7.4.4-1.deb_amd64.deb \ + && dpkg -i powershell_7.4.4-1.deb_amd64.deb + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv + +ENV UV_SYSTEM_PYTHON=1 + +COPY . /code + +WORKDIR /code + +RUN uv pip install -r requirements.txt diff --git a/scripts/docker/compose.yaml b/scripts/docker/compose.yaml new file mode 100644 index 0000000000..efe17f9efd --- /dev/null +++ b/scripts/docker/compose.yaml @@ -0,0 +1,8 @@ +services: + typer: + build: + context: ../../ + dockerfile: scripts/docker/Dockerfile + volumes: + - ../../:/code + command: sleep infinity diff --git a/scripts/docs-live.sh b/scripts/docs-live.sh deleted file mode 100755 index 5342a9e59f..0000000000 --- a/scripts/docs-live.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -mkdocs serve --dev-addr 127.0.0.1:8008 diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100644 index 0000000000..aa03002cef --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,186 @@ +import logging +import os +import re +import shutil +import subprocess +from http.server import HTTPServer, SimpleHTTPRequestHandler +from pathlib import Path + +import typer + +logging.basicConfig(level=logging.INFO) + +mkdocs_name = "mkdocs.yml" +docs_path = Path("docs") +en_docs_path = Path("") + +app = typer.Typer() + + +@app.callback() +def callback() -> None: + # For MacOS with Cairo + os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" + + +def generate_readme_content() -> str: + en_index = en_docs_path / "docs" / "index.md" + content = en_index.read_text("utf-8") + match_pre = re.search(r"\n\n", content) + if not match_pre: + raise RuntimeError("Couldn't find pre section (