@@ -352,7 +352,7 @@ $ pip install -r requirements.txt
//// tab | `uv`
-If you have
.
+That will download a compressed file with the Typer code, normally from [PyPI](https://pypi.org/project/typer/).
It will also **download** files for other packages that Typer depends on.
@@ -607,7 +607,7 @@ $ .venv\Scripts\Activate.ps1
//// tab | Windows Bash
-Or if you use Bash for Windows (e.g.
):
+Or if you use Bash for Windows (e.g. [Git Bash](https://gitforwindows.org/)):
@@ -619,13 +619,13 @@ $ source .venv/Scripts/activate
////
-That command will create or modify some [environment variables](environment-variables.md){.internal-link target=_blank} that will be available for the next commands.
+That command will create or modify some [environment variables](environment-variables.md) that will be available for the next commands.
One of those variables is the `PATH` variable.
/// tip
-You can learn more about the `PATH` environment variable in the [Environment Variables](environment-variables.md#path-environment-variable){.internal-link target=_blank} section.
+You can learn more about the `PATH` environment variable in the [Environment Variables](environment-variables.md#path-environment-variable) section.
///
@@ -826,7 +826,7 @@ This is a simple guide to get you started and teach you how everything works **u
There are many **alternatives** to managing virtual environments, package dependencies (requirements), projects.
-Once you are ready and want to use a tool to **manage the entire project**, packages dependencies, virtual environments, etc. I would suggest you try
uv.
+Once you are ready and want to use a tool to **manage the entire project**, packages dependencies, virtual environments, etc. I would suggest you try [uv](https://github.com/astral-sh/uv).
`uv` can do a lot of things, it can:
diff --git a/mkdocs.yml b/mkdocs.yml
index 6e09fb21c4..75b9c43262 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -4,6 +4,7 @@ site_description: Typer, build great CLIs. Easy to code. Based on Python type hi
site_url: https://typer.tiangolo.com/
theme:
+ variant: classic
name: material
custom_dir: docs/overrides
palette:
@@ -49,24 +50,13 @@ theme:
- toc.follow
icon:
- repo: fontawesome/brands/github-alt
+ repo: octicons/mark-github-24
logo: img/icon.svg
favicon: img/favicon.png
language: en
repo_name: fastapi/typer
repo_url: https://github.com/fastapi/typer
plugins:
- # Material for MkDocs
- search:
- social:
- typeset:
- # Other plugins
- macros:
- include_yaml:
- - members: data/members.yml
- redirects:
- redirect_maps:
- typer-cli.md: tutorial/typer-command.md
mkdocstrings:
handlers:
python:
@@ -90,7 +80,7 @@ plugins:
nav:
- Typer: index.md
- features.md
- - Tutorial - User Guide:
+ - "":
- tutorial/index.md
- environment-variables.md
- virtual-environments.md
@@ -99,14 +89,14 @@ nav:
- tutorial/typer-app.md
- tutorial/printing.md
- tutorial/terminating.md
- - CLI Arguments:
+ - "":
- 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
- - CLI Options:
+ - "":
- tutorial/options/index.md
- tutorial/options/help.md
- tutorial/options/required.md
@@ -115,7 +105,7 @@ nav:
- tutorial/options/name.md
- tutorial/options/callback-and-context.md
- tutorial/options/version.md
- - Commands:
+ - "":
- tutorial/commands/index.md
- tutorial/commands/arguments.md
- tutorial/commands/options.md
@@ -125,7 +115,7 @@ nav:
- tutorial/commands/one-or-multiple.md
- tutorial/commands/context.md
- tutorial/options-autocompletion.md
- - CLI Parameter Types:
+ - "":
- tutorial/parameter-types/index.md
- tutorial/parameter-types/number.md
- tutorial/parameter-types/bool.md
@@ -135,14 +125,14 @@ nav:
- tutorial/parameter-types/path.md
- tutorial/parameter-types/file.md
- tutorial/parameter-types/custom-types.md
- - SubCommands - Command Groups:
+ - "":
- 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
- - Multiple Values:
+ - "":
- tutorial/multiple-values/index.md
- tutorial/multiple-values/multiple-options.md
- tutorial/multiple-values/options-with-multiple-values.md
@@ -156,19 +146,19 @@ nav:
- tutorial/exceptions.md
- tutorial/one-file-per-command.md
- tutorial/typer-command.md
- - Reference (Code API):
+ - tutorial/click.md
+ - "":
- reference/index.md
- reference/typer.md
- reference/run_launch.md
- reference/parameters.md
- reference/file_objects.md
- reference/context.md
- - Resources:
+ - "":
- resources/index.md
- help-typer.md
- contributing.md
- - management-tasks.md
- - About:
+ - "":
- about/index.md
- alternatives.md
- management.md
@@ -180,6 +170,7 @@ markdown_extensions:
targets:
include:
- "*"
+ zensical.extensions.macros:
# Python Markdown
abbr:
attr_list:
@@ -208,18 +199,6 @@ markdown_extensions:
# pymdownx blocks
pymdownx.blocks.admonition:
- types:
- - note
- - attention
- - caution
- - danger
- - error
- - tip
- - hint
- - warning
- # Custom types
- - info
- - check
pymdownx.blocks.details:
pymdownx.blocks.tab:
alternate_style: True
@@ -230,16 +209,14 @@ markdown_extensions:
extra:
social:
- - icon: fontawesome/brands/github-alt
+ - icon: octicons/mark-github-24
link: https://github.com/fastapi/typer
- - icon: fontawesome/brands/twitter
- link: https://twitter.com/tiangolo
+ - 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
@@ -251,5 +228,5 @@ extra_javascript:
- js/termynal.js
- js/custom.js
-hooks:
- - scripts/mkdocs_hooks.py
+validation:
+ unresolved_references: false
diff --git a/pyproject.toml b/pyproject.toml
index 89cbde9893..ad636bb93d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,10 +33,10 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
- "click >= 8.2.1",
"shellingham >=1.3.0",
"rich >=13.8.0",
"annotated-doc >=0.0.2",
+ "colorama; platform_system == 'Windows'",
]
readme = "README.md"
@@ -55,6 +55,7 @@ dev = [
{ include-group = "tests" },
{ include-group = "docs" },
"prek >=0.3.2",
+ "zizmor >=1.23.1",
]
docs = [
"cairosvg >=2.8.2",
@@ -62,12 +63,10 @@ docs = [
"griffe-warnings-deprecated >=1.1.0",
"markdown-include-variants >=0.0.8",
"mdx-include >=1.4.1",
- "mkdocs-macros-plugin >=1.5.0",
- "mkdocs-material >=9.7.1",
- "mkdocs-redirects >=1.2.1",
- "mkdocstrings[python] >=0.30.1",
+ "mkdocstrings[python] >=1.0.3",
"pillow >=11.3.0",
"pyyaml >=5.3.1",
+ "zensical>=0.0.42",
]
github-actions = [
"httpx >=0.27.0",
@@ -163,7 +162,6 @@ ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"C901", # too complex
- "W191", # indentation contains tabs
"TID252", # relative imports okay
]
@@ -191,7 +189,7 @@ ignore = [
"docs_src/*" = ["TID"]
[tool.ruff.lint.isort]
-known-third-party = ["typer", "click"]
+known-third-party = ["typer"]
# For docs_src/subcommands/tutorial003/
known-first-party = ["reigns", "towns", "lands", "items", "users"]
@@ -212,3 +210,21 @@ Use 'typer._completion_shared._get_shell_name' instead of using \
[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/docs.py b/scripts/docs.py
index 4aafaa98c4..802dab53af 100644
--- a/scripts/docs.py
+++ b/scripts/docs.py
@@ -82,7 +82,7 @@ def live(dirty: bool = False) -> None:
en.
"""
# Enable line numbers during local development to make it easier to highlight
- args = ["mkdocs", "serve", "--dev-addr", "127.0.0.1:8008"]
+ args = ["zensical", "serve", "--dev-addr", "127.0.0.1:8008"]
if dirty:
args.append("--dirty")
subprocess.run(args, env={**os.environ, "LINENUMS": "true"}, check=True)
@@ -94,7 +94,7 @@ def build() -> None:
Build the docs.
"""
print("Building docs")
- subprocess.run(["mkdocs", "build"], check=True)
+ subprocess.run(["zensical", "build"], check=True)
typer.secho("Successfully built docs", color=typer.colors.GREEN)
@@ -103,7 +103,7 @@ def serve() -> None:
"""
A quick server to preview a built site.
- For development, prefer the command live (or just mkdocs serve).
+ For development, prefer the command live (or just zensical serve).
This is here only to preview the documentation site.
diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py
deleted file mode 100644
index 783ff23302..0000000000
--- a/scripts/mkdocs_hooks.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from typing import Any
-
-from mkdocs.config.defaults import MkDocsConfig
-from mkdocs.structure.files import Files
-from mkdocs.structure.nav import Link, Navigation, Section
-from mkdocs.structure.pages import Page
-
-
-def generate_renamed_section_items(
- items: list[Page | Section | Link], *, config: MkDocsConfig
-) -> list[Page | Section | Link]:
- new_items: list[Page | Section | Link] = []
- for item in items:
- if isinstance(item, Section):
- new_title = item.title
- new_children = generate_renamed_section_items(item.children, config=config)
- first_child = new_children[0]
- if isinstance(first_child, Page):
- if first_child.file.src_path.endswith("index.md"):
- # Read the source so that the title is parsed and available
- first_child.read_source(config=config)
- new_title = first_child.title or new_title
- # Creating a new section makes it render it collapsed by default
- # no idea why, so, let's just modify the existing one
- # new_section = Section(title=new_title, children=new_children)
- item.title = new_title
- item.children = new_children
- new_items.append(item)
- else:
- new_items.append(item)
- return new_items
-
-
-def on_nav(
- nav: Navigation, *, config: MkDocsConfig, files: Files, **kwargs: Any
-) -> Navigation:
- new_items = generate_renamed_section_items(nav.items, config=config)
- return Navigation(items=new_items, pages=nav.pages)
diff --git a/scripts/prepare_release.py b/scripts/prepare_release.py
new file mode 100644
index 0000000000..73ea40f2ff
--- /dev/null
+++ b/scripts/prepare_release.py
@@ -0,0 +1,209 @@
+"""Prepare a release by updating the package version and release notes."""
+
+import re
+from datetime import date
+from pathlib import Path
+from typing import Annotated, Literal
+
+import typer
+
+VERSION_PATTERN = re.compile(r'(?m)^__version__ = "(\d+\.\d+\.\d+)"$')
+VERSION_HEADING_PATTERN = re.compile(r"(?m)^## (\d+\.\d+\.\d+)(?: \([^)]+\))?$")
+RELEASE_NOTES_HEADER = "# Release Notes\n\n"
+LATEST_CHANGES_HEADER = "## Latest Changes"
+BumpType = Literal["major", "minor", "patch"]
+
+app = typer.Typer()
+
+
+def parse_version(version: str) -> tuple[int, int, int]:
+ match = re.fullmatch(r"\d+\.\d+\.\d+", version)
+ if not match:
+ raise ValueError(f"Invalid version: {version!r}. Expected format: X.Y.Z")
+ major, minor, patch = version.split(".")
+ return int(major), int(minor), int(patch)
+
+
+def get_current_version(content: str, version_file: Path) -> str:
+ matches = list(VERSION_PATTERN.finditer(content))
+ if len(matches) != 1:
+ raise RuntimeError(
+ f"Expected exactly one __version__ assignment in {version_file}, "
+ f"found {len(matches)}"
+ )
+ return matches[0].group(1)
+
+
+def bump_version(version: str, bump: BumpType) -> str:
+ major, minor, patch = parse_version(version)
+ if bump == "major":
+ return f"{major + 1}.0.0"
+ if bump == "minor":
+ return f"{major}.{minor + 1}.0"
+ return f"{major}.{minor}.{patch + 1}"
+
+
+def update_version_file(content: str, version: str, version_file: Path) -> str:
+ current_version = get_current_version(content, version_file)
+ if parse_version(version) <= parse_version(current_version):
+ raise RuntimeError(
+ f"New version {version} must be greater than current version {current_version}"
+ )
+ return VERSION_PATTERN.sub(f'__version__ = "{version}"', content, count=1)
+
+
+def update_release_notes(
+ content: str, version: str, release_date: date, release_notes_file: Path
+) -> str:
+ if not content.startswith(RELEASE_NOTES_HEADER):
+ raise RuntimeError(
+ f"{release_notes_file} must start with {RELEASE_NOTES_HEADER!r}"
+ )
+ if re.search(rf"^## {re.escape(version)}(?: \([^)]+\))?$", content, re.M):
+ raise RuntimeError(f"Release notes already contain a section for {version}")
+
+ latest_header = f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n"
+ if not content.startswith(latest_header):
+ raise RuntimeError(f"{release_notes_file} must start with {latest_header!r}")
+
+ release_header = f"## {version} ({release_date.isoformat()})"
+ return content.replace(
+ latest_header,
+ f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n\n{release_header}\n",
+ 1,
+ )
+
+
+def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str:
+ version_heading = re.compile(rf"(?m)^## {re.escape(version)}(?: \([^)]+\))?$")
+ match = version_heading.search(content)
+ if not match:
+ raise RuntimeError(
+ f"Could not find release notes section for {version} in {release_notes_file}"
+ )
+
+ next_match = VERSION_HEADING_PATTERN.search(content, match.end())
+ end = next_match.start() if next_match else len(content)
+ body = content[match.end() : end].strip()
+ if not body:
+ raise RuntimeError(
+ f"Release notes section for {version} in {release_notes_file} is empty"
+ )
+ return f"{body}\n"
+
+
+@app.command()
+def prepare(
+ bump: Annotated[
+ BumpType,
+ typer.Argument(
+ envvar="PREPARE_RELEASE_BUMP",
+ help="The release bump to make: major, minor, or patch.",
+ ),
+ ],
+ version_file: Annotated[
+ Path,
+ typer.Option(
+ envvar="PREPARE_RELEASE_VERSION_FILE",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ readable=True,
+ writable=True,
+ help="Path to the Python file containing the __version__ assignment.",
+ ),
+ ],
+ release_notes_file: Annotated[
+ Path,
+ typer.Option(
+ envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ readable=True,
+ writable=True,
+ help="Path to the release notes Markdown file.",
+ ),
+ ],
+ release_date: Annotated[
+ str,
+ typer.Option(
+ "--date",
+ envvar="PREPARE_RELEASE_DATE",
+ help="Release date in YYYY-MM-DD format. Defaults to today.",
+ ),
+ ] = date.today().isoformat(),
+) -> None:
+ parsed_release_date = date.fromisoformat(release_date or date.today().isoformat())
+
+ version_file_content = version_file.read_text()
+ release_notes_content = release_notes_file.read_text()
+ version = bump_version(
+ get_current_version(version_file_content, version_file), bump
+ )
+
+ version_file.write_text(
+ update_version_file(version_file_content, version, version_file)
+ )
+ release_notes_file.write_text(
+ update_release_notes(
+ release_notes_content, version, parsed_release_date, release_notes_file
+ )
+ )
+
+ typer.echo(f"Prepared release {version} ({parsed_release_date.isoformat()})")
+
+
+@app.command()
+def current_version(
+ version_file: Annotated[
+ Path,
+ typer.Option(
+ envvar="PREPARE_RELEASE_VERSION_FILE",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ readable=True,
+ help="Path to the Python file containing the __version__ assignment.",
+ ),
+ ],
+) -> None:
+ typer.echo(get_current_version(version_file.read_text(), version_file))
+
+
+@app.command()
+def release_notes(
+ version_file: Annotated[
+ Path,
+ typer.Option(
+ envvar="PREPARE_RELEASE_VERSION_FILE",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ readable=True,
+ help="Path to the Python file containing the __version__ assignment.",
+ ),
+ ],
+ release_notes_file: Annotated[
+ Path,
+ typer.Option(
+ envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",
+ exists=True,
+ file_okay=True,
+ dir_okay=False,
+ readable=True,
+ help="Path to the release notes Markdown file.",
+ ),
+ ],
+) -> None:
+ version = get_current_version(version_file.read_text(), version_file)
+ typer.echo(
+ get_release_notes_body(
+ release_notes_file.read_text(), version, release_notes_file
+ ),
+ nl=False,
+ )
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/assets/completion_argument.py b/tests/assets/completion_argument.py
index f91e2b7cfb..e2754c4357 100644
--- a/tests/assets/completion_argument.py
+++ b/tests/assets/completion_argument.py
@@ -1,10 +1,10 @@
-import click
import typer
+from typer import _click
app = typer.Typer()
-def shell_complete(ctx: click.Context, param: click.Parameter, incomplete: str):
+def shell_complete(ctx: _click.Context, param: _click.Parameter, incomplete: str):
typer.echo(f"ctx: {ctx.info_name}", err=True)
typer.echo(f"arg is: {param.name}", err=True)
typer.echo(f"incomplete is: {incomplete}", err=True)
diff --git a/tests/assets/hidden_commands.py b/tests/assets/hidden_commands.py
new file mode 100644
index 0000000000..a51809b803
--- /dev/null
+++ b/tests/assets/hidden_commands.py
@@ -0,0 +1,30 @@
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def visible():
+ """Visible command."""
+
+
+@app.command(hidden=True)
+def hidden_decorated():
+ """Hidden via @app.command(hidden=True)."""
+
+
+def hidden_var():
+ """Hidden via app.command(name, hidden=True)(fn)."""
+
+
+app.command("hidden-var", hidden=True)(hidden_var)
+
+hidden_subgroup = typer.Typer(hidden=True)
+
+
+@hidden_subgroup.command()
+def sub():
+ """Hidden subgroup command."""
+
+
+app.add_typer(hidden_subgroup, name="hidden-subgroup")
diff --git a/tests/assets/corner_cases.py b/tests/assets/hidden_option.py
similarity index 100%
rename from tests/assets/corner_cases.py
rename to tests/assets/hidden_option.py
diff --git a/tests/atomic_write_example.py b/tests/atomic_write_example.py
new file mode 100644
index 0000000000..a190cc154f
--- /dev/null
+++ b/tests/atomic_write_example.py
@@ -0,0 +1,63 @@
+import time
+
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def write_atomic(
+ config: typer.FileText = typer.Option(..., mode="w", atomic=True),
+ pause: float = typer.Option(0.3),
+) -> None:
+ config.write("atomic-content-1\n")
+ config.flush()
+ typer.echo("halfway")
+ time.sleep(pause)
+ config.write("atomic-content-2\n")
+ config.flush()
+ typer.echo("written atomically")
+
+
+@app.command()
+def write_atomic_binary(
+ config: typer.FileBinaryWrite = typer.Option(..., atomic=True, lazy=False),
+) -> None:
+ config.write(b"\x00\x01binary-atomic\n")
+ typer.echo("written binary atomically")
+
+
+@app.command()
+def api_atomic(
+ config: typer.FileText = typer.Option(..., mode="w", atomic=True, lazy=False),
+) -> None:
+ typer.echo(f"name={config.name}")
+ typer.echo(f"repr={repr(config)}")
+ with config as entered:
+ typer.echo(f"entered={entered is config}")
+ entered.write("atomic-api-done\n")
+
+
+@app.command()
+def invalid_atomic_append(
+ config: typer.FileText = typer.Option(..., mode="a", atomic=True, lazy=False),
+) -> None:
+ typer.echo(config.name) # pragma: no cover
+
+
+@app.command()
+def invalid_atomic_exclusive(
+ config: typer.FileText = typer.Option(..., mode="x", atomic=True, lazy=False),
+) -> None:
+ typer.echo(config.name) # pragma: no cover
+
+
+@app.command()
+def invalid_atomic_read(
+ config: typer.FileText = typer.Option(..., mode="r", atomic=True, lazy=False),
+) -> None:
+ typer.echo(config.name) # pragma: no cover
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_annotated.py b/tests/test_annotated.py
index c487eae9d0..af3cc0680b 100644
--- a/tests/test_annotated.py
+++ b/tests/test_annotated.py
@@ -2,6 +2,7 @@
from pathlib import Path
from typing import Annotated
+import pytest
import typer
from typer.testing import CliRunner
@@ -95,3 +96,14 @@ def custom_parser(
result = runner.invoke(app, "/some/quirky/path/implementation")
assert result.exit_code == 0
+
+
+def test_annotated_option_invalid():
+ app = typer.Typer()
+
+ @app.command()
+ def cmd(value: Annotated[str, typer.Option(..., "foo-bar")]):
+ print(value) # pragma: no cover
+
+ with pytest.raises(ValueError, match="Invalid start character for option"):
+ runner.invoke(app, ["--help"], catch_exceptions=False)
diff --git a/tests/test_atomic_file.py b/tests/test_atomic_file.py
new file mode 100644
index 0000000000..010f899034
--- /dev/null
+++ b/tests/test_atomic_file.py
@@ -0,0 +1,122 @@
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+from . import atomic_write_example as mod
+
+
+def test_atomic_write(tmp_path: Path) -> None:
+ original_content = "existing-content\n"
+ output_file = tmp_path / "atomic-write-target.txt"
+ output_file.write_text(original_content, encoding="utf-8")
+
+ process = subprocess.Popen(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ mod.__file__,
+ "write-atomic",
+ f"--config={output_file}",
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ assert process.stdout is not None
+
+ # Halfway of writing the file, check that the original content is still there
+ halfway_line = process.stdout.readline().strip()
+ assert halfway_line == "halfway"
+ assert output_file.read_text(encoding="utf-8") == original_content
+
+ # Only at the end, the full new content is visible
+ stdout, stderr = process.communicate(timeout=5)
+ assert process.returncode == 0, stderr
+ assert "written atomically" in stdout
+ assert (
+ output_file.read_text(encoding="utf-8")
+ == "atomic-content-1\natomic-content-2\n"
+ )
+
+
+def test_atomic_binary_write(tmp_path: Path) -> None:
+ output_file = tmp_path / "atomic-binary.bin"
+
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ mod.__file__,
+ "write-atomic-binary",
+ f"--config={output_file}",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+
+ assert result.returncode == 0, result.stderr
+ assert "written binary atomically" in result.stdout
+ assert output_file.read_bytes() == b"\x00\x01binary-atomic\n"
+
+
+def test_atomic_api(tmp_path: Path) -> None:
+ output_file = tmp_path / "atomic-api.txt"
+
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ mod.__file__,
+ "api-atomic",
+ f"--config={output_file}",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+
+ assert result.returncode == 0, result.stderr
+ assert f"name={output_file}" in result.stdout
+ assert "repr=<_io.TextIOWrapper" in result.stdout
+ assert "entered=True" in result.stdout
+ assert output_file.read_text(encoding="utf-8") == "atomic-api-done\n"
+
+
+@pytest.mark.parametrize(
+ ("command_name", "expected_message"),
+ [
+ ("invalid-atomic-append", "Appending to an existing file is not supported"),
+ ("invalid-atomic-exclusive", "Use the `overwrite`-parameter instead."),
+ ("invalid-atomic-read", "Atomic writes only make sense with `w`-mode."),
+ ],
+)
+def test_atomic_mode_invalid_options(
+ tmp_path: Path, command_name: str, expected_message: str
+) -> None:
+ output_file = tmp_path / "atomic-invalid-mode.txt"
+ output_file.write_text("existing-content\n", encoding="utf-8")
+
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ mod.__file__,
+ command_name,
+ f"--config={output_file}",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+
+ assert result.returncode != 0
+ combined_output = f"{result.stdout}\n{result.stderr}"
+ assert expected_message in combined_output
diff --git a/tests/test_cli/test_help.py b/tests/test_cli/test_help.py
index 64e5495c9a..e829c5801b 100644
--- a/tests/test_cli/test_help.py
+++ b/tests/test_cli/test_help.py
@@ -1,6 +1,11 @@
import subprocess
import sys
+import typer
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
def test_script_help():
result = subprocess.run(
@@ -36,3 +41,119 @@ def test_not_python():
encoding="utf-8",
)
assert "Could not import as Python file" in result.stderr
+
+
+def test_short_help() -> None:
+ app = typer.Typer(
+ rich_markup_mode=None,
+ context_settings={"max_content_width": 50},
+ )
+
+ @app.command(help=" \n\t ")
+ def empty() -> None:
+ pass # pragma: no cover
+
+ @app.command(help="\b first sentence.")
+ def marker() -> None:
+ pass # pragma: no cover
+
+ # Forcing truncation
+ @app.command(help=f"{'x' * 30} {'y' * 5} z trailing")
+ def long() -> None:
+ pass # pragma: no cover
+
+ result = runner.invoke(app, ["--help"], terminal_width=50)
+ assert result.exit_code == 0
+ assert "empty" in result.output
+ assert "marker" in result.output
+ assert "long" in result.output
+ assert "first sentence." in result.output
+ assert f"{'x' * 30}..." in result.output
+
+
+def test_help_wrapping() -> None:
+ app = typer.Typer(
+ rich_markup_mode=None,
+ context_settings={"max_content_width": 50},
+ )
+
+ @app.command(
+ help=(
+ "Wrapped paragraph has enough words to wrap in help output.\n"
+ "\n"
+ "\n"
+ "\b\n"
+ "RAW-LINE-ONE stays on one line even with many many many words.\n"
+ "RAW-LINE-TWO keeps original formatting.\n"
+ "\n"
+ "Final paragraph wraps normally as well."
+ )
+ )
+ def cmd() -> None:
+ pass # pragma: no cover
+
+ result = runner.invoke(app, ["cmd", "--help"], terminal_width=50)
+ assert result.exit_code == 0
+ assert "Wrapped paragraph has enough words to wrap" in result.output
+ assert (
+ "RAW-LINE-ONE stays on one line even with many many many words."
+ in result.output
+ )
+ assert "RAW-LINE-TWO keeps original formatting." in result.output
+ assert "Final paragraph wraps normally as well." in result.output
+
+
+def test_help_wrapping_long_name() -> None:
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.command()
+ def cmd(value: str) -> None:
+ pass # pragma: no cover
+
+ result = runner.invoke(
+ app,
+ ["cmd", "--help"],
+ terminal_width=40,
+ prog_name="very-long-program-name-that-forces-wrap",
+ )
+ assert result.exit_code == 0
+
+ output_lines = result.output.splitlines()
+ usage_idx = output_lines.index("Usage: very-long-program-name-that-forces-wrap ")
+ args_line = output_lines[usage_idx + 1]
+ assert args_line.lstrip() == "[OPTIONS] VALUE"
+ assert args_line.startswith(" ")
+
+
+def test_format_long_help_option() -> None:
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.command()
+ def cmd(
+ very_long: str = typer.Option(
+ ...,
+ "--this-is-a-very-very-very-long-option-name",
+ help="Description is rendered in the next line for long option labels.",
+ ),
+ ) -> None:
+ pass # pragma: no cover
+
+ result = runner.invoke(app, ["cmd", "--help"], terminal_width=80)
+ assert result.exit_code == 0
+
+ output_lines = result.output.splitlines()
+ option_idx = next(
+ i
+ for i, line in enumerate(output_lines)
+ if "--this-is-a-very-very-very-long-option-name" in line
+ )
+ assert "Description is rendered" not in output_lines[option_idx]
+ first_desc_line = output_lines[option_idx + 1]
+ assert first_desc_line.lstrip().startswith("Description is rendered")
+ continuation_block = " ".join(
+ line.strip() for line in output_lines[option_idx + 1 :] if line.startswith(" ")
+ )
+ assert (
+ "Description is rendered in the next line for long option labels."
+ in continuation_block
+ )
diff --git a/tests/test_cli/test_parser.py b/tests/test_cli/test_parser.py
new file mode 100644
index 0000000000..9d9b641e4b
--- /dev/null
+++ b/tests/test_cli/test_parser.py
@@ -0,0 +1,67 @@
+import subprocess
+import sys
+
+import typer
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+def test_double_dash() -> None:
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "hello",
+ "--",
+ "--name",
+ "Camila",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Got unexpected extra argument" in result.stderr
+ assert "--name Camila" in result.stderr
+
+
+def test_unknown_short_option() -> None:
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ "-m",
+ "typer",
+ "tests/assets/cli/sample.py",
+ "run",
+ "hello",
+ "-x",
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "No such option: -x" in result.stderr
+
+
+def test_ignore_unknown_short_option() -> None:
+ app = typer.Typer(
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
+ )
+
+ @app.command()
+ def main(
+ ctx: typer.Context, all_: bool = typer.Option(False, "--all", "-a")
+ ) -> None:
+ assert all_
+ print(ctx.args)
+
+ result = runner.invoke(app, ["-azq"])
+ assert result.exit_code == 0
+ assert "['-zq']" in result.output
diff --git a/tests/test_cli/test_program_name.py b/tests/test_cli/test_program_name.py
new file mode 100644
index 0000000000..4c20942596
--- /dev/null
+++ b/tests/test_cli/test_program_name.py
@@ -0,0 +1,13 @@
+from typer import _click
+
+
+def test_detect_program_name_submodule_path() -> None:
+ class MainModule:
+ __package__ = "example"
+
+ program_name = _click.utils._detect_program_name(
+ path="/tmp/cli.py",
+ _main=MainModule(),
+ )
+
+ assert program_name == "python -m example.cli"
diff --git a/tests/test_completion/choice_case_insensitive_example.py b/tests/test_completion/choice_case_insensitive_example.py
new file mode 100644
index 0000000000..1f208dd0b5
--- /dev/null
+++ b/tests/test_completion/choice_case_insensitive_example.py
@@ -0,0 +1,19 @@
+from enum import Enum
+
+import typer
+
+app = typer.Typer()
+
+
+class User(str, Enum):
+ rick = "rick"
+ morty = "morty"
+
+
+@app.command()
+def main(name: User = typer.Option(User.rick, "--name", case_sensitive=False)):
+ print(name.value)
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_completion/choice_example.py b/tests/test_completion/choice_example.py
new file mode 100644
index 0000000000..2c47d0fdc4
--- /dev/null
+++ b/tests/test_completion/choice_example.py
@@ -0,0 +1,19 @@
+from enum import Enum
+
+import typer
+
+app = typer.Typer()
+
+
+class User(str, Enum):
+ rick = "rick"
+ morty = "morty"
+
+
+@app.command()
+def main(name: User = typer.Option(User.rick, "--name")):
+ print(name.value)
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_completion/completion_option_then_argument.py b/tests/test_completion/completion_option_then_argument.py
new file mode 100644
index 0000000000..ae5abd8d2e
--- /dev/null
+++ b/tests/test_completion/completion_option_then_argument.py
@@ -0,0 +1,23 @@
+import typer
+
+app = typer.Typer()
+
+
+def complete_name(ctx, args, incomplete):
+ return ["opt-choice"] # pragma: no cover
+
+
+def complete_target(ctx, args, incomplete):
+ return ["arg-choice"]
+
+
+@app.command()
+def main(
+ name: str = typer.Option(..., "--name", autocompletion=complete_name),
+ target: str = typer.Argument(..., autocompletion=complete_target),
+):
+ print(name, target) # pragma: no cover
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_completion/file_example.py b/tests/test_completion/file_example.py
new file mode 100644
index 0000000000..56556d2006
--- /dev/null
+++ b/tests/test_completion/file_example.py
@@ -0,0 +1,12 @@
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def main(config: typer.FileText = typer.Option(...)):
+ print(config.read())
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_completion/test_completion.py b/tests/test_completion/test_completion.py
index 049ec4f6af..e9be4e25d1 100644
--- a/tests/test_completion/test_completion.py
+++ b/tests/test_completion/test_completion.py
@@ -3,9 +3,12 @@
import sys
from pathlib import Path
+from typer._click.shell_completion import CompletionItem
+
from docs_src.typer_app import tutorial001_py310 as mod
from ..utils import needs_bash, needs_linux, requires_completion_permission
+from . import completion_option_then_argument as mod_option_arg
@needs_bash
@@ -164,3 +167,27 @@ def test_completion_source_pwsh():
"Register-ArgumentCompleter -Native -CommandName tutorial001_py310.py -ScriptBlock $scriptblock"
in result.stdout
)
+
+
+def test_completion_option_argument() -> None:
+ file_name = Path(mod_option_arg.__file__).name
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod_option_arg.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ f"_{file_name.upper()}_COMPLETE": "complete_bash",
+ "COMP_WORDS": f"{file_name} --name chosen ",
+ "COMP_CWORD": "3",
+ },
+ )
+ assert "arg-choice" in result.stdout
+ assert "opt-choice" not in result.stdout
+
+
+def test_completion_item_getattr() -> None:
+ item = CompletionItem("demo", source="envvar")
+
+ assert item.source == "envvar"
+ assert item.missing is None
diff --git a/tests/test_completion/test_completion_choice.py b/tests/test_completion/test_completion_choice.py
new file mode 100644
index 0000000000..7cc4d81235
--- /dev/null
+++ b/tests/test_completion/test_completion_choice.py
@@ -0,0 +1,32 @@
+import os
+import subprocess
+import sys
+
+from . import choice_example as mod
+
+
+def test_script() -> None:
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--name", "rick"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert result.returncode == 0
+ assert "rick" in result.stdout
+
+
+def test_completion_choice_bash() -> None:
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_CHOICE_EXAMPLE.PY_COMPLETE": "complete_bash",
+ "COMP_WORDS": "choice_example.py --name mo",
+ "COMP_CWORD": "2",
+ },
+ )
+ assert result.returncode == 0
+ assert "morty" in result.stdout
+ assert "rick" not in result.stdout
diff --git a/tests/test_completion/test_completion_choice_no_case.py b/tests/test_completion/test_completion_choice_no_case.py
new file mode 100644
index 0000000000..f14097761d
--- /dev/null
+++ b/tests/test_completion/test_completion_choice_no_case.py
@@ -0,0 +1,32 @@
+import os
+import subprocess
+import sys
+
+from . import choice_case_insensitive_example as mod
+
+
+def test_script() -> None:
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--name", "rick"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert result.returncode == 0
+ assert "rick" in result.stdout
+
+
+def test_completion_choice_bash_case_insensitive() -> None:
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_CHOICE_CASE_INSENSITIVE_EXAMPLE.PY_COMPLETE": "complete_bash",
+ "COMP_WORDS": "choice_case_insensitive_example.py --name MO",
+ "COMP_CWORD": "2",
+ },
+ )
+ assert result.returncode == 0
+ assert "morty" in result.stdout
+ assert "rick" not in result.stdout
diff --git a/tests/test_completion/test_completion_file.py b/tests/test_completion/test_completion_file.py
new file mode 100644
index 0000000000..782d3a04bc
--- /dev/null
+++ b/tests/test_completion/test_completion_file.py
@@ -0,0 +1,39 @@
+import os
+import subprocess
+import sys
+
+from . import file_example as mod
+
+
+def test_script() -> None:
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "coverage",
+ "run",
+ mod.__file__,
+ "--config",
+ mod.__file__,
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert result.returncode == 0
+ assert "def main(config: typer.FileText = typer.Option(...)):" in result.stdout
+
+
+def test_completion_file_bash() -> None:
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_FILE_EXAMPLE.PY_COMPLETE": "complete_bash",
+ "COMP_WORDS": "file_example.py --config file_ex",
+ "COMP_CWORD": "2",
+ },
+ )
+ assert result.returncode == 0
+ assert "file_ex" in result.stdout
diff --git a/tests/test_completion/test_completion_option_colon.py b/tests/test_completion/test_completion_option_colon.py
index f106eca862..8818ee4a1a 100644
--- a/tests/test_completion/test_completion_option_colon.py
+++ b/tests/test_completion/test_completion_option_colon.py
@@ -78,7 +78,9 @@ def test_completion_colon_zsh_all():
},
)
assert "alpine\\\\:hello" in result.stdout
+ assert "fake image\\\\: for testing" in result.stdout
assert "alpine\\\\:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
assert "nvidia/cuda\\\\:10.0-devel-ubuntu18.04" in result.stdout
@@ -94,7 +96,9 @@ def test_completion_colon_zsh_partial():
},
)
assert "alpine\\\\:hello" in result.stdout
+ assert "fake image\\\\: for testing" in result.stdout
assert "alpine\\\\:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
assert "nvidia/cuda\\\\:10.0-devel-ubuntu18.04" not in result.stdout
@@ -110,7 +114,9 @@ def test_completion_colon_zsh_single():
},
)
assert "alpine\\\\:hello" in result.stdout
+ assert "fake image\\\\: for testing" in result.stdout
assert "alpine\\\\:latest" not in result.stdout
+ assert "latest alpine image" not in result.stdout
assert "nvidia/cuda\\\\:10.0-devel-ubuntu18.04" not in result.stdout
@@ -127,7 +133,9 @@ def test_completion_colon_powershell_all():
},
)
assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
assert "alpine:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
assert "nvidia/cuda:10.0-devel-ubuntu18.04" in result.stdout
@@ -144,7 +152,9 @@ def test_completion_colon_powershell_partial():
},
)
assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
assert "alpine:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
assert "nvidia/cuda:10.0-devel-ubuntu18.04" not in result.stdout
@@ -161,10 +171,45 @@ def test_completion_colon_powershell_single():
},
)
assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
assert "alpine:latest" not in result.stdout
+ assert "latest alpine image" not in result.stdout
assert "nvidia/cuda:10.0-devel-ubuntu18.04" not in result.stdout
+def test_completion_powershell_option_equals_value() -> None:
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_COLON_EXAMPLE.PY_COMPLETE": "complete_powershell",
+ "_TYPER_COMPLETE_ARGS": "colon_example.py --name=alpine",
+ "_TYPER_COMPLETE_WORD_TO_COMPLETE": "--name=alpine",
+ },
+ )
+ assert "alpine:hello" in result.stdout
+ assert "alpine:latest" in result.stdout
+ assert "nvidia/cuda:10.0-devel-ubuntu18.04" not in result.stdout
+
+
+def test_completion_powershell_option_equals_only() -> None:
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_COLON_EXAMPLE.PY_COMPLETE": "complete_powershell",
+ "_TYPER_COMPLETE_ARGS": "colon_example.py --name=",
+ "_TYPER_COMPLETE_WORD_TO_COMPLETE": "=",
+ },
+ )
+ assert result.returncode == 0
+ assert result.stdout.strip() == ""
+
+
def test_completion_colon_pwsh_all():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
@@ -178,7 +223,9 @@ def test_completion_colon_pwsh_all():
)
assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
assert "alpine:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
assert "nvidia/cuda:10.0-devel-ubuntu18.04" in result.stdout
@@ -195,7 +242,9 @@ def test_completion_colon_pwsh_partial():
},
)
assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
assert "alpine:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
assert "nvidia/cuda:10.0-devel-ubuntu18.04" not in result.stdout
@@ -212,8 +261,64 @@ def test_completion_colon_pwsh_single():
},
)
assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
assert "alpine:latest" not in result.stdout
+ assert "latest alpine image" not in result.stdout
+ assert "nvidia/cuda:10.0-devel-ubuntu18.04" not in result.stdout
+
+
+def test_completion_colon_fish_all():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_COLON_EXAMPLE.PY_COMPLETE": "complete_fish",
+ "_TYPER_COMPLETE_ARGS": "colon_example.py --name ",
+ "_TYPER_COMPLETE_FISH_ACTION": "get-args",
+ },
+ )
+ assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
+ assert "alpine:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
+ assert "nvidia/cuda:10.0-devel-ubuntu18.04" in result.stdout
+
+
+def test_completion_colon_fish_partial():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_COLON_EXAMPLE.PY_COMPLETE": "complete_fish",
+ "_TYPER_COMPLETE_ARGS": "colon_example.py --name alpine",
+ "_TYPER_COMPLETE_FISH_ACTION": "get-args",
+ },
+ )
+ assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
+ assert "alpine:latest" in result.stdout
+ assert "latest alpine image" in result.stdout
assert "nvidia/cuda:10.0-devel-ubuntu18.04" not in result.stdout
-# TODO: tests for complete_fish
+def test_completion_colon_fish_single():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, " "],
+ capture_output=True,
+ encoding="utf-8",
+ env={
+ **os.environ,
+ "_COLON_EXAMPLE.PY_COMPLETE": "complete_fish",
+ "_TYPER_COMPLETE_ARGS": "colon_example.py --name alpine:hell",
+ "_TYPER_COMPLETE_FISH_ACTION": "get-args",
+ },
+ )
+ assert "alpine:hello" in result.stdout
+ assert "fake image: for testing" in result.stdout
+ assert "alpine:latest" not in result.stdout
+ assert "latest alpine image" not in result.stdout
+ assert "nvidia/cuda:10.0-devel-ubuntu18.04" not in result.stdout
diff --git a/tests/test_core.py b/tests/test_core.py
new file mode 100644
index 0000000000..2cb759918b
--- /dev/null
+++ b/tests/test_core.py
@@ -0,0 +1,379 @@
+from typing import Annotated
+
+import pytest
+import typer
+import typer._completion_shared
+import typer.completion
+from typer import _click
+from typer.core import TyperArgument, TyperCommand, TyperGroup, TyperOption, _split_opt
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+def test_human_readable_name() -> None:
+ app = typer.Typer()
+
+ @app.command()
+ def main(
+ my_arg_1: Annotated[str, typer.Argument()],
+ my_arg_2: Annotated[str, typer.Argument(metavar="META_ARG")],
+ my_opt: Annotated[str, typer.Option()],
+ ):
+ pass # pragma: no cover
+
+ command = typer.main.get_command(app)
+ params = {param.name: param for param in command.params}
+
+ assert params["my_arg_1"].human_readable_name == "MY_ARG_1"
+ assert params["my_arg_2"].human_readable_name == "META_ARG"
+ assert params["my_opt"].human_readable_name == "my_opt"
+
+
+def test_parameter_metavar() -> None:
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.command()
+ def cmd(name: Annotated[str, typer.Option(metavar="CUSTOM")]) -> None:
+ pass # pragma: no cover
+
+ result = runner.invoke(app, ["--help"])
+ assert result.exit_code == 0
+ assert "--name CUSTOM" in result.output
+
+
+def test_parameter_nargs_gt_1() -> None:
+ param = TyperArgument(param_decls=["value"], type=str, nargs=2)
+ ctx = _click.Context(TyperCommand(name="cmd"))
+
+ assert param.type_cast_value(ctx, ("one", "two")) == ("one", "two")
+
+ with pytest.raises(
+ _click.exceptions.BadParameter, match="Takes 2 values but 1 given."
+ ):
+ param.type_cast_value(ctx, ("one",))
+
+
+def test_parameter_constructor() -> None:
+ # no param_decl and expose_value is False: sets name to None
+ arg = TyperArgument(param_decls=[], expose_value=False)
+ assert arg.name is None
+ assert arg.opts == []
+ assert arg.secondary_opts == []
+
+ # no param_decl and expose_value is True: raises
+ with pytest.raises(TypeError, match="does not have a name."):
+ TyperArgument(param_decls=[], expose_value=True)
+
+ # len(param_decl) > 1: raises
+ with pytest.raises(TypeError, match="take exactly one parameter declaration"):
+ TyperArgument(param_decls=["first", "second"])
+
+ # duplicated identifier in option declarations: raises
+ with pytest.raises(TypeError, match="Name 'name' defined twice"):
+ TyperOption(param_decls=["name", "name"], required=False)
+
+ # same true/false flag in boolean option declaration: raises
+ with pytest.raises(ValueError, match="cannot use the same flag for true/false"):
+ TyperOption(param_decls=["flag", "--flag/--flag"], required=False, is_flag=True)
+
+ # inferred name is not a valid identifier: sets name to None
+ unnamed_option = TyperOption(param_decls=["--123"], required=False)
+ assert unnamed_option.name is None
+
+ # no param_decl and prompt=True: raises
+ with pytest.raises(TypeError, match="'name' is required with 'prompt=True'."):
+ TyperOption(param_decls=[], expose_value=False, prompt=True, required=False)
+
+ # count works
+ option = TyperOption(
+ param_decls=["verbose", "--verbose", "-v"],
+ type=None,
+ default=0,
+ required=False,
+ count=True,
+ )
+ assert isinstance(option.type, _click.types.IntRange)
+ assert option.type.min == 0
+
+
+def test_option_error_hint() -> None:
+ option = TyperOption(
+ param_decls=["name", "--name"],
+ required=False,
+ show_envvar=True,
+ envvar="APP_NAME",
+ )
+ hint = option.get_error_hint(_click.Context(TyperCommand(name="cmd")))
+ assert "(env var: 'APP_NAME')" in hint
+
+
+def test_group_init() -> None:
+ group_no_commands = TyperGroup(name="root", commands=None)
+ assert group_no_commands.commands == {}
+
+ named = TyperCommand(name="named")
+ unnamed = TyperCommand(name=None)
+ group_command_sequence = TyperGroup(name="root", commands=[named, unnamed])
+ assert group_command_sequence.commands == {"named": named}
+
+
+@pytest.mark.parametrize("with_result_callback", [False, True])
+def test_group_result_callback(with_result_callback: bool) -> None:
+ called = {"child": False, "result_callback": False}
+
+ def child_callback() -> None:
+ called["child"] = True
+ return None
+
+ def result_callback(value, **kwargs): # type: ignore[no-untyped-def]
+ called["result_callback"] = True
+ return value
+
+ child = TyperCommand(name="child", callback=child_callback)
+ group = TyperGroup(
+ name="root",
+ commands={"child": child},
+ result_callback=result_callback if with_result_callback else None,
+ )
+ ctx = group.make_context("root", ["child"])
+
+ result = group.invoke(ctx)
+
+ assert result is None
+ assert called["child"] is True
+ assert called["result_callback"] is with_result_callback
+ assert ctx.invoked_subcommand == "child"
+
+
+def test_group_add_command() -> None:
+ group = TyperGroup(name="root")
+ unnamed_command = TyperCommand(name=None)
+
+ with pytest.raises(TypeError, match="Command has no name."):
+ group.add_command(unnamed_command)
+
+
+def test_group_click_resolve_command() -> None:
+ child = TyperCommand(name="child")
+ group = TyperGroup(name="root", commands={"child": child})
+ ctx = group.make_context("root", ["CHILD"], token_normalize_func=str.lower)
+
+ cmd_name, cmd, remaining = group._click_resolve_command(ctx, ["CHILD"])
+
+ assert cmd_name == "child"
+ assert cmd is child
+ assert remaining == []
+
+
+@pytest.mark.parametrize(
+ ("envvar", "auto_prefix", "set_env", "expected"),
+ [
+ ("APP_NAME", None, True, "my-precious"),
+ (None, "APP", True, "my-precious"),
+ (None, None, False, None),
+ ],
+)
+def test_option_resolve_envvar(
+ monkeypatch: pytest.MonkeyPatch,
+ envvar: str | None,
+ auto_prefix: str | None,
+ set_env: bool,
+ expected: str | None,
+) -> None:
+ option = TyperOption(
+ param_decls=["name", "--name"],
+ required=False,
+ envvar=envvar,
+ )
+ if set_env:
+ monkeypatch.setenv("APP_NAME", "my-precious")
+
+ ctx = _click.Context(TyperCommand(name="cmd"), auto_envvar_prefix=auto_prefix)
+ assert option.resolve_envvar_value(ctx) == expected
+
+
+def test_option_resolve_envvar_list(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ option = TyperOption(
+ param_decls=["name", "--name"],
+ required=False,
+ envvar=["APP_NAME_1", "APP_NAME_2"],
+ )
+ monkeypatch.delenv("APP_NAME_1", raising=False)
+ monkeypatch.delenv("APP_NAME_2", raising=False)
+ ctx = _click.Context(TyperCommand(name="cmd"))
+
+ assert option.resolve_envvar_value(ctx) is None
+
+
+def test_context_auto_envvar() -> None:
+ app = typer.Typer(context_settings={"auto_envvar_prefix": "APP"})
+ sub_app = typer.Typer()
+
+ @sub_app.command()
+ def clone(ctx: typer.Context) -> None:
+ print(ctx.auto_envvar_prefix)
+
+ app.add_typer(sub_app, name="beth")
+
+ result = runner.invoke(app, ["beth", "clone"])
+ assert result.exit_code == 0
+ assert "APP_BETH_CLONE" in result.stdout
+
+
+def test_context_with_resource() -> None:
+ events: list[str] = []
+
+ class DemoResource:
+ def __enter__(self) -> str:
+ events.append("enter")
+ return "pickle-rick"
+
+ def __exit__(self, *args: object) -> None:
+ events.append("exit")
+
+ app = typer.Typer()
+
+ @app.command()
+ def cmd(ctx: typer.Context) -> None:
+ value = ctx.with_resource(DemoResource())
+ assert value == "pickle-rick"
+ assert events == ["enter"]
+ print("I'm a pickle")
+
+ result = runner.invoke(app)
+
+ assert result.exit_code == 0
+ assert "I'm a pickle" in result.stdout
+ assert events == ["enter", "exit"]
+
+
+def test_context_find_root() -> None:
+ app = typer.Typer()
+ sub_app = typer.Typer()
+
+ @sub_app.command()
+ def child(ctx: typer.Context) -> None:
+ root = ctx.find_root()
+ assert root.parent is None
+ assert root is ctx.parent.parent
+ print("ok")
+
+ app.add_typer(sub_app, name="sub")
+
+ result = runner.invoke(app, ["sub", "child"])
+ assert result.exit_code == 0
+ assert "ok" in result.stdout
+
+
+def test_context_find_object() -> None:
+ class Marker:
+ pass
+
+ marker = Marker()
+ app = typer.Typer()
+
+ @app.callback()
+ def callback(ctx: typer.Context) -> None:
+ ctx.obj = marker
+
+ @app.command()
+ def child(ctx: typer.Context) -> None:
+ assert ctx.find_object(Marker) is marker
+ assert ctx.find_object(str) is None
+ print("ok")
+
+ result = runner.invoke(app, ["child"])
+ assert result.exit_code == 0
+ assert "ok" in result.stdout
+
+
+def test_context_lookup_default_callable() -> None:
+ app = typer.Typer()
+
+ @app.command()
+ def child(ctx: typer.Context) -> None:
+ ctx.default_map = {"planet": lambda: "Earth"}
+ assert ctx.lookup_default("planet") == "Earth"
+ value = ctx.lookup_default("planet", call=False)
+ assert callable(value)
+ print("ok")
+
+ result = runner.invoke(app)
+ assert result.exit_code == 0
+ assert "ok" in result.stdout
+
+
+def test_context_abort() -> None:
+ app = typer.Typer()
+
+ @app.command()
+ def cmd(ctx: typer.Context) -> None:
+ ctx.abort()
+
+ result = runner.invoke(app, standalone_mode=False)
+ assert result.exit_code == 1
+ assert isinstance(result.exception, _click.core.Abort)
+
+
+def test_command_help_disabled() -> None:
+ app = typer.Typer()
+
+ @app.command(add_help_option=False)
+ def cmd() -> None:
+ pass # pragma: no cover
+
+ result = runner.invoke(app, ["--help"], standalone_mode=False)
+ assert result.exit_code == 1
+ assert isinstance(result.exception, _click.exceptions.NoSuchOption)
+ assert result.exception.option_name == "--help"
+
+
+def test_command_help_deprecated() -> None:
+ app = typer.Typer(rich_markup_mode=None, epilog="Built with love")
+
+ @app.command(short_help="Shorty", help="Regular help text.", deprecated=True)
+ def one() -> None:
+ pass # pragma: no cover
+
+ @app.command()
+ def two() -> None:
+ pass # pragma: no cover
+
+ result = runner.invoke(app, ["one", "--help"])
+ assert result.exit_code == 0
+ assert "Regular help text. (DEPRECATED)" in result.output
+
+ result = runner.invoke(app, ["--help"])
+ assert result.exit_code == 0
+ assert "Built with love" in result.output
+ assert "oneShorty(DEPRECATED)" in result.output.replace(" ", "")
+
+
+@pytest.mark.parametrize(
+ ("value", "expected_prefix", "expected_opt"),
+ [
+ ("--verbose", "--", "verbose"),
+ ("//verbose", "//", "verbose"),
+ ("-verbose", "-", "verbose"),
+ ("verbose", "", "verbose"),
+ ],
+)
+def test_split_opt(value: str, expected_prefix: str, expected_opt: str) -> None:
+ prefix, opt = _split_opt(value)
+ assert prefix == expected_prefix
+ assert opt == expected_opt
+
+
+def test_nargs_default_map():
+ app = typer.Typer()
+
+ @app.command()
+ def main(names: list[str] = typer.Option(None)):
+ print(names) # pragma: no cover
+
+ result = runner.invoke(app, [], default_map={"names": "not-a-list"})
+ assert result.exit_code == 2
+ assert "Invalid value" in result.output
diff --git a/tests/test_corner_cases.py b/tests/test_corner_cases.py
deleted file mode 100644
index dfef7ddb60..0000000000
--- a/tests/test_corner_cases.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import pytest
-import typer.core
-from typer.testing import CliRunner
-
-from tests.assets import corner_cases as mod
-
-runner = CliRunner()
-
-
-def test_hidden_option():
- result = runner.invoke(mod.app, ["--help"])
- assert result.exit_code == 0
- assert "Say hello" in result.output
- assert "--name" not in result.output
- assert "/lastname" in result.output
- assert "TEST_LASTNAME" in result.output
- assert "(dynamic)" in result.output
-
-
-def test_hidden_option_no_rich(monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr(typer.core, "HAS_RICH", False)
-
- result = runner.invoke(mod.app, ["--help"])
- assert result.exit_code == 0
- assert "Say hello" in result.output
- assert "--name" not in result.output
- assert "/lastname" in result.output
- assert "TEST_LASTNAME" in result.output
- assert "(dynamic)" in result.output
-
-
-def test_coverage_call():
- result = runner.invoke(mod.app)
- assert result.exit_code == 0
- assert "Hello John Doe, it seems you have 42" in result.output
diff --git a/tests/test_hidden.py b/tests/test_hidden.py
new file mode 100644
index 0000000000..0d0874d2ba
--- /dev/null
+++ b/tests/test_hidden.py
@@ -0,0 +1,52 @@
+import pytest
+import typer.core
+from typer.testing import CliRunner
+
+from tests.assets import hidden_commands as mod_command
+from tests.assets import hidden_option as mod_option
+from tests.utils import needs_rich
+
+runner = CliRunner()
+
+
+@pytest.mark.parametrize(
+ "use_rich",
+ [
+ pytest.param(False),
+ pytest.param(True, marks=needs_rich),
+ ],
+)
+def test_hidden_option(monkeypatch: pytest.MonkeyPatch, use_rich: bool) -> None:
+ monkeypatch.setattr(typer.core, "HAS_RICH", use_rich)
+
+ result = runner.invoke(mod_option.app, ["--help"])
+ assert result.exit_code == 0
+ assert "Say hello" in result.output
+ assert "--name" not in result.output
+ assert "/lastname" in result.output
+ assert "TEST_LASTNAME" in result.output
+ assert "(dynamic)" in result.output
+
+
+def test_coverage_call() -> None:
+ result = runner.invoke(mod_option.app)
+ assert result.exit_code == 0
+ assert "Hello John Doe, it seems you have 42" in result.output
+
+
+@pytest.mark.parametrize(
+ "use_rich",
+ [
+ pytest.param(False),
+ pytest.param(True, marks=needs_rich),
+ ],
+)
+def test_hidden_commands(monkeypatch: pytest.MonkeyPatch, use_rich: bool) -> None:
+ monkeypatch.setattr(typer.core, "HAS_RICH", use_rich)
+
+ result = runner.invoke(mod_command.app, ["--help"])
+ assert result.exit_code == 0
+ assert "visible" in result.output
+ assert "hidden-decorated" not in result.output
+ assert "hidden-var" not in result.output
+ assert "hidden-subgroup" not in result.output
diff --git a/tests/test_launch.py b/tests/test_launch.py
index c15d6c57da..1a97c197aa 100644
--- a/tests/test_launch.py
+++ b/tests/test_launch.py
@@ -1,10 +1,11 @@
+import io
import subprocess
from unittest.mock import patch
import pytest
import typer
-from tests.utils import needs_windows
+from tests.utils import needs_linux, needs_macos, needs_windows
url = "http://example.com"
@@ -51,19 +52,94 @@ def test_launch_url_no_xdg_open():
mock_webbrowser_open.assert_called_once_with(url)
-def test_calls_original_launch_when_not_passing_urls():
- with patch("typer.main.click.launch", return_value=0) as launch_mock:
- typer.launch("not a url")
+@pytest.fixture
+def allow_dev_null(monkeypatch):
+ real_open = open
- launch_mock.assert_called_once_with("not a url", wait=False, locate=False)
+ def fake_open(path, *args, **kwargs):
+ if path == "/dev/null":
+ return io.StringIO()
+ return real_open(path, *args, **kwargs) # pragma: no cover
+
+ monkeypatch.setattr("builtins.open", fake_open)
+
+
+@needs_macos
+def test_open_url_macos(monkeypatch, allow_dev_null):
+ recorded: list[list[str]] = []
+
+ class Proc:
+ def wait(self) -> int:
+ return 42
+
+ def fake_popen(args, **kwargs):
+ recorded.append(list(args))
+ return Proc()
+
+ monkeypatch.setattr(subprocess, "Popen", fake_popen)
+
+ assert typer.launch("/path/to/file", wait=True, locate=True) == 42
+ assert recorded[0][:3] == ["open", "-W", "-R"]
+ assert recorded[0][-1] == "/path/to/file"
+
+
+@needs_windows
+def test_launch_files_windows(monkeypatch):
+ calls: list[list[str]] = []
+
+ def fake_call(args):
+ calls.append(list(args))
+ return 0
+
+ monkeypatch.setattr(subprocess, "call", fake_call)
+
+ assert typer.launch("C:/Tools/readme.txt", wait=True, locate=False) == 0
+ assert typer.launch("file:///C:/tmp/a.txt", wait=False, locate=True) == 0
+ assert calls.pop(0) == ["start", "/WAIT", "", "C:/Tools/readme.txt"]
+ assert calls.pop(0) == ["explorer", "/select,/C:/tmp/a.txt"]
+
+ monkeypatch.setattr(subprocess, "call", lambda a: (_ for _ in ()).throw(OSError()))
+ assert typer.launch("D:/no/such/file.txt", wait=False, locate=False) == 127
+
+
+@needs_linux
+def test_open_url_linux_wait(monkeypatch):
+ class Proc:
+ def __init__(self, code: int = 0) -> None:
+ self._code = code
+
+ def wait(self) -> int:
+ return self._code
+
+ monkeypatch.setattr(subprocess, "Popen", lambda *a, **k: Proc(7))
+
+ assert typer.launch("/file", wait=True, locate=False) == 7
+
+
+@needs_linux
+def test_open_url_linux_locate(monkeypatch):
+ recorded: list[list[str]] = []
+
+ class Proc:
+ def wait(self) -> int:
+ return 0 # pragma: no cover
+
+ def fake_popen(args, **kwargs):
+ recorded.append(list(args))
+ return Proc()
+
+ monkeypatch.setattr(subprocess, "Popen", fake_popen)
+
+ assert typer.launch("/tmp/sub/file.txt", wait=False, locate=True) == 0
+ assert recorded[-1] == ["xdg-open", "/tmp/sub"]
@needs_windows
def test_launch_file():
with (
- patch("click._termui_impl.sys.platform", "win32"),
- patch("click._termui_impl.WIN", True),
- patch("click._termui_impl.CYGWIN", False),
+ patch("typer._click._termui_impl.sys.platform", "win32"),
+ patch("typer._click._termui_impl.WIN", True),
+ patch("typer._click._termui_impl.CYGWIN", False),
patch("subprocess.call", return_value=0) as call_mock,
):
result = typer.launch("C:/tmp/file.txt", locate=True)
diff --git a/tests/test_others.py b/tests/test_others.py
index b389ed353f..d2cc8696f1 100644
--- a/tests/test_others.py
+++ b/tests/test_others.py
@@ -6,12 +6,11 @@
from typing import Annotated
from unittest import mock
-import click
import pytest
import typer
import typer._completion_shared
import typer.completion
-from typer.core import _split_opt
+from typer import _click
from typer.main import solve_typer_info_defaults, solve_typer_info_help
from typer.models import ParameterInfo, TyperInfo
from typer.testing import CliRunner
@@ -37,14 +36,14 @@ def test_too_many_parsers():
def custom_parser(value: str) -> int:
return int(value) # pragma: no cover
- class CustomClickParser(click.ParamType):
+ class CustomClickParser(_click.types.ParamType):
name = "custom_parser"
def convert(
self,
value: str,
- param: click.Parameter | None,
- ctx: click.Context | None,
+ param: _click.Parameter | None,
+ ctx: _click.Context | None,
) -> typing.Any:
return int(value) # pragma: no cover
@@ -61,14 +60,14 @@ def test_valid_parser_permutations():
def custom_parser(value: str) -> int:
return int(value) # pragma: no cover
- class CustomClickParser(click.ParamType):
+ class CustomClickParser(_click.types.ParamType):
name = "custom_parser"
def convert(
self,
value: str,
- param: click.Parameter | None,
- ctx: click.Context | None,
+ param: _click.Parameter | None,
+ ctx: _click.Context | None,
) -> typing.Any:
return int(value) # pragma: no cover
@@ -104,7 +103,7 @@ def name_callback(ctx, param, val1, val2):
def main(name: str = typer.Option(..., callback=name_callback)):
pass # pragma: no cover
- with pytest.raises(click.ClickException) as exc_info:
+ with pytest.raises(_click.ClickException) as exc_info:
runner.invoke(app, ["--name", "Camila"])
assert (
exc_info.value.message == "Too many CLI parameter callback function parameters"
@@ -127,6 +126,135 @@ def main(name: str = typer.Option(..., callback=name_callback)):
assert "value is: Camila" in result.stdout
+@pytest.mark.parametrize(
+ ("param_hint", "option_decls", "expected_message"),
+ [
+ ("--name", (), "Invalid value for --name"),
+ (None, ("--name", "-n"), "Invalid value for '--name' / '-n'"),
+ ],
+)
+def test_bad_parameter_callback(
+ param_hint: str | None, option_decls: tuple[str, ...], expected_message: str
+) -> None:
+ app = typer.Typer()
+
+ def my_bad(value: str) -> str:
+ kwargs = {"param_hint": param_hint} if param_hint is not None else {}
+ raise typer.BadParameter("custom validation failed", **kwargs)
+
+ @app.command()
+ def main(name: str = typer.Option(..., *option_decls, callback=my_bad)) -> None:
+ typer.echo(name) # pragma: no cover
+
+ result = runner.invoke(app, ["--name", "Camila"])
+ assert result.exit_code == 2
+ assert expected_message in result.stderr
+ assert "custom validation failed" in result.stderr
+
+
+def test_bad_parameter_main() -> None:
+ app = typer.Typer()
+
+ @app.command()
+ def main() -> None:
+ raise typer.BadParameter("custom validation failed")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 2
+ assert "Invalid value: custom validation failed" in result.stderr
+
+
+@pytest.mark.parametrize(
+ ("kw", "msg"),
+ [
+ (
+ {"param_hint": ["--name", "-n"], "param_type": "parameter"},
+ "Missing parameter '--name' / '-n'.",
+ ),
+ ({"param_type": "value"}, "Missing value."),
+ ],
+)
+def test_missing_parameter_msg(kw: dict[str, object], msg: str) -> None:
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.command()
+ def main() -> None:
+ raise typer._click.exceptions.MissingParameter(**kw)
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 2
+ assert msg in result.stderr
+
+
+def test_missing_parameter_callback_msg() -> None:
+ def my_cb(ctx: typer.Context, param: typer.CallbackParam, value: str) -> str:
+ raise typer._click.exceptions.MissingParameter(
+ message="My bad", ctx=ctx, param=param, param_type="parameter"
+ )
+
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.command()
+ def main(
+ mode: Annotated[
+ typing.Literal["alpha", "beta"],
+ typer.Option(..., "--mode", callback=my_cb),
+ ],
+ ) -> None:
+ typer.echo(mode) # pragma: no cover
+
+ result = runner.invoke(app, ["--mode", "alpha"])
+ assert result.exit_code == 2
+ assert "Missing parameter '--mode'." in result.stderr
+ assert "My bad. Choose from:" in result.stderr
+ assert "alpha" in result.stderr
+ assert "beta" in result.stderr
+ result_msg = runner.invoke(app, ["--mode", "alpha"], standalone_mode=False)
+ assert isinstance(result_msg.exception, typer._click.exceptions.MissingParameter)
+ assert str(result_msg.exception) == "My bad"
+
+
+def test_missing_parameter_str() -> None:
+ def my_cb(ctx: typer.Context, param: typer.CallbackParam, value: str) -> str:
+ raise typer._click.exceptions.MissingParameter(ctx=ctx, param=param)
+
+ app = typer.Typer()
+
+ @app.command()
+ def main(mode: str = typer.Option(..., "--mode", callback=my_cb)) -> None:
+ typer.echo(mode) # pragma: no cover
+
+ result2 = runner.invoke(app, ["--mode", "alpha"], standalone_mode=False)
+ assert isinstance(result2.exception, typer._click.exceptions.MissingParameter)
+ assert str(result2.exception) == "Missing parameter: mode"
+
+
+def test_click_exception_show_default_file() -> None:
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.command()
+ def main() -> None:
+ raise typer._click.ClickException("custom click failure")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 1
+ assert "custom click" in result.stderr
+ assert "failure" in result.stderr
+
+
+def test_no_args_is_help_show() -> None:
+ app = typer.Typer(rich_markup_mode=None)
+
+ @app.callback(invoke_without_command=True, no_args_is_help=True)
+ def main() -> None:
+ return None # pragma: no cover
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 2
+ assert "Usage:" in result.stderr
+ assert "Show this message and exit." in result.stderr
+
+
def test_callback_3_untyped_parameters():
app = typer.Typer()
@@ -169,6 +297,18 @@ def main(
assert "Hello World" in result.stdout
+def test_multiple_bool_flags() -> None:
+ app = typer.Typer()
+
+ @app.command()
+ def main(choices: list[bool] = typer.Option([], "--accept/--reject")) -> None:
+ print(choices)
+
+ result = runner.invoke(app, ["--accept", "--reject", "--accept"])
+ assert result.exit_code == 0
+ assert "[True, False, True]" in result.stdout
+
+
def test_empty_list_default_generator():
def empty_list() -> list[str]:
return []
@@ -185,6 +325,32 @@ def main(
assert "[]" in result.output
+def test_option_envvar():
+ app = typer.Typer()
+
+ @app.command()
+ def main(user: Annotated[str, typer.Option(envvar="ME")]):
+ print(f"Hello {user}")
+
+ result = runner.invoke(app, env={"ME": "rick"})
+ assert result.exit_code == 0
+ assert "Hello rick" in result.output
+
+
+def test_option_envvar_list():
+ app = typer.Typer()
+
+ @app.command()
+ def main(users: Annotated[list[str], typer.Option(envvar="ME")]):
+ for u in users:
+ print(f"Hello {u}")
+
+ result = runner.invoke(app, env={"ME": "rick morty"})
+ assert result.exit_code == 0
+ assert "Hello rick" in result.output
+ assert "Hello morty" in result.output
+
+
def test_completion_argument():
file_path = Path(__file__).parent / "assets/completion_argument.py"
result = subprocess.run(
@@ -266,7 +432,7 @@ def name_callback(ctx, args, incomplete, val2):
def main(name: str = typer.Option(..., autocompletion=name_callback)):
pass # pragma: no cover
- with pytest.raises(click.ClickException) as exc_info:
+ with pytest.raises(_click.ClickException) as exc_info:
runner.invoke(app, ["--name", "Camila"])
assert exc_info.value.message == "Invalid autocompletion callback parameters: val2"
@@ -303,24 +469,6 @@ def main(name: str):
assert "Show this message and exit." in result.stdout
-def test_split_opt():
- prefix, opt = _split_opt("--verbose")
- assert prefix == "--"
- assert opt == "verbose"
-
- prefix, opt = _split_opt("//verbose")
- assert prefix == "//"
- assert opt == "verbose"
-
- prefix, opt = _split_opt("-verbose")
- assert prefix == "-"
- assert opt == "verbose"
-
- prefix, opt = _split_opt("verbose")
- assert prefix == ""
- assert opt == "verbose"
-
-
def test_options_metadata_typer_default():
app = typer.Typer(options_metavar="[options]")
diff --git a/tests/test_prepare_release.py b/tests/test_prepare_release.py
new file mode 100644
index 0000000000..024b125466
--- /dev/null
+++ b/tests/test_prepare_release.py
@@ -0,0 +1,295 @@
+from datetime import date
+from pathlib import Path
+
+import pytest
+from typer.testing import CliRunner
+
+from scripts.prepare_release import (
+ BumpType,
+ app,
+ bump_version,
+ get_release_notes_body,
+ update_release_notes,
+ update_version_file,
+)
+
+runner = CliRunner()
+
+
+@pytest.mark.parametrize(
+ ("current_version", "bump", "new_version"),
+ [
+ ("0.26.2", "major", "1.0.0"),
+ ("0.26.2", "minor", "0.27.0"),
+ ("0.26.2", "patch", "0.26.3"),
+ ],
+)
+def test_bump_version(current_version: str, bump: BumpType, new_version: str) -> None:
+ assert bump_version(current_version, bump) == new_version
+
+
+def test_update_version_file() -> None:
+ content = '"""Typer."""\n\n__version__ = "0.26.2"\n'
+
+ new_content = update_version_file(content, "0.26.3", Path("typer/__init__.py"))
+
+ assert new_content == '"""Typer."""\n\n__version__ = "0.26.3"\n'
+
+
+def test_update_version_file_requires_newer_version() -> None:
+ content = '__version__ = "0.26.2"\n'
+
+ with pytest.raises(RuntimeError, match="must be greater"):
+ update_version_file(content, "0.26.2", Path("typer/__init__.py"))
+
+
+def test_update_release_notes() -> None:
+ content = """# Release Notes
+
+## Latest Changes
+
+### Fixes
+
+* Fix something.
+
+## 0.26.2 (2026-05-27)
+
+### Fixes
+
+* Previous fix.
+"""
+
+ new_content = update_release_notes(
+ content, "0.26.3", date(2026, 5, 28), Path("docs/release-notes.md")
+ )
+
+ assert (
+ new_content
+ == """# Release Notes
+
+## Latest Changes
+
+## 0.26.3 (2026-05-28)
+
+### Fixes
+
+* Fix something.
+
+## 0.26.2 (2026-05-27)
+
+### Fixes
+
+* Previous fix.
+"""
+ )
+
+
+def test_update_release_notes_rejects_existing_version() -> None:
+ content = """# Release Notes
+
+## Latest Changes
+
+## 0.26.3 (2026-05-28)
+"""
+
+ with pytest.raises(RuntimeError, match="already contain"):
+ update_release_notes(
+ content, "0.26.3", date(2026, 5, 28), Path("docs/release-notes.md")
+ )
+
+
+def test_get_release_notes_body_with_dated_heading() -> None:
+ content = """# Release Notes
+
+## Latest Changes
+
+## 0.26.3 (2026-05-28)
+
+### Fixes
+
+* Fix something.
+
+## 0.26.2 (2026-05-27)
+
+### Fixes
+
+* Previous fix.
+"""
+
+ body = get_release_notes_body(content, "0.26.3", Path("docs/release-notes.md"))
+
+ assert (
+ body
+ == """### Fixes
+
+* Fix something.
+"""
+ )
+
+
+def test_get_release_notes_body_with_plain_heading() -> None:
+ content = """# Release Notes
+
+## Latest Changes
+
+## 0.26.3
+
+### Fixes
+
+* Fix something.
+"""
+
+ body = get_release_notes_body(content, "0.26.3", Path("docs/release-notes.md"))
+
+ assert body == "### Fixes\n\n* Fix something.\n"
+
+
+def test_get_release_notes_body_allows_non_version_h2_content() -> None:
+ content = """# Release Notes
+
+## Latest Changes
+
+## 0.26.3
+
+## Highlights
+
+* Fix something.
+
+## 0.26.2
+
+* Previous fix.
+"""
+
+ body = get_release_notes_body(content, "0.26.3", Path("docs/release-notes.md"))
+
+ assert body == "## Highlights\n\n* Fix something.\n"
+
+
+def test_get_release_notes_body_requires_version_section() -> None:
+ content = "# Release Notes\n\n## Latest Changes\n"
+
+ with pytest.raises(RuntimeError, match="Could not find"):
+ get_release_notes_body(content, "0.26.3", Path("docs/release-notes.md"))
+
+
+def test_get_release_notes_body_requires_non_empty_section() -> None:
+ content = """# Release Notes
+
+## Latest Changes
+
+## 0.26.3
+
+## 0.26.2
+
+* Previous fix.
+"""
+
+ with pytest.raises(RuntimeError, match="is empty"):
+ get_release_notes_body(content, "0.26.3", Path("docs/release-notes.md"))
+
+
+def test_cli_updates_configured_files(tmp_path: Path) -> None:
+ version_file = tmp_path / "package" / "__init__.py"
+ version_file.parent.mkdir()
+ version_file.write_text('__version__ = "0.26.2"\n')
+ release_notes_file = tmp_path / "release-notes.md"
+ release_notes_file.write_text(
+ """# Release Notes
+
+## Latest Changes
+
+### Fixes
+
+* Fix something.
+"""
+ )
+
+ result = runner.invoke(
+ app,
+ [
+ "prepare",
+ "patch",
+ "--version-file",
+ str(version_file),
+ "--release-notes-file",
+ str(release_notes_file),
+ "--date",
+ "2026-05-28",
+ ],
+ )
+
+ assert result.exit_code == 0, result.output
+ assert "Prepared release 0.26.3 (2026-05-28)" in result.output
+ assert version_file.read_text() == '__version__ = "0.26.3"\n'
+ assert "## 0.26.3 (2026-05-28)" in release_notes_file.read_text()
+
+
+def test_cli_accepts_env_vars(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+ version_file = tmp_path / "package" / "__init__.py"
+ version_file.parent.mkdir()
+ version_file.write_text('__version__ = "0.26.2"\n')
+ release_notes_file = tmp_path / "docs" / "release-notes.md"
+ release_notes_file.parent.mkdir()
+ release_notes_file.write_text("# Release Notes\n\n## Latest Changes\n")
+ monkeypatch.setenv("PREPARE_RELEASE_BUMP", "minor")
+ monkeypatch.setenv("PREPARE_RELEASE_VERSION_FILE", str(version_file))
+ monkeypatch.setenv("PREPARE_RELEASE_RELEASE_NOTES_FILE", str(release_notes_file))
+ monkeypatch.setenv("PREPARE_RELEASE_DATE", "2026-05-28")
+
+ result = runner.invoke(app, ["prepare"])
+
+ assert result.exit_code == 0, result.output
+ assert "Prepared release 0.27.0 (2026-05-28)" in result.output
+ assert version_file.read_text() == '__version__ = "0.27.0"\n'
+ assert "## 0.27.0 (2026-05-28)" in release_notes_file.read_text()
+
+
+def test_cli_prints_current_version(tmp_path: Path) -> None:
+ version_file = tmp_path / "package" / "__init__.py"
+ version_file.parent.mkdir()
+ version_file.write_text('__version__ = "0.26.2"\n')
+
+ result = runner.invoke(
+ app,
+ [
+ "current-version",
+ "--version-file",
+ str(version_file),
+ ],
+ )
+
+ assert result.exit_code == 0, result.output
+ assert result.output == "0.26.2\n"
+
+
+def test_cli_prints_release_notes(tmp_path: Path) -> None:
+ version_file = tmp_path / "package" / "__init__.py"
+ version_file.parent.mkdir()
+ version_file.write_text('__version__ = "0.26.3"\n')
+ release_notes_file = tmp_path / "release-notes.md"
+ release_notes_file.write_text(
+ """# Release Notes
+
+## Latest Changes
+
+## 0.26.3 (2026-05-28)
+
+### Fixes
+
+* Fix something.
+"""
+ )
+
+ result = runner.invoke(
+ app,
+ [
+ "release-notes",
+ "--version-file",
+ str(version_file),
+ "--release-notes-file",
+ str(release_notes_file),
+ ],
+ )
+
+ assert result.exit_code == 0, result.output
+ assert result.output == "### Fixes\n\n* Fix something.\n"
diff --git a/tests/test_progress_bar.py b/tests/test_progress_bar.py
new file mode 100644
index 0000000000..c684476dec
--- /dev/null
+++ b/tests/test_progress_bar.py
@@ -0,0 +1,397 @@
+"""
+Tests for the Progress bar functionality.
+Created after vendoring Click to ensure test coverage is back up to 100%.
+"""
+
+import io
+import shutil
+
+import pytest
+import typer
+from typer import progressbar
+from typer._click import _termui_impl
+from typer.testing import CliRunner
+
+runner = CliRunner()
+
+
+def _fake_clock(monkeypatch: pytest.MonkeyPatch) -> list[float]:
+ clock = [0.0]
+ monkeypatch.setattr(_termui_impl.time, "time", lambda: clock[0])
+ return clock
+
+
+def _pbar(**kw):
+ return progressbar(file=kw.pop("file", io.StringIO()), **kw)
+
+
+@pytest.mark.parametrize(
+ ("iterable", "length", "hidden", "label", "expected_count"),
+ [
+ (["a", "b"], None, False, "Processing", 2),
+ (None, 3, False, "Counting", 3),
+ (["x", "y"], None, True, "Hidden", 2),
+ ],
+)
+def test_progressbar(iterable, length, hidden, label, expected_count):
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ bar_out = io.StringIO()
+ count = 0
+ with progressbar(
+ iterable, length=length, hidden=hidden, label=label, file=bar_out
+ ) as bar:
+ for _ in bar:
+ count += 1
+ typer.echo(f"count={count}")
+ typer.echo(f"bar={bar_out.getvalue()!r}")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ assert f"count={expected_count}" in result.stdout
+ assert (label in result.stdout) == (not hidden)
+
+
+@pytest.mark.parametrize(
+ ("label", "pbar_kw", "must_contain", "must_not_contain"),
+ [
+ pytest.param(
+ "TTY",
+ {
+ "show_pos": True,
+ "show_percent": True,
+ "item_show_func": lambda item: f"item={item}",
+ },
+ ("TTY", "1/1", "100%", "item=x"),
+ (),
+ ),
+ pytest.param(
+ "HeurPct",
+ {},
+ ("HeurPct", "100%"),
+ ("1/1",),
+ ),
+ pytest.param(
+ "HeurPos",
+ {"show_pos": True},
+ ("HeurPos", "1/1"),
+ ("100%",),
+ ),
+ ],
+)
+def test_progressbar_tty(
+ monkeypatch, label: str, pbar_kw: dict, must_contain, must_not_contain
+):
+ monkeypatch.setattr(_termui_impl, "isatty", lambda f: True)
+ _fake_clock(monkeypatch)
+
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ bar_out = io.StringIO()
+ with progressbar(
+ ["x"],
+ label=label,
+ file=bar_out,
+ bar_template="%(label)s %(info)s",
+ width=1,
+ **pbar_kw,
+ ) as bar:
+ for _ in bar:
+ pass
+ typer.echo(bar_out.getvalue())
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ for part in must_contain:
+ assert part in result.stdout
+ for part in must_not_contain:
+ assert part not in result.stdout
+
+
+def test_progressbar_tty_show_eta(monkeypatch):
+ monkeypatch.setattr(_termui_impl, "isatty", lambda f: True)
+ clock = _fake_clock(monkeypatch)
+ clock[0] = 1_000.0
+
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ bar_out = io.StringIO()
+ with progressbar(
+ ["a", "b"],
+ label="ETA",
+ file=bar_out,
+ show_pos=True,
+ show_percent=False,
+ show_eta=True,
+ bar_template="%(label)s %(info)s",
+ width=1,
+ ) as bar:
+ for i, _ in enumerate(bar):
+ if i == 0:
+ clock[0] = 1_001.0
+ typer.echo(bar_out.getvalue())
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ for part in ("ETA", "1/2", "00:00:01"):
+ assert part in result.stdout
+
+
+def test_progressbar_autowidth(monkeypatch):
+ monkeypatch.setattr(_termui_impl, "isatty", lambda f: True)
+ call = [0]
+ real_get_terminal_size = shutil.get_terminal_size
+
+ def fake_get_terminal_size(*args, **kwargs):
+ # Pytest (and others) call get_terminal_size(fallback=...); only stub no-arg calls
+ if args or kwargs:
+ return real_get_terminal_size(*args, **kwargs)
+ col = 120 if call[0] == 0 else 40
+ call[0] += 1
+ return type("TS", (), {"columns": col, "lines": 24})()
+
+ monkeypatch.setattr(shutil, "get_terminal_size", fake_get_terminal_size)
+
+ state: dict[str, object] = {}
+
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ out = io.StringIO()
+ with progressbar(["a", "b"], width=0, label="AW", file=out) as bar:
+ state["autowidth"] = bar.autowidth
+ for _ in bar:
+ pass
+ state["call_count"] = call[0]
+ state["out"] = out.getvalue()
+ typer.echo("done")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ assert state["autowidth"] is True
+ assert state["call_count"] >= 2
+ out = str(state["out"])
+ assert "\r" in out and "AW" in out
+ assert "0%" in out and "50%" in out and "100%" in out
+
+
+def test_progress_bar_iter():
+ not_entered = _pbar(iterable=[1, 2], length=2)
+ with pytest.raises(RuntimeError, match="with block"):
+ iter(not_entered)
+
+ entered = _pbar(iterable=[10, 20], length=2)
+ with entered:
+ iterator = iter(entered)
+ assert next(iterator) == 10
+ assert next(entered) == 20
+ with pytest.raises(StopIteration):
+ next(iterator)
+
+
+def test_progress_bar_time(monkeypatch):
+ clock = _fake_clock(monkeypatch)
+ state: dict[str, object] = {}
+ clock[0] = 1_000.0
+
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ bar = _pbar(iterable=None, length=10)
+ state["tpi0"] = bar.time_per_iteration
+ clock[0] = 1_000.5
+ bar.make_step(1)
+ state["avg_after_one"] = list(bar.avg)
+ state["tpi_after_one"] = bar.time_per_iteration
+ clock[0] = 1_001.0
+ bar.make_step(1)
+ state["pos2"] = bar.pos
+ state["avg2"] = list(bar.avg)
+ state["tpi2"] = bar.time_per_iteration
+ clock[0] = 1_002.0
+ bar.make_step(1)
+ state["avg3"] = list(bar.avg)
+ state["tpi3"] = bar.time_per_iteration
+ typer.echo("ok")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ assert state["tpi0"] == 0.0
+ assert state["avg_after_one"] == [] and state["tpi_after_one"] == 0.0
+ assert state["pos2"] == 2
+ assert state["avg2"] == [(1_001.0 - 1_000.0) / 2.0]
+ assert state["tpi2"] == pytest.approx(0.5)
+ assert state["avg3"] == [0.5, (1_002.0 - 1_000.0) / 3.0]
+ assert state["tpi3"] == pytest.approx(sum(state["avg3"]) / 2.0) # type: ignore[arg-type]
+
+
+def test_progress_bar_time_zero_steps(monkeypatch):
+ clock = _fake_clock(monkeypatch)
+ state: dict[str, object] = {}
+ clock[0] = 2_000.0
+
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ bar = _pbar(iterable=None, length=3)
+ clock[0] = 2_001.0
+ bar.make_step(0)
+ state["pos"] = bar.pos
+ state["avg"] = list(bar.avg)
+ state["tpi"] = bar.time_per_iteration
+ typer.echo("ok")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ assert state["pos"] == 0
+ assert state["avg"] == [1.0]
+ assert state["tpi"] == pytest.approx(1.0)
+
+
+def test_progress_bar_eta(monkeypatch):
+ state: dict[str, object] = {}
+
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ state["eta0"] = _pbar(iterable=[1, 2], length=None).eta
+
+ done = _pbar(iterable=None, length=5)
+ done.pos, done.finished = 2, True
+ state["eta_done"] = done.eta
+
+ fresh = _pbar(iterable=None, length=5)
+ state["eta_known_fresh"] = fresh.eta_known
+ state["fmt_eta_fresh"] = fresh.format_eta()
+
+ clock = _fake_clock(monkeypatch)
+ clock[0] = 5_000.0
+ bar = _pbar(iterable=None, length=10)
+ clock[0] = 5_001.0
+ bar.make_step(3)
+ state["pos3"] = bar.pos
+ state["eta_after"] = bar.eta
+ state["tpi"] = bar.time_per_iteration
+
+ cases_out = []
+ for t0, t1, length, n_steps, _expected_fmt, expected_eta_int in (
+ (9_000.0, 9_001.0, 10, 1, "00:00:09", None),
+ (1_000.0, 100_000.0, 2, 1, "1d 03:30:00", 99_000),
+ ):
+ clock2 = _fake_clock(monkeypatch)
+ clock2[0] = t0
+ b = _pbar(iterable=None, length=length)
+ clock2[0] = t1
+ b.make_step(n_steps)
+ cases_out.append(
+ (
+ b.eta_known,
+ b.format_eta(),
+ int(b.eta) if expected_eta_int is not None else None,
+ )
+ )
+
+ state["cases"] = cases_out
+
+ clock3 = _fake_clock(monkeypatch)
+ clock3[0] = 3_000.0
+ b2 = _pbar(iterable=None, length=2)
+ clock3[0] = 3_001.0
+ b2.make_step(1)
+ state["fmt_before_finish"] = b2.format_eta()
+ b2.finish()
+ state["fmt_after_finish"] = b2.format_eta()
+ typer.echo("ok")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ assert state["eta0"] == 0.0
+ assert state["eta_done"] == 0.0
+ assert not state["eta_known_fresh"] and state["fmt_eta_fresh"] == ""
+ assert state["pos3"] == 3
+ assert state["eta_after"] == pytest.approx(state["tpi"] * (10 - 3)) # type: ignore[operator]
+
+ (ek1, fmt1, ei1), (ek2, fmt2, ei2) = state["cases"] # type: ignore[misc]
+ assert ek1 and fmt1 == "00:00:09" and ei1 is None
+ assert ek2 and fmt2 == "1d 03:30:00" and ei2 == 99_000
+
+ assert state["fmt_before_finish"] != ""
+ assert state["fmt_after_finish"] == ""
+
+
+@pytest.mark.parametrize(
+ ("width", "fill_char", "empty_char", "expected_bar", "finished", "sample_timing"),
+ [
+ pytest.param(4, "X", "-", "XXXX", True, False, id="finished"),
+ pytest.param(4, "#", "-", "----", False, False, id="no_timing_yet"),
+ pytest.param(5, "*", ".", None, False, True, id="indeterminate"),
+ ],
+)
+def test_progress_bar_unknown_length(
+ monkeypatch,
+ width: int,
+ fill_char: str,
+ empty_char: str,
+ expected_bar: str | None,
+ finished: bool,
+ sample_timing: bool,
+):
+ clock: list[float] | None = _fake_clock(monkeypatch) if sample_timing else None
+ if clock is not None:
+ clock[0] = 100.0
+
+ state: dict[str, object] = {}
+
+ class _IterableWithoutLength:
+ def __iter__(self):
+ return iter((1, 2, 3))
+
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ bar = _pbar(
+ iterable=_IterableWithoutLength(),
+ length=None,
+ width=width,
+ fill_char=fill_char,
+ empty_char=empty_char,
+ )
+ assert bar.length is None
+
+ if sample_timing:
+ assert clock is not None
+ clock[0] = 101.0
+ bar.make_step(1)
+ assert bar.time_per_iteration > 0
+ rendered = bar.format_bar()
+ assert len(rendered) == width
+ assert rendered.count(fill_char) == 1
+ assert rendered.count(empty_char) == width - 1
+ state["branch"] = "sample_timing"
+ elif finished:
+ bar.finished = True
+ state["bar"] = bar.format_bar()
+ state["branch"] = "finished"
+ else:
+ assert bar.time_per_iteration == 0.0
+ state["bar"] = bar.format_bar()
+ state["branch"] = "no_timing"
+ typer.echo("ok")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ if sample_timing:
+ assert state["branch"] == "sample_timing"
+ else:
+ assert state["bar"] == expected_bar
diff --git a/tests/test_termui.py b/tests/test_termui.py
new file mode 100644
index 0000000000..8f908923db
--- /dev/null
+++ b/tests/test_termui.py
@@ -0,0 +1,437 @@
+"""
+Tests for the termui, echo, and CliRunner isolation functionality.
+Created after vendoring Click to ensure test coverage is back up to 100%.
+"""
+
+import io
+import os
+from contextlib import contextmanager
+from typing import Literal
+
+import pytest
+import typer
+from typer._click import _termui_impl, termui
+from typer.testing import CliRunner
+
+from tests.utils import needs_windows, skip_if_windows
+
+
+def test_raw_terminal(monkeypatch):
+ runner = CliRunner()
+ app = typer.Typer()
+ state = {"entered": 0, "exited": 0}
+
+ @contextmanager
+ def fake_raw_terminal():
+ state["entered"] += 1
+ try:
+ yield 42
+ finally:
+ state["exited"] += 1
+
+ monkeypatch.setattr(_termui_impl, "raw_terminal", fake_raw_terminal)
+
+ @app.command()
+ def main():
+ with termui.raw_terminal() as fd:
+ typer.echo(f"fd={fd}")
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ assert "fd=42" in result.stdout
+ assert state["entered"] == 1
+ assert state["exited"] == 1
+
+
+def test_getchar(monkeypatch):
+ # Cached path: call the existing _getchar directly.
+ cached_state = {"echo": None}
+
+ def cached_getchar(echo: bool) -> str:
+ cached_state["echo"] = echo
+ return "x"
+
+ monkeypatch.setattr(termui, "_getchar", cached_getchar)
+ assert termui.getchar(echo=True) == "x"
+ assert cached_state["echo"] is True
+
+ # Lazy-load path: _getchar is None, so import/cache _termui_impl.getchar.
+ lazy_state = {"calls": 0}
+
+ def lazy_getchar(echo: bool) -> str:
+ lazy_state["calls"] += 1
+ return "y" if not echo else "z"
+
+ monkeypatch.setattr(termui, "_getchar", None)
+ monkeypatch.setattr(_termui_impl, "getchar", lazy_getchar)
+
+ assert termui.getchar(echo=False) == "y"
+ assert termui._getchar is lazy_getchar
+ assert termui.getchar(echo=True) == "z"
+ assert lazy_state["calls"] == 2
+
+
+def test_clirunner_getchar(monkeypatch) -> None:
+ runner = CliRunner()
+ app = typer.Typer()
+
+ @app.command()
+ def main() -> None:
+ first = termui.getchar(echo=False)
+ second = termui.getchar(echo=True)
+ typer.echo(f"\nfirst={first};second={second}")
+
+ monkeypatch.setattr(termui, "_getchar", None)
+ result = runner.invoke(app, [], input="ab")
+ assert result.exit_code == 0, result.output
+ assert result.stdout.splitlines() == ["b", "first=a;second=b"]
+
+
+def test_clirunner_env_none(monkeypatch) -> None:
+ runner = CliRunner()
+ app = typer.Typer()
+ env_key = "TYPER_TEST_ENV_REMOVE"
+ monkeypatch.setenv(env_key, "present")
+
+ @app.command()
+ def main() -> None:
+ typer.echo(f"inside={os.environ.get(env_key)}")
+
+ result = runner.invoke(app, [], env={env_key: None})
+ assert result.exit_code == 0, result.output
+ assert "inside=None" in result.stdout
+ assert os.environ.get(env_key) == "present"
+
+
+@pytest.mark.parametrize(
+ ("exit_value", "expected_exit_code", "expected_stdout"),
+ [
+ (None, 0, ""),
+ ("bad-exit", 1, "bad-exit\n"),
+ ],
+)
+def test_clirunner_invoke_system_exit_branches(
+ exit_value: object,
+ expected_exit_code: int,
+ expected_stdout: str,
+) -> None:
+ runner = CliRunner()
+ app = typer.Typer()
+
+ @app.command()
+ def main() -> None:
+ raise SystemExit(exit_value)
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == expected_exit_code
+ assert result.stdout == expected_stdout
+ if expected_exit_code:
+ assert isinstance(result.exception, SystemExit)
+ else:
+ assert result.exception is None
+
+
+@needs_windows
+def test_termui_impl_windows_raw_terminal():
+ with _termui_impl.raw_terminal() as fd:
+ assert fd == -1
+ with termui.raw_terminal() as fd:
+ assert fd == -1
+
+
+@needs_windows
+def test_termui_impl_windows_getchar(monkeypatch):
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "a")
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwche", lambda: "b")
+ assert _termui_impl.getchar(echo=False) == "a"
+ assert _termui_impl.getchar(echo=True) == "b"
+
+ seq_null = iter(["\x00", "K"])
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: next(seq_null))
+ assert _termui_impl.getchar(echo=False) == "\x00K"
+
+ seq_e0 = iter(["\xe0", "H"])
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: next(seq_e0))
+ assert _termui_impl.getchar(echo=False) == "\xe0H"
+
+ seq_echo = iter(["\x00", "M"])
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwche", lambda: next(seq_echo))
+ assert _termui_impl.getchar(echo=True) == "\x00M"
+
+ seq_e0_echo = iter(["\xe0", "Z"])
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwche", lambda: next(seq_e0_echo))
+ assert _termui_impl.getchar(echo=True) == "\xe0Z"
+
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "\x03")
+ with pytest.raises(KeyboardInterrupt):
+ _termui_impl.getchar(echo=False)
+
+ monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "\x1a")
+ with pytest.raises(EOFError):
+ _termui_impl.getchar(echo=False)
+
+
+@skip_if_windows
+@pytest.mark.parametrize("use_stdin_tty", [True, False])
+def test_termui_impl_posix_raw_terminal(monkeypatch, use_stdin_tty: bool):
+ state: dict[str, object] = {}
+ flushed: list[None] = []
+ fake_tty = None
+
+ if use_stdin_tty:
+ expected_fd = 14
+ old_termios = "old_settings"
+ monkeypatch.setattr(
+ _termui_impl, "isatty", lambda s: s is _termui_impl.sys.stdin
+ )
+ monkeypatch.setattr(_termui_impl.sys.stdin, "fileno", lambda: expected_fd)
+ else:
+ expected_fd = 27
+ old_termios = "old"
+ monkeypatch.setattr(
+ _termui_impl,
+ "isatty",
+ lambda s: s is not _termui_impl.sys.stdin,
+ )
+
+ class FakeTTY:
+ def __init__(self) -> None:
+ self.closed = False
+
+ def fileno(self) -> int:
+ return expected_fd
+
+ def close(self) -> None:
+ self.closed = True
+
+ fake_tty = FakeTTY()
+ real_open = open
+
+ def fake_open(path, *args, **kwargs):
+ if path == "/dev/tty":
+ return fake_tty
+ return real_open(path, *args, **kwargs) # pragma: no cover
+
+ monkeypatch.setattr("builtins.open", fake_open)
+
+ def tcgetattr(fd: int) -> str:
+ state["tcgetattr_fd"] = fd
+ return old_termios
+
+ def setraw(fd: int) -> None:
+ state["setraw_fd"] = fd
+
+ def tcsetattr(fd: int, when: int, old: str) -> None:
+ state["tcsetattr"] = (fd, when, old)
+
+ monkeypatch.setattr(_termui_impl.termios, "tcgetattr", tcgetattr)
+ monkeypatch.setattr(_termui_impl.tty, "setraw", setraw)
+ monkeypatch.setattr(_termui_impl.termios, "tcsetattr", tcsetattr)
+ monkeypatch.setattr(
+ _termui_impl.sys.stdout, "flush", lambda *a, **k: flushed.append(None)
+ )
+
+ with _termui_impl.raw_terminal() as fd:
+ assert fd == expected_fd
+
+ assert state["tcgetattr_fd"] == expected_fd
+ assert state["setraw_fd"] == expected_fd
+ assert state["tcsetattr"] == (
+ expected_fd,
+ _termui_impl.termios.TCSADRAIN,
+ old_termios,
+ )
+ assert flushed == [None]
+ if fake_tty is not None:
+ assert fake_tty.closed is True
+
+
+@skip_if_windows
+def test_termui_impl_posix_getchar(monkeypatch):
+ @contextmanager
+ def fake_raw():
+ yield 7
+
+ monkeypatch.setattr(_termui_impl, "raw_terminal", fake_raw)
+ monkeypatch.setattr(_termui_impl.os, "read", lambda fd, n: b"q")
+ monkeypatch.setattr(_termui_impl, "get_best_encoding", lambda stdin: "utf-8")
+ monkeypatch.setattr(_termui_impl, "isatty", lambda f: f is _termui_impl.sys.stdout)
+ written: list[str] = []
+ monkeypatch.setattr(_termui_impl.sys.stdout, "write", lambda s: written.append(s))
+
+ assert _termui_impl.getchar(echo=True) == "q"
+ assert written == ["q"]
+
+
+@skip_if_windows
+def test_termui_impl_posix_getchar_eof(monkeypatch):
+ @contextmanager
+ def fake_raw():
+ yield 5
+
+ monkeypatch.setattr(_termui_impl, "raw_terminal", fake_raw)
+ monkeypatch.setattr(_termui_impl.os, "read", lambda fd, n: b"\x04")
+ monkeypatch.setattr(_termui_impl, "get_best_encoding", lambda stdin: "utf-8")
+ monkeypatch.setattr(_termui_impl, "isatty", lambda f: False)
+
+ with pytest.raises(EOFError):
+ _termui_impl.getchar(echo=False)
+
+
+def test_prompt():
+ runner = CliRunner()
+ app = typer.Typer()
+ fake_file = io.StringIO("data")
+ fake_file.name = "demo.txt"
+
+ @app.command()
+ def main(
+ accept: bool = typer.Option(True, prompt=True),
+ name: str = typer.Option(..., prompt=True),
+ flavor: Literal["a", "b"] = typer.Option(..., prompt=True),
+ city: str = typer.Option("London", prompt=True),
+ config: str = typer.Option(fake_file, prompt=True),
+ password: str = typer.Option(
+ ...,
+ prompt=True,
+ hide_input=True,
+ confirmation_prompt=True,
+ ),
+ ):
+ typer.echo(
+ f"accept={accept};name={name};flavor={flavor};city={city};config={config};pass_len={len(password)}"
+ )
+
+ result = runner.invoke(app, [], input="\nAda\na\n\ncustom.ini\nsecret\nsecret\n")
+ assert result.exit_code == 0, result.output
+ assert (
+ "accept=True;name=Ada;flavor=a;city=London;config=custom.ini;pass_len=6"
+ in result.stdout
+ )
+ assert "(a, b): " in result.stdout
+ assert "[demo.txt]: " in result.stdout
+
+
+def test_hidden_prompt_func(monkeypatch):
+ monkeypatch.setattr("getpass.getpass", lambda prompt: "secret")
+ assert termui.hidden_prompt_func("Password: ") == "secret"
+
+
+def test_echo_stdout_missing(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr("sys.stdout", None)
+ typer.echo("ignored")
+
+
+def test_echo_stringifies() -> None:
+ stream = io.StringIO()
+ typer.echo(123, file=stream, nl=False)
+ assert stream.getvalue() == "123"
+
+
+def test_echo_bytes() -> None:
+ buffer = io.BytesIO()
+ stream = io.TextIOWrapper(buffer, encoding="utf-8")
+ typer.echo(b"abc", file=stream, nl=True)
+ assert buffer.getvalue() == b"abc\n"
+
+
+def test_echo_empty_output() -> None:
+ class FlushTrackingTextStream(io.StringIO):
+ def __init__(self) -> None:
+ super().__init__()
+ self.flush_count = 0
+
+ def flush(self) -> None:
+ self.flush_count += 1
+ super().flush()
+
+ def write(self, s: str) -> int:
+ raise AssertionError("Empty output") # pragma: no cover
+
+ stream = FlushTrackingTextStream()
+ typer.echo("", file=stream, nl=False)
+ assert stream.flush_count == 1
+
+
+@needs_windows
+def test_echo_windows_color_none(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ class TtyStream(io.StringIO):
+ def isatty(self) -> bool:
+ return True
+
+ stream = TtyStream()
+ monkeypatch.setattr("typer._click.utils.auto_wrap_for_ansi", None)
+ typer.echo("\x1b[31mred\x1b[0m", file=stream, nl=False, color=None)
+ assert stream.getvalue() == "red"
+
+
+@pytest.mark.parametrize(
+ ("flag", "true_code", "false_code"),
+ [
+ ("bold", "\x1b[1m", "\x1b[22m"),
+ ("dim", "\x1b[2m", "\x1b[22m"),
+ ("underline", "\x1b[4m", "\x1b[24m"),
+ ("overline", "\x1b[53m", "\x1b[55m"),
+ ("italic", "\x1b[3m", "\x1b[23m"),
+ ("blink", "\x1b[5m", "\x1b[25m"),
+ ("reverse", "\x1b[7m", "\x1b[27m"),
+ ("strikethrough", "\x1b[9m", "\x1b[29m"),
+ ],
+)
+def test_style(flag, true_code, false_code):
+ runner = CliRunner()
+ app = typer.Typer()
+
+ @app.command()
+ def main():
+ # testing an int and a str on purpose
+ typer.echo("TRUE=" + typer.style("42", **{flag: True}), color=True)
+ typer.echo("FALSE=" + typer.style(666, **{flag: False}), color=True)
+
+ result = runner.invoke(app, [])
+ assert result.exit_code == 0, result.output
+ lines = [line for line in result.stdout.splitlines() if line]
+ true_line = next(line for line in lines if line.startswith("TRUE="))
+ false_line = next(line for line in lines if line.startswith("FALSE="))
+ assert "42" in true_line
+ assert "666" in false_line
+
+ assert true_code in true_line
+ assert true_code not in false_line
+ assert false_code in false_line
+ assert false_code not in true_line
+
+
+def test_style_color():
+ fg_int = typer.style("x", fg=123)
+ assert "\x1b[38;5;123m" in fg_int
+
+ bg_list = typer.style("x", bg=[1, 2, 3])
+ assert "\x1b[48;2;1;2;3m" in bg_list
+
+ with pytest.raises(TypeError, match="Unknown color"):
+ typer.style("x", fg="not-a-color")
+
+ with pytest.raises(TypeError, match="Unknown color"):
+ typer.style("x", bg="not-a-color")
+
+
+def test_termui_launch(monkeypatch):
+ captured = {}
+
+ def fake_open_url(url, wait=False, locate=False):
+ captured["url"] = url
+ captured["wait"] = wait
+ captured["locate"] = locate
+ return 7
+
+ monkeypatch.setattr(_termui_impl, "open_url", fake_open_url)
+ rv = termui.launch("https://example.com", wait=True, locate=True)
+ assert rv == 7
+ assert captured == {
+ "url": "https://example.com",
+ "wait": True,
+ "locate": True,
+ }
diff --git a/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial001.py
index 6941a35c37..32c07214c0 100644
--- a/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial001.py
+++ b/tests/test_tutorial/test_parameter_types/test_bool/test_tutorial001.py
@@ -4,6 +4,7 @@
from types import ModuleType
import pytest
+import typer
from typer.testing import CliRunner
runner = CliRunner()
@@ -22,6 +23,12 @@ def get_mod(request: pytest.FixtureRequest) -> ModuleType:
return mod
+def test_type_repr(mod: ModuleType):
+ command = typer.main.get_command(mod.app)
+ force_param = next(param for param in command.params if param.name == "force")
+ assert repr(force_param.type) == "BOOL"
+
+
def test_help(mod: ModuleType):
result = runner.invoke(mod.app, ["--help"])
assert result.exit_code == 0
diff --git a/tests/test_tutorial/test_parameter_types/test_datetime/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_datetime/test_tutorial001.py
index 9dfcdf19e3..eea63c5a8b 100644
--- a/tests/test_tutorial/test_parameter_types/test_datetime/test_tutorial001.py
+++ b/tests/test_tutorial/test_parameter_types/test_datetime/test_tutorial001.py
@@ -1,6 +1,8 @@
import subprocess
import sys
+from datetime import datetime
+import typer
from typer.testing import CliRunner
from docs_src.parameter_types.datetime import tutorial001_py310 as mod
@@ -9,6 +11,12 @@
app = mod.app
+def test_type_repr():
+ command = typer.main.get_command(app)
+ birth_param = next(param for param in command.params if param.name == "birth")
+ assert repr(birth_param.type) == "DateTime"
+
+
def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
@@ -22,6 +30,15 @@ def test_main():
assert "Birth hour: 10" in result.output
+def test_main_datetime_object():
+ result = runner.invoke(
+ app, [], default_map={"birth": datetime(1956, 1, 31, 10, 0, 0)}
+ )
+ assert result.exit_code == 0
+ assert "Interesting day to be born: 1956-01-31 10:00:00" in result.output
+ assert "Birth hour: 10" in result.output
+
+
def test_invalid():
result = runner.invoke(app, ["july-19-1989"])
assert result.exit_code != 0
diff --git a/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py
index 4b2f422fbb..ee0daa9f06 100644
--- a/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py
+++ b/tests/test_tutorial/test_parameter_types/test_index/test_tutorial001.py
@@ -1,6 +1,7 @@
import subprocess
import sys
+import typer
from typer.testing import CliRunner
from docs_src.parameter_types.index import tutorial001_py310 as mod
@@ -9,6 +10,16 @@
app = mod.app
+def test_type_repr():
+ command = typer.main.get_command(app)
+ age_param = next(param for param in command.params if param.name == "age")
+ height_meters_param = next(
+ param for param in command.params if param.name == "height_meters"
+ )
+ assert repr(age_param.type) == "INT"
+ assert repr(height_meters_param.type) == "FLOAT"
+
+
def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
diff --git a/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py
index 6b9445a97f..ffe3ad09a0 100644
--- a/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py
+++ b/tests/test_tutorial/test_parameter_types/test_number/test_tutorial001.py
@@ -24,6 +24,19 @@ def get_mod(request: pytest.FixtureRequest) -> ModuleType:
return mod
+def test_type_repr(mod: ModuleType):
+ command = typer.main.get_command(mod.app)
+
+ id_param = next(param for param in command.params if param.name == "id")
+ assert repr(id_param.type) == "
"
+
+ age_param = next(param for param in command.params if param.name == "age")
+ assert repr(age_param.type) == "=18>"
+
+ score_param = next(param for param in command.params if param.name == "score")
+ assert repr(score_param.type) == ""
+
+
def test_help(mod: ModuleType):
result = runner.invoke(mod.app, ["--help"])
assert result.exit_code == 0
diff --git a/tests/test_tutorial/test_parameter_types/test_uuid/test_tutorial001.py b/tests/test_tutorial/test_parameter_types/test_uuid/test_tutorial001.py
index cad8c69cc4..7b79e81405 100644
--- a/tests/test_tutorial/test_parameter_types/test_uuid/test_tutorial001.py
+++ b/tests/test_tutorial/test_parameter_types/test_uuid/test_tutorial001.py
@@ -1,6 +1,8 @@
import subprocess
import sys
+import uuid
+import typer
from typer.testing import CliRunner
from docs_src.parameter_types.uuid import tutorial001_py310 as mod
@@ -9,6 +11,12 @@
app = mod.app
+def test_type_repr():
+ command = typer.main.get_command(app)
+ user_id_param = next(param for param in command.params if param.name == "user_id")
+ assert repr(user_id_param.type) == "UUID"
+
+
def test_main():
result = runner.invoke(app, ["d48edaa6-871a-4082-a196-4daab372d4a1"])
assert result.exit_code == 0
@@ -16,6 +24,14 @@ def test_main():
assert "UUID version is: 4" in result.output
+def test_main_with_uuid_object():
+ user_id = uuid.UUID("d48edaa6-871a-4082-a196-4daab372d4a1")
+ result = runner.invoke(app, [], default_map={"user_id": user_id})
+ assert result.exit_code == 0
+ assert "USER_ID is d48edaa6-871a-4082-a196-4daab372d4a1" in result.output
+ assert "UUID version is: 4" in result.output
+
+
def test_invalid_uuid():
result = runner.invoke(app, ["7479706572-72756c6573"])
assert result.exit_code != 0
diff --git a/tests/test_type_conversion.py b/tests/test_type_conversion.py
index 83edd1ecb5..26709db0e2 100644
--- a/tests/test_type_conversion.py
+++ b/tests/test_type_conversion.py
@@ -1,12 +1,15 @@
+import os
from enum import Enum
from pathlib import Path
from typing import Any
-import click
import pytest
import typer
+from typer import _click, models
from typer.testing import CliRunner
+from tests.utils import needs_linux, needs_windows
+
runner = CliRunner()
@@ -133,6 +136,18 @@ def tuple_recursive_conversion(container: type_annotation):
assert result.exit_code == 0
+def test_tuple_wrong_arity():
+ app = typer.Typer()
+
+ @app.command()
+ def tuple_arity(value: tuple[str, str] = typer.Option(...)):
+ print(value) # pragma: no cover
+
+ result = runner.invoke(app, [], default_map={"value": ("only-one",)})
+ assert result.exit_code == 2
+ assert "2 values are required, but 1 given." in result.output
+
+
def test_custom_parse():
app = typer.Typer()
@@ -146,15 +161,29 @@ def custom_parser(
assert result.exit_code == 0
+def test_custom_parse_value_error():
+ app = typer.Typer()
+
+ @app.command()
+ def custom_parser(
+ hex_value: int = typer.Argument(None, parser=lambda x: int(x, 0)),
+ ):
+ print(hex_value) # pragma: no cover
+
+ result = runner.invoke(app, ["not-a-hex"])
+ assert result.exit_code == 2
+ assert "Invalid value" in result.output
+
+
def test_custom_click_type():
- class BaseNumberParamType(click.ParamType):
+ class BaseNumberParamType(_click.types.ParamType):
name = "base_integer"
def convert(
self,
value: Any,
- param: click.Parameter | None,
- ctx: click.Context | None,
+ param: _click.Parameter | None,
+ ctx: _click.Context | None,
) -> Any:
return int(value, 0)
@@ -168,3 +197,204 @@ def custom_click_type(
result = runner.invoke(app, ["0x56"])
assert result.exit_code == 0
+
+
+def test_int_range_open_bound_clamp():
+ app = typer.Typer()
+
+ @app.command()
+ def custom_click_type(
+ value: int = typer.Argument(
+ ...,
+ click_type=_click.types.IntRange(min=1, min_open=True, clamp=True),
+ ),
+ ):
+ print(value)
+
+ result = runner.invoke(app, ["1"])
+ assert result.exit_code == 0
+ assert "2" in result.output
+
+
+def test_bool_convert_invalid():
+ app = typer.Typer()
+
+ @app.command()
+ def main(value: bool):
+ print(value) # pragma: no cover
+
+ result = runner.invoke(app, ["maybe"])
+ assert result.exit_code == 2
+ assert "is not a valid boolean" in result.output
+ assert "yes" in result.output
+ assert "false" in result.output
+
+
+@pytest.mark.parametrize(
+ ("arg_enc", "system_enc", "raw_value", "expected_output"),
+ [
+ pytest.param("latin-1", "utf-8", b"\xff", "ÿ"),
+ pytest.param("ascii", "latin-1", b"\xff", "ÿ"),
+ pytest.param("ascii", "utf-16", b"\xff", "�"),
+ pytest.param("ascii", "ascii", b"\xff", "�"),
+ ],
+)
+def test_string_param_type_converts_bytes(
+ monkeypatch: pytest.MonkeyPatch,
+ arg_enc: str,
+ system_enc: str,
+ raw_value: bytes,
+ expected_output: str,
+):
+ app = typer.Typer()
+
+ @app.command()
+ def show(name: str = typer.Option(...)):
+ print(name)
+
+ command = typer.main.get_command(app)
+ name_param = next(param for param in command.params if param.name == "name")
+ assert repr(name_param.type) == "STRING"
+
+ monkeypatch.setattr(_click.types, "_get_argv_encoding", lambda: arg_enc)
+ monkeypatch.setattr(_click.types.sys, "getfilesystemencoding", lambda: system_enc)
+
+ result = runner.invoke(app, [], default_map={"name": raw_value})
+ assert result.exit_code == 0
+ assert expected_output in result.output
+
+
+@pytest.mark.parametrize("path_type", [str, bytes, Path])
+def test_path_coerced(path_type) -> None:
+ # Ensure coerce_path_result works correctly
+ app = typer.Typer()
+
+ @app.command()
+ def show(path: Any = typer.Option(..., path_type=path_type)):
+ print(path)
+
+ result = runner.invoke(app, ["--path", "dir/my_awesome_file.txt"])
+ assert result.exit_code == 0
+ assert "my_awesome_file" in result.output
+
+
+@pytest.mark.parametrize(
+ ("create_file", "option_kwargs", "deny_mode", "expected_error"),
+ [
+ (True, {"file_okay": False, "dir_okay": True}, None, "is a file"),
+ (False, {"file_okay": True, "dir_okay": False}, None, "is a directory"),
+ (True, {"readable": True}, os.R_OK, "is not readable"),
+ (True, {"readable": False, "writable": True}, os.W_OK, "is not writable"),
+ ],
+)
+def test_path_convert_failures(
+ monkeypatch: pytest.MonkeyPatch,
+ tmp_path: Path,
+ create_file: bool,
+ option_kwargs: dict[str, bool],
+ deny_mode: int | None,
+ expected_error: str,
+) -> None:
+ app = typer.Typer()
+
+ @app.command()
+ def show(path: Path = typer.Option(..., **option_kwargs)):
+ print(path) # pragma: no cover
+
+ if deny_mode is not None:
+ original_access = os.access
+
+ def fake_access(path: str, mode: int) -> bool:
+ if mode == deny_mode:
+ return False
+ return original_access(path, mode) # pragma: no cover
+
+ monkeypatch.setattr(models.os, "access", fake_access)
+
+ path = tmp_path / "some_path"
+ if create_file:
+ path.write_text("hello")
+ else:
+ path.mkdir()
+ result = runner.invoke(app, ["--path", str(path)])
+
+ assert result.exit_code != 0
+ assert expected_error in result.output
+
+
+def test_convert_type():
+ from typer._click.types import convert_type
+
+ # str
+ assert convert_type(str) is _click.types.STRING
+ assert convert_type(None) is _click.types.STRING
+ assert convert_type(None, default=["a"]) is _click.types.STRING
+
+ # tuples
+ tuple_type = convert_type((str, int))
+ assert isinstance(tuple_type, _click.types.Tuple)
+ assert [type(item) for item in tuple_type.types] == [
+ type(_click.types.STRING),
+ type(_click.types.INT),
+ ]
+
+ guessed_tuple = convert_type(None, default=[(1, "x")])
+ assert isinstance(guessed_tuple, _click.types.Tuple)
+ assert [type(item) for item in guessed_tuple.types] == [
+ type(_click.types.INT),
+ type(_click.types.STRING),
+ ]
+
+ # numbers
+ assert convert_type(int) is _click.types.INT
+ assert convert_type(float) is _click.types.FLOAT
+ assert convert_type(bool) is _click.types.BOOL
+
+ param_type = _click.types.IntRange(min=0, max=10)
+ assert convert_type(param_type) is param_type
+
+ guessed_int = convert_type(None, default=42)
+ assert guessed_int is _click.types.INT
+
+ # custom type
+ class CustomType:
+ pass
+
+ guessed_unknown = convert_type(None, default=CustomType())
+ assert guessed_unknown is _click.types.STRING
+
+ func_type = convert_type(CustomType)
+ assert isinstance(func_type, _click.types.FuncParamType)
+ assert func_type.name == "CustomType"
+
+
+@pytest.mark.parametrize(
+ ("platform_case", "stdin_encoding", "filesystem_encoding"),
+ [
+ pytest.param("windows", None, "utf-8", marks=needs_windows),
+ pytest.param("linux", "latin-1", "utf-8", marks=needs_linux),
+ pytest.param("linux", None, "latin-1", marks=needs_linux),
+ ],
+)
+def test_argv_encoding(
+ monkeypatch: pytest.MonkeyPatch,
+ platform_case: str,
+ stdin_encoding: str | None,
+ filesystem_encoding: str,
+) -> None:
+ sys = _click._compat.sys
+ if platform_case == "windows":
+ import locale
+
+ monkeypatch.setattr(locale, "getpreferredencoding", lambda: "latin-1")
+ else:
+
+ class FakeStdin:
+ def __init__(self, encoding: str | None) -> None:
+ self.encoding = encoding
+
+ monkeypatch.setattr(sys, "stdin", FakeStdin(stdin_encoding))
+ monkeypatch.setattr(sys, "getfilesystemencoding", lambda: filesystem_encoding)
+
+ converted = _click.types.STRING.convert(b"\xff", None, None)
+ assert converted == "ÿ"
diff --git a/tests/test_types.py b/tests/test_types.py
index adb100eb82..caeef451aa 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1,6 +1,8 @@
from enum import Enum
+import pytest
import typer
+from typer import _click
from typer.testing import CliRunner
app = typer.Typer(context_settings={"token_normalize_func": str.lower})
@@ -12,23 +14,108 @@ class User(str, Enum):
@app.command()
-def hello(name: User = User.rick) -> None:
+def hello_option(name: User = User.rick) -> None:
print(f"Hello {name.value}!")
+@app.command()
+def hello_argument(name: User) -> None:
+ print(f"Hello {name.value}!")
+
+
+@app.command()
+def hello_no_choices(
+ name: User = typer.Option(..., "--name", show_choices=False),
+):
+ print(f"Hello {name.value}!")
+
+
+@app.command()
+def hello_all(names: list[str] = typer.Argument(["World"], envvar="NAMES")) -> None:
+ for name in names:
+ print(f"Hello {name}!")
+
+
+@app.command()
+def split_variadic_and_pair(items: list[str], pair: tuple[str, str]) -> None:
+ print(f"items={items}")
+ print(f"pair={pair}")
+
+
runner = CliRunner()
def test_enum_choice() -> None:
- # This test is only for coverage of the new custom TyperChoice class
- result = runner.invoke(app, ["--name", "morty"], catch_exceptions=False)
+ result = runner.invoke(
+ app, ["hello-option", "--name", "morty"], catch_exceptions=False
+ )
assert result.exit_code == 0
assert "Hello Morty!" in result.output
- result = runner.invoke(app, ["--name", "Rick"])
+ result = runner.invoke(app, ["hello-option", "--name", "Rick"])
+ assert result.exit_code == 0
+ assert "Hello Rick!" in result.output
+
+ result = runner.invoke(app, ["hello-option", "--name", "RICK"])
assert result.exit_code == 0
assert "Hello Rick!" in result.output
- result = runner.invoke(app, ["--name", "RICK"])
+ result = runner.invoke(app, ["hello-no-choices", "--name", "RICK"])
assert result.exit_code == 0
assert "Hello Rick!" in result.output
+
+ result = runner.invoke(app, ["hello-argument", "RICK"])
+ assert result.exit_code == 0
+ assert "Hello Rick!" in result.output
+
+
+def test_enum_choice_repr() -> None:
+ root_command = typer.main.get_command(app)
+ command = root_command.commands["hello-option"]
+ name_param = next(param for param in command.params if param.name == "name")
+ assert repr(name_param.type).startswith("Choice([")
+
+
+def test_enum_choice_help() -> None:
+ result = runner.invoke(app, ["hello-argument", "--help"])
+ assert result.exit_code == 0
+ assert "{rick|morty}" in result.output
+
+ result = runner.invoke(app, ["hello-option", "--help"])
+ assert result.exit_code == 0
+ assert "[rick|morty]" in result.output
+
+ result = runner.invoke(app, ["hello-no-choices", "--help"])
+ assert result.exit_code == 0
+ assert "--name" in result.output
+ assert "rick|morty" not in result.output
+
+
+def test_enum_choice_missing_message() -> None:
+ result = runner.invoke(app, ["hello-argument"])
+ assert result.exit_code != 0
+ assert "Missing argument" in result.output
+ assert "Choose from:" in result.output
+ assert "rick" in result.output
+ assert "morty" in result.output
+
+
+def test_split_envvar_value(monkeypatch) -> None:
+ # This will use split_envvar_value to produce two strings from the envvar
+ monkeypatch.setenv("NAMES", "Rick Morty")
+ result = runner.invoke(app, ["hello-all"])
+ assert result.exit_code == 0
+ assert "Hello Rick!" in result.output
+ assert "Hello Morty!" in result.output
+
+
+def test_list_pair() -> None:
+ result = runner.invoke(app, ["split-variadic-and-pair", "a", "b", "c", "x", "y"])
+ assert result.exit_code == 0
+ assert "items=['a', 'b', 'c']" in result.output
+ assert "pair=('x', 'y')" in result.output
+
+
+def test_float_range_open_bounds_with_clamp_not_allowed():
+ with pytest.raises(TypeError, match="Clamping is not supported for open bounds."):
+ _click.types.FloatRange(min=0.0, min_open=True, clamp=True)
diff --git a/tests/test_types_file.py b/tests/test_types_file.py
new file mode 100644
index 0000000000..e67c9b34ab
--- /dev/null
+++ b/tests/test_types_file.py
@@ -0,0 +1,384 @@
+import subprocess
+import sys
+from io import BytesIO, StringIO
+from pathlib import Path
+
+import pytest
+import typer
+from typer._click._compat import get_best_encoding, should_strip_ansi
+from typer._click.utils import PacifyFlushWrapper
+from typer.testing import CliRunner
+
+from tests.utils import needs_linux, needs_windows
+
+app = typer.Typer()
+
+
+@app.command()
+def read_text(file_in: typer.FileText = typer.Option(..., lazy=True)):
+ data = file_in.read()
+ typer.echo(f"text-len={len(data)}")
+
+
+@app.command()
+def write_text(file_out: typer.FileTextWrite = typer.Option(..., lazy=None)):
+ file_out.write("This is a single line\n")
+ typer.echo("1 line written")
+
+
+@app.command()
+def write_lazy(file_out: typer.FileTextWrite = typer.Option(..., lazy=True)):
+ file_out.write("This is a single lazy line\n")
+ typer.echo("1 line written")
+
+
+@app.command()
+def probe_lazy_file_behaviors(
+ file_in: typer.FileText = typer.Option(..., lazy=True),
+ file_out: typer.FileTextWrite = typer.Option(..., lazy=True),
+):
+ typer.echo(f"repr-before={repr(file_out)}")
+ file_out.write("repr-opened\n")
+ typer.echo(f"repr-after={repr(file_out)}")
+ with file_in as stream:
+ typer.echo(f"context-len={len(stream.read())}")
+ stream.seek(0)
+ first_line = next(iter(stream), "")
+ typer.echo(f"first-line={first_line.rstrip()}")
+
+
+@app.command()
+def write_binary(file_out: typer.FileBinaryWrite = typer.Option(...)):
+ file_out.write(b"binary-written\n")
+
+
+@app.command()
+def write_binary_stderr():
+ stream = typer.get_binary_stream("stderr")
+ stream.write(b"binary-stderr\n")
+ stream.flush()
+
+
+@app.command()
+def read_binary(file_in: typer.FileBinaryRead = typer.Option(...)):
+ data = file_in.read()
+ typer.echo(f"binary-len={len(data)}")
+
+
+runner = CliRunner()
+
+
+def test_text_stdin_dash() -> None:
+ result = runner.invoke(app, ["read-text", "--file-in=-"], input="hello\n")
+ assert result.exit_code == 0
+ assert "text-len=6" in result.output
+
+
+def test_lazy_file(tmp_path: Path) -> None:
+ # dash: written to stdout
+ result = runner.invoke(app, ["write-text", "--file-out=-"])
+ assert result.exit_code == 0
+ assert "This is a single line" in result.output
+ assert "1 line written" in result.output
+
+ # lazy + file
+ file_path = tmp_path / "example.txt"
+ result = runner.invoke(app, ["write-lazy", f"--file-out={file_path}"])
+ assert result.exit_code == 0
+ assert "This is a single lazy line" not in result.output
+ assert "1 line written" in result.output
+ assert file_path.exists()
+ assert file_path.read_text() == "This is a single lazy line\n"
+
+ # lazy probe: unopened/opened repr, context manager, and iteration.
+ result = runner.invoke(
+ app,
+ [
+ "probe-lazy-file-behaviors",
+ f"--file-in={file_path}",
+ f"--file-out={tmp_path / 'repr-opened.txt'}",
+ ],
+ )
+ assert result.exit_code == 0
+ assert "repr-before= None:
+ stream = StringIO()
+ result = runner.invoke(
+ app, ["write-text"], default_map={"write-text": {"file_out": stream}}
+ )
+ assert result.exit_code == 0
+ assert "1 line written" in result.output
+ assert stream.getvalue() == "This is a single line\n"
+
+
+def test_binary_dash() -> None:
+ result = runner.invoke(app, ["write-binary", "--file-out=-"])
+ assert result.exit_code == 0
+ assert result.stdout_bytes == b"binary-written\n"
+
+ result = runner.invoke(
+ app, ["read-binary", "--file-in=-"], input=b"\x00\x01\x02abc"
+ )
+ assert result.exit_code == 0
+ assert "binary-len=6" in result.output
+
+
+def test_binary_stderr() -> None:
+ result = subprocess.run(
+ [
+ sys.executable,
+ "-c",
+ "from tests.test_types_file import app; app()",
+ "write-binary-stderr",
+ ],
+ capture_output=True,
+ )
+ assert result.returncode == 0
+ assert result.stderr == b"binary-stderr\n"
+
+
+@pytest.mark.parametrize(
+ ("errors_arg", "expected_errors"),
+ [
+ (None, "replace"),
+ ("strict", "strict"),
+ ],
+)
+def test_get_text_stream_errors(
+ monkeypatch,
+ errors_arg: str | None,
+ expected_errors: str,
+) -> None:
+ class BinaryStdout(BytesIO):
+ pass
+
+ binary_stdout = BinaryStdout()
+ monkeypatch.setattr(sys, "stdout", binary_stdout)
+
+ text_stream = typer.get_text_stream("stdout", encoding=None, errors=errors_arg)
+ text_stream.write("stream-text")
+ text_stream.flush()
+
+ assert text_stream.errors == expected_errors
+ assert text_stream.writable() is True
+ assert binary_stdout.getvalue() == b"stream-text"
+
+
+def test_get_best_encoding() -> None:
+ """Test that ASCII is being transformed into UTF-8"""
+
+ class AsciiStream:
+ encoding = "ascii"
+
+ class Utf8Stream:
+ encoding = "utf-8"
+
+ class UnknownStream:
+ encoding = "unknown"
+
+ assert get_best_encoding(AsciiStream()) == "utf-8"
+ assert get_best_encoding(Utf8Stream()) == "utf-8"
+ assert get_best_encoding(UnknownStream()) == "unknown"
+
+
+def test_pacify_flush_wrapper() -> None:
+ class Wrapped:
+ def __init__(self) -> None:
+ self.name = "wrapped-stream"
+
+ def flush(self) -> None:
+ return None # pragma: no cover
+
+ wrapped = PacifyFlushWrapper(Wrapped())
+ assert wrapped.name == "wrapped-stream"
+
+
+def test_text_stream_isatty(monkeypatch) -> None:
+ class BinaryStdout(BytesIO):
+ def isatty(self) -> bool:
+ return True
+
+ binary_stdout = BinaryStdout()
+ monkeypatch.setattr(sys, "stdout", binary_stdout)
+ text_stream = typer.get_text_stream("stdout", encoding="utf-8", errors=None)
+ assert text_stream.isatty() is True
+
+
+def test_text_stream_buffer_read1(monkeypatch) -> None:
+ class BinaryStdinNoRead1:
+ def __init__(self, data: bytes) -> None:
+ self._data = data
+ self._pos = 0
+
+ def read(self, size: int = -1) -> bytes:
+ if size < 0:
+ size = len(self._data) - self._pos # pragma: no cover
+ chunk = self._data[self._pos : self._pos + size]
+ self._pos += len(chunk)
+ return chunk
+
+ binary_stdin = BinaryStdinNoRead1(b"hello")
+ monkeypatch.setattr(sys, "stdin", binary_stdin)
+ text_stream = typer.get_text_stream("stdin", encoding="utf-8", errors=None)
+ assert text_stream._stream.read1(4) == b"hell"
+
+
+def test_binary_stream(monkeypatch) -> None:
+ binary_stdin = BytesIO(b"hello")
+ binary_stdout = BytesIO()
+ monkeypatch.setattr(sys, "stdin", binary_stdin)
+ monkeypatch.setattr(sys, "stdout", binary_stdout)
+
+ assert typer.get_binary_stream("stdin") is binary_stdin
+ assert typer.get_binary_stream("stdout") is binary_stdout
+
+
+def test_binary_stream_raises(monkeypatch) -> None:
+ class TextOnlyStdin:
+ def read(self, n: int = -1) -> str:
+ return "hello"
+
+ monkeypatch.setattr(sys, "stdin", TextOnlyStdin())
+ with pytest.raises(RuntimeError, match="Was not able to determine binary stream"):
+ typer.get_binary_stream("stdin")
+
+
+def test_stream_unknown() -> None:
+ with pytest.raises(TypeError, match="Unknown standard stream 'Plumbus'"):
+ typer.get_binary_stream("Plumbus") # type: ignore[arg-type]
+
+ with pytest.raises(TypeError, match="Unknown standard stream 'Fleeb'"):
+ typer.get_text_stream("Fleeb") # type: ignore[arg-type]
+
+
+def test_format_filename() -> None:
+ filename = b"folder/subdir/demo.txt"
+ assert typer.format_filename(filename, shorten=True) == "demo.txt"
+
+
+def test_file_error(monkeypatch, tmp_path: Path) -> None:
+ file_path = tmp_path / "cannot-open.txt"
+
+ def fake_open(path, *args, **kwargs):
+ if Path(path) == file_path:
+ raise OSError()
+
+ monkeypatch.setattr("builtins.open", fake_open)
+ result = runner.invoke(app, ["write-text", f"--file-out={file_path}"])
+ assert result.exit_code == 1
+ assert "Could not open file" in result.output
+ assert "cannot-open.txt" in result.output
+ assert "unknown error" in result.output
+
+
+@needs_windows
+def test_app_dir_windows_fallback(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("APPDATA", raising=False)
+ monkeypatch.setattr("os.path.expanduser", lambda _path: r"C:\Users\Tester")
+
+ assert typer.get_app_dir("My App", roaming=True) == r"C:\Users\Tester\My App"
+
+
+@needs_linux
+def test_app_dir_force_posix(monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr("os.path.expanduser", lambda _path: "/home/tester/.my-app")
+
+ assert typer.get_app_dir("My App", force_posix=True) == "/home/tester/.my-app"
+
+
+def test_text_stream_binary_buffer(monkeypatch) -> None:
+ class TextStdinWithBinaryBuffer:
+ def __init__(self, data: bytes) -> None:
+ self.buffer = BytesIO(data)
+ self.encoding = "latin-1"
+
+ def read(self, n: int = -1) -> str:
+ raise OSError("text stream is not readable directly")
+
+ class TextStdoutWithBinaryBuffer:
+ def __init__(self) -> None:
+ self.buffer = BytesIO()
+ self.encoding = "latin-1"
+
+ def write(self, s: str) -> int:
+ raise OSError("text stream is not writable directly")
+
+ stdin = TextStdinWithBinaryBuffer(b"hello")
+ stdout = TextStdoutWithBinaryBuffer()
+
+ monkeypatch.setattr(sys, "stdin", stdin)
+ monkeypatch.setattr(sys, "stdout", stdout)
+
+ text_stdin = typer.get_text_stream("stdin", encoding="utf-8", errors=None)
+ text_stdout = typer.get_text_stream("stdout", encoding="utf-8", errors=None)
+
+ assert text_stdin.read() == "hello"
+ text_stdout.write("ok")
+ text_stdout.flush()
+ assert stdout.buffer.getvalue() == b"ok"
+
+
+def test_text_stream_binary_stream(monkeypatch) -> None:
+ binary_stdout = BytesIO()
+ monkeypatch.setattr(sys, "stdout", binary_stdout)
+ text_stream = typer.get_text_stream("stdout", encoding="utf-8", errors=None)
+ text_stream.write("ok")
+ text_stream.flush()
+ assert binary_stdout.getvalue() == b"ok"
+
+
+def test_text_stream_stdout_no_binary(
+ monkeypatch,
+) -> None:
+ class TextStdoutNoBinaryFallback:
+ encoding = "utf-8"
+ errors = "strict"
+
+ def write(self, s: str) -> int:
+ if isinstance(s, bytes):
+ raise TypeError("bytes not supported")
+ return len(s)
+
+ stdout = TextStdoutNoBinaryFallback()
+ monkeypatch.setattr(sys, "stdout", stdout)
+ text_stream = typer.get_text_stream("stdout", encoding="utf-8", errors="replace")
+ assert text_stream is stdout
+
+
+def test_jupyter_wrapped_stream(monkeypatch) -> None:
+ class JupyterLikeStdout(BytesIO):
+ __module__ = "ipykernel.iostream"
+
+ def isatty(self) -> bool:
+ return False
+
+ binary_stdout = JupyterLikeStdout()
+ monkeypatch.setattr(sys, "stdout", binary_stdout)
+ text_stream = typer.get_text_stream("stdout", encoding="utf-8", errors=None)
+ assert should_strip_ansi(text_stream, color=None) is False
+
+
+def test_should_strip_ansi(monkeypatch) -> None:
+ class NonTtyStdin(BytesIO):
+ def isatty(self) -> bool:
+ return False
+
+ stdin = NonTtyStdin()
+ monkeypatch.setattr(sys, "stdin", stdin)
+ assert should_strip_ansi(stream=None, color=None) is True
+ assert should_strip_ansi(stream=None, color=True) is False
+ assert should_strip_ansi(stream=None, color=False) is True
diff --git a/tests/test_win_console.py b/tests/test_win_console.py
new file mode 100644
index 0000000000..13c390cfe2
--- /dev/null
+++ b/tests/test_win_console.py
@@ -0,0 +1,326 @@
+"""
+Tests for the Windows console functionality.
+Created after vendoring Click to ensure test coverage is back up to 100%.
+"""
+
+import ctypes
+import io
+import sys
+
+import pytest
+import typer
+from typer.testing import CliRunner
+
+from .utils import needs_windows
+
+pytestmark = needs_windows
+
+
+if sys.platform == "win32":
+ from typer._click import _compat, _winconsole
+
+
+def _identity_buffer(obj, writable=False): # noqa: ARG001
+ return obj
+
+
+def _route_console_stream(target_name, wrapper, state=None):
+ def patched_windows_stream(stream, encoding, errors): # noqa: ARG001
+ current_target = getattr(sys, target_name)
+ if stream is current_target:
+ if state is not None and target_name == "stderr":
+ state["stderr_wrap_calls"] += 1
+ buffer = getattr(stream, "buffer", None)
+ return wrapper(buffer) if buffer else None
+ return None
+
+ return patched_windows_stream
+
+
+def _capture_write_console(state):
+ def fake_write_console(handle, buffer, units_to_write, units_written_ptr, reserved): # noqa: ARG001
+ state["write_calls"] += 1
+ bytes_to_write = units_to_write * 2
+ state["written"].extend(buffer[:bytes_to_write])
+ units_written_ptr._obj.value = units_to_write
+ return 1
+
+ return fake_write_console
+
+
+def test_winconsole_stdin(monkeypatch):
+ runner = CliRunner()
+ app = typer.Typer()
+
+ @app.command()
+ def read_name(config: typer.FileText = typer.Option(...)) -> None:
+ name = config.readline().strip()
+ typer.echo(f"Hello {name}")
+
+ utf16_data = bytearray("Rick\r\n".encode("utf-16-le"))
+ state = {"pos": 0, "read_calls": 0}
+
+ def fake_read_console(handle, buffer, units_to_read, units_read_ptr, reserved): # noqa: ARG001
+ state["read_calls"] += 1
+ max_bytes = units_to_read * 2
+ chunk = utf16_data[state["pos"] : state["pos"] + max_bytes]
+ if chunk:
+ buffer[0 : len(chunk)] = chunk
+ state["pos"] += len(chunk)
+ units_read_ptr._obj.value = len(chunk) // 2
+ return 1
+
+ return 1 # pragma: no cover
+
+ monkeypatch.setattr(_winconsole, "get_buffer", _identity_buffer)
+ monkeypatch.setattr(_winconsole, "ReadConsoleW", fake_read_console)
+ monkeypatch.setattr(_winconsole, "GetLastError", lambda: 0)
+ monkeypatch.setattr(
+ _compat,
+ "_get_windows_console_stream",
+ _route_console_stream("stdin", _winconsole._get_text_stdin),
+ )
+
+ result = runner.invoke(app, ["--config", "-"])
+ assert result.exit_code == 0, result.output
+ assert "Hello Rick" in result.stdout
+ assert state["read_calls"] > 0
+
+
+def test_winconsole_stdout(monkeypatch):
+ runner = CliRunner()
+ app = typer.Typer()
+ state = {"write_calls": 0, "written": bytearray()}
+
+ @app.command()
+ def write_message(out: typer.FileTextWrite = typer.Option(...)) -> None:
+ out.write("Hello Summer\n")
+
+ monkeypatch.setattr(_winconsole, "get_buffer", _identity_buffer)
+ monkeypatch.setattr(_winconsole, "WriteConsoleW", _capture_write_console(state))
+ monkeypatch.setattr(_winconsole, "GetLastError", lambda: 0)
+ monkeypatch.setattr(
+ _compat,
+ "_get_windows_console_stream",
+ _route_console_stream("stdout", _winconsole._get_text_stdout),
+ )
+
+ result = runner.invoke(app, ["--out", "-"])
+ assert result.exit_code == 0, result.output
+ assert state["write_calls"] > 0
+ assert _winconsole._WindowsConsoleWriter(1).isatty() is True
+ decoded = state["written"].decode("utf-16-le", errors="ignore")
+ assert "Hello Summer\r\n" in decoded
+
+
+def test_winconsole_stderr(monkeypatch):
+ runner = CliRunner()
+ app = typer.Typer()
+ state = {"write_calls": 0, "written": bytearray(), "stderr_wrap_calls": 0}
+
+ @app.command()
+ def main() -> None:
+ typer.echo("Ran out of adventure time!", err=True)
+
+ monkeypatch.setattr(_winconsole, "get_buffer", _identity_buffer)
+ monkeypatch.setattr(_winconsole, "WriteConsoleW", _capture_write_console(state))
+ monkeypatch.setattr(_winconsole, "GetLastError", lambda: 0)
+ monkeypatch.setattr(
+ _compat,
+ "_get_windows_console_stream",
+ _route_console_stream("stderr", _winconsole._get_text_stderr, state),
+ )
+
+ result = runner.invoke(app)
+ assert result.exit_code == 0, result.output
+ assert state["stderr_wrap_calls"] > 0
+ assert state["write_calls"] > 0
+ decoded = state["written"].decode("utf-16-le", errors="ignore")
+ assert "Ran out of adventure time!\r\n" in decoded
+
+
+@pytest.mark.parametrize(
+ ("writable", "source", "expected_flags"),
+ [
+ (True, bytearray(b"python"), 1), # PyBUF_WRITABLE
+ (False, b"python", 0), # PyBUF_SIMPLE
+ ],
+)
+def test_get_buffer(monkeypatch, writable, source, expected_flags):
+ state = {"flags": None, "released": 0}
+ if writable:
+ backing = (_winconsole.c_char * len(source)).from_buffer(source)
+ else:
+ backing = (_winconsole.c_char * len(source)).from_buffer_copy(source)
+ backing_ptr = _winconsole.c_void_p(ctypes.addressof(backing))
+
+ def fake_object_get_buffer(obj, buf_ref, flags): # noqa: ARG001
+ state["flags"] = flags
+ buf = buf_ref._obj
+ buf.buf = backing_ptr
+ buf.len = len(source)
+
+ def fake_buffer_release(buf_ref): # noqa: ARG001
+ state["released"] += 1
+
+ monkeypatch.setattr(_winconsole, "PyObject_GetBuffer", fake_object_get_buffer)
+ monkeypatch.setattr(_winconsole, "PyBuffer_Release", fake_buffer_release)
+
+ probe = source if writable else b"x"
+ out = _winconsole.get_buffer(probe, writable=writable)
+ if writable:
+ # mutate the first byte of "python" to obtain another beloved programming language
+ out[0] = b"c"
+ assert source == bytearray(b"cython")
+ else:
+ assert bytes(out[: len(source)]) == source
+ assert state["flags"] == expected_flags
+ assert state["released"] == 1
+
+
+def test_isatty():
+ assert _winconsole._WindowsConsoleRawIOBase(None).isatty() is True
+ assert _winconsole._WindowsConsoleReader(0).isatty() is True
+ assert _winconsole._WindowsConsoleReader(1).isatty() is True
+
+
+def test_console_stream():
+ class NamedBytesIO(io.BytesIO):
+ name = "fake-buffer"
+
+ def isatty(self):
+ return False
+
+ stream = _winconsole.ConsoleStream(
+ io.TextIOWrapper(io.BytesIO(), encoding="utf-8"), NamedBytesIO()
+ )
+ assert stream.isatty() is False
+ assert stream.name == "fake-buffer"
+ assert "fake-buffer" in repr(stream)
+ assert "utf-8" in repr(stream)
+
+ # test writelines
+ stream.writelines(["hello", " ", "world"])
+ stream._text_stream.flush()
+ assert stream._text_stream.buffer.getvalue().decode("utf-8") == "hello world"
+
+ # Cover bytes write path.
+ assert stream.write(b"!") == 1
+ assert stream.buffer.getvalue().endswith(b"!")
+
+
+@pytest.mark.parametrize(
+ ("error", "msg"),
+ [
+ (0, "ERROR_SUCCESS"), # ERROR_SUCCESS
+ (8, "ERROR_NOT_ENOUGH_MEMORY"), # ERROR_NOT_ENOUGH_MEMORY
+ (342, "Windows error 342"),
+ ],
+)
+def test_error_message(error, msg):
+ writer = _winconsole._WindowsConsoleWriter
+ assert writer._get_error_message(error) == msg
+
+
+def test_is_console():
+ assert _winconsole._is_console(object()) is False
+
+
+def test_get_windows_console_stream_factory_and_buffer_paths(monkeypatch):
+ monkeypatch.setattr(_winconsole, "_is_console", lambda f: True)
+ monkeypatch.setattr(_winconsole, "get_buffer", object())
+
+ class FakeStream:
+ def __init__(self, fd, buffer=None):
+ self._fd = fd
+ self.buffer = buffer
+
+ def fileno(self):
+ return self._fd
+
+ wrapped = {"called": False, "buffer": None}
+
+ def fake_factory(buffer):
+ wrapped["called"] = True
+ wrapped["buffer"] = buffer
+ return "wrapped", buffer
+
+ monkeypatch.setattr(_winconsole, "_stream_factories", {7: fake_factory})
+
+ # Known console stream preconditions pass, but no stream factory for this fd.
+ get_stream = _winconsole._get_windows_console_stream
+ assert get_stream(FakeStream(99, object()), "utf-16-le", "strict") is None
+
+ # Factory exists, but stream has no usable .buffer.
+ assert get_stream(FakeStream(7, None), "utf-16-le", "strict") is None
+
+ # Factory exists and buffer is present, so wrapper result is returned.
+ raw_buffer = object()
+ out = get_stream(FakeStream(7, raw_buffer), "utf-16-le", "strict")
+ assert out == ("wrapped", raw_buffer)
+ assert wrapped["called"] is True
+ assert wrapped["buffer"] is raw_buffer
+
+
+def test_windows_console_reader(monkeypatch):
+ reader = _winconsole._WindowsConsoleReader(42)
+
+ # Empty input buffer returns early
+ assert reader.readinto(bytearray()) == 0
+
+ # Require an even number of bytes
+ with pytest.raises(ValueError):
+ reader.readinto(bytearray(3))
+
+ def writable_buffer(obj, writable=False): # noqa: ARG001
+ return (ctypes.c_char * len(obj)).from_buffer(obj)
+
+ monkeypatch.setattr(_winconsole, "get_buffer", writable_buffer)
+
+ def patch_console(read_console, error):
+ monkeypatch.setattr(_winconsole, "ReadConsoleW", read_console)
+ monkeypatch.setattr(_winconsole, "GetLastError", lambda: error)
+
+ def make_read(payload=b"", rv=1, units_read=None):
+ def read_console(handle, buffer, units_to_read, units_read_ptr, reserved): # noqa: ARG001
+ bytes_to_copy = min(len(payload), units_to_read * 2)
+ if bytes_to_copy:
+ buffer[0:bytes_to_copy] = payload[:bytes_to_copy]
+ read_units = units_read if units_read is not None else bytes_to_copy // 2
+ units_read_ptr._obj.value = read_units
+ return rv
+
+ return read_console
+
+ # Normal successful read returns the number of bytes read
+ patch_console(make_read(payload=b"A\x00B\x00"), _winconsole.ERROR_SUCCESS)
+ assert reader.readinto(bytearray(4)) == 4
+
+ # CTRL+Z (EOF) should be translated into an empty read
+ patch_console(
+ make_read(payload=_winconsole.EOF + b"\x00", units_read=1),
+ _winconsole.ERROR_SUCCESS,
+ )
+ assert reader.readinto(bytearray(2)) == 0
+
+ # An aborted read should sleep briefly while waiting for KeyboardInterrupt
+ sleep_state = {"calls": 0}
+
+ def fake_sleep(seconds):
+ sleep_state["calls"] += 1
+ assert seconds == 0.1
+
+ monkeypatch.setattr(
+ _winconsole, "time", type("FakeTime", (), {"sleep": fake_sleep})
+ )
+ patch_console(
+ make_read(payload=b"Z\x00", units_read=1),
+ _winconsole.ERROR_OPERATION_ABORTED,
+ )
+ assert reader.readinto(bytearray(2)) == 2
+ assert sleep_state["calls"] == 1
+
+ # Failed reads propagate a Windows error
+ patch_console(make_read(rv=0), _winconsole.ERROR_NOT_ENOUGH_MEMORY)
+ with pytest.raises(OSError, match="Windows error"):
+ reader.readinto(bytearray(2))
diff --git a/tests/utils.py b/tests/utils.py
index 35c441d365..7cb285bc53 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -9,9 +9,16 @@
needs_linux = pytest.mark.skipif(
not sys.platform.startswith("linux"), reason="Test requires Linux"
)
+needs_macos = pytest.mark.skipif(
+ not sys.platform.startswith("darwin"), reason="Test requires macOS"
+)
needs_windows = pytest.mark.skipif(
not sys.platform.startswith("win"), reason="Test requires Windows"
)
+skip_if_windows = pytest.mark.skipif(
+ sys.platform == "win32",
+ reason="Test should not be run on Windows",
+)
needs_rich = pytest.mark.skipif(not HAS_RICH, reason="Test requires Rich")
diff --git a/typer-cli/README.md b/typer-cli/README.md
deleted file mode 100644
index d20b1ee8fe..0000000000
--- a/typer-cli/README.md
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
- Typer, build great CLIs. Easy to code. Based on Python type hints.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
----
-
-**Documentation**: https://typer.tiangolo.com/tutorial/typer-command/
-
-**Source Code**: 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 type hints.
-
-It's also a command line tool to run scripts, automatically converting them to CLI applications.
-
-## Typer CLI
-
-⚠️ Do not install this package. ⚠️
-
-This package, `typer-cli`, does nothing other than depend on `typer`.
-
-All the functionality has been integrated into `typer`.
-
-The only reason this package exists is as a migration path for old projects that used to depend on `typer-cli`, so that they can get the latest version of `typer`.
-
-You **should not** install this package.
-
-Install instead:
-
-```bash
-pip install typer
-```
-
-That includes the `typer` command.
-
-This package is deprecated and will stop receiving any updates and published versions.
-
-## License
-
-This project is licensed under the terms of the MIT license.
diff --git a/typer-slim/README.md b/typer-slim/README.md
deleted file mode 100644
index 279cd6736c..0000000000
--- a/typer-slim/README.md
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
- Typer, build great CLIs. Easy to code. Based on Python type hints.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
----
-
-**Documentation**: https://typer.tiangolo.com/tutorial/typer-command/
-
-**Source Code**: 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 type hints.
-
-It's also a command line tool to run scripts, automatically converting them to CLI applications.
-
-## `typer-slim`
-
-⚠️ Do not install this package. ⚠️
-
-This package, `typer-slim`, does nothing other than depend on `typer`.
-
-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.
-
-However, since version 0.22.0, we have stopped supporting this, and `typer-slim` now simply installs (all of) Typer.
-
-If you want to disable Rich globally, you can set an environmental variable `TYPER_USE_RICH` to `False` or `0`.
-
-The only reason this package exists is as a migration path for old projects that used to depend on `typer-slim`, so that they can get the latest version of `typer`.
-
-You **should not** install this package.
-
-Install instead:
-
-```bash
-pip install typer
-```
-
-This package is deprecated and will stop receiving any updates and published versions.
-
-## License
-
-This project is licensed under the terms of the MIT license.
diff --git a/typer/.agents/skills/typer/SKILL.md b/typer/.agents/skills/typer/SKILL.md
index 19b63c491f..60cf47a391 100644
--- a/typer/.agents/skills/typer/SKILL.md
+++ b/typer/.agents/skills/typer/SKILL.md
@@ -112,7 +112,7 @@ def main(name: str = typer.Argument(default="World")):
print(f"Hello {name}")
```
-Similarly, the old style could use ellipsis (...) to explicitely mark an argument as required.
+Similarly, the old style could use ellipsis (...) to explicitly mark an argument as required.
```python
# DO NOT DO THIS: old style. Use Annotated without a default value instead.
@@ -125,7 +125,29 @@ def main(name: str = typer.Argument(default=...)):
## CLI Options
-CLI options are declared in a similar fashion as arguments, but will be called on the CLI with a single dash (single letter) or 2 dashes (full name):
+CLI options are declared in a similar fashion as arguments, but will be called on the CLI with a single dash (single letter) or 2 dashes (full name).
+
+The CLI option name is automatically generated from the variable name, so `user_name` becomes `--user-name` automatically:
+
+```python
+from typing import Annotated
+
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def main(user_name: Annotated[str, typer.Option()]):
+ # On the CLI, the required user name can be specified with --user-name
+ print(f"Hello {user_name}")
+
+
+if __name__ == "__main__":
+ app()
+```
+
+If you want to specify a different name, or want to add a short version, declare them in the `typer.Option`:
```python
from typing import Annotated
@@ -259,7 +281,7 @@ if __name__ == "__main__":
## Click
-Originally, Typer was built on Click. However, going forward Typer will vendor Click. As such, Click extensions should not be used anymore.
+Originally, Typer was built on Click. However, since version 0.26.0, Typer has vendored Click. As such, Click extensions should not be used anymore.
Other settings of `Option` and `Argument` that came from Click but shouldn't be used in Typer anymore, include: `expose_value`, `shell_complete`, `show_choices`, `errors`, `prompt_required`, `is_flag`, `flag_value` and `allow_from_autoenv`.
diff --git a/typer/__init__.py b/typer/__init__.py
index 548408fe98..70f2dd167e 100644
--- a/typer/__init__.py
+++ b/typer/__init__.py
@@ -1,31 +1,24 @@
"""Typer, build great CLIs. Easy to code. Based on Python type hints."""
-__version__ = "0.25.1"
+__version__ = "0.26.5"
from shutil import get_terminal_size as get_terminal_size
-from click.exceptions import Abort as Abort
-from click.exceptions import BadParameter as BadParameter
-from click.exceptions import Exit as Exit
-from click.termui import clear as clear
-from click.termui import confirm as confirm
-from click.termui import echo_via_pager as echo_via_pager
-from click.termui import edit as edit
-from click.termui import getchar as getchar
-from click.termui import pause as pause
-from click.termui import progressbar as progressbar
-from click.termui import prompt as prompt
-from click.termui import secho as secho
-from click.termui import style as style
-from click.termui import unstyle as unstyle
-from click.utils import echo as echo
-from click.utils import format_filename as format_filename
-from click.utils import get_app_dir as get_app_dir
-from click.utils import get_binary_stream as get_binary_stream
-from click.utils import get_text_stream as get_text_stream
-from click.utils import open_file as open_file
-
from . import colors as colors
+from ._click.exceptions import Abort as Abort
+from ._click.exceptions import BadParameter as BadParameter
+from ._click.exceptions import Exit as Exit
+from ._click.termui import confirm as confirm
+from ._click.termui import getchar as getchar
+from ._click.termui import progressbar as progressbar
+from ._click.termui import prompt as prompt
+from ._click.termui import secho as secho
+from ._click.termui import style as style
+from ._click.utils import echo as echo
+from ._click.utils import format_filename as format_filename
+from ._click.utils import get_app_dir as get_app_dir
+from ._click.utils import get_binary_stream as get_binary_stream
+from ._click.utils import get_text_stream as get_text_stream
from .main import Typer as Typer
from .main import launch as launch
from .main import run as run
diff --git a/typer/_click/LICENSE.txt b/typer/_click/LICENSE.txt
new file mode 100644
index 0000000000..d12a849186
--- /dev/null
+++ b/typer/_click/LICENSE.txt
@@ -0,0 +1,28 @@
+Copyright 2014 Pallets
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py
new file mode 100644
index 0000000000..48c9edcddd
--- /dev/null
+++ b/typer/_click/__init__.py
@@ -0,0 +1,11 @@
+"""
+Code taken and adapted from Click: https://github.com/pallets/click/releases/tag/8.3.1
+"""
+
+from .core import Command as Command
+from .core import Context as Context
+from .core import Parameter as Parameter
+from .exceptions import ClickException as ClickException
+from .formatting import HelpFormatter as HelpFormatter
+from .termui import launch as launch
+from .utils import echo as echo
diff --git a/typer/_click/_compat.py b/typer/_click/_compat.py
new file mode 100644
index 0000000000..6ed0ceb8ac
--- /dev/null
+++ b/typer/_click/_compat.py
@@ -0,0 +1,569 @@
+import codecs
+import io
+import os
+import re
+import sys
+from collections.abc import Callable, Mapping, MutableMapping
+from types import TracebackType
+from typing import (
+ IO,
+ Any,
+ BinaryIO,
+ TextIO,
+ cast,
+)
+from weakref import WeakKeyDictionary
+
+CYGWIN = sys.platform.startswith("cygwin")
+WIN = sys.platform.startswith("win")
+auto_wrap_for_ansi: Callable[[TextIO], TextIO] | None = None
+_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
+
+
+def _make_text_stream(
+ stream: BinaryIO,
+ encoding: str | None,
+ errors: str,
+) -> TextIO:
+ if encoding is None:
+ encoding = get_best_encoding(stream)
+ return _NonClosingTextIOWrapper(
+ stream,
+ encoding,
+ errors,
+ line_buffering=True,
+ )
+
+
+def is_ascii_encoding(encoding: str) -> bool:
+ """Checks if a given encoding is ascii."""
+ try:
+ return codecs.lookup(encoding).name == "ascii"
+ except LookupError:
+ return False
+
+
+def get_best_encoding(stream: IO[Any]) -> str:
+ """Returns the default stream encoding if not found."""
+ rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
+ if is_ascii_encoding(rv):
+ return "utf-8"
+ return rv
+
+
+class _NonClosingTextIOWrapper(io.TextIOWrapper):
+ def __init__(
+ self,
+ stream: BinaryIO,
+ encoding: str | None,
+ errors: str | None,
+ **extra: Any,
+ ) -> None:
+ self._stream = stream = cast(BinaryIO, _FixupStream(stream))
+ super().__init__(stream, encoding, errors, **extra)
+
+ def __del__(self) -> None:
+ try:
+ self.detach()
+ except Exception: # pragma: no cover
+ pass
+
+ def isatty(self) -> bool:
+ # https://bitbucket.org/pypy/pypy/issue/1803
+ return self._stream.isatty()
+
+
+class _FixupStream:
+ """The new io interface needs more from streams than streams
+ traditionally implement. As such, this fix-up code is necessary in
+ some circumstances.
+ """
+
+ def __init__(
+ self,
+ stream: BinaryIO,
+ ):
+ self._stream = stream
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._stream, name)
+
+ def read1(self, size: int) -> bytes:
+ f = getattr(self._stream, "read1", None)
+
+ if f is not None:
+ return cast(bytes, f(size))
+
+ return self._stream.read(size)
+
+ def readable(self) -> bool:
+ return True
+
+ def writable(self) -> bool:
+ return True
+
+ def seekable(self) -> bool:
+ x = getattr(self._stream, "seekable", None)
+ if x is not None:
+ return cast(bool, x())
+ return False
+
+
+def _is_binary_reader(stream: IO[Any], default: bool = False) -> bool:
+ try:
+ return isinstance(stream.read(0), bytes)
+ except Exception: # pragma: no cover
+ return default
+ # This happens in some cases where the stream was already
+ # closed. In this case, we assume the default.
+
+
+def _is_binary_writer(stream: IO[Any], default: bool = False) -> bool:
+ try:
+ stream.write(b"")
+ except Exception: # pragma: no cover
+ try:
+ stream.write("")
+ return False
+ except Exception:
+ pass
+ return default
+ return True
+
+
+def _find_binary_reader(stream: IO[Any]) -> BinaryIO | None:
+ # We need to figure out if the given stream is already binary.
+ # This can happen because the official docs recommend detaching
+ # the streams to get binary streams. Some code might do this, so
+ # we need to deal with this case explicitly.
+ if _is_binary_reader(stream, False):
+ return cast(BinaryIO, stream)
+
+ buf = getattr(stream, "buffer", None)
+
+ # Same situation here; this time we assume that the buffer is
+ # actually binary in case it's closed.
+ if buf is not None and _is_binary_reader(buf, True):
+ return cast(BinaryIO, buf)
+
+ return None
+
+
+def _find_binary_writer(stream: IO[Any]) -> BinaryIO | None:
+ # We need to figure out if the given stream is already binary.
+ # This can happen because the official docs recommend detaching
+ # the streams to get binary streams. Some code might do this, so
+ # we need to deal with this case explicitly.
+ if _is_binary_writer(stream, False):
+ return cast(BinaryIO, stream)
+
+ buf = getattr(stream, "buffer", None)
+
+ # Same situation here; this time we assume that the buffer is
+ # actually binary in case it's closed.
+ if buf is not None and _is_binary_writer(buf, True):
+ return cast(BinaryIO, buf)
+
+ return None
+
+
+def _stream_is_misconfigured(stream: TextIO) -> bool:
+ """A stream is misconfigured if its encoding is ASCII."""
+ # If the stream does not have an encoding set, we assume it's set
+ # to ASCII. This appears to happen in certain unittest
+ # environments. It's not quite clear what the correct behavior is
+ # but this at least will force Click to recover somehow.
+ return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
+
+
+def _is_compat_stream_attr(stream: TextIO, attr: str, value: str | None) -> bool:
+ """A stream attribute is compatible if it is equal to the
+ desired value or the desired value is unset and the attribute
+ has a value.
+ """
+ stream_value = getattr(stream, attr, None)
+ return stream_value == value or (value is None and stream_value is not None)
+
+
+def _is_compatible_text_stream(
+ stream: TextIO, encoding: str | None, errors: str | None
+) -> bool:
+ """Check if a stream's encoding and errors attributes are
+ compatible with the desired values.
+ """
+ return _is_compat_stream_attr(
+ stream, "encoding", encoding
+ ) and _is_compat_stream_attr(stream, "errors", errors)
+
+
+def _force_correct_text_stream(
+ text_stream: IO[Any],
+ encoding: str | None,
+ errors: str | None,
+ is_binary: Callable[[IO[Any], bool], bool],
+ find_binary: Callable[[IO[Any]], BinaryIO | None],
+) -> TextIO:
+ if is_binary(text_stream, False):
+ binary_reader = cast(BinaryIO, text_stream)
+ else:
+ text_stream = cast(TextIO, text_stream)
+ # If the stream looks compatible, and won't default to a
+ # misconfigured ascii encoding, return it as-is.
+ if _is_compatible_text_stream(text_stream, encoding, errors) and not (
+ encoding is None and _stream_is_misconfigured(text_stream)
+ ):
+ return text_stream
+
+ # Otherwise, get the underlying binary reader.
+ possible_binary_reader = find_binary(text_stream)
+
+ # If that's not possible, silently use the original reader
+ # and get mojibake instead of exceptions.
+ if possible_binary_reader is None:
+ return text_stream
+
+ binary_reader = possible_binary_reader
+
+ # Default errors to replace instead of strict in order to get
+ # something that works.
+ if errors is None:
+ errors = "replace"
+
+ # Wrap the binary stream in a text stream with the correct
+ # encoding parameters.
+ return _make_text_stream(
+ binary_reader,
+ encoding,
+ errors,
+ )
+
+
+def _force_correct_text_reader(
+ text_reader: IO[Any],
+ encoding: str | None,
+ errors: str | None,
+) -> TextIO:
+ return _force_correct_text_stream(
+ text_reader,
+ encoding,
+ errors,
+ _is_binary_reader,
+ _find_binary_reader,
+ )
+
+
+def _force_correct_text_writer(
+ text_writer: IO[Any],
+ encoding: str | None,
+ errors: str | None,
+) -> TextIO:
+ return _force_correct_text_stream(
+ text_writer,
+ encoding,
+ errors,
+ _is_binary_writer,
+ _find_binary_writer,
+ )
+
+
+def get_binary_stdin() -> BinaryIO:
+ reader = _find_binary_reader(sys.stdin)
+ if reader is None: # pragma: no cover
+ raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
+ return reader
+
+
+def get_binary_stdout() -> BinaryIO:
+ writer = _find_binary_writer(sys.stdout)
+ if writer is None: # pragma: no cover
+ raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
+ return writer
+
+
+def get_binary_stderr() -> BinaryIO:
+ writer = _find_binary_writer(sys.stderr)
+ if writer is None: # pragma: no cover
+ raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
+ return writer
+
+
+def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> TextIO:
+ rv = _get_windows_console_stream(sys.stdin, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_reader(sys.stdin, encoding, errors)
+
+
+def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> TextIO:
+ rv = _get_windows_console_stream(sys.stdout, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_writer(sys.stdout, encoding, errors)
+
+
+def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> TextIO:
+ rv = _get_windows_console_stream(sys.stderr, encoding, errors)
+ if rv is not None:
+ return rv
+ return _force_correct_text_writer(sys.stderr, encoding, errors)
+
+
+def _wrap_io_open(
+ file: str | os.PathLike[str] | int,
+ mode: str,
+ encoding: str | None,
+ errors: str | None,
+) -> IO[Any]:
+ """Handles not passing ``encoding`` and ``errors`` in binary mode."""
+ if "b" in mode:
+ return open(file, mode)
+
+ return open(file, mode, encoding=encoding, errors=errors)
+
+
+def open_stream(
+ filename: str | os.PathLike[str],
+ mode: str = "r",
+ encoding: str | None = None,
+ errors: str | None = "strict",
+ atomic: bool = False,
+) -> tuple[IO[Any], bool]:
+ binary = "b" in mode
+ filename = os.fspath(filename)
+
+ # Standard streams first, ignoring the atomic flag.
+ if os.fsdecode(filename) == "-":
+ if any(m in mode for m in ["w", "a", "x"]):
+ if binary:
+ return get_binary_stdout(), False
+ return get_text_stdout(encoding=encoding, errors=errors), False
+ if binary:
+ return get_binary_stdin(), False
+ return get_text_stdin(encoding=encoding, errors=errors), False
+
+ # Non-atomic writes directly go out through the regular open functions.
+ if not atomic:
+ return _wrap_io_open(filename, mode, encoding, errors), True
+
+ # Some usability stuff for atomic writes
+ if "a" in mode:
+ raise ValueError(
+ "Appending to an existing file is not supported, because that"
+ " would involve an expensive `copy`-operation to a temporary"
+ " file. Open the file in normal `w`-mode and copy explicitly"
+ " if that's what you're after."
+ )
+ if "x" in mode:
+ raise ValueError("Use the `overwrite`-parameter instead.")
+ if "w" not in mode:
+ raise ValueError("Atomic writes only make sense with `w`-mode.")
+
+ # Atomic writes are more complicated. They work by opening a file
+ # as a proxy in the same folder and then using the fdopen
+ # functionality to wrap it in a Python file. Then we wrap it in an
+ # atomic file that moves the file over on close.
+ import errno
+ import random
+
+ try:
+ perm: int | None = os.stat(filename).st_mode
+ except OSError: # pragma: no cover
+ perm = None
+
+ flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
+
+ if binary:
+ flags |= getattr(os, "O_BINARY", 0)
+
+ while True:
+ tmp_filename = os.path.join(
+ os.path.dirname(filename),
+ f".__atomic-write{random.randrange(1 << 32):08x}",
+ )
+ try:
+ fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
+ break
+ except OSError as e: # pragma: no cover
+ if e.errno == errno.EEXIST or (
+ os.name == "nt"
+ and e.errno == errno.EACCES
+ and os.path.isdir(e.filename)
+ and os.access(e.filename, os.W_OK)
+ ):
+ continue
+ raise
+
+ if perm is not None:
+ os.chmod(tmp_filename, perm) # in case perm includes bits in umask
+
+ f = _wrap_io_open(fd, mode, encoding, errors)
+ af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
+ return cast(IO[Any], af), True
+
+
+class _AtomicFile:
+ def __init__(self, f: IO[Any], tmp_filename: str, real_filename: str) -> None:
+ self._f = f
+ self._tmp_filename = tmp_filename
+ self._real_filename = real_filename
+ self.closed = False
+
+ @property
+ def name(self) -> str:
+ return self._real_filename
+
+ def close(self, delete: bool = False) -> None:
+ if self.closed:
+ return # pragma: no cover
+ self._f.close()
+ os.replace(self._tmp_filename, self._real_filename)
+ self.closed = True
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._f, name)
+
+ def __enter__(self) -> "_AtomicFile":
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.close(delete=exc_type is not None)
+
+ def __repr__(self) -> str:
+ return repr(self._f)
+
+
+def strip_ansi(value: str) -> str:
+ return _ansi_re.sub("", value)
+
+
+def _is_jupyter_kernel_output(stream: IO[Any]) -> bool:
+ while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
+ stream = stream._stream
+
+ return stream.__class__.__module__.startswith("ipykernel.")
+
+
+def should_strip_ansi(stream: IO[Any] | None = None, color: bool | None = None) -> bool:
+ if color is None:
+ if stream is None:
+ stream = sys.stdin
+ return not isatty(stream) and not _is_jupyter_kernel_output(stream)
+ return not color
+
+
+# On Windows, wrap the output streams with colorama to support ANSI
+# color codes.
+# NOTE: double check is needed so mypy does not analyze this on Linux
+if sys.platform.startswith("win") and WIN:
+ from ._winconsole import _get_windows_console_stream
+
+ def _get_argv_encoding() -> str:
+ import locale
+
+ return locale.getpreferredencoding()
+
+ _ansi_stream_wrappers: MutableMapping[TextIO, TextIO] = WeakKeyDictionary()
+
+ def auto_wrap_for_ansi(stream: TextIO, color: bool | None = None) -> TextIO:
+ """Support ANSI color and style codes on Windows by wrapping a
+ stream with colorama.
+ """
+ try:
+ cached = _ansi_stream_wrappers.get(stream)
+ except Exception: # pragma: no cover
+ cached = None
+
+ if cached is not None:
+ return cached
+
+ import colorama
+
+ strip = should_strip_ansi(stream, color)
+ ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
+ rv = cast(TextIO, ansi_wrapper.stream)
+ _write = rv.write
+
+ def _safe_write(s: str) -> int:
+ try:
+ return _write(s)
+ except BaseException: # pragma: no cover
+ ansi_wrapper.reset_all()
+ raise
+
+ rv.write = _safe_write # type: ignore[method-assign] # ty: ignore[invalid-assignment]
+
+ try:
+ _ansi_stream_wrappers[stream] = rv
+ except Exception: # pragma: no cover
+ pass
+
+ return rv
+
+else:
+
+ def _get_argv_encoding() -> str:
+ return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding()
+
+ def _get_windows_console_stream(
+ f: TextIO, encoding: str | None, errors: str | None
+ ) -> TextIO | None:
+ return None
+
+
+def term_len(x: str) -> int:
+ return len(strip_ansi(x))
+
+
+def isatty(stream: IO[Any]) -> bool:
+ try:
+ return stream.isatty()
+ except Exception: # pragma: no cover
+ return False
+
+
+def _make_cached_stream_func(
+ src_func: Callable[[], TextIO],
+ wrapper_func: Callable[[], TextIO],
+) -> Callable[[], TextIO]:
+ cache: MutableMapping[TextIO, TextIO] = WeakKeyDictionary()
+
+ def func() -> TextIO:
+ stream = src_func()
+
+ try:
+ rv = cache.get(stream)
+ except Exception: # pragma: no cover
+ rv = None
+ if rv is not None:
+ return rv
+ rv = wrapper_func()
+ try:
+ cache[stream] = rv
+ except Exception: # pragma: no cover
+ pass
+ return rv
+
+ return func
+
+
+_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
+_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
+_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
+
+
+binary_streams: Mapping[str, Callable[[], BinaryIO]] = {
+ "stdin": get_binary_stdin,
+ "stdout": get_binary_stdout,
+ "stderr": get_binary_stderr,
+}
+
+text_streams: Mapping[str, Callable[[str | None, str | None], TextIO]] = {
+ "stdin": get_text_stdin,
+ "stdout": get_text_stdout,
+ "stderr": get_text_stderr,
+}
diff --git a/typer/_click/_termui_impl.py b/typer/_click/_termui_impl.py
new file mode 100644
index 0000000000..c621810cbf
--- /dev/null
+++ b/typer/_click/_termui_impl.py
@@ -0,0 +1,522 @@
+"""
+To keep the import times down, some infrequently used termui functionality
+is placed here and only imported as needed.
+"""
+
+import contextlib
+import math
+import os
+import sys
+import time
+from collections.abc import Callable, Iterable, Iterator
+from io import StringIO
+from types import TracebackType
+from typing import Generic, TextIO, TypeVar, cast
+
+from ._compat import (
+ CYGWIN,
+ WIN,
+ _default_text_stdout,
+ get_best_encoding,
+ isatty,
+ term_len,
+)
+from .utils import echo
+
+V = TypeVar("V")
+
+if os.name == "nt":
+ BEFORE_BAR = "\r"
+ AFTER_BAR = "\n"
+else:
+ BEFORE_BAR = "\r\033[?25l"
+ AFTER_BAR = "\033[?25h\n"
+
+
+class ProgressBar(Generic[V]):
+ def __init__(
+ self,
+ iterable: Iterable[V] | None,
+ length: int | None = None,
+ fill_char: str = "#",
+ empty_char: str = " ",
+ bar_template: str = "%(bar)s",
+ info_sep: str = " ",
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ item_show_func: Callable[[V | None], str | None] | None = None,
+ label: str | None = None,
+ file: TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+ width: int = 30,
+ ) -> None:
+ self.fill_char = fill_char
+ self.empty_char = empty_char
+ self.bar_template = bar_template
+ self.info_sep = info_sep
+ self.hidden = hidden
+ self.show_eta = show_eta
+ self.show_percent = show_percent
+ self.show_pos = show_pos
+ self.item_show_func = item_show_func
+ self.label: str = label or ""
+
+ if file is None:
+ file = _default_text_stdout()
+
+ # There are no standard streams attached to write to. For example,
+ # pythonw on Windows.
+ if file is None: # pragma: no cover
+ file = StringIO()
+
+ self.file = file
+ self.color = color
+ self.update_min_steps = update_min_steps
+ self._completed_intervals = 0
+ self.width: int = width
+ self.autowidth: bool = width == 0
+
+ if length is None:
+ from operator import length_hint
+
+ length = length_hint(iterable, -1)
+
+ if length == -1: # pragma: no cover
+ length = None
+ if iterable is None:
+ if length is None: # pragma: no cover
+ raise TypeError("iterable or length is required")
+ iterable = cast("Iterable[V]", range(length))
+ self.iter: Iterable[V] = iter(iterable)
+ self.length = length
+ self.pos: int = 0
+ self.avg: list[float] = []
+ self.last_eta: float
+ self.start: float
+ self.start = self.last_eta = time.time()
+ self.eta_known: bool = False
+ self.finished: bool = False
+ self.max_width: int | None = None
+ self.entered: bool = False
+ self.current_item: V | None = None
+ self._is_atty = isatty(self.file)
+ self._last_line: str | None = None
+
+ def __enter__(self) -> "ProgressBar[V]":
+ self.entered = True
+ self.render_progress()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.render_finish()
+
+ def __iter__(self) -> Iterator[V]:
+ if not self.entered:
+ raise RuntimeError("You need to use progress bars in a with block.")
+ self.render_progress()
+ return self.generator()
+
+ def __next__(self) -> V:
+ # Iteration is defined in terms of a generator function,
+ # returned by iter(self); use that to define next(). This works
+ # because `self.iter` is an iterable consumed by that generator,
+ # so it is re-entry safe. Calling `next(self.generator())`
+ # twice works and does "what you want".
+ return next(iter(self))
+
+ def render_finish(self) -> None:
+ if self.hidden or not self._is_atty:
+ return
+ self.file.write(AFTER_BAR)
+ self.file.flush()
+
+ @property
+ def pct(self) -> float:
+ if self.finished:
+ return 1.0
+ return min(self.pos / (float(self.length or 1) or 1), 1.0)
+
+ @property
+ def time_per_iteration(self) -> float:
+ if not self.avg:
+ return 0.0
+ return sum(self.avg) / float(len(self.avg))
+
+ @property
+ def eta(self) -> float:
+ if self.length is not None and not self.finished:
+ return self.time_per_iteration * (self.length - self.pos)
+ return 0.0
+
+ def format_eta(self) -> str:
+ if self.eta_known:
+ t = int(self.eta)
+ seconds = t % 60
+ t //= 60
+ minutes = t % 60
+ t //= 60
+ hours = t % 24
+ t //= 24
+ if t > 0:
+ return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
+ else:
+ return f"{hours:02}:{minutes:02}:{seconds:02}"
+ return ""
+
+ def format_pos(self) -> str:
+ pos = str(self.pos)
+ if self.length is not None:
+ pos += f"/{self.length}"
+ return pos
+
+ def format_pct(self) -> str:
+ return f"{int(self.pct * 100): 4}%"[1:]
+
+ def format_bar(self) -> str:
+ if self.length is not None:
+ bar_length = int(self.pct * self.width)
+ bar = self.fill_char * bar_length
+ bar += self.empty_char * (self.width - bar_length)
+ elif self.finished:
+ bar = self.fill_char * self.width
+ else:
+ chars = list(self.empty_char * (self.width or 1))
+ if self.time_per_iteration != 0:
+ chars[
+ int(
+ (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
+ * self.width
+ )
+ ] = self.fill_char
+ bar = "".join(chars)
+ return bar
+
+ def format_progress_line(self) -> str:
+ show_percent = self.show_percent
+
+ info_bits = []
+ if self.length is not None and show_percent is None:
+ show_percent = not self.show_pos
+
+ if self.show_pos:
+ info_bits.append(self.format_pos())
+ if show_percent:
+ info_bits.append(self.format_pct())
+ if self.show_eta and self.eta_known and not self.finished:
+ info_bits.append(self.format_eta())
+ if self.item_show_func is not None:
+ item_info = self.item_show_func(self.current_item)
+ if item_info is not None:
+ info_bits.append(item_info)
+
+ return (
+ self.bar_template
+ % {
+ "label": self.label,
+ "bar": self.format_bar(),
+ "info": self.info_sep.join(info_bits),
+ }
+ ).rstrip()
+
+ def render_progress(self) -> None:
+ if self.hidden:
+ return
+
+ if not self._is_atty:
+ # Only output the label once if the output is not a TTY.
+ if self._last_line != self.label:
+ self._last_line = self.label
+ echo(self.label, file=self.file, color=self.color)
+ return
+
+ buf = []
+ # Update width in case the terminal has been resized
+ if self.autowidth:
+ import shutil
+
+ old_width = self.width
+ self.width = 0
+ clutter_length = term_len(self.format_progress_line())
+ new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
+ if new_width < old_width and self.max_width is not None:
+ buf.append(BEFORE_BAR)
+ buf.append(" " * self.max_width)
+ self.max_width = new_width
+ self.width = new_width
+
+ clear_width = self.width
+ if self.max_width is not None:
+ clear_width = self.max_width
+
+ buf.append(BEFORE_BAR)
+ line = self.format_progress_line()
+ line_len = term_len(line)
+ if self.max_width is None or self.max_width < line_len:
+ self.max_width = line_len
+
+ buf.append(line)
+ buf.append(" " * (clear_width - line_len))
+ line = "".join(buf)
+ # Render the line only if it changed.
+
+ if line != self._last_line:
+ self._last_line = line
+ echo(line, file=self.file, color=self.color, nl=False)
+ self.file.flush()
+
+ def make_step(self, n_steps: int) -> None:
+ self.pos += n_steps
+ if self.length is not None and self.pos >= self.length:
+ self.finished = True
+
+ if (time.time() - self.last_eta) < 1.0:
+ return
+
+ self.last_eta = time.time()
+
+ # self.avg is a rolling list of length <= 7 of steps where steps are
+ # defined as time elapsed divided by the total progress through
+ # self.length.
+ if self.pos:
+ step = (time.time() - self.start) / self.pos
+ else:
+ step = time.time() - self.start
+
+ self.avg = self.avg[-6:] + [step]
+
+ self.eta_known = self.length is not None
+
+ def update(self, n_steps: int) -> None:
+ """Update the progress bar by advancing a specified number of steps."""
+ self._completed_intervals += n_steps
+
+ if self._completed_intervals >= self.update_min_steps:
+ self.make_step(self._completed_intervals)
+ self.render_progress()
+ self._completed_intervals = 0
+
+ def finish(self) -> None:
+ self.eta_known = False
+ self.current_item = None
+ self.finished = True
+
+ def generator(self) -> Iterator[V]:
+ """Return a generator which yields the items added to the bar
+ during construction, and updates the progress bar *after* the
+ yielded block returns.
+ """
+ # WARNING: the iterator interface for `ProgressBar` relies on
+ # this and only works because this is a simple generator which
+ # doesn't create or manage additional state. If this function
+ # changes, the impact should be evaluated both against
+ # `iter(bar)` and `next(bar)`. `next()` in particular may call
+ # `self.generator()` repeatedly, and this must remain safe in
+ # order for that interface to work.
+ if not self.entered: # pragma: no cover
+ raise RuntimeError("You need to use progress bars in a with block.")
+
+ if not self._is_atty:
+ yield from self.iter
+ else:
+ for rv in self.iter:
+ self.current_item = rv
+
+ # This allows show_item_func to be updated before the
+ # item is processed. Only trigger at the beginning of
+ # the update interval.
+ if self._completed_intervals == 0:
+ self.render_progress()
+
+ yield rv
+ self.update(1)
+
+ self.finish()
+ self.render_progress()
+
+
+def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
+ import subprocess
+
+ def _unquote_file(url: str) -> str:
+ from urllib.parse import unquote
+
+ if url.startswith("file://"):
+ url = unquote(url[7:])
+
+ return url
+
+ if sys.platform == "darwin":
+ args = ["open"]
+ if wait:
+ args.append("-W")
+ if locate:
+ args.append("-R")
+ args.append(_unquote_file(url))
+ null = open("/dev/null", "w")
+ try:
+ return subprocess.Popen(args, stderr=null).wait()
+ finally:
+ null.close()
+ elif WIN:
+ if locate:
+ url = _unquote_file(url)
+ args = ["explorer", f"/select,{url}"]
+ else:
+ args = ["start"]
+ if wait:
+ args.append("/WAIT")
+ args.append("")
+ args.append(url)
+ try:
+ return subprocess.call(args)
+ except OSError:
+ # Command not found
+ return 127
+ elif CYGWIN: # pragma: no cover
+ if locate:
+ url = _unquote_file(url)
+ args = ["cygstart", os.path.dirname(url)]
+ else:
+ args = ["cygstart"]
+ if wait:
+ args.append("-w")
+ args.append(url)
+ try:
+ return subprocess.call(args)
+ except OSError:
+ # Command not found
+ return 127
+
+ try:
+ if locate:
+ url = os.path.dirname(_unquote_file(url)) or "."
+ else:
+ url = _unquote_file(url)
+ c = subprocess.Popen(["xdg-open", url])
+ if wait:
+ return c.wait()
+ return 0
+ except OSError: # pragma: no cover
+ # TODO: remove this part, doesn't get hit by Typer code paths?
+ if url.startswith(("http://", "https://")) and not locate and not wait:
+ import webbrowser
+
+ webbrowser.open(url)
+ return 0
+ return 1
+
+
+def _translate_ch_to_exc(ch: str) -> None:
+ if ch == "\x03":
+ raise KeyboardInterrupt()
+
+ if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
+ raise EOFError()
+
+ if ch == "\x1a" and WIN: # Windows, Ctrl+Z
+ raise EOFError()
+
+ return None
+
+
+if sys.platform == "win32":
+ import msvcrt
+
+ @contextlib.contextmanager
+ def raw_terminal() -> Iterator[int]:
+ yield -1
+
+ def getchar(echo: bool) -> str:
+ # The function `getch` will return a bytes object corresponding to
+ # the pressed character. Since Windows 10 build 1803, it will also
+ # return \x00 when called a second time after pressing a regular key.
+ #
+ # `getwch` does not share this probably-bugged behavior. Moreover, it
+ # returns a Unicode object by default, which is what we want.
+ #
+ # Either of these functions will return \x00 or \xe0 to indicate
+ # a special key, and you need to call the same function again to get
+ # the "rest" of the code. The fun part is that \u00e0 is
+ # "latin small letter a with grave", so if you type that on a French
+ # keyboard, you _also_ get a \xe0.
+ # E.g., consider the Up arrow. This returns \xe0 and then \x48. The
+ # resulting Unicode string reads as "a with grave" + "capital H".
+ # This is indistinguishable from when the user actually types
+ # "a with grave" and then "capital H".
+ #
+ # When \xe0 is returned, we assume it's part of a special-key sequence
+ # and call `getwch` again, but that means that when the user types
+ # the \u00e0 character, `getchar` doesn't return until a second
+ # character is typed.
+ # The alternative is returning immediately, but that would mess up
+ # cross-platform handling of arrow keys and others that start with
+ # \xe0. Another option is using `getch`, but then we can't reliably
+ # read non-ASCII characters, because return values of `getch` are
+ # limited to the current 8-bit codepage.
+ #
+ # Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
+ # is doing the right thing in more situations than with `getch`.
+
+ if echo:
+ func = cast(Callable[[], str], msvcrt.getwche)
+ else:
+ func = cast(Callable[[], str], msvcrt.getwch)
+
+ rv = func()
+
+ if rv in ("\x00", "\xe0"):
+ # \x00 and \xe0 are control characters that indicate special key,
+ # see above.
+ rv += func()
+
+ _translate_ch_to_exc(rv)
+ return rv
+
+else:
+ import termios
+ import tty
+
+ @contextlib.contextmanager
+ def raw_terminal() -> Iterator[int]:
+ f: TextIO | None
+ fd: int
+
+ if not isatty(sys.stdin):
+ f = open("/dev/tty")
+ fd = f.fileno()
+ else:
+ fd = sys.stdin.fileno()
+ f = None
+
+ try:
+ old_settings = termios.tcgetattr(fd)
+
+ try:
+ tty.setraw(fd)
+ yield fd
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ sys.stdout.flush()
+
+ if f is not None:
+ f.close()
+ except termios.error: # pragma: no cover
+ pass
+
+ def getchar(echo: bool) -> str:
+ with raw_terminal() as fd:
+ ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
+
+ if echo and isatty(sys.stdout): # pragma: no cover
+ sys.stdout.write(ch)
+
+ _translate_ch_to_exc(ch)
+ return ch
diff --git a/typer/_click/_textwrap.py b/typer/_click/_textwrap.py
new file mode 100644
index 0000000000..9f9636b31f
--- /dev/null
+++ b/typer/_click/_textwrap.py
@@ -0,0 +1,46 @@
+import textwrap
+from collections.abc import Iterator
+from contextlib import contextmanager
+
+
+class TextWrapper(textwrap.TextWrapper):
+ def _handle_long_word(
+ self,
+ reversed_chunks: list[str],
+ cur_line: list[str],
+ cur_len: int,
+ width: int,
+ ) -> None:
+ space_left = max(width - cur_len, 1)
+
+ last = reversed_chunks[-1]
+ cut = last[:space_left]
+ res = last[space_left:]
+ cur_line.append(cut)
+ reversed_chunks[-1] = res
+
+ @contextmanager
+ def extra_indent(self, indent: str) -> Iterator[None]:
+ old_initial_indent = self.initial_indent
+ old_subsequent_indent = self.subsequent_indent
+ self.initial_indent += indent
+ self.subsequent_indent += indent
+
+ try:
+ yield
+ finally:
+ self.initial_indent = old_initial_indent
+ self.subsequent_indent = old_subsequent_indent
+
+ def indent_only(self, text: str) -> str:
+ rv = []
+
+ for idx, line in enumerate(text.splitlines()):
+ indent = self.initial_indent
+
+ if idx > 0:
+ indent = self.subsequent_indent
+
+ rv.append(f"{indent}{line}")
+
+ return "\n".join(rv)
diff --git a/typer/_click/_winconsole.py b/typer/_click/_winconsole.py
new file mode 100644
index 0000000000..f6dedbfd6e
--- /dev/null
+++ b/typer/_click/_winconsole.py
@@ -0,0 +1,300 @@
+# This module is based on the excellent work by Adam Bartoš who
+# provided a lot of what went into the implementation here in
+# the discussion to issue1602 in the Python bug tracker.
+#
+# There are some general differences in regards to how this works
+# compared to the original patches as we do not need to patch
+# the entire interpreter but just work in our little world of
+# echo and prompt.
+import io
+import sys
+import time
+from collections.abc import Callable, Iterable, Mapping
+from ctypes import (
+ POINTER,
+ Array,
+ Structure,
+ byref,
+ c_char,
+ c_char_p,
+ c_int,
+ c_ssize_t,
+ c_ulong,
+ c_void_p,
+ py_object,
+)
+from ctypes.wintypes import DWORD, HANDLE, LPCWSTR, LPWSTR
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ AnyStr,
+ BinaryIO,
+ Literal,
+ TextIO,
+ cast,
+)
+
+from ._compat import _NonClosingTextIOWrapper
+
+assert sys.platform == "win32"
+import msvcrt # noqa: E402
+from ctypes import WINFUNCTYPE, windll # noqa: E402
+
+c_ssize_p = POINTER(c_ssize_t)
+
+kernel32 = windll.kernel32
+GetStdHandle = kernel32.GetStdHandle
+ReadConsoleW = kernel32.ReadConsoleW
+WriteConsoleW = kernel32.WriteConsoleW
+GetConsoleMode = kernel32.GetConsoleMode
+GetLastError = kernel32.GetLastError
+GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
+CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
+ ("CommandLineToArgvW", windll.shell32)
+)
+LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
+
+STDIN_HANDLE = GetStdHandle(-10)
+STDOUT_HANDLE = GetStdHandle(-11)
+STDERR_HANDLE = GetStdHandle(-12)
+
+PyBUF_SIMPLE = 0
+PyBUF_WRITABLE = 1
+
+ERROR_SUCCESS = 0
+ERROR_NOT_ENOUGH_MEMORY = 8
+ERROR_OPERATION_ABORTED = 995
+
+STDIN_FILENO = 0
+STDOUT_FILENO = 1
+STDERR_FILENO = 2
+
+EOF = b"\x1a"
+MAX_BYTES_WRITTEN = 32767
+
+if TYPE_CHECKING:
+ try:
+ # Using `typing_extensions.Buffer` instead of `collections.abc`
+ # on Windows for some reason does not have `Sized` implemented.
+ from collections.abc import Buffer # type: ignore
+ except ImportError:
+ from typing_extensions import Buffer
+
+try:
+ from ctypes import pythonapi
+except ImportError: # pragma: no cover
+ # On PyPy we cannot get buffers so our ability to operate here is
+ # severely limited.
+ get_buffer = None
+else:
+
+ class Py_buffer(Structure):
+ _fields_ = [ # noqa: RUF012
+ ("buf", c_void_p),
+ ("obj", py_object),
+ ("len", c_ssize_t),
+ ("itemsize", c_ssize_t),
+ ("readonly", c_int),
+ ("ndim", c_int),
+ ("format", c_char_p),
+ ("shape", c_ssize_p),
+ ("strides", c_ssize_p),
+ ("suboffsets", c_ssize_p),
+ ("internal", c_void_p),
+ ]
+
+ PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
+ PyBuffer_Release = pythonapi.PyBuffer_Release
+
+ def get_buffer(obj: "Buffer", writable: bool = False) -> Array[c_char]:
+ buf = Py_buffer()
+ flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
+ PyObject_GetBuffer(py_object(obj), byref(buf), flags)
+
+ try:
+ buffer_type = c_char * buf.len
+ out: Array[c_char] = buffer_type.from_address(buf.buf)
+ return out
+ finally:
+ PyBuffer_Release(byref(buf))
+
+
+class _WindowsConsoleRawIOBase(io.RawIOBase):
+ def __init__(self, handle: int | None) -> None:
+ self.handle = handle
+
+ def isatty(self) -> Literal[True]:
+ super().isatty()
+ return True
+
+
+class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
+ def readable(self) -> Literal[True]:
+ return True
+
+ def readinto(self, b: "Buffer") -> int:
+ bytes_to_be_read = len(b)
+ if not bytes_to_be_read:
+ return 0
+ elif bytes_to_be_read % 2:
+ raise ValueError(
+ "cannot read odd number of bytes from UTF-16-LE encoded console"
+ )
+
+ buffer = get_buffer(b, writable=True)
+ code_units_to_be_read = bytes_to_be_read // 2
+ code_units_read = c_ulong()
+
+ rv = ReadConsoleW(
+ HANDLE(self.handle),
+ buffer,
+ code_units_to_be_read,
+ byref(code_units_read),
+ None,
+ )
+ if GetLastError() == ERROR_OPERATION_ABORTED:
+ # wait for KeyboardInterrupt
+ time.sleep(0.1)
+ if not rv:
+ raise OSError(f"Windows error: {GetLastError()}")
+
+ if buffer[0] == EOF:
+ return 0
+ return 2 * code_units_read.value
+
+
+class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
+ def writable(self) -> Literal[True]:
+ return True
+
+ @staticmethod
+ def _get_error_message(errno: int) -> str:
+ if errno == ERROR_SUCCESS:
+ return "ERROR_SUCCESS"
+ elif errno == ERROR_NOT_ENOUGH_MEMORY:
+ return "ERROR_NOT_ENOUGH_MEMORY"
+ return f"Windows error {errno}"
+
+ def write(self, b: "Buffer") -> int:
+ bytes_to_be_written = len(b)
+ buf = get_buffer(b)
+ code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
+ code_units_written = c_ulong()
+
+ WriteConsoleW(
+ HANDLE(self.handle),
+ buf,
+ code_units_to_be_written,
+ byref(code_units_written),
+ None,
+ )
+ bytes_written = 2 * code_units_written.value
+
+ if bytes_written == 0 and bytes_to_be_written > 0:
+ raise OSError(self._get_error_message(GetLastError())) # pragma: no cover
+ return bytes_written
+
+
+class ConsoleStream:
+ def __init__(self, text_stream: TextIO, byte_stream: BinaryIO) -> None:
+ self._text_stream = text_stream
+ self.buffer = byte_stream
+
+ @property
+ def name(self) -> str:
+ return self.buffer.name
+
+ def write(self, x: AnyStr) -> int:
+ if isinstance(x, str):
+ return self._text_stream.write(x)
+ try:
+ self.flush()
+ except Exception: # pragma: no cover
+ pass
+ return self.buffer.write(x)
+
+ def writelines(self, lines: Iterable[AnyStr]) -> None:
+ for line in lines:
+ self.write(line)
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._text_stream, name)
+
+ def isatty(self) -> bool:
+ return self.buffer.isatty()
+
+ def __repr__(self) -> str:
+ return f""
+
+
+def _get_text_stdin(buffer_stream: BinaryIO) -> TextIO:
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return cast(TextIO, ConsoleStream(text_stream, buffer_stream))
+
+
+def _get_text_stdout(buffer_stream: BinaryIO) -> TextIO:
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return cast(TextIO, ConsoleStream(text_stream, buffer_stream))
+
+
+def _get_text_stderr(buffer_stream: BinaryIO) -> TextIO:
+ text_stream = _NonClosingTextIOWrapper(
+ io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
+ "utf-16-le",
+ "strict",
+ line_buffering=True,
+ )
+ return cast(TextIO, ConsoleStream(text_stream, buffer_stream))
+
+
+_stream_factories: Mapping[int, Callable[[BinaryIO], TextIO]] = {
+ 0: _get_text_stdin,
+ 1: _get_text_stdout,
+ 2: _get_text_stderr,
+}
+
+
+def _is_console(f: TextIO) -> bool:
+ if not hasattr(f, "fileno"):
+ return False
+
+ try:
+ fileno = f.fileno()
+ except (OSError, io.UnsupportedOperation):
+ return False
+
+ handle = msvcrt.get_osfhandle(fileno)
+ return bool(GetConsoleMode(handle, byref(DWORD())))
+
+
+def _get_windows_console_stream(
+ f: TextIO, encoding: str | None, errors: str | None
+) -> TextIO | None:
+ if (
+ get_buffer is None
+ or encoding not in {"utf-16-le", None}
+ or errors not in {"strict", None}
+ or not _is_console(f)
+ ):
+ return None
+
+ func = _stream_factories.get(f.fileno())
+ if func is None:
+ return None
+
+ b = getattr(f, "buffer", None)
+
+ if b is None:
+ return None
+
+ return func(b)
diff --git a/typer/_click/core.py b/typer/_click/core.py
new file mode 100644
index 0000000000..580b558b9f
--- /dev/null
+++ b/typer/_click/core.py
@@ -0,0 +1,1111 @@
+import enum
+import inspect
+import os
+from abc import ABC, abstractmethod
+from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence
+from contextlib import AbstractContextManager, ExitStack, contextmanager
+from types import TracebackType
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Literal,
+ NoReturn,
+ TypeVar,
+ Union,
+ cast,
+ overload,
+)
+
+from . import types
+from .exceptions import (
+ Abort,
+ BadParameter,
+ Exit,
+ MissingParameter,
+ NoArgsIsHelpError,
+ UsageError,
+)
+from .formatting import HelpFormatter
+from .globals import pop_context, push_context
+from .parser import _OptionParser
+from .termui import style
+from .utils import echo, make_default_short_help
+
+if TYPE_CHECKING:
+ from ..core import TyperOption
+ from .shell_completion import CompletionItem
+
+F = TypeVar("F", bound="Callable[..., Any]")
+V = TypeVar("V")
+
+
+def _complete_visible_commands(
+ ctx: "Context", incomplete: str
+) -> Iterator[tuple[str, "Command"]]:
+ """List all the subcommands of a group that start with the
+ incomplete value and aren't hidden.
+ """
+ # avoid circular imports
+ from ..core import TyperGroup
+
+ multi = cast(TyperGroup, ctx.command)
+
+ for name in multi.list_commands(ctx):
+ if name.startswith(incomplete):
+ command = multi.get_command(ctx, name)
+
+ if command is not None and not command.hidden:
+ yield name, command
+
+
+@contextmanager
+def augment_usage_errors(
+ ctx: "Context", param: Union["Parameter", None] = None
+) -> Iterator[None]:
+ """Context manager that attaches extra information to exceptions."""
+ try:
+ yield
+ except BadParameter as e:
+ if e.ctx is None:
+ e.ctx = ctx
+ if param is not None and e.param is None:
+ e.param = param
+ raise
+ except UsageError as e: # pragma: no cover
+ if e.ctx is None:
+ e.ctx = ctx
+ raise
+
+
+def iter_params_for_processing(
+ invocation_order: Sequence["Parameter"],
+ declaration_order: Sequence["Parameter"],
+) -> list["Parameter"]:
+ """Returns all declared parameters in the order they should be processed.
+
+ The declared parameters are re-shuffled depending on the order in which
+ they were invoked, as well as the eagerness of each parameters.
+
+ The invocation order takes precedence over the declaration order. I.e. the
+ order in which the user provided them to the CLI is respected.
+
+ This behavior and its effect on callback evaluation is detailed at:
+ https://click.palletsprojects.com/en/stable/advanced/#callback-evaluation-order
+ """
+
+ def sort_key(item: Parameter) -> tuple[bool, float]:
+ try:
+ idx: float = invocation_order.index(item)
+ except ValueError:
+ idx = float("inf")
+
+ return not item.is_eager, idx
+
+ return sorted(declaration_order, key=sort_key)
+
+
+class ParameterSource(enum.Enum):
+ """This is an `Enum` that indicates the source of a
+ parameter's value.
+ """
+
+ COMMANDLINE = enum.auto()
+ """The value was provided by the command line args."""
+ ENVIRONMENT = enum.auto()
+ """The value was provided with an environment variable."""
+ DEFAULT = enum.auto()
+ """Used the default specified by the parameter."""
+ DEFAULT_MAP = enum.auto()
+ """Used a default provided by `Context.default_map`."""
+ PROMPT = enum.auto()
+ """Used a prompt to confirm a default or provide a value."""
+
+
+class Context:
+ """The context is a special internal object that holds state relevant
+ for the script execution at every single level. It's normally invisible
+ to commands unless they opt-in to getting access to it.
+
+ The context is useful as it can pass internal objects around and can
+ control special execution features such as reading data from
+ environment variables.
+
+ A context can be used as context manager in which case it will call
+ `close` on teardown.
+ """
+
+ formatter_class: type[HelpFormatter] = HelpFormatter
+
+ def __init__(
+ self,
+ command: "Command",
+ parent: Union["Context", None] = None,
+ info_name: str | None = None,
+ obj: Any | None = None,
+ auto_envvar_prefix: str | None = None,
+ default_map: MutableMapping[str, Any] | None = None,
+ terminal_width: int | None = None,
+ max_content_width: int | None = None,
+ resilient_parsing: bool = False,
+ allow_extra_args: bool | None = None,
+ allow_interspersed_args: bool | None = None,
+ ignore_unknown_options: bool | None = None,
+ help_option_names: list[str] | None = None,
+ token_normalize_func: Callable[[str], str] | None = None,
+ color: bool | None = None,
+ show_default: bool | None = None,
+ ) -> None:
+ self.parent = parent
+ self.command = command
+ self.info_name = info_name
+ # Map of parameter names to their parsed values.
+ self.params: dict[str, Any] = {}
+ # the leftover arguments.
+ self.args: list[str] = []
+ # protected arguments. used to implement nested parsing.
+ self._protected_args: list[str] = []
+ # the collected prefixes of the command's options.
+ self._opt_prefixes: set[str] = set(parent._opt_prefixes) if parent else set()
+
+ if obj is None and parent is not None:
+ obj = parent.obj
+
+ self.obj: Any = obj
+ self._meta: dict[str, Any] = getattr(parent, "meta", {})
+
+ # A dictionary (-like object) with defaults for parameters.
+ if (
+ default_map is None
+ and info_name is not None
+ and parent is not None
+ and parent.default_map is not None
+ ):
+ default_map = parent.default_map.get(info_name)
+
+ self.default_map: MutableMapping[str, Any] | None = default_map
+
+ # This flag indicates if a subcommand is going to be executed.
+ self.invoked_subcommand: str | None = None
+
+ if terminal_width is None and parent is not None:
+ terminal_width = parent.terminal_width
+
+ # The width of the terminal (None is autodetection).
+ self.terminal_width: int | None = terminal_width
+
+ if max_content_width is None and parent is not None:
+ max_content_width = parent.max_content_width
+
+ self.max_content_width: int | None = max_content_width
+
+ if allow_extra_args is None:
+ allow_extra_args = command.allow_extra_args
+
+ self.allow_extra_args = allow_extra_args
+
+ if allow_interspersed_args is None:
+ allow_interspersed_args = command.allow_interspersed_args
+
+ self.allow_interspersed_args: bool = allow_interspersed_args
+
+ if ignore_unknown_options is None:
+ ignore_unknown_options = command.ignore_unknown_options
+
+ self.ignore_unknown_options: bool = ignore_unknown_options
+
+ if help_option_names is None:
+ if parent is not None:
+ help_option_names = parent.help_option_names
+ else:
+ help_option_names = ["--help"]
+
+ self.help_option_names: list[str] = help_option_names
+
+ if token_normalize_func is None and parent is not None:
+ token_normalize_func = parent.token_normalize_func
+
+ # An optional normalization function for tokens. (options, choices, commands etc.)
+ self.token_normalize_func: Callable[[str], str] | None = token_normalize_func
+
+ # Indicates if resilient parsing is enabled.
+ self.resilient_parsing: bool = resilient_parsing
+
+ # If there is no envvar prefix yet, but the parent has one and
+ # the command on this level has a name, we can expand the envvar
+ # prefix automatically.
+ if auto_envvar_prefix is None:
+ if (
+ parent is not None
+ and parent.auto_envvar_prefix is not None
+ and self.info_name is not None
+ ):
+ auto_envvar_prefix = (
+ f"{parent.auto_envvar_prefix}_{self.info_name.upper()}"
+ )
+ else:
+ auto_envvar_prefix = auto_envvar_prefix.upper()
+
+ if auto_envvar_prefix is not None:
+ auto_envvar_prefix = auto_envvar_prefix.replace("-", "_")
+
+ self.auto_envvar_prefix: str | None = auto_envvar_prefix
+
+ if color is None and parent is not None:
+ color = parent.color
+
+ # Controls if styling output is wanted or not.
+ self.color: bool | None = color
+
+ if show_default is None and parent is not None:
+ show_default = parent.show_default
+
+ # Show option default values when formatting help text.
+ self.show_default: bool | None = show_default
+
+ self._close_callbacks: list[Callable[[], Any]] = []
+ self._depth = 0
+ self._parameter_source: dict[str, ParameterSource] = {}
+ self._exit_stack = ExitStack()
+
+ def __enter__(self) -> "Context":
+ self._depth += 1
+ push_context(self)
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> bool | None:
+ self._depth -= 1
+ exit_result: bool | None = None
+ if self._depth == 0:
+ exit_result = self._close_with_exception_info(exc_type, exc_value, tb)
+ pop_context()
+
+ return exit_result
+
+ @contextmanager
+ def scope(self, cleanup: bool = True) -> Iterator["Context"]:
+ """This helper method can be used with the context object to promote
+ it to the current thread local (see `get_current_context`).
+ The default behavior of this is to invoke the cleanup functions which
+ can be disabled by setting `cleanup` to `False`. The cleanup
+ functions are typically used for things such as closing file handles.
+
+ If the cleanup is intended the context object can also be directly
+ used as a context manager.
+ """
+ if not cleanup:
+ self._depth += 1
+ try:
+ with self as rv:
+ yield rv
+ finally:
+ if not cleanup:
+ self._depth -= 1
+
+ @property
+ def meta(self) -> dict[str, Any]:
+ """This is a dictionary which is shared with all the contexts
+ that are nested. It exists so that click utilities can store some
+ state here if they need to. It is however the responsibility of
+ that code to manage this dictionary well.
+
+ The keys are supposed to be unique dotted strings. For instance
+ module paths are a good choice for it. What is stored in there is
+ irrelevant for the operation of click. However what is important is
+ that code that places data here adheres to the general semantics of
+ the system.
+ """
+ return self._meta
+
+ def make_formatter(self) -> HelpFormatter:
+ """Creates the HelpFormatter for the help and
+ usage output.
+ """
+ return self.formatter_class(
+ width=self.terminal_width, max_width=self.max_content_width
+ )
+
+ def with_resource(self, context_manager: AbstractContextManager[V]) -> V:
+ """Register a resource as if it were used in a ``with``
+ statement. The resource will be cleaned up when the context is
+ popped.
+
+ Uses `contextlib.ExitStack.enter_context`. It calls the
+ resource's ``__enter__()`` method and returns the result. When
+ the context is popped, it closes the stack, which calls the
+ resource's ``__exit__()`` method.
+
+ To register a cleanup function for something that isn't a
+ context manager, use `call_on_close`. Or use something
+ from `contextlib` to turn it into a context manager first.
+ """
+ return self._exit_stack.enter_context(context_manager)
+
+ def call_on_close(self, f: Callable[..., Any]) -> Callable[..., Any]:
+ """Register a function to be called when the context tears down.
+
+ This can be used to close resources opened during the script
+ execution. Resources that support Python's context manager
+ protocol which would be used in a ``with`` statement should be
+ registered with `with_resource` instead.
+ """
+ return self._exit_stack.callback(f)
+
+ def close(self) -> None:
+ """Invoke all close callbacks registered with `call_on_close`,
+ and exit all context managers entered with `with_resource`.
+ """
+ self._close_with_exception_info(None, None, None)
+
+ def _close_with_exception_info(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> bool | None:
+ """Unwind the exit stack by calling its `__exit__` providing the exception
+ information to allow for exception handling by the various resources registered
+ using `with_resource`
+ """
+ exit_result = self._exit_stack.__exit__(exc_type, exc_value, tb)
+ # In case the context is reused, create a new exit stack.
+ self._exit_stack = ExitStack()
+
+ return exit_result
+
+ @property
+ def command_path(self) -> str:
+ """The computed command path. This is used for the ``usage``
+ information on the help page. It's automatically created by
+ combining the info names of the chain of contexts to the root.
+ """
+ rv = ""
+ if self.info_name is not None:
+ rv = self.info_name
+ if self.parent is not None:
+ parent_command_path = [self.parent.command_path]
+
+ if isinstance(self.parent.command, Command):
+ for param in self.parent.command.get_params(self):
+ parent_command_path.extend(param.get_usage_pieces(self))
+
+ rv = f"{' '.join(parent_command_path)} {rv}"
+ return rv.lstrip()
+
+ def find_root(self) -> "Context":
+ """Finds the outermost context."""
+ node = self
+ while node.parent is not None:
+ node = node.parent
+ return node
+
+ def find_object(self, object_type: type[V]) -> V | None:
+ """Finds the closest object of a given type."""
+ node: Context | None = self
+
+ while node is not None:
+ if isinstance(node.obj, object_type):
+ return node.obj
+
+ node = node.parent
+
+ return None
+
+ def ensure_object(self, object_type: type[V]) -> V:
+ """Like `find_object` but sets the innermost object to a
+ new instance of `object_type` if it does not exist.
+ """
+ rv = self.find_object(object_type)
+ if rv is None:
+ self.obj = rv = object_type()
+ return rv
+
+ @overload
+ def lookup_default(self, name: str, call: Literal[True] = True) -> Any | None: ...
+
+ @overload
+ def lookup_default(
+ self, name: str, call: Literal[False] = ...
+ ) -> Any | Callable[[], Any] | None: ...
+
+ def lookup_default(self, name: str, call: bool = True) -> Any | None:
+ """Get the default for a parameter from `default_map`."""
+ if self.default_map is not None:
+ value = self.default_map.get(name)
+
+ if call and callable(value):
+ return value()
+
+ return value
+
+ return None
+
+ def fail(self, message: str) -> NoReturn:
+ """Aborts the execution of the program with a specific error
+ message.
+ """
+ raise UsageError(message, self)
+
+ def abort(self) -> NoReturn:
+ """Aborts the script."""
+ raise Abort()
+
+ def exit(self, code: int = 0) -> NoReturn:
+ """Exits the application with a given exit code."""
+ self.close()
+ raise Exit(code)
+
+ def get_usage(self) -> str:
+ """Helper method to get formatted usage string for the current
+ context and command.
+ """
+ return self.command.get_usage(self)
+
+ def get_help(self) -> str:
+ """Helper method to get formatted help page for the current
+ context and command.
+ """
+ return self.command.get_help(self)
+
+ def invoke(self, callback: Callable[..., V], /, *args: Any, **kwargs: Any) -> V:
+ """Invokes a command callback in exactly the way it expects. There
+ are two ways to invoke this method:
+
+ 1. the first argument can be a callback and all other arguments and
+ keyword arguments are forwarded directly to the function.
+ 2. the first argument is a click command object. In that case all
+ arguments are forwarded as well but proper click parameters
+ (options and click arguments) must be keyword arguments and Click
+ will fill in defaults.
+ """
+ ctx = self
+
+ with augment_usage_errors(self):
+ with ctx:
+ return callback(*args, **kwargs)
+
+ def set_parameter_source(self, name: str, source: ParameterSource) -> None:
+ """Set the source of a parameter. This indicates the location
+ from which the value of the parameter was obtained.
+ """
+ self._parameter_source[name] = source
+
+ def get_parameter_source(self, name: str) -> ParameterSource | None:
+ """Get the source of a parameter. This indicates the location
+ from which the value of the parameter was obtained.
+
+ This can be useful for determining when a user specified a value
+ on the command line that is the same as the default value. It
+ will be `ParameterSource.DEFAULT` only if the
+ value was actually taken from the default.
+ """
+ return self._parameter_source.get(name)
+
+
+class Command(ABC):
+ """Commands are the basic building block of command line interfaces in
+ Click. A basic command handles command line parsing and might dispatch
+ more parsing to commands nested below it.
+ """
+
+ context_class: type[Context] = Context
+ allow_extra_args = False
+ allow_interspersed_args = True
+ ignore_unknown_options = False
+
+ def __init__(
+ self,
+ name: str | None,
+ context_settings: MutableMapping[str, Any] | None = None,
+ callback: Callable[..., Any] | None = None,
+ params: list["Parameter"] | None = None,
+ help: str | None = None,
+ epilog: str | None = None,
+ short_help: str | None = None,
+ options_metavar: str | None = "[OPTIONS]",
+ add_help_option: bool = True,
+ no_args_is_help: bool = False,
+ hidden: bool = False,
+ deprecated: bool | str = False,
+ ) -> None:
+ self.name = name
+
+ if context_settings is None:
+ context_settings = {}
+
+ self.context_settings: MutableMapping[str, Any] = context_settings
+
+ self.callback = callback
+ self.params: list[Parameter] = params or []
+ self.help = help
+ self.epilog = epilog
+ self.options_metavar = options_metavar
+ self.short_help = short_help
+ self.add_help_option = add_help_option
+ self._help_option: TyperOption | None = None
+ self.no_args_is_help = no_args_is_help
+ self.hidden = hidden
+ self.deprecated = deprecated
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} {self.name}>"
+
+ def get_usage(self, ctx: Context) -> str:
+ """Formats the usage line into a string and returns it."""
+ formatter = ctx.make_formatter()
+ self.format_usage(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
+
+ def get_params(self, ctx: Context) -> list["Parameter"]:
+ params = self.params
+ help_option = self.get_help_option(ctx)
+
+ if help_option is not None:
+ params = [*params, help_option]
+
+ return params
+
+ def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the usage line into the formatter."""
+ pieces = self.collect_usage_pieces(ctx)
+ formatter.write_usage(ctx.command_path, " ".join(pieces))
+
+ def collect_usage_pieces(self, ctx: Context) -> list[str]:
+ """Returns all the pieces that go into the usage line and returns
+ it as a list of strings.
+ """
+ rv = [self.options_metavar] if self.options_metavar else []
+
+ for param in self.get_params(ctx):
+ rv.extend(param.get_usage_pieces(ctx))
+
+ return rv
+
+ def get_help_option_names(self, ctx: Context) -> list[str]:
+ """Returns the names for the help option."""
+ all_names = set(ctx.help_option_names)
+ for param in self.params:
+ all_names.difference_update(param.opts)
+ all_names.difference_update(param.secondary_opts)
+ return list(all_names)
+
+ def get_help_option(self, ctx: Context) -> Union["TyperOption", None]:
+ """Returns the help option object."""
+ help_option_names = self.get_help_option_names(ctx)
+
+ if not help_option_names or not self.add_help_option:
+ return None
+
+ # Cache the help option object in private _help_option attribute to
+ # avoid creating it multiple times. Not doing this will break the
+ # callback ordering by iter_params_for_processing(), which relies on
+ # object comparison.
+ if self._help_option is None:
+ # Avoid circular import.
+ from .decorators import help_option
+
+ # Apply help_option decorator and pop resulting option
+ help_option(help_option_names)(self)
+ self._help_option = cast("TyperOption", self.params.pop())
+
+ return self._help_option
+
+ def make_parser(self, ctx: Context) -> _OptionParser:
+ """Creates the underlying option parser for this command."""
+ parser = _OptionParser(ctx)
+ for param in self.get_params(ctx):
+ param.add_to_parser(parser, ctx)
+ return parser
+
+ def get_help(self, ctx: Context) -> str:
+ """Formats the help into a string and returns it."""
+ formatter = ctx.make_formatter()
+ self.format_help(ctx, formatter)
+ return formatter.getvalue().rstrip("\n")
+
+ def get_short_help_str(self, limit: int = 45) -> str:
+ """Gets short help for the command or makes it by shortening the
+ long help string.
+ """
+ if self.short_help:
+ text = inspect.cleandoc(self.short_help)
+ elif self.help:
+ text = make_default_short_help(self.help, limit)
+ else:
+ text = ""
+
+ if self.deprecated:
+ deprecated_message = (
+ f"(DEPRECATED: {self.deprecated})"
+ if isinstance(self.deprecated, str)
+ else "(DEPRECATED)"
+ )
+ text = f"{text} {deprecated_message}"
+
+ return text.strip()
+
+ def format_help(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the help into the formatter if it exists."""
+ self.format_usage(ctx, formatter)
+ self.format_help_text(ctx, formatter)
+ self.format_options(ctx, formatter)
+ self.format_epilog(ctx, formatter)
+
+ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the help text to the formatter if it exists."""
+ if self.help is not None:
+ # truncate the help text to the first form feed
+ text = inspect.cleandoc(self.help).partition("\f")[0]
+ else:
+ text = ""
+
+ if self.deprecated:
+ deprecated_message = (
+ f"(DEPRECATED: {self.deprecated})"
+ if isinstance(self.deprecated, str)
+ else "(DEPRECATED)"
+ )
+ text = f"{text} {deprecated_message}"
+
+ if text:
+ formatter.write_paragraph()
+
+ with formatter.indentation():
+ formatter.write_text(text)
+
+ @abstractmethod
+ def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
+ pass # pragma: no cover
+
+ def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
+ """Writes the epilog into the formatter if it exists."""
+ if self.epilog:
+ epilog = inspect.cleandoc(self.epilog)
+ formatter.write_paragraph()
+
+ with formatter.indentation():
+ formatter.write_text(epilog)
+
+ def make_context(
+ self,
+ info_name: str | None,
+ args: list[str],
+ parent: Context | None = None,
+ **extra: Any,
+ ) -> Context:
+ """This function when given an info name and arguments will kick
+ off the parsing and create a new `Context`. It does not
+ invoke the actual command callback though.
+
+ To quickly customize the context class used without overriding
+ this method, set the `context_class` attribute.
+ """
+ for key, value in self.context_settings.items():
+ if key not in extra:
+ extra[key] = value
+
+ ctx = self.context_class(self, info_name=info_name, parent=parent, **extra)
+
+ with ctx.scope(cleanup=False):
+ self.parse_args(ctx, args)
+ return ctx
+
+ def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
+ if not args and self.no_args_is_help and not ctx.resilient_parsing:
+ raise NoArgsIsHelpError(ctx) # pragma: no cover
+
+ parser = self.make_parser(ctx)
+ opts, args, param_order = parser.parse_args(args=args)
+
+ for param in iter_params_for_processing(param_order, self.get_params(ctx)):
+ _, args = param.handle_parse_result(ctx, opts, args)
+
+ if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
+ ctx.fail(f"Got unexpected extra argument(s) ({' '.join(map(str, args))})")
+
+ ctx.args = args
+ ctx._opt_prefixes.update(parser._opt_prefixes)
+ return args
+
+ def invoke(self, ctx: Context) -> Any:
+ """Given a context, this invokes the attached callback (if it exists)
+ in the right way.
+ """
+ if self.deprecated:
+ extra_message = (
+ f" {self.deprecated}" if isinstance(self.deprecated, str) else ""
+ )
+ message = f"DeprecationWarning: The command {self.name!r} is deprecated.{extra_message}"
+ echo(style(message, fg="red"), err=True)
+
+ if self.callback is not None:
+ return ctx.invoke(self.callback, **ctx.params)
+
+ def shell_complete(self, ctx: Context, incomplete: str) -> list["CompletionItem"]:
+ """Return a list of completions for the incomplete value. Looks
+ at the names of options and chained multi-commands.
+
+ Any command could be part of a chained multi-command, so sibling
+ commands are valid at any point during command completion.
+ """
+ # avoid circular imports
+ from .shell_completion import CompletionItem
+
+ results: list[CompletionItem] = []
+
+ if incomplete and not incomplete[0].isalnum():
+ # avoid circular imports
+ from ..core import TyperOption
+
+ for param in self.get_params(ctx):
+ if (
+ not isinstance(param, TyperOption)
+ or param.hidden
+ or (
+ not param.multiple
+ and ctx.get_parameter_source(param.name) # type: ignore
+ is ParameterSource.COMMANDLINE
+ )
+ ):
+ continue
+
+ results.extend(
+ CompletionItem(name, help=param.help)
+ for name in [*param.opts, *param.secondary_opts]
+ if name.startswith(incomplete)
+ )
+
+ return results
+
+ @abstractmethod
+ def main(
+ self,
+ args: Sequence[str] | None = None,
+ prog_name: str | None = None,
+ complete_var: str | None = None,
+ standalone_mode: bool = True,
+ windows_expand_args: bool = True,
+ **extra: Any,
+ ) -> Any:
+ pass # pragma: no cover
+
+ @abstractmethod
+ def _main_shell_completion(
+ self,
+ ctx_args: MutableMapping[str, Any],
+ prog_name: str,
+ complete_var: str | None = None,
+ ) -> None:
+ pass # pragma: no cover
+
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
+ """Alias for self.main"""
+ return self.main(*args, **kwargs)
+
+
+class Parameter(ABC):
+ r"""A parameter to a command comes in two versions: they are either
+ `Option`\s or `Argument`\s.
+
+ Some settings are supported by both options and arguments.
+ """
+
+ param_type_name = "parameter"
+
+ def __init__(
+ self,
+ param_decls: Sequence[str] | None = None,
+ type: types.ParamType | Any | None = None,
+ required: bool = False,
+ default: Any | Callable[[], Any] | None = None,
+ callback: Callable[[Context, "Parameter", Any], Any] | None = None,
+ nargs: int | None = None,
+ multiple: bool = False,
+ metavar: str | None = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: str | Sequence[str] | None = None,
+ shell_complete: Callable[
+ [Context, "Parameter", str], list["CompletionItem"] | list[str]
+ ]
+ | None = None,
+ ) -> None:
+ self.name: str | None
+ self.opts: list[str]
+ self.secondary_opts: list[str]
+ self.name, self.opts, self.secondary_opts = self._parse_decls(
+ param_decls or (), expose_value
+ )
+ self.type: types.ParamType = types.convert_type(type, default)
+
+ # Default nargs to what the type tells us if we have that
+ # information available.
+ if nargs is None:
+ if self.type.is_composite:
+ nargs = self.type.arity
+ else:
+ nargs = 1
+
+ self.required = required
+ self.callback = callback
+ self.nargs = nargs
+ self.multiple = multiple
+ self.expose_value = expose_value
+ self.default: Any | Callable[[], Any] | None = default
+ self.is_eager = is_eager
+ self.metavar = metavar
+ self.envvar = envvar
+ self._custom_shell_complete = shell_complete
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} {self.name}>"
+
+ @abstractmethod
+ def _parse_decls(
+ self, decls: Sequence[str], expose_value: bool
+ ) -> tuple[str | None, list[str], list[str]]:
+ pass # pragma: no cover
+
+ @property
+ def human_readable_name(self) -> str:
+ """Returns the human readable name of this parameter. This is the
+ same as the name for options, but the metavar for arguments.
+ """
+ assert self.name is not None, "self.name should be set"
+ return self.name
+
+ def make_metavar(self, ctx: Context) -> str:
+ if self.metavar is not None:
+ return self.metavar
+
+ metavar = self.type.get_metavar(param=self, ctx=ctx)
+
+ if metavar is None:
+ metavar = self.type.name.upper()
+
+ if self.nargs != 1:
+ metavar += "..."
+
+ return metavar
+
+ @overload
+ def get_default(self, ctx: Context, call: Literal[True] = True) -> Any | None: ...
+
+ @overload
+ def get_default(
+ self, ctx: Context, call: bool = ...
+ ) -> Any | Callable[[], Any] | None: ...
+
+ def get_default(
+ self, ctx: Context, call: bool = True
+ ) -> Any | Callable[[], Any] | None:
+ """Get the default for the parameter"""
+ value = ctx.lookup_default(self.name, call=False) # type: ignore
+
+ if value is None:
+ value = self.default
+
+ if call and callable(value):
+ value = value()
+
+ return value
+
+ @abstractmethod
+ def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None:
+ pass # pragma: no cover
+
+ def consume_value(
+ self, ctx: Context, opts: Mapping[str, Any]
+ ) -> tuple[Any, ParameterSource]:
+ value = opts.get(self.name) # type: ignore
+ source = ParameterSource.COMMANDLINE
+
+ if value is None:
+ value = self.value_from_envvar(ctx)
+ source = ParameterSource.ENVIRONMENT
+
+ if value is None:
+ value = ctx.lookup_default(self.name) # type: ignore
+ source = ParameterSource.DEFAULT_MAP
+
+ if value is None:
+ value = self.get_default(ctx)
+ source = ParameterSource.DEFAULT
+
+ return value, source
+
+ def type_cast_value(self, ctx: Context, value: Any) -> Any:
+ """Convert and validate a value against the parameter's
+ `type`, `multiple`, and `nargs`.
+ """
+ if value is None:
+ return () if self.multiple or self.nargs == -1 else None
+
+ def check_iter(value: Any) -> Iterator[Any]:
+ if isinstance(value, str):
+ raise BadParameter("Value must be an iterable.", ctx=ctx, param=self)
+ else:
+ return iter(value)
+
+ # Define the conversion function based on nargs and type.
+ if self.nargs == 1 or self.type.is_composite:
+
+ def convert(value: Any) -> Any:
+ return self.type(value, param=self, ctx=ctx)
+
+ elif self.nargs == -1:
+
+ def convert(value: Any) -> Any: # tuple[t.Any, ...]
+ return tuple(self.type(x, self, ctx) for x in check_iter(value))
+
+ # TODO: evaluate whether we need to keep this in Typer
+ else: # nargs > 1
+
+ def convert(value: Any) -> Any: # tuple[t.Any, ...]
+ value = tuple(check_iter(value))
+
+ if len(value) != self.nargs:
+ raise BadParameter(
+ f"Takes {self.nargs} values but {len(value)} given.",
+ ctx=ctx,
+ param=self,
+ )
+
+ return tuple(self.type(x, self, ctx) for x in value)
+
+ if self.multiple:
+ return tuple(convert(x) for x in check_iter(value))
+
+ return convert(value)
+
+ @abstractmethod
+ def value_is_missing(self, value: Any) -> bool:
+ pass # pragma: no cover
+
+ def process_value(self, ctx: Context, value: Any) -> Any:
+ """Process the value of this parameter"""
+ value = self.type_cast_value(ctx, value)
+
+ if self.required and self.value_is_missing(value):
+ raise MissingParameter(ctx=ctx, param=self)
+
+ if self.callback is not None:
+ value = self.callback(ctx, self, value)
+
+ return value
+
+ def resolve_envvar_value(self, ctx: Context) -> str | None:
+ """Returns the value found in the environment variable(s) attached to this
+ parameter.
+
+ Environment variables values are `always returned as strings
+ `_.
+
+ This method returns ``None`` if:
+
+ - the `envvar` property is not set on `Parameter`,
+ - the environment variable is not found in the environment,
+ - the variable is found in the environment but its value is empty (i.e. the
+ environment variable is present but has an empty string).
+
+ If `envvar` is setup with multiple environment variables,
+ then only the first non-empty value is returned.
+ """
+ if self.envvar is None:
+ return None
+
+ if isinstance(self.envvar, str):
+ rv = os.environ.get(self.envvar)
+
+ if rv:
+ return rv
+ else:
+ for envvar in self.envvar:
+ rv = os.environ.get(envvar)
+
+ # Return the first non-empty value of the list of environment variables.
+ if rv:
+ return rv
+ # Else, absence of value is interpreted as an environment variable that
+ # is not set, so proceed to the next one.
+
+ return None
+
+ def value_from_envvar(self, ctx: Context) -> str | Sequence[str] | None:
+ """Process the raw environment variable string for this parameter.
+
+ Returns the string as-is or splits it into a sequence of strings if the
+ parameter is expecting multiple values (i.e. its `nargs` property is set
+ to a value other than ``1``).
+ """
+ rv: Any | None = self.resolve_envvar_value(ctx)
+
+ if rv is not None and self.nargs != 1:
+ rv = self.type.split_envvar_value(rv)
+
+ return rv
+
+ def handle_parse_result(
+ self, ctx: Context, opts: Mapping[str, Any], args: list[str]
+ ) -> tuple[Any, list[str]]:
+ """Process the value produced by the parser from user input.
+
+ Always process the value through the Parameter's `type`, wherever it
+ comes from.
+
+ If the parameter is deprecated, this method warn the user about it. But only if
+ the value has been explicitly set by the user (and as such, is not coming from
+ a default).
+ """
+ with augment_usage_errors(ctx, param=self):
+ value, source = self.consume_value(ctx, opts)
+
+ ctx.set_parameter_source(self.name, source) # type: ignore
+
+ # Process the value through the parameter's type.
+ try:
+ value = self.process_value(ctx, value)
+ except Exception:
+ if not ctx.resilient_parsing:
+ raise
+ value = None
+
+ if self.expose_value:
+ ctx.params[self.name] = value # type: ignore
+
+ return value, args
+
+ @abstractmethod
+ def get_help_record(self, ctx: Context) -> tuple[str, str] | None:
+ pass # pragma: no cover
+
+ def get_usage_pieces(self, ctx: Context) -> list[str]:
+ return []
+
+ def get_error_hint(self, ctx: Context) -> str:
+ """Get a stringified version of the param for use in error messages to
+ indicate which param caused the error.
+ """
+ hint_list = self.opts or [self.human_readable_name]
+ return " / ".join(f"'{x}'" for x in hint_list)
+
+ def shell_complete(self, ctx: Context, incomplete: str) -> list["CompletionItem"]:
+ """Return a list of completions for the incomplete value. If a
+ ``shell_complete`` function was given during init, it is used.
+ Otherwise, the `type` `ParamType.shell_complete` function is used.
+ """
+ if self._custom_shell_complete is not None:
+ results = self._custom_shell_complete(ctx, self, incomplete)
+
+ if results and isinstance(results[0], str):
+ from .shell_completion import CompletionItem
+
+ results = [CompletionItem(c) for c in results]
+
+ return cast("list[CompletionItem]", results)
+
+ return self.type.shell_complete(ctx, self, incomplete)
diff --git a/typer/_click/decorators.py b/typer/_click/decorators.py
new file mode 100644
index 0000000000..28ad656a8c
--- /dev/null
+++ b/typer/_click/decorators.py
@@ -0,0 +1,60 @@
+from collections.abc import Callable
+from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar
+
+from .core import Command, Context, Parameter
+from .utils import echo
+
+if TYPE_CHECKING:
+ from ..core import TyperGroup, TyperOption
+
+ GrpType = TypeVar("GrpType", bound=TyperGroup)
+
+
+P = ParamSpec("P")
+
+R = TypeVar("R")
+T = TypeVar("T")
+_AnyCallable = Callable[..., Any]
+
+
+CmdType = TypeVar("CmdType", bound=Command)
+
+
+def option(
+ param_decls: list[str], cls: type["TyperOption"] | None = None, **attrs: Any
+) -> Callable[[Command], Command]:
+ """Attaches an option to the command."""
+ if cls is None:
+ # avoid circular imports
+ from ..core import TyperOption
+
+ cls = TyperOption
+
+ def decorator(f: Command) -> Command:
+ param = cls(param_decls=param_decls, **attrs)
+ f.params.append(param)
+ return f
+
+ return decorator
+
+
+def help_option(param_decls: list[str]) -> Callable[[Command], Command]:
+ """Help option which prints the help page and exits the program."""
+
+ def show_help(ctx: Context, param: Parameter, value: bool) -> None:
+ """Callback that print the help page on ```` and exits."""
+ if value and not ctx.resilient_parsing:
+ echo(ctx.get_help(), color=ctx.color)
+ ctx.exit()
+
+ assert len(param_decls) > 0, "At least one help option should be provided"
+
+ return option(
+ param_decls,
+ is_flag=True,
+ expose_value=False,
+ is_eager=True,
+ help="Show this message and exit.",
+ callback=show_help,
+ required=False,
+ )
diff --git a/typer/_click/exceptions.py b/typer/_click/exceptions.py
new file mode 100644
index 0000000000..af2af260a6
--- /dev/null
+++ b/typer/_click/exceptions.py
@@ -0,0 +1,260 @@
+from collections.abc import Sequence
+from typing import IO, TYPE_CHECKING, Any, Union
+
+from ._compat import get_text_stderr
+from .globals import resolve_color_default
+from .utils import echo, format_filename
+
+if TYPE_CHECKING:
+ from .core import Command, Context, Parameter
+
+
+def _join_param_hints(param_hint: Sequence[str] | str | None) -> str | None:
+ if param_hint is not None and not isinstance(param_hint, str):
+ return " / ".join(repr(x) for x in param_hint)
+
+ return param_hint
+
+
+class ClickException(Exception):
+ """An exception that Click can handle and show to the user."""
+
+ exit_code = 1
+
+ def __init__(self, message: str) -> None:
+ super().__init__(message)
+ # The context will be removed by the time we print the message, so cache
+ # the color settings here to be used later on (in `show`)
+ self.show_color: bool | None = resolve_color_default()
+ self.message = message
+
+ def format_message(self) -> str:
+ return self.message
+
+ def __str__(self) -> str:
+ return self.message
+
+ def show(self, file: IO[Any] | None = None) -> None:
+ if file is None:
+ file = get_text_stderr()
+
+ echo(
+ f"Error: {self.format_message()}",
+ file=file,
+ color=self.show_color,
+ )
+
+
+class UsageError(ClickException):
+ """An internal exception that signals a usage error. This typically
+ aborts any further handling.
+ """
+
+ exit_code = 2
+
+ def __init__(self, message: str, ctx: Union["Context", None] = None) -> None:
+ super().__init__(message)
+ self.ctx = ctx
+ self.cmd: Command | None = self.ctx.command if self.ctx else None
+
+ def show(self, file: IO[Any] | None = None) -> None:
+ if file is None:
+ file = get_text_stderr()
+ color = None
+ hint = ""
+ if (
+ self.ctx is not None
+ and self.ctx.command.get_help_option(self.ctx) is not None
+ ):
+ command = self.ctx.command_path
+ option = self.ctx.help_option_names[0]
+ hint = f"Try '{command} {option}' for help.\n"
+ if self.ctx is not None:
+ color = self.ctx.color
+ echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
+ echo(
+ f"Error: {self.format_message()}",
+ file=file,
+ color=color,
+ )
+
+
+class BadParameter(UsageError):
+ """An exception that formats out a standardized error message for a
+ bad parameter. This is useful when thrown from a callback or type as
+ Click will attach contextual information to it (for instance, which
+ parameter it is).
+ """
+
+ def __init__(
+ self,
+ message: str,
+ ctx: Union["Context", None] = None,
+ param: Union["Parameter", None] = None,
+ param_hint: Sequence[str] | str | None = None,
+ ) -> None:
+ super().__init__(message, ctx)
+ self.param = param
+ self.param_hint = param_hint
+
+ def format_message(self) -> str:
+ if self.param_hint is not None:
+ param_hint = self.param_hint
+ elif self.param is not None:
+ param_hint = self.param.get_error_hint(self.ctx) # type: ignore
+ else:
+ return f"Invalid value: {self.message}"
+
+ hint = _join_param_hints(param_hint)
+ return f"Invalid value for {hint}: {self.message}"
+
+
+class MissingParameter(BadParameter):
+ """Raised if click required an option or argument but it was not
+ provided when invoking the script.
+ """
+
+ def __init__(
+ self,
+ message: str | None = None,
+ ctx: Union["Context", None] = None,
+ param: Union["Parameter", None] = None,
+ param_hint: Sequence[str] | str | None = None,
+ param_type: str | None = None,
+ ) -> None:
+ super().__init__(message or "", ctx, param, param_hint)
+ self.param_type = param_type
+
+ def format_message(self) -> str:
+ if self.param_hint is not None:
+ param_hint: Sequence[str] | str | None = self.param_hint
+ elif self.param is not None:
+ param_hint = self.param.get_error_hint(self.ctx) # type: ignore
+ else:
+ param_hint = None
+
+ param_hint = _join_param_hints(param_hint)
+ param_hint = f" {param_hint}" if param_hint else ""
+
+ param_type = self.param_type
+ if param_type is None and self.param is not None:
+ param_type = self.param.param_type_name
+
+ msg = self.message
+ if self.param is not None:
+ msg_extra = self.param.type.get_missing_message(
+ param=self.param, ctx=self.ctx
+ )
+ if msg_extra:
+ if msg:
+ msg += f". {msg_extra}"
+ else:
+ msg = msg_extra
+
+ msg = f" {msg}" if msg else ""
+
+ # Translate param_type for known types.
+ if param_type == "argument":
+ missing = "Missing argument"
+ elif param_type == "option":
+ missing = "Missing option"
+ elif param_type == "parameter":
+ missing = "Missing parameter"
+ else:
+ missing = f"Missing {param_type}"
+
+ return f"{missing}{param_hint}.{msg}"
+
+ def __str__(self) -> str:
+ if not self.message:
+ param_name = self.param.name if self.param else None
+ return f"Missing parameter: {param_name}"
+ else:
+ return self.message
+
+
+class NoSuchOption(UsageError):
+ """Raised if click attempted to handle an option that does not
+ exist.
+ """
+
+ def __init__(
+ self,
+ option_name: str,
+ message: str | None = None,
+ possibilities: Sequence[str] | None = None,
+ ctx: Union["Context", None] = None,
+ ) -> None:
+ if message is None:
+ message = f"No such option: {option_name}"
+
+ super().__init__(message, ctx)
+ self.option_name = option_name
+ self.possibilities = possibilities
+
+ def format_message(self) -> str:
+ if not self.possibilities:
+ return self.message
+
+ possibility_str = ", ".join(sorted(self.possibilities))
+ suggest = (f"(Possible options: {possibility_str})",)
+ return f"{self.message} {suggest}"
+
+
+class BadOptionUsage(UsageError):
+ """Raised if an option is generally supplied but the use of the option
+ was incorrect. This is for instance raised if the number of arguments
+ for an option is not correct.
+ """
+
+ def __init__(
+ self, option_name: str, message: str, ctx: Union["Context", None] = None
+ ) -> None:
+ super().__init__(message, ctx)
+ self.option_name = option_name
+
+
+class BadArgumentUsage(UsageError):
+ """Raised if an argument is generally supplied but the use of the argument
+ was incorrect. This is for instance raised if the number of values
+ for an argument is not correct.
+ """
+
+
+class NoArgsIsHelpError(UsageError):
+ def __init__(self, ctx: "Context") -> None:
+ self.ctx: Context
+ super().__init__(ctx.get_help(), ctx=ctx)
+
+ def show(self, file: IO[Any] | None = None) -> None:
+ echo(self.format_message(), file=file, err=True, color=self.ctx.color)
+
+
+class FileError(ClickException):
+ """Raised if a file cannot be opened."""
+
+ def __init__(self, filename: str, hint: str | None = None) -> None:
+ if hint is None:
+ hint = "unknown error"
+
+ super().__init__(hint)
+ self.ui_filename: str = format_filename(filename)
+ self.filename = filename
+
+ def format_message(self) -> str:
+ return f"Could not open file {self.ui_filename!r}: {self.message}"
+
+
+class Abort(RuntimeError):
+ """An internal signalling exception that signals Click to abort."""
+
+
+class Exit(RuntimeError):
+ """An exception that indicates that the application should exit with some
+ status code.
+ """
+
+ __slots__ = ("exit_code",)
+
+ def __init__(self, code: int = 0) -> None:
+ self.exit_code: int = code
diff --git a/typer/_click/formatting.py b/typer/_click/formatting.py
new file mode 100644
index 0000000000..b5eaab3bd0
--- /dev/null
+++ b/typer/_click/formatting.py
@@ -0,0 +1,272 @@
+from collections.abc import Iterable, Iterator, Sequence
+from contextlib import contextmanager
+
+from ._compat import term_len
+from .parser import _split_opt
+
+# Can force a width. This is used by the test system
+FORCED_WIDTH: int | None = None
+
+
+def measure_table(rows: Iterable[tuple[str, str]]) -> tuple[int, ...]:
+ widths: dict[int, int] = {}
+
+ for row in rows:
+ for idx, col in enumerate(row):
+ widths[idx] = max(widths.get(idx, 0), term_len(col))
+
+ return tuple(y for x, y in sorted(widths.items()))
+
+
+def iter_rows(
+ rows: Iterable[tuple[str, str]], col_count: int
+) -> Iterator[tuple[str, ...]]:
+ for row in rows:
+ yield row + ("",) * (col_count - len(row))
+
+
+def wrap_text(
+ text: str,
+ width: int = 78,
+ initial_indent: str = "",
+ subsequent_indent: str = "",
+ preserve_paragraphs: bool = False,
+) -> str:
+ """A helper function that intelligently wraps text. By default, it
+ assumes that it operates on a single paragraph of text but if the
+ `preserve_paragraphs` parameter is provided it will intelligently
+ handle paragraphs (defined by two empty lines).
+
+ If paragraphs are handled, a paragraph can be prefixed with an empty
+ line containing the ``\\b`` character (``\\x08``) to indicate that
+ no rewrapping should happen in that block.
+ """
+ from ._textwrap import TextWrapper
+
+ text = text.expandtabs()
+ wrapper = TextWrapper(
+ width,
+ initial_indent=initial_indent,
+ subsequent_indent=subsequent_indent,
+ replace_whitespace=False,
+ )
+ if not preserve_paragraphs:
+ return wrapper.fill(text)
+
+ p: list[tuple[int, bool, str]] = []
+ buf: list[str] = []
+ indent = None
+
+ def _flush_par() -> None:
+ if not buf:
+ return
+ if buf[0].strip() == "\b":
+ p.append((indent or 0, True, "\n".join(buf[1:])))
+ else:
+ p.append((indent or 0, False, " ".join(buf)))
+ del buf[:]
+
+ for line in text.splitlines():
+ if not line:
+ _flush_par()
+ indent = None
+ else:
+ if indent is None:
+ orig_len = term_len(line)
+ line = line.lstrip()
+ indent = orig_len - term_len(line)
+ buf.append(line)
+ _flush_par()
+
+ rv = []
+ for indent, raw, text in p:
+ with wrapper.extra_indent(" " * indent):
+ if raw:
+ rv.append(wrapper.indent_only(text))
+ else:
+ rv.append(wrapper.fill(text))
+
+ return "\n\n".join(rv)
+
+
+class HelpFormatter:
+ """This class helps with formatting text-based help pages. It's
+ usually just needed for very special internal cases, but it's also
+ exposed so that developers can write their own fancy outputs.
+
+ At present, it always writes into memory.
+ """
+
+ def __init__(
+ self,
+ indent_increment: int = 2,
+ width: int | None = None,
+ max_width: int | None = None,
+ ) -> None:
+ self.indent_increment = indent_increment
+ if max_width is None:
+ max_width = 80
+ if width is None:
+ import shutil
+
+ width = FORCED_WIDTH
+ if width is None:
+ width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
+ self.width = width
+ self.current_indent: int = 0
+ self.buffer: list[str] = []
+
+ def write(self, string: str) -> None:
+ """Writes a unicode string into the internal buffer."""
+ self.buffer.append(string)
+
+ def indent(self) -> None:
+ """Increases the indentation."""
+ self.current_indent += self.indent_increment
+
+ def dedent(self) -> None:
+ """Decreases the indentation."""
+ self.current_indent -= self.indent_increment
+
+ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None:
+ """Writes a usage line into the buffer."""
+ if prefix is None:
+ prefix = "Usage: "
+
+ usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
+ text_width = self.width - self.current_indent
+
+ if text_width >= (term_len(usage_prefix) + 20):
+ # The arguments will fit to the right of the prefix.
+ indent = " " * term_len(usage_prefix)
+ self.write(
+ wrap_text(
+ args,
+ text_width,
+ initial_indent=usage_prefix,
+ subsequent_indent=indent,
+ )
+ )
+ else:
+ # The prefix is too long, put the arguments on the next line.
+ self.write(usage_prefix)
+ self.write("\n")
+ indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
+ self.write(
+ wrap_text(
+ args, text_width, initial_indent=indent, subsequent_indent=indent
+ )
+ )
+
+ self.write("\n")
+
+ def write_heading(self, heading: str) -> None:
+ """Writes a heading into the buffer."""
+ self.write(f"{'':>{self.current_indent}}{heading}:\n")
+
+ def write_paragraph(self) -> None:
+ """Writes a paragraph into the buffer."""
+ if self.buffer:
+ self.write("\n")
+
+ def write_text(self, text: str) -> None:
+ """Writes re-indented text into the buffer. This rewraps and
+ preserves paragraphs.
+ """
+ indent = " " * self.current_indent
+ self.write(
+ wrap_text(
+ text,
+ self.width,
+ initial_indent=indent,
+ subsequent_indent=indent,
+ preserve_paragraphs=True,
+ )
+ )
+ self.write("\n")
+
+ def write_dl(
+ self,
+ rows: Sequence[tuple[str, str]],
+ col_max: int = 30,
+ col_spacing: int = 2,
+ ) -> None:
+ """Writes a definition list into the buffer. This is how options
+ and commands are usually formatted.
+ """
+ rows = list(rows)
+ widths = measure_table(rows)
+ if len(widths) != 2: # pragma: no cover
+ raise TypeError("Expected two columns for definition list")
+
+ first_col = min(widths[0], col_max) + col_spacing
+
+ for first, second in iter_rows(rows, len(widths)):
+ self.write(f"{'':>{self.current_indent}}{first}")
+ if not second:
+ self.write("\n")
+ continue
+ if term_len(first) <= first_col - col_spacing:
+ self.write(" " * (first_col - term_len(first)))
+ else:
+ self.write("\n")
+ self.write(" " * (first_col + self.current_indent))
+
+ text_width = max(self.width - first_col - 2, 10)
+ wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
+ lines = wrapped_text.splitlines()
+
+ if lines:
+ self.write(f"{lines[0]}\n")
+
+ for line in lines[1:]:
+ self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
+ else: # pragma: no cover
+ self.write("\n")
+
+ @contextmanager
+ def section(self, name: str) -> Iterator[None]:
+ """Helpful context manager that writes a paragraph, a heading,
+ and the indents.
+ """
+ self.write_paragraph()
+ self.write_heading(name)
+ self.indent()
+ try:
+ yield
+ finally:
+ self.dedent()
+
+ @contextmanager
+ def indentation(self) -> Iterator[None]:
+ """A context manager that increases the indentation."""
+ self.indent()
+ try:
+ yield
+ finally:
+ self.dedent()
+
+ def getvalue(self) -> str:
+ """Returns the buffer contents."""
+ return "".join(self.buffer)
+
+
+def join_options(options: Sequence[str]) -> tuple[str, bool]:
+ """Given a list of option strings this joins them in the most appropriate
+ way and returns them in the form ``(formatted_string,
+ any_prefix_is_slash)`` where the second item in the tuple is a flag that
+ indicates if any of the option prefixes was a slash.
+ """
+ rv = []
+ any_prefix_is_slash = False
+
+ for opt in options:
+ prefix = _split_opt(opt)[0]
+
+ if prefix == "/":
+ any_prefix_is_slash = True
+
+ rv.append((len(prefix), opt))
+
+ rv.sort(key=lambda x: x[0])
+ return ", ".join(x[1] for x in rv), any_prefix_is_slash
diff --git a/typer/_click/globals.py b/typer/_click/globals.py
new file mode 100644
index 0000000000..372dc40749
--- /dev/null
+++ b/typer/_click/globals.py
@@ -0,0 +1,61 @@
+from threading import local
+from typing import TYPE_CHECKING, Literal, Union, cast, overload
+
+if TYPE_CHECKING:
+ from .core import Context
+
+_local = local()
+
+
+@overload
+def get_current_context(silent: Literal[False] = False) -> "Context": ...
+
+
+@overload
+def get_current_context(silent: bool = ...) -> Union["Context", None]: ...
+
+
+def get_current_context(silent: bool = False) -> Union["Context", None]:
+ """Returns the current click context. This can be used as a way to
+ access the current context object from anywhere. This is a more implicit
+ alternative to the `pass_context` decorator. This function is
+ primarily useful for helpers such as `echo` which might be
+ interested in changing its behavior based on the current context.
+
+ To push the current context, `Context.scope` can be used.
+ """
+ try:
+ return cast("Context", _local.stack[-1])
+ except (AttributeError, IndexError) as e:
+ if not silent:
+ raise RuntimeError(
+ "There is no active click context."
+ ) from e # pragma: no cover
+
+ return None
+
+
+def push_context(ctx: "Context") -> None:
+ """Pushes a new context to the current stack."""
+ _local.__dict__.setdefault("stack", []).append(ctx)
+
+
+def pop_context() -> None:
+ """Removes the top level from the stack."""
+ _local.stack.pop()
+
+
+def resolve_color_default(color: bool | None = None) -> bool | None:
+ """Internal helper to get the default value of the color flag. If a
+ value is passed it's returned unchanged, otherwise it's looked up from
+ the current context.
+ """
+ if color is not None:
+ return color
+
+ ctx = get_current_context(silent=True)
+
+ if ctx is not None:
+ return ctx.color
+
+ return None
diff --git a/typer/_click/parser.py b/typer/_click/parser.py
new file mode 100644
index 0000000000..71eb3003cc
--- /dev/null
+++ b/typer/_click/parser.py
@@ -0,0 +1,459 @@
+"""
+This module started out as largely a copy paste from the stdlib's
+optparse module with the features removed that we do not need from
+optparse because we implement them in Click on a higher level (for
+instance type handling, help formatting and a lot more).
+
+The plan is to remove more and more from here over time.
+
+The reason this is a different module and not optparse from the stdlib
+is that there are differences in 2.x and 3.x about the error messages
+generated and optparse in the stdlib uses gettext for no good reason
+and might cause us issues.
+
+Click uses parts of optparse written by Gregory P. Ward and maintained
+by the Python Software Foundation. This is limited to code in parser.py.
+
+Copyright 2001-2006 Gregory P. Ward. All rights reserved.
+Copyright 2002-2006 Python Software Foundation. All rights reserved.
+"""
+
+# This code uses parts of optparse written by Gregory P. Ward and
+# maintained by the Python Software Foundation.
+# Copyright 2001-2006 Gregory P. Ward
+# Copyright 2002-2006 Python Software Foundation
+from collections import deque
+from collections.abc import Sequence
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ TypeVar,
+ Union,
+)
+
+from .exceptions import BadArgumentUsage, BadOptionUsage, NoSuchOption, UsageError
+
+if TYPE_CHECKING:
+ from typer.core import TyperArgument as CoreArgument
+ from typer.core import TyperOption as CoreOption
+
+ from .core import Context
+ from .core import Parameter as CoreParameter
+
+V = TypeVar("V")
+
+
+def _unpack_args(
+ args: Sequence[str], nargs_spec: Sequence[int]
+) -> tuple[Sequence[str | Sequence[str | None] | None], list[str]]:
+ """Given an iterable of arguments and an iterable of nargs specifications,
+ it returns a tuple with all the unpacked arguments at the first index
+ and all remaining arguments as the second.
+
+ The nargs specification is the number of arguments that should be consumed
+ or `-1` to indicate that this position should eat up all the remainders.
+ """
+ args = deque(args)
+ nargs_spec = deque(nargs_spec)
+ rv: list[str | tuple[str | None, ...] | None] = []
+ spos: int | None = None
+
+ def _fetch(c: deque[V]) -> V | None:
+ try:
+ if spos is None:
+ return c.popleft()
+ else:
+ return c.pop()
+ except IndexError:
+ return None
+
+ while nargs_spec:
+ nargs = _fetch(nargs_spec)
+ assert nargs is not None
+
+ if nargs == 1:
+ rv.append(_fetch(args))
+ elif nargs > 1:
+ x = [_fetch(args) for _ in range(nargs)]
+
+ # If we're reversed, we're pulling in the arguments in reverse,
+ # so we need to turn them around.
+ if spos is not None:
+ x.reverse()
+
+ rv.append(tuple(x))
+ elif nargs < 0:
+ if spos is not None: # pragma: no cover
+ raise TypeError("Cannot have two nargs < 0")
+
+ spos = len(rv)
+ rv.append(None)
+
+ # spos is the position of the wildcard (star). If it's not `None`,
+ # we fill it with the remainder.
+ if spos is not None:
+ rv[spos] = tuple(args)
+ args = []
+ rv[spos + 1 :] = reversed(rv[spos + 1 :])
+
+ return tuple(rv), list(args)
+
+
+def _split_opt(opt: str) -> tuple[str, str]:
+ first = opt[:1]
+ if first.isalnum():
+ return "", opt
+ if opt[1:2] == first:
+ return opt[:2], opt[2:]
+ return first, opt[1:]
+
+
+def _normalize_opt(opt: str, ctx: Union["Context", None]) -> str:
+ if ctx is None or ctx.token_normalize_func is None:
+ return opt
+ prefix, opt = _split_opt(opt)
+ return f"{prefix}{ctx.token_normalize_func(opt)}"
+
+
+class _Option:
+ def __init__(
+ self,
+ obj: "CoreOption",
+ opts: Sequence[str],
+ dest: str | None,
+ action: str = "store",
+ nargs: int = 1,
+ const: Any | None = None,
+ ):
+ self._short_opts = []
+ self._long_opts = []
+ self.prefixes: set[str] = set()
+
+ for opt in opts:
+ prefix, value = _split_opt(opt)
+ if not prefix: # pragma: no cover
+ raise ValueError(f"Invalid start character for option ({opt})")
+ self.prefixes.add(prefix[0])
+ if len(prefix) == 1 and len(value) == 1:
+ self._short_opts.append(opt)
+ else:
+ self._long_opts.append(opt)
+ self.prefixes.add(prefix)
+
+ self.dest = dest
+ self.action = action
+ self.nargs = nargs
+ self.const = const
+ self.obj = obj
+
+ @property
+ def takes_value(self) -> bool:
+ return self.action in ("store", "append")
+
+ def process(self, value: Any, state: "_ParsingState") -> None:
+ if self.action == "store":
+ state.opts[self.dest] = value # type: ignore
+ elif self.action == "store_const":
+ state.opts[self.dest] = self.const # type: ignore
+ elif self.action == "append":
+ state.opts.setdefault(self.dest, []).append(value) # type: ignore
+ elif self.action == "append_const":
+ state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
+ elif self.action == "count":
+ state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore
+ else: # pragma: no cover
+ raise ValueError(f"unknown action '{self.action}'")
+ state.order.append(self.obj)
+
+
+class _Argument:
+ def __init__(self, obj: "CoreArgument", dest: str | None, nargs: int = 1):
+ self.dest = dest
+ self.nargs = nargs
+ self.obj = obj
+
+ def process(
+ self,
+ value: str | Sequence[str | None] | None,
+ state: "_ParsingState",
+ ) -> None:
+ if self.nargs > 1:
+ assert value is not None
+ holes = sum(1 for x in value if x is None)
+ if holes == len(value):
+ value = None
+ elif holes != 0:
+ raise BadArgumentUsage(
+ f"Argument {self.dest!r} takes {self.nargs} values."
+ )
+
+ if self.nargs == -1 and self.obj.envvar is not None and value == ():
+ # Replace empty tuple with None so that a value from the
+ # environment may be tried.
+ value = None
+
+ state.opts[self.dest] = value # type: ignore
+ state.order.append(self.obj)
+
+
+class _ParsingState:
+ def __init__(self, rargs: list[str]) -> None:
+ self.opts: dict[str, Any] = {}
+ self.largs: list[str] = []
+ self.rargs = rargs
+ self.order: list[CoreParameter] = []
+
+
+class _OptionParser:
+ """The option parser is an internal class that is ultimately used to
+ parse options and arguments. It's modelled after optparse and brings
+ a similar but vastly simplified API. It should generally not be used
+ directly as the high level Click classes wrap it for you.
+
+ It's not nearly as extensible as optparse or argparse as it does not
+ implement features that are implemented on a higher level (such as
+ types or defaults).
+ """
+
+ def __init__(self, ctx: Union["Context", None] = None) -> None:
+ self.ctx = ctx
+ # This controls how the parser deals with interspersed arguments.
+ # If this is set to `False`, the parser will stop on the first
+ # non-option. Click uses this to implement nested subcommands
+ # safely.
+ self.allow_interspersed_args: bool = True
+ # This tells the parser how to deal with unknown options. By
+ # default it will error out (which is sensible), but there is a
+ # second mode where it will ignore it and continue processing
+ # after shifting all the unknown options into the resulting args.
+ self.ignore_unknown_options: bool = False
+
+ if ctx is not None:
+ self.allow_interspersed_args = ctx.allow_interspersed_args
+ self.ignore_unknown_options = ctx.ignore_unknown_options
+
+ self._short_opt: dict[str, _Option] = {}
+ self._long_opt: dict[str, _Option] = {}
+ self._opt_prefixes = {"-", "--"}
+ self._args: list[_Argument] = []
+
+ def add_option(
+ self,
+ obj: "CoreOption",
+ opts: Sequence[str],
+ dest: str | None,
+ action: str = "store",
+ nargs: int = 1,
+ const: Any | None = None,
+ ) -> None:
+ """Adds a new option named `dest` to the parser. The destination
+ is not inferred (unlike with optparse) and needs to be explicitly
+ provided. Action can be any of ``store``, ``store_const``,
+ ``append``, ``append_const`` or ``count``.
+
+ The `obj` can be used to identify the option in the order list
+ that is returned from the parser.
+ """
+ opts = [_normalize_opt(opt, self.ctx) for opt in opts]
+ option = _Option(obj, opts, dest, action=action, nargs=nargs, const=const)
+ self._opt_prefixes.update(option.prefixes)
+ for opt in option._short_opts:
+ self._short_opt[opt] = option
+ for opt in option._long_opts:
+ self._long_opt[opt] = option
+
+ def add_argument(
+ self, obj: "CoreArgument", dest: str | None, nargs: int = 1
+ ) -> None:
+ """Adds a positional argument named `dest` to the parser.
+
+ The `obj` can be used to identify the option in the order list
+ that is returned from the parser.
+ """
+ self._args.append(_Argument(obj, dest=dest, nargs=nargs))
+
+ def parse_args(
+ self, args: list[str]
+ ) -> tuple[dict[str, Any], list[str], list["CoreParameter"]]:
+ """Parses positional arguments and returns ``(values, args, order)``
+ for the parsed options and arguments as well as the leftover
+ arguments if there are any. The order is a list of objects as they
+ appear on the command line. If arguments appear multiple times they
+ will be memorized multiple times as well.
+ """
+ state = _ParsingState(args)
+ try:
+ self._process_args_for_options(state)
+ self._process_args_for_args(state)
+ except UsageError:
+ if self.ctx is None or not self.ctx.resilient_parsing:
+ raise
+ return state.opts, state.largs, state.order
+
+ def _process_args_for_args(self, state: _ParsingState) -> None:
+ pargs, args = _unpack_args(
+ state.largs + state.rargs, [x.nargs for x in self._args]
+ )
+
+ for idx, arg in enumerate(self._args):
+ arg.process(pargs[idx], state)
+
+ state.largs = args
+ state.rargs = []
+
+ def _process_args_for_options(self, state: _ParsingState) -> None:
+ while state.rargs:
+ arg = state.rargs.pop(0)
+ arglen = len(arg)
+ # Double dashes always handled explicitly regardless of what
+ # prefixes are valid.
+ if arg == "--":
+ return
+ elif arg[:1] in self._opt_prefixes and arglen > 1:
+ self._process_opts(arg, state)
+ elif self.allow_interspersed_args:
+ state.largs.append(arg)
+ else:
+ state.rargs.insert(0, arg)
+ return
+
+ # Say this is the original argument list:
+ # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
+ # ^
+ # (we are about to process arg(i)).
+ #
+ # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
+ # [arg0, ..., arg(i-1)] (any options and their arguments will have
+ # been removed from largs).
+ #
+ # The while loop will usually consume 1 or more arguments per pass.
+ # If it consumes 1 (eg. arg is an option that takes no arguments),
+ # then after _process_arg() is done the situation is:
+ #
+ # largs = subset of [arg0, ..., arg(i)]
+ # rargs = [arg(i+1), ..., arg(N-1)]
+ #
+ # If allow_interspersed_args is false, largs will always be
+ # *empty* -- still a subset of [arg0, ..., arg(i-1)], but
+ # not a very interesting subset!
+
+ def _match_long_opt(
+ self, opt: str, explicit_value: str | None, state: _ParsingState
+ ) -> None:
+ if opt not in self._long_opt:
+ from difflib import get_close_matches
+
+ possibilities = get_close_matches(opt, self._long_opt)
+ raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
+
+ option = self._long_opt[opt]
+ if option.takes_value:
+ # At this point it's safe to modify rargs by injecting the
+ # explicit value, because no exception is raised in this
+ # branch. This means that the inserted value will be fully
+ # consumed.
+ if explicit_value is not None:
+ state.rargs.insert(0, explicit_value)
+
+ value = self._get_value_from_state(opt, option, state)
+
+ elif explicit_value is not None: # pragma: no cover
+ raise BadOptionUsage(opt, f"Option {opt!r} does not take a value.")
+
+ else:
+ value = None
+
+ option.process(value, state)
+
+ def _match_short_opt(self, arg: str, state: _ParsingState) -> None:
+ stop = False
+ i = 1
+ prefix = arg[0]
+ unknown_options = []
+
+ for ch in arg[1:]:
+ opt = _normalize_opt(f"{prefix}{ch}", self.ctx)
+ option = self._short_opt.get(opt)
+ i += 1
+
+ if not option:
+ if self.ignore_unknown_options:
+ unknown_options.append(ch)
+ continue
+ raise NoSuchOption(opt, ctx=self.ctx)
+ if option.takes_value:
+ # Any characters left in arg? Pretend they're the
+ # next arg, and stop consuming characters of arg.
+ if i < len(arg):
+ state.rargs.insert(0, arg[i:])
+ stop = True
+
+ value = self._get_value_from_state(opt, option, state)
+
+ else:
+ value = None
+
+ option.process(value, state)
+
+ if stop:
+ break
+
+ # If we got any unknown options we recombine the string of the
+ # remaining options and re-attach the prefix, then report that
+ # to the state as new 'largs'. This way there is basic combinatorics
+ # that can be achieved while still ignoring unknown arguments.
+ if self.ignore_unknown_options and unknown_options:
+ state.largs.append(f"{prefix}{''.join(unknown_options)}")
+
+ def _get_value_from_state(
+ self, option_name: str, option: _Option, state: _ParsingState
+ ) -> str | Sequence[str]:
+ nargs = option.nargs
+
+ value: str | Sequence[str]
+
+ if len(state.rargs) < nargs:
+ msg = "an argument." if nargs == 1 else f"{nargs} arguments."
+ raise BadOptionUsage(
+ option_name,
+ f"Option {option_name!r} requires {msg}",
+ )
+ elif nargs == 1:
+ value = state.rargs.pop(0)
+ else:
+ value = tuple(state.rargs[:nargs])
+ del state.rargs[:nargs]
+
+ return value
+
+ def _process_opts(self, arg: str, state: _ParsingState) -> None:
+ explicit_value = None
+ # Long option handling happens in two parts. The first part is
+ # supporting explicitly attached values. In any case, we will try
+ # to long match the option first.
+ if "=" in arg:
+ long_opt, explicit_value = arg.split("=", 1)
+ else:
+ long_opt = arg
+ norm_long_opt = _normalize_opt(long_opt, self.ctx)
+
+ # At this point we will match the (assumed) long option through
+ # the long option matching code. Note that this allows options
+ # like "-foo" to be matched as long options.
+ try:
+ self._match_long_opt(norm_long_opt, explicit_value, state)
+ except NoSuchOption:
+ # At this point the long option matching failed, and we need
+ # to try with short options. However there is a special rule
+ # which says, that if we have a two character options prefix
+ # (applies to "--foo" for instance), we do not dispatch to the
+ # short option code and will instead raise the no option
+ # error.
+ if arg[:2] not in self._opt_prefixes:
+ self._match_short_opt(arg, state)
+ return
+
+ if not self.ignore_unknown_options:
+ raise
+
+ state.largs.append(arg)
diff --git a/typer/_click/py.typed b/typer/_click/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/typer/_click/shell_completion.py b/typer/_click/shell_completion.py
new file mode 100644
index 0000000000..4490f18810
--- /dev/null
+++ b/typer/_click/shell_completion.py
@@ -0,0 +1,306 @@
+import re
+from abc import ABC, abstractmethod
+from collections.abc import MutableMapping
+from typing import Any, ClassVar, TypeVar
+
+from .core import Command, Context, Parameter, ParameterSource
+
+
+class CompletionItem:
+ """Represents a completion value and metadata about the value. The
+ default metadata is ``type`` to indicate special shell handling,
+ and ``help`` if a shell supports showing a help string next to the
+ value.
+
+ Arbitrary parameters can be passed when creating the object, and
+ accessed using ``item.attr``. If an attribute wasn't passed,
+ accessing it returns ``None``.
+ """
+
+ __slots__ = ("value", "type", "help", "_info")
+
+ def __init__(
+ self,
+ value: Any,
+ type: str = "plain",
+ help: str | None = None,
+ **kwargs: Any,
+ ) -> None:
+ self.value: Any = value
+ self.type: str = type
+ self.help: str | None = help
+ self._info = kwargs
+
+ def __getattr__(self, name: str) -> Any:
+ return self._info.get(name)
+
+
+class ShellComplete(ABC):
+ """Base class for providing shell completion support. A subclass for
+ a given shell will override attributes and methods to implement the
+ completion instructions (``source`` and ``complete``).
+ """
+
+ name: ClassVar[str]
+ """Name to register the shell as with `add_completion_class`.
+ This is used in completion instructions (``{name}_source`` and
+ ``{name}_complete``).
+ """
+
+ source_template: ClassVar[str]
+ """Completion script template formatted by `source`. This must
+ be provided by subclasses.
+ """
+
+ def __init__(
+ self,
+ cli: Command,
+ ctx_args: MutableMapping[str, Any],
+ prog_name: str,
+ complete_var: str,
+ ) -> None:
+ self.cli = cli
+ self.ctx_args = ctx_args
+ self.prog_name = prog_name
+ self.complete_var = complete_var
+
+ @property
+ def func_name(self) -> str:
+ """The name of the shell function defined by the completion
+ script.
+ """
+ safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII)
+ return f"_{safe_name}_completion"
+
+ @abstractmethod
+ def source_vars(self) -> dict[str, Any]:
+ """Vars for formatting `source_template`."""
+ pass # pragma: no cover
+
+ def source(self) -> str:
+ """Produce the shell script that defines the completion
+ function. By default this ``%``-style formats
+ `source_template` with the dict returned by `source_vars`.
+ """
+ return self.source_template % self.source_vars()
+
+ @abstractmethod
+ def get_completion_args(self) -> tuple[list[str], str]:
+ """Use the env vars defined by the shell script to return a
+ tuple of ``args, incomplete``. This must be implemented by
+ subclasses.
+ """
+ pass # pragma: no cover
+
+ def get_completions(self, args: list[str], incomplete: str) -> list[CompletionItem]:
+ """Determine the context and last complete command or parameter
+ from the complete args. Call that object's ``shell_complete``
+ method to get the completions for the incomplete value.
+ """
+ ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
+ obj, incomplete = _resolve_incomplete(ctx, args, incomplete)
+ return obj.shell_complete(ctx, incomplete)
+
+ @abstractmethod
+ def format_completion(self, item: CompletionItem) -> str:
+ """Format a completion item into the form recognized by the
+ shell script. This must be implemented by subclasses.
+ """
+ pass # pragma: no cover
+
+ def complete(self) -> str:
+ """Produce the completion data to send back to the shell.
+
+ By default this calls `get_completion_args`, gets the
+ completions, then calls `format_completion` for each
+ completion.
+ """
+ args, incomplete = self.get_completion_args()
+ completions = self.get_completions(args, incomplete)
+ out = [self.format_completion(item) for item in completions]
+ return "\n".join(out)
+
+
+ShellCompleteType = TypeVar("ShellCompleteType", bound="type[ShellComplete]")
+
+
+_available_shells: dict[str, type[ShellComplete]] = {}
+
+
+def add_completion_class(cls: ShellCompleteType, name: str) -> ShellCompleteType:
+ """Register a `ShellComplete` subclass under the given name.
+ The name will be provided by the completion instruction environment
+ variable during completion.
+ """
+ _available_shells[name] = cls
+
+ return cls
+
+
+def get_completion_class(shell: str) -> type[ShellComplete] | None:
+ """Look up a registered `ShellComplete` subclass by the name
+ provided by the completion instruction environment variable. If the
+ name isn't registered, returns ``None``.
+ """
+ return _available_shells.get(shell)
+
+
+def split_arg_string(string: str) -> list[str]:
+ """Split an argument string as with `shlex.split`, but don't
+ fail if the string is incomplete. Ignores a missing closing quote or
+ incomplete escape sequence and uses the partial token as-is.
+ """
+ import shlex
+
+ lex = shlex.shlex(string, posix=True)
+ lex.whitespace_split = True
+ lex.commenters = ""
+ out = []
+
+ try:
+ for token in lex:
+ out.append(token)
+ except ValueError: # pragma: no cover
+ # Raised when end-of-string is reached in an invalid state. Use
+ # the partial token as-is. The quote or escape character is in
+ # lex.state, not lex.token.
+ out.append(lex.token)
+
+ return out
+
+
+def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
+ """Determine if the given parameter is an argument that can still
+ accept values.
+ """
+ # avoid circular imports
+ from ..core import TyperArgument
+
+ if not isinstance(param, TyperArgument):
+ return False
+
+ assert param.name is not None
+ # Will be None if expose_value is False.
+ value = ctx.params.get(param.name)
+ return (
+ param.nargs == -1
+ or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE
+ or (
+ param.nargs > 1
+ and isinstance(value, (tuple, list))
+ and len(value) < param.nargs
+ )
+ )
+
+
+def _start_of_option(ctx: Context, value: str) -> bool:
+ """Check if the value looks like the start of an option."""
+ if not value:
+ return False
+
+ c = value[0]
+ return c in ctx._opt_prefixes
+
+
+def _is_incomplete_option(ctx: Context, args: list[str], param: Parameter) -> bool:
+ """Determine if the given parameter is an option that needs a value."""
+ # avoid circular imports
+ from ..core import TyperOption
+
+ if not isinstance(param, TyperOption):
+ return False
+
+ if param.is_flag or param.count:
+ return False
+
+ last_option = None
+
+ for index, arg in enumerate(reversed(args)):
+ if index + 1 > param.nargs:
+ break
+
+ if _start_of_option(ctx, arg):
+ last_option = arg
+ break
+
+ return last_option is not None and last_option in param.opts
+
+
+def _resolve_context(
+ cli: Command,
+ ctx_args: MutableMapping[str, Any],
+ prog_name: str,
+ args: list[str],
+) -> Context:
+ """Produce the context hierarchy starting with the command and
+ traversing the complete arguments. This only follows the commands,
+ it doesn't trigger input prompts or callbacks.
+ """
+ # avoid circular imports
+ from ..core import TyperGroup
+
+ ctx_args["resilient_parsing"] = True
+ with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx:
+ args = ctx._protected_args + ctx.args
+
+ while args:
+ command = ctx.command
+
+ if isinstance(command, TyperGroup):
+ # if not command.chain:
+ name, cmd, args = command.resolve_command(ctx, args)
+
+ if cmd is None:
+ return ctx
+
+ with cmd.make_context(
+ name, args, parent=ctx, resilient_parsing=True
+ ) as sub_ctx:
+ ctx = sub_ctx
+ args = ctx._protected_args + ctx.args
+ else: # pragma: no cover
+ break
+
+ return ctx
+
+
+def _resolve_incomplete(
+ ctx: Context, args: list[str], incomplete: str
+) -> tuple[Command | Parameter, str]:
+ """Find the Click object that will handle the completion of the
+ incomplete value. Return the object and the incomplete value.
+ """
+ # Different shells treat an "=" between a long option name and
+ # value differently. Might keep the value joined, return the "="
+ # as a separate item, or return the split name and value. Always
+ # split and discard the "=" to make completion easier.
+ if incomplete == "=":
+ incomplete = ""
+ elif "=" in incomplete and _start_of_option(ctx, incomplete):
+ name, _, incomplete = incomplete.partition("=")
+ args.append(name)
+
+ # The "--" marker tells Click to stop treating values as options
+ # even if they start with the option character. If it hasn't been
+ # given and the incomplete arg looks like an option, the current
+ # command will provide option name completions.
+ if "--" not in args and _start_of_option(ctx, incomplete):
+ return ctx.command, incomplete
+
+ params = ctx.command.get_params(ctx)
+
+ # If the last complete arg is an option name with an incomplete
+ # value, the option will provide value completions.
+ for param in params:
+ if _is_incomplete_option(ctx, args, param):
+ return param, incomplete
+
+ # It's not an option name or value. The first argument without a
+ # parsed value will provide value completions.
+ for param in params:
+ if _is_incomplete_argument(ctx, param):
+ return param, incomplete
+
+ # There were no unparsed arguments, the command may be a group that
+ # will provide command name completions.
+ return ctx.command, incomplete
diff --git a/typer/_click/termui.py b/typer/_click/termui.py
new file mode 100644
index 0000000000..0a8c82574d
--- /dev/null
+++ b/typer/_click/termui.py
@@ -0,0 +1,430 @@
+import io
+from collections.abc import Callable, Iterable
+from contextlib import AbstractContextManager
+from typing import IO, TYPE_CHECKING, Any, AnyStr, TextIO, TypeVar, overload
+
+from .exceptions import Abort, UsageError
+from .globals import resolve_color_default
+from .types import ParamType, convert_type
+from .utils import LazyFile, echo
+
+if TYPE_CHECKING:
+ from ._termui_impl import ProgressBar
+
+V = TypeVar("V")
+
+# The prompt functions to use. The doc tools currently override these
+# functions to customize how they work.
+visible_prompt_func: Callable[[str], str] = input
+
+_ansi_colors = {
+ "black": 30,
+ "red": 31,
+ "green": 32,
+ "yellow": 33,
+ "blue": 34,
+ "magenta": 35,
+ "cyan": 36,
+ "white": 37,
+ "reset": 39,
+ "bright_black": 90,
+ "bright_red": 91,
+ "bright_green": 92,
+ "bright_yellow": 93,
+ "bright_blue": 94,
+ "bright_magenta": 95,
+ "bright_cyan": 96,
+ "bright_white": 97,
+}
+_ansi_reset_all = "\033[0m"
+
+
+def hidden_prompt_func(prompt: str) -> str:
+ import getpass
+
+ return getpass.getpass(prompt)
+
+
+def _build_prompt(
+ text: str,
+ suffix: str,
+ show_default: bool = False,
+ default: Any | None = None,
+ show_choices: bool = True,
+ type: ParamType | None = None,
+) -> str:
+ # prevent circular imports
+ from .._types import TyperChoice
+
+ prompt = text
+ if type is not None and show_choices and isinstance(type, TyperChoice):
+ prompt += f" ({', '.join(map(str, type.choices))})"
+ if default is not None and show_default:
+ prompt = f"{prompt} [{_format_default(default)}]"
+ return f"{prompt}{suffix}"
+
+
+def _format_default(default: Any) -> Any:
+ if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
+ return default.name
+
+ return default
+
+
+def prompt(
+ text: str,
+ default: Any | None = None,
+ hide_input: bool = False,
+ confirmation_prompt: bool | str = False,
+ type: ParamType | Any | None = None,
+ value_proc: Callable[[str], Any] | None = None,
+ prompt_suffix: str = ": ",
+ show_default: bool = True,
+ err: bool = False,
+ show_choices: bool = True,
+) -> Any:
+ """Prompts a user for input. This is a convenience function that can
+ be used to prompt a user for input later.
+
+ If the user aborts the input by sending an interrupt signal, this
+ function will catch it and raise an `Abort` exception.
+ """
+
+ def prompt_func(text: str) -> str:
+ f = hidden_prompt_func if hide_input else visible_prompt_func
+ try:
+ # Write the prompt separately so that we get nice
+ # coloring through colorama on Windows
+ echo(text[:-1], nl=False, err=err)
+ # Echo the last character to stdout to work around an issue where
+ # readline causes backspace to clear the whole line.
+ return f(text[-1:])
+ except (KeyboardInterrupt, EOFError): # pragma: no cover
+ # getpass doesn't print a newline if the user aborts input with ^C.
+ # Allegedly this behavior is inherited from getpass(3).
+ # A doc bug has been filed at https://bugs.python.org/issue24711
+ if hide_input:
+ echo(None, err=err)
+ raise Abort() from None
+
+ if value_proc is None:
+ value_proc = convert_type(type, default)
+
+ prompt = _build_prompt(
+ text, prompt_suffix, show_default, default, show_choices, type
+ )
+
+ if confirmation_prompt:
+ if confirmation_prompt is True:
+ confirmation_prompt = "Repeat for confirmation"
+
+ confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
+
+ while True:
+ while True:
+ value = prompt_func(prompt)
+ if value:
+ break
+ elif default is not None:
+ value = default
+ break
+ try:
+ result = value_proc(value)
+ except UsageError as e: # pragma: no cover
+ if hide_input:
+ echo("Error: The value you entered was invalid.", err=err)
+ else:
+ echo(f"Error: {e.message}", err=err)
+ continue
+ if not confirmation_prompt:
+ return result
+ while True:
+ value2 = prompt_func(confirmation_prompt)
+ is_empty = not value and not value2
+ if value2 or is_empty:
+ break
+ if value == value2:
+ return result
+ echo("Error: The two entered values do not match.", err=err)
+
+
+def confirm(
+ text: str,
+ default: bool | None = False,
+ abort: bool = False,
+ prompt_suffix: str = ": ",
+ show_default: bool = True,
+ err: bool = False,
+) -> bool:
+ """Prompts for confirmation (yes/no question).
+
+ If the user aborts the input by sending a interrupt signal this
+ function will catch it and raise an `Abort` exception.
+ """
+ prompt = _build_prompt(
+ text,
+ prompt_suffix,
+ show_default,
+ "y/n" if default is None else ("Y/n" if default else "y/N"),
+ )
+
+ while True:
+ try:
+ # Write the prompt separately so that we get nice
+ # coloring through colorama on Windows
+ echo(prompt[:-1], nl=False, err=err)
+ # Echo the last character to stdout to work around an issue where
+ # readline causes backspace to clear the whole line.
+ value = visible_prompt_func(prompt[-1:]).lower().strip()
+ except (KeyboardInterrupt, EOFError): # pragma: no cover
+ raise Abort() from None
+ if value in ("y", "yes"):
+ rv = True
+ elif value in ("n", "no"):
+ rv = False
+ elif default is not None and value == "":
+ rv = default
+ else: # pragma: no cover
+ echo("Error: invalid input", err=err)
+ continue
+ break
+ if abort and not rv:
+ raise Abort()
+ return rv
+
+
+@overload
+def progressbar(
+ *,
+ length: int,
+ label: str | None = None,
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ fill_char: str = "#",
+ empty_char: str = "-",
+ bar_template: str = "%(label)s [%(bar)s] %(info)s",
+ info_sep: str = " ",
+ width: int = 36,
+ file: TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+) -> "ProgressBar[int]": ...
+
+
+@overload
+def progressbar(
+ iterable: Iterable[V] | None = None,
+ length: int | None = None,
+ label: str | None = None,
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ item_show_func: Callable[[V | None], str | None] | None = None,
+ fill_char: str = "#",
+ empty_char: str = "-",
+ bar_template: str = "%(label)s [%(bar)s] %(info)s",
+ info_sep: str = " ",
+ width: int = 36,
+ file: TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+) -> "ProgressBar[V]": ...
+
+
+def progressbar(
+ iterable: Iterable[V] | None = None,
+ length: int | None = None,
+ label: str | None = None,
+ hidden: bool = False,
+ show_eta: bool = True,
+ show_percent: bool | None = None,
+ show_pos: bool = False,
+ item_show_func: Callable[[V | None], str | None] | None = None,
+ fill_char: str = "#",
+ empty_char: str = "-",
+ bar_template: str = "%(label)s [%(bar)s] %(info)s",
+ info_sep: str = " ",
+ width: int = 36,
+ file: TextIO | None = None,
+ color: bool | None = None,
+ update_min_steps: int = 1,
+) -> "ProgressBar[V]":
+ """This function creates an iterable context manager that can be used
+ to iterate over something while showing a progress bar. It will
+ either iterate over the `iterable` or `length` items (that are counted
+ up). While iteration happens, this function will print a rendered
+ progress bar to the given `file` (defaults to stdout) and will attempt
+ to calculate remaining time and more. By default, this progress bar
+ will not be rendered if the file is not a terminal.
+
+ The context manager creates the progress bar. When the context
+ manager is entered the progress bar is already created. With every
+ iteration over the progress bar, the iterable passed to the bar is
+ advanced and the bar is updated. When the context manager exits,
+ a newline is printed and the progress bar is finalized on screen.
+
+ Note: The progress bar is currently designed for use cases where the
+ total progress can be expected to take at least several seconds.
+ Because of this, the ProgressBar class object won't display
+ progress that is considered too fast, and progress where the time
+ between steps is less than a second.
+
+ No printing must happen or the progress bar will be unintentionally
+ destroyed.
+ """
+ from ._termui_impl import ProgressBar
+
+ color = resolve_color_default(color)
+ return ProgressBar(
+ iterable=iterable,
+ length=length,
+ hidden=hidden,
+ show_eta=show_eta,
+ show_percent=show_percent,
+ show_pos=show_pos,
+ item_show_func=item_show_func,
+ fill_char=fill_char,
+ empty_char=empty_char,
+ bar_template=bar_template,
+ info_sep=info_sep,
+ file=file,
+ label=label,
+ width=width,
+ color=color,
+ update_min_steps=update_min_steps,
+ )
+
+
+def _interpret_color(color: int | tuple[int, int, int] | str, offset: int = 0) -> str:
+ if isinstance(color, int):
+ return f"{38 + offset};5;{color:d}"
+
+ if isinstance(color, (tuple, list)):
+ r, g, b = color
+ return f"{38 + offset};2;{r:d};{g:d};{b:d}"
+
+ return str(_ansi_colors[color] + offset)
+
+
+def style(
+ text: Any,
+ fg: int | tuple[int, int, int] | str | None = None,
+ bg: int | tuple[int, int, int] | str | None = None,
+ bold: bool | None = None,
+ dim: bool | None = None,
+ underline: bool | None = None,
+ overline: bool | None = None,
+ italic: bool | None = None,
+ blink: bool | None = None,
+ reverse: bool | None = None,
+ strikethrough: bool | None = None,
+ reset: bool = True,
+) -> str:
+ """Styles a text with ANSI styles and returns the new string. By
+ default the styling is self contained which means that at the end
+ of the string a reset code is issued. This can be prevented by
+ passing ``reset=False``.
+ """
+ if not isinstance(text, str):
+ text = str(text)
+
+ bits = []
+
+ if fg:
+ try:
+ bits.append(f"\033[{_interpret_color(fg)}m")
+ except KeyError:
+ raise TypeError(f"Unknown color {fg!r}") from None
+
+ if bg:
+ try:
+ bits.append(f"\033[{_interpret_color(bg, 10)}m")
+ except KeyError:
+ raise TypeError(f"Unknown color {bg!r}") from None
+
+ if bold is not None:
+ bits.append(f"\033[{1 if bold else 22}m")
+ if dim is not None:
+ bits.append(f"\033[{2 if dim else 22}m")
+ if underline is not None:
+ bits.append(f"\033[{4 if underline else 24}m")
+ if overline is not None:
+ bits.append(f"\033[{53 if overline else 55}m")
+ if italic is not None:
+ bits.append(f"\033[{3 if italic else 23}m")
+ if blink is not None:
+ bits.append(f"\033[{5 if blink else 25}m")
+ if reverse is not None:
+ bits.append(f"\033[{7 if reverse else 27}m")
+ if strikethrough is not None:
+ bits.append(f"\033[{9 if strikethrough else 29}m")
+ bits.append(text)
+ if reset:
+ bits.append(_ansi_reset_all)
+ return "".join(bits)
+
+
+def secho(
+ message: Any | None = None,
+ file: IO[AnyStr] | None = None,
+ nl: bool = True,
+ err: bool = False,
+ color: bool | None = None,
+ **styles: Any,
+) -> None:
+ """This function combines `echo` and `style` into one call."""
+ if message is not None and not isinstance(message, (bytes, bytearray)):
+ message = style(message, **styles)
+
+ return echo(message, file=file, nl=nl, err=err, color=color)
+
+
+def launch(url: str, wait: bool = False, locate: bool = False) -> int:
+ """This function launches the given URL (or filename) in the default
+ viewer application for this file type. If this is an executable, it
+ might launch the executable in a new session. The return value is
+ the exit code of the launched application. Usually, ``0`` indicates
+ success.
+ """
+ from ._termui_impl import open_url
+
+ return open_url(url, wait=wait, locate=locate)
+
+
+# If this is provided, getchar() calls into this instead. This is used
+# for unittesting purposes.
+_getchar: Callable[[bool], str] | None = None
+
+
+def getchar(echo: bool = False) -> str:
+ """Fetches a single character from the terminal and returns it. This
+ will always return a unicode character and under certain rare
+ circumstances this might return more than one character. The
+ situations which more than one character is returned is when for
+ whatever reason multiple characters end up in the terminal buffer or
+ standard input was not actually a terminal.
+
+ Note that this will always read from the terminal, even if something
+ is piped into the standard input.
+
+ Note for Windows: in rare cases when typing non-ASCII characters, this
+ function might wait for a second character and then return both at once.
+ This is because certain Unicode characters look like special-key markers.
+ """
+ global _getchar
+
+ if _getchar is None:
+ from ._termui_impl import getchar as f
+
+ _getchar = f
+
+ return _getchar(echo)
+
+
+def raw_terminal() -> AbstractContextManager[int]:
+ from ._termui_impl import raw_terminal as f
+
+ return f()
diff --git a/typer/_click/types.py b/typer/_click/types.py
new file mode 100644
index 0000000000..5ccf15fe1b
--- /dev/null
+++ b/typer/_click/types.py
@@ -0,0 +1,695 @@
+import os
+import sys
+from collections.abc import Callable, Sequence
+from datetime import datetime
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ ClassVar,
+ Literal,
+ NoReturn,
+ TypedDict,
+ TypeGuard,
+ TypeVar,
+ Union,
+ cast,
+)
+
+from ._compat import _get_argv_encoding, open_stream
+from .exceptions import BadParameter
+from .utils import LazyFile, format_filename, safecall
+
+if TYPE_CHECKING:
+ from .core import Context, Parameter
+ from .shell_completion import CompletionItem
+
+ParamTypeValue = TypeVar("ParamTypeValue")
+
+
+class ParamType:
+ """Represents the type of a parameter. Validates and converts values
+ from the command line or Python into the correct type.
+
+ To implement a custom type, subclass and implement at least the
+ following:
+
+ - The `name` class attribute must be set.
+ - Calling an instance of the type with ``None`` must return
+ ``None``. This is already implemented by default.
+ - `convert` must convert string values to the correct type.
+ - `convert` must accept values that are already the correct
+ type.
+ - It must be able to convert a value if the ``ctx`` and ``param``
+ arguments are ``None``. This can occur when converting prompt
+ input.
+ """
+
+ is_composite: ClassVar[bool] = False
+ arity: ClassVar[int] = 1
+ name: str
+
+ # if a list of this type is expected and the value is pulled from a
+ # string environment variable, this is what splits it up. `None`
+ # means any whitespace. For all parameters the general rule is that
+ # whitespace splits them up. The exception are paths and files which
+ # are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
+ # Windows).
+ envvar_list_splitter: ClassVar[str | None] = None
+
+ def __call__(
+ self,
+ value: Any,
+ param: Union["Parameter", None] = None,
+ ctx: Union["Context", None] = None,
+ ) -> Any:
+ if value is not None:
+ return self.convert(value, param, ctx)
+
+ def get_metavar(self, param: "Parameter", ctx: "Context") -> str | None:
+ """Returns the metavar default for this param if it provides one."""
+ pass # pragma: no cover
+
+ def get_missing_message(
+ self, param: "Parameter", ctx: Union["Context", None]
+ ) -> str | None:
+ """Optionally might return extra information about a missing
+ parameter.
+ """
+ pass # pragma: no cover
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ pass # pragma: no cover
+
+ def split_envvar_value(self, rv: str) -> Sequence[str]:
+ """Given a value from an environment variable this splits it up
+ into small chunks depending on the defined envvar list splitter.
+
+ If the splitter is set to `None`, which means that whitespace splits,
+ then leading and trailing whitespace is ignored. Otherwise, leading
+ and trailing splitters usually lead to empty items being included.
+ """
+ return (rv or "").split(self.envvar_list_splitter)
+
+ def fail(
+ self,
+ message: str,
+ param: Union["Parameter", None] = None,
+ ctx: Union["Context", None] = None,
+ ) -> NoReturn:
+ """Helper method to fail with an invalid value message."""
+ raise BadParameter(message, ctx=ctx, param=param)
+
+ def shell_complete(
+ self, ctx: "Context", param: "Parameter", incomplete: str
+ ) -> list["CompletionItem"]:
+ """Return a list of `CompletionItem` objects for the
+ incomplete value. Most types do not provide completions, but
+ some do, and this allows custom types to provide custom
+ completions as well.
+ """
+ return []
+
+
+class CompositeParamType(ParamType):
+ is_composite = True
+
+ @property
+ def arity(self) -> int: # type: ignore
+ raise NotImplementedError() # pragma: no cover
+
+
+class FuncParamType(ParamType):
+ def __init__(self, func: Callable[[Any], Any]) -> None:
+ self.name: str = getattr(func, "__name__", "function")
+ self.func = func
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ try:
+ return self.func(value)
+ except ValueError:
+ try:
+ value = str(value)
+ except UnicodeError: # pragma: no cover
+ assert isinstance(value, bytes)
+ value = value.decode("utf-8", "replace")
+
+ self.fail(value, param, ctx)
+
+
+class StringParamType(ParamType):
+ name = "text"
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ if isinstance(value, bytes):
+ enc = _get_argv_encoding()
+ try:
+ value = value.decode(enc)
+ except UnicodeError:
+ fs_enc = sys.getfilesystemencoding()
+ if fs_enc != enc:
+ try:
+ value = value.decode(fs_enc)
+ except UnicodeError:
+ value = value.decode("utf-8", "replace")
+ else:
+ value = value.decode("utf-8", "replace")
+ return value
+ return str(value)
+
+ def __repr__(self) -> str:
+ return "STRING"
+
+
+class DateTime(ParamType):
+ """The DateTime type converts date strings into `datetime` objects.
+
+ The format strings which are checked are configurable, but default to some
+ common (non-timezone aware) ISO 8601 formats.
+
+ When specifying *DateTime* formats, you should only pass a list or a tuple.
+ Other iterables, like generators, may lead to surprising results.
+
+ The format strings are processed using ``datetime.strptime``, and this
+ consequently defines the format strings which are allowed.
+
+ Parsing is tried using each format, in order, and the first format which
+ parses successfully is used.
+ """
+
+ name = "datetime"
+
+ def __init__(self, formats: Sequence[str] | None = None):
+ self.formats: Sequence[str] = formats or [
+ "%Y-%m-%d",
+ "%Y-%m-%dT%H:%M:%S",
+ "%Y-%m-%d %H:%M:%S",
+ ]
+
+ def get_metavar(self, param: "Parameter", ctx: "Context") -> str | None:
+ return f"[{'|'.join(self.formats)}]"
+
+ def _try_to_convert_date(self, value: Any, format: str) -> datetime | None:
+ try:
+ return datetime.strptime(value, format)
+ except ValueError:
+ return None
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ if isinstance(value, datetime):
+ return value
+
+ for format in self.formats:
+ converted = self._try_to_convert_date(value, format)
+
+ if converted is not None:
+ return converted
+
+ formats_str = ", ".join(map(repr, self.formats))
+ self.fail(
+ f"{value!r} does not match the formats {formats_str}.",
+ param,
+ ctx,
+ )
+
+ def __repr__(self) -> str:
+ return "DateTime"
+
+
+class _NumberParamTypeBase(ParamType):
+ _number_class: ClassVar[type[Any]]
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ try:
+ return self._number_class(value)
+ except ValueError:
+ self.fail(
+ f"{value!r} is not a valid {self.name}.",
+ param,
+ ctx,
+ )
+
+
+class _NumberRangeBase(_NumberParamTypeBase):
+ def __init__(
+ self,
+ min: float | None = None,
+ max: float | None = None,
+ min_open: bool = False,
+ max_open: bool = False,
+ clamp: bool = False,
+ ) -> None:
+ self.min = min
+ self.max = max
+ self.min_open = min_open
+ self.max_open = max_open
+ self.clamp = clamp
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ import operator
+
+ rv = super().convert(value, param, ctx)
+ lt_min: bool = self.min is not None and (
+ operator.le if self.min_open else operator.lt
+ )(rv, self.min)
+ gt_max: bool = self.max is not None and (
+ operator.ge if self.max_open else operator.gt
+ )(rv, self.max)
+
+ if self.clamp:
+ if lt_min:
+ return self._clamp(self.min, 1, self.min_open) # type: ignore[arg-type]
+
+ if gt_max:
+ return self._clamp(self.max, -1, self.max_open) # type: ignore[arg-type]
+
+ if lt_min or gt_max:
+ self.fail(
+ f"{rv} is not in the range {self._describe_range()}.",
+ param,
+ ctx,
+ )
+
+ return rv
+
+ def _clamp(self, bound: float, dir: Literal[1, -1], open: bool) -> float:
+ """Find the valid value to clamp to bound in the given
+ direction.
+ """
+ raise NotImplementedError # pragma: no cover
+
+ def _describe_range(self) -> str:
+ """Describe the range for use in help text."""
+ if self.min is None:
+ op = "<" if self.max_open else "<="
+ return f"x{op}{self.max}"
+
+ if self.max is None:
+ op = ">" if self.min_open else ">="
+ return f"x{op}{self.min}"
+
+ lop = "<" if self.min_open else "<="
+ rop = "<" if self.max_open else "<="
+ return f"{self.min}{lop}x{rop}{self.max}"
+
+ def __repr__(self) -> str:
+ clamp = " clamped" if self.clamp else ""
+ return f"<{type(self).__name__} {self._describe_range()}{clamp}>"
+
+
+class IntParamType(_NumberParamTypeBase):
+ name = "integer"
+ _number_class = int
+
+ def __repr__(self) -> str:
+ return "INT"
+
+
+class IntRange(_NumberRangeBase, IntParamType):
+ """Restrict an `INT` value to a range of accepted values. See
+
+ If ``min`` or ``max`` are not passed, any value is accepted in that
+ direction. If ``min_open`` or ``max_open`` are enabled, the
+ corresponding boundary is not included in the range.
+
+ If ``clamp`` is enabled, a value outside the range is clamped to the
+ boundary instead of failing.
+ """
+
+ name = "integer range"
+
+ def _clamp( # type: ignore
+ self, bound: int, dir: Literal[1, -1], open: bool
+ ) -> int:
+ if not open:
+ return bound
+
+ return bound + dir
+
+
+class FloatParamType(_NumberParamTypeBase):
+ name = "float"
+ _number_class = float
+
+ def __repr__(self) -> str:
+ return "FLOAT"
+
+
+class FloatRange(_NumberRangeBase, FloatParamType):
+ """Restrict a `FLOAT` value to a range of accepted
+ values. See `ranges`.
+
+ If ``min`` or ``max`` are not passed, any value is accepted in that
+ direction. If ``min_open`` or ``max_open`` are enabled, the
+ corresponding boundary is not included in the range.
+
+ If ``clamp`` is enabled, a value outside the range is clamped to the
+ boundary instead of failing. This is not supported if either
+ boundary is marked ``open``.
+ """
+
+ name = "float range"
+
+ def __init__(
+ self,
+ min: float | None = None,
+ max: float | None = None,
+ min_open: bool = False,
+ max_open: bool = False,
+ clamp: bool = False,
+ ) -> None:
+ super().__init__(
+ min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
+ )
+
+ if (min_open or max_open) and clamp:
+ raise TypeError("Clamping is not supported for open bounds.")
+
+ def _clamp(self, bound: float, dir: Literal[1, -1], open: bool) -> float:
+ if not open:
+ return bound
+
+ # Could use math.nextafter here, but clamping an
+ # open float range doesn't seem to be particularly useful. It's
+ # left up to the user to write a callback to do it if needed.
+ raise RuntimeError(
+ "Clamping is not supported for open bounds."
+ ) # pragma: no cover
+
+
+class BoolParamType(ParamType):
+ name = "boolean"
+
+ bool_states: dict[str, bool] = {
+ "1": True,
+ "0": False,
+ "yes": True,
+ "no": False,
+ "true": True,
+ "false": False,
+ "on": True,
+ "off": False,
+ "t": True,
+ "f": False,
+ "y": True,
+ "n": False,
+ # Absence of value is considered False.
+ "": False,
+ }
+ """A mapping of string values to boolean states.
+
+ Mapping is inspired by `configparser.ConfigParser.BOOLEAN_STATES`
+ and extends it.
+ """
+
+ @staticmethod
+ def str_to_bool(value: str | bool) -> bool | None:
+ """Convert a string to a boolean value.
+
+ If the value is already a boolean, it is returned as-is. If the value is a
+ string, it is stripped of whitespaces and lower-cased, then checked against
+ the known boolean states pre-defined in the `BoolParamType.bool_states` mapping
+ above.
+
+ Returns `None` if the value does not match any known boolean state.
+ """
+ if isinstance(value, bool):
+ return value
+ return BoolParamType.bool_states.get(value.strip().lower())
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> bool:
+ normalized = self.str_to_bool(value)
+ if normalized is None:
+ states = ", ".join(sorted(self.bool_states))
+ self.fail(
+ f"{value!r} is not a valid boolean. Recognized values: {states}",
+ param,
+ ctx,
+ )
+ return normalized
+
+ def __repr__(self) -> str:
+ return "BOOL"
+
+
+class UUIDParameterType(ParamType):
+ name = "uuid"
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ import uuid
+
+ if isinstance(value, uuid.UUID):
+ return value
+
+ value = value.strip()
+
+ try:
+ return uuid.UUID(value)
+ except ValueError:
+ self.fail(f"{value!r} is not a valid UUID.", param, ctx)
+
+ def __repr__(self) -> str:
+ return "UUID"
+
+
+class File(ParamType):
+ """Declares a parameter to be a file for reading or writing. The file
+ is automatically closed once the context tears down (after the command
+ finished working).
+
+ Files can be opened for reading or writing. The special value ``-``
+ indicates stdin or stdout depending on the mode.
+
+ By default, the file is opened for reading text data, but it can also be
+ opened in binary mode or for writing. The encoding parameter can be used
+ to force a specific encoding.
+
+ The `lazy` flag controls if the file should be opened immediately or upon
+ first IO. The default is to be non-lazy for standard input and output
+ streams as well as files opened for reading, `lazy` otherwise. When opening a
+ file lazily for reading, it is still opened temporarily for validation, but
+ will not be held open until first IO. lazy is mainly useful when opening
+ for writing to avoid creating the file until it is needed.
+
+ Files can also be opened atomically in which case all writes go into a
+ separate file in the same folder and upon completion the file will
+ be moved over to the original location. This is useful if a file
+ regularly read by other users is modified.
+ """
+
+ name = "filename"
+ envvar_list_splitter: ClassVar[str] = os.path.pathsep
+
+ def __init__(
+ self,
+ mode: str = "r",
+ encoding: str | None = None,
+ errors: str | None = "strict",
+ lazy: bool | None = None,
+ atomic: bool = False,
+ ) -> None:
+ self.mode = mode
+ self.encoding = encoding
+ self.errors = errors
+ self.lazy = lazy
+ self.atomic = atomic
+
+ def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool:
+ if self.lazy is not None:
+ return self.lazy
+ if os.fspath(value) == "-":
+ return False
+ elif "w" in self.mode:
+ return True
+ return False
+
+ def convert(
+ self,
+ value: str | os.PathLike[str] | IO[Any],
+ param: Union["Parameter", None],
+ ctx: Union["Context", None],
+ ) -> IO[Any]:
+ if _is_file_like(value):
+ return value
+
+ value = cast("str | os.PathLike[str]", value)
+
+ try:
+ lazy = self.resolve_lazy_flag(value)
+
+ if lazy:
+ lf = LazyFile(
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+
+ if ctx is not None:
+ ctx.call_on_close(lf.close_intelligently)
+
+ return cast("IO[Any]", lf)
+
+ f, should_close = open_stream(
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+
+ # If a context is provided, we automatically close the file
+ # at the end of the context execution (or flush out). If a
+ # context does not exist, it's the caller's responsibility to
+ # properly close the file. This for instance happens when the
+ # type is used with prompts.
+ if ctx is not None:
+ if should_close:
+ ctx.call_on_close(safecall(f.close))
+ else:
+ ctx.call_on_close(safecall(f.flush))
+
+ return f
+ except OSError as e: # pragma: no cover
+ self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx)
+
+ def shell_complete(
+ self, ctx: "Context", param: "Parameter", incomplete: str
+ ) -> list["CompletionItem"]:
+ """Return a special completion marker that tells the completion
+ system to use the shell to provide file path completions.
+ """
+ from .shell_completion import CompletionItem
+
+ return [CompletionItem(incomplete, type="file")]
+
+
+def _is_file_like(value: Any) -> TypeGuard[IO[Any]]:
+ return hasattr(value, "read") or hasattr(value, "write")
+
+
+class Tuple(CompositeParamType):
+ """The default behavior of Click is to apply a type on a value directly.
+ This works well in most cases, except for when `nargs` is set to a fixed
+ count and different types should be used for different items. In this
+ case the `Tuple` type can be used. This type can only be used
+ if `nargs` is set to a fixed number.
+
+ For more information see `tuple-type`.
+
+ This can be selected by using a Python tuple literal as a type.
+ """
+
+ def __init__(self, types: Sequence[type[Any] | ParamType]) -> None:
+ self.types: Sequence[ParamType] = [convert_type(ty) for ty in types]
+
+ @property
+ def name(self) -> str: # type: ignore[override]
+ return f"<{' '.join(ty.name for ty in self.types)}>"
+
+ @property
+ def arity(self) -> int: # type: ignore
+ return len(self.types)
+
+ def convert(
+ self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
+ ) -> Any:
+ len_type = len(self.types)
+ len_value = len(value)
+
+ if len_value != len_type:
+ self.fail(
+ f"{len_type} values are required, but {len_value} given.",
+ param=param,
+ ctx=ctx,
+ )
+
+ return tuple(
+ ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False)
+ )
+
+
+def convert_type(ty: Any | None, default: Any | None = None) -> ParamType:
+ """Find the most appropriate `ParamType` for the given Python
+ type. If the type isn't provided, it can be inferred from a default
+ value.
+ """
+ guessed_type = False
+
+ if ty is None and default is not None:
+ if isinstance(default, (tuple, list)):
+ # If the default is empty, ty will remain None and will
+ # return STRING.
+ if default:
+ item = default[0]
+
+ # A tuple of tuples needs to detect the inner types.
+ # Can't call convert recursively because that would
+ # incorrectly unwind the tuple to a single type.
+ if isinstance(item, (tuple, list)):
+ ty = tuple(map(type, item))
+ else:
+ ty = type(item)
+ else:
+ ty = type(default)
+
+ guessed_type = True
+
+ if isinstance(ty, tuple):
+ return Tuple(ty)
+
+ if isinstance(ty, ParamType):
+ return ty
+
+ if ty is str or ty is None:
+ return STRING
+
+ if ty is int:
+ return INT
+
+ if ty is float:
+ return FLOAT
+
+ if ty is bool:
+ return BOOL
+
+ if guessed_type:
+ return STRING
+
+ return FuncParamType(ty)
+
+
+# A unicode string parameter type which is the implicit default. This
+# can also be selected by using ``str`` as type.
+STRING = StringParamType()
+
+# An integer parameter. This can also be selected by using ``int`` as
+# type.
+INT = IntParamType()
+
+# A floating point value parameter. This can also be selected by using
+# ``float`` as type.
+FLOAT = FloatParamType()
+
+# A boolean parameter. This is the default for boolean flags. This can
+# also be selected by using ``bool`` as a type.
+BOOL = BoolParamType()
+
+# A UUID parameter.
+UUID = UUIDParameterType()
+
+
+class OptionHelpExtra(TypedDict, total=False):
+ envvars: tuple[str, ...]
+ default: str
+ range: str
+ required: str
diff --git a/typer/_click/utils.py b/typer/_click/utils.py
new file mode 100644
index 0000000000..ac8e5ba3f2
--- /dev/null
+++ b/typer/_click/utils.py
@@ -0,0 +1,470 @@
+import os
+import re
+import sys
+from collections.abc import Callable, Iterable, Iterator
+from functools import update_wrapper
+from types import ModuleType, TracebackType
+from typing import (
+ IO,
+ Any,
+ AnyStr,
+ BinaryIO,
+ Literal,
+ ParamSpec,
+ TextIO,
+ TypeVar,
+ cast,
+)
+
+from ._compat import (
+ WIN,
+ _default_text_stderr,
+ _default_text_stdout,
+ _find_binary_writer,
+ auto_wrap_for_ansi,
+ binary_streams,
+ open_stream,
+ should_strip_ansi,
+ strip_ansi,
+ text_streams,
+)
+from .globals import resolve_color_default
+
+P = ParamSpec("P")
+R = TypeVar("R")
+
+
+def _posixify(name: str) -> str:
+ return "-".join(name.split()).lower()
+
+
+def safecall(func: Callable[P, R]) -> Callable[P, R | None]:
+ """Wraps a function so that it swallows exceptions."""
+
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
+ try:
+ return func(*args, **kwargs)
+ except Exception: # pragma: no cover
+ pass
+ return None # pragma: no cover
+
+ return update_wrapper(wrapper, func)
+
+
+def make_default_short_help(help: str, max_length: int = 45) -> str:
+ """Returns a condensed version of help string."""
+ # Consider only the first paragraph.
+ paragraph_end = help.find("\n\n")
+
+ if paragraph_end != -1:
+ help = help[:paragraph_end]
+
+ # Collapse newlines, tabs, and spaces.
+ words = help.split()
+
+ if not words:
+ return ""
+
+ # The first paragraph started with a "no rewrap" marker, ignore it.
+ if words[0] == "\b":
+ words = words[1:]
+
+ total_length = 0
+ last_index = len(words) - 1
+
+ for i, word in enumerate(words):
+ total_length += len(word) + (i > 0)
+
+ if total_length > max_length: # too long, truncate
+ break
+
+ if word[-1] == ".": # sentence end, truncate without "..."
+ return " ".join(words[: i + 1])
+
+ if total_length == max_length and i != last_index:
+ break # not at sentence end, truncate with "..."
+ else:
+ return " ".join(words) # no truncation needed
+
+ # Account for the length of the suffix.
+ total_length += len("...")
+
+ # remove words until the length is short enough
+ while i > 0:
+ total_length -= len(words[i]) + (i > 0)
+
+ if total_length <= max_length:
+ break
+
+ i -= 1
+
+ return " ".join(words[:i]) + "..."
+
+
+class LazyFile:
+ """A lazy file works like a regular file but it does not fully open
+ the file but it does perform some basic checks early to see if the
+ filename parameter does make sense. This is useful for safely opening
+ files for writing.
+ """
+
+ def __init__(
+ self,
+ filename: str | os.PathLike[str],
+ mode: str = "r",
+ encoding: str | None = None,
+ errors: str | None = "strict",
+ atomic: bool = False,
+ ):
+ self.name: str = os.fspath(filename)
+ self.mode = mode
+ self.encoding = encoding
+ self.errors = errors
+ self.atomic = atomic
+ self._f: IO[Any] | None
+ self.should_close: bool
+
+ if self.name == "-":
+ self._f, self.should_close = open_stream(filename, mode, encoding, errors)
+ else:
+ if "r" in mode:
+ # Open and close the file in case we're opening it for
+ # reading so that we can catch at least some errors in
+ # some cases early.
+ open(filename, mode).close()
+ self._f = None
+ self.should_close = True
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self.open(), name)
+
+ def __repr__(self) -> str:
+ if self._f is not None:
+ return repr(self._f)
+ return f""
+
+ def open(self) -> IO[Any]:
+ """Opens the file if it's not yet open. This call might fail with
+ a `FileError`. Not handling this error will produce an error
+ that Click shows.
+ """
+ if self._f is not None:
+ return self._f
+ try:
+ rv, self.should_close = open_stream(
+ self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
+ )
+ except OSError as e:
+ from .exceptions import FileError
+
+ raise FileError(self.name, hint=e.strerror) from e
+ self._f = rv
+ return rv
+
+ def close(self) -> None:
+ """Closes the underlying file, no matter what."""
+ if self._f is not None:
+ self._f.close()
+
+ def close_intelligently(self) -> None:
+ """This function only closes the file if it was opened by the lazy
+ file wrapper. For instance this will never close stdin.
+ """
+ if self.should_close:
+ self.close()
+
+ def __enter__(self) -> "LazyFile":
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ self.close_intelligently()
+
+ def __iter__(self) -> Iterator[AnyStr]:
+ self.open()
+ return iter(self._f) # type: ignore
+
+
+def echo(
+ message: Any | None = None,
+ file: IO[Any] | None = None,
+ nl: bool = True,
+ err: bool = False,
+ color: bool | None = None,
+) -> None:
+ """Print a message and newline to stdout or a file. This should be
+ used instead of `print` because it provides better support
+ for different data, files, and environments.
+
+ Compared to `print`, this does the following:
+
+ - Ensures that the output encoding is not misconfigured on Linux.
+ - Supports Unicode in the Windows console.
+ - Supports writing to binary outputs, and supports writing bytes
+ to text outputs.
+ - Supports colors and styles on Windows.
+ - Removes ANSI color and style codes if the output does not look
+ like an interactive terminal.
+ - Always flushes the output.
+ """
+ if file is None:
+ if err:
+ file = _default_text_stderr()
+ else:
+ file = _default_text_stdout()
+
+ # There are no standard streams attached to write to. For example,
+ # pythonw on Windows.
+ if file is None:
+ return
+
+ # Convert non bytes/text into the native string type.
+ if message is not None and not isinstance(message, (str, bytes, bytearray)):
+ out: str | bytes | bytearray | None = str(message)
+ else:
+ out = message
+
+ if nl:
+ out = out or ""
+ if isinstance(out, str):
+ out += "\n"
+ else:
+ out += b"\n"
+
+ if not out:
+ file.flush()
+ return
+
+ # If there is a message and the value looks like bytes, we manually
+ # need to find the binary stream and write the message in there.
+ # This is done separately so that most stream types will work as you
+ # would expect. Eg: you can write to StringIO for other cases.
+ if isinstance(out, (bytes, bytearray)):
+ binary_file = _find_binary_writer(file)
+
+ if binary_file is not None:
+ file.flush()
+ binary_file.write(out)
+ binary_file.flush()
+ return
+
+ # ANSI style code support. For no message or bytes, nothing happens.
+ # When outputting to a file instead of a terminal, strip codes.
+ else:
+ color = resolve_color_default(color)
+
+ if should_strip_ansi(file, color):
+ out = strip_ansi(out)
+ elif WIN:
+ if auto_wrap_for_ansi is not None:
+ file = auto_wrap_for_ansi(file, color) # type: ignore[arg-type,call-arg]
+ elif not color:
+ out = strip_ansi(out)
+
+ file.write(out) # type: ignore[arg-type]
+ file.flush()
+
+
+def get_binary_stream(name: Literal["stdin", "stdout", "stderr"]) -> BinaryIO:
+ """Returns a system stream for byte processing."""
+ opener = binary_streams.get(name)
+ if opener is None:
+ raise TypeError(f"Unknown standard stream '{name}'")
+ return opener()
+
+
+def get_text_stream(
+ name: Literal["stdin", "stdout", "stderr"],
+ encoding: str | None = None,
+ errors: str | None = "strict",
+) -> TextIO:
+ """Returns a system stream for text processing. This usually returns
+ a wrapped stream around a binary stream returned from
+ `get_binary_stream` but it also can take shortcuts for already
+ correctly configured streams.
+ """
+ opener = text_streams.get(name)
+ if opener is None:
+ raise TypeError(f"Unknown standard stream '{name}'")
+ return opener(encoding, errors)
+
+
+def format_filename(
+ filename: str | bytes | os.PathLike[str] | os.PathLike[bytes],
+ shorten: bool = False,
+) -> str:
+ """Format a filename as a string for display. Ensures the filename can be
+ displayed by replacing any invalid bytes or surrogate escapes in the name
+ with the replacement character ``�``.
+
+ Invalid bytes or surrogate escapes will raise an error when written to a
+ stream with ``errors="strict"``. This will typically happen with ``stdout``
+ when the locale is something like ``en_GB.UTF-8``.
+
+ Many scenarios *are* safe to write surrogates though, due to PEP 538 and
+ PEP 540, including:
+
+ - Writing to ``stderr``, which uses ``errors="backslashreplace"``.
+ - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens
+ stdout and stderr with ``errors="surrogateescape"``.
+ - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``.
+ - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``.
+ Python opens stdout and stderr with ``errors="surrogateescape"``.
+ """
+ if shorten:
+ filename = os.path.basename(filename)
+ else:
+ filename = os.fspath(filename)
+
+ if isinstance(filename, bytes):
+ filename = filename.decode(sys.getfilesystemencoding(), "replace")
+ else:
+ filename = filename.encode("utf-8", "surrogateescape").decode(
+ "utf-8", "replace"
+ )
+
+ return filename
+
+
+def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str:
+ r"""Returns the config folder for the application. The default behavior
+ is to return whatever is most appropriate for the operating system.
+
+ To give you an idea, for an app called ``"Foo Bar"``, something like
+ the following folders could be returned:
+
+ Mac OS X:
+ ``~/Library/Application Support/Foo Bar``
+ Mac OS X (POSIX):
+ ``~/.foo-bar``
+ Unix:
+ ``~/.config/foo-bar``
+ Unix (POSIX):
+ ``~/.foo-bar``
+ Windows (roaming):
+ ``C:\Users\\AppData\Roaming\Foo Bar``
+ Windows (not roaming):
+ ``C:\Users\\AppData\Local\Foo Bar``
+ """
+ if WIN:
+ key = "APPDATA" if roaming else "LOCALAPPDATA"
+ folder = os.environ.get(key)
+ if folder is None:
+ folder = os.path.expanduser("~")
+ return os.path.join(folder, app_name)
+ if force_posix:
+ return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}"))
+ if sys.platform == "darwin":
+ return os.path.join(
+ os.path.expanduser("~/Library/Application Support"), app_name
+ )
+ return os.path.join(
+ os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
+ _posixify(app_name),
+ )
+
+
+class PacifyFlushWrapper:
+ """This wrapper is used to catch and suppress BrokenPipeErrors resulting
+ from ``.flush()`` being called on broken pipe during the shutdown/final-GC
+ of the Python interpreter. Notably ``.flush()`` is always called on
+ ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
+ other cleanup code, and the case where the underlying file is not a broken
+ pipe, all calls and attributes are proxied.
+ """
+
+ def __init__(self, wrapped: IO[Any]) -> None:
+ self.wrapped = wrapped
+
+ def flush(self) -> None:
+ try:
+ self.wrapped.flush()
+ except OSError as e: # pragma: no cover
+ import errno
+
+ if e.errno != errno.EPIPE:
+ raise
+
+ def __getattr__(self, attr: str) -> Any:
+ return getattr(self.wrapped, attr)
+
+
+def _detect_program_name(
+ path: str | None = None, _main: ModuleType | None = None
+) -> str:
+ """Determine the command used to run the program, for use in help
+ text. If a file or entry point was executed, the file name is
+ returned. If ``python -m`` was used to execute a module or package,
+ ``python -m name`` is returned.
+
+ This doesn't try to be too precise, the goal is to give a concise
+ name for help text. Files are only shown as their name without the
+ path. ``python`` is only shown for modules, and the full path to
+ ``sys.executable`` is not shown.
+ """
+ if _main is None:
+ _main = sys.modules["__main__"]
+
+ if not path:
+ path = sys.argv[0]
+
+ # The value of __package__ indicates how Python was called. It may
+ # not exist if a setuptools script is installed as an egg. It may be
+ # set incorrectly for entry points created with pip on Windows.
+ # It is set to "" inside a Shiv or PEX zipapp.
+ if getattr(_main, "__package__", None) in {None, ""} or (
+ os.name == "nt"
+ and _main.__package__ == ""
+ and not os.path.exists(path)
+ and os.path.exists(f"{path}.exe")
+ ):
+ # Executed a file, like "python app.py".
+ return os.path.basename(path)
+
+ # Executed a module, like "python -m example".
+ # Rewritten by Python from "-m script" to "/path/to/script.py".
+ # Need to look at main module to determine how it was executed.
+ py_module = cast(str, _main.__package__)
+ name = os.path.splitext(os.path.basename(path))[0]
+
+ # A submodule like "example.cli".
+ if name != "__main__":
+ py_module = f"{py_module}.{name}"
+
+ return f"python -m {py_module.lstrip('.')}"
+
+
+def _expand_args(
+ args: Iterable[str],
+ *,
+ user: bool = True,
+ env: bool = True,
+ glob_recursive: bool = True,
+) -> list[str]:
+ """Simulate Unix shell expansion with Python functions."""
+ from glob import glob
+
+ out = []
+
+ for arg in args:
+ if user:
+ arg = os.path.expanduser(arg)
+
+ if env:
+ arg = os.path.expandvars(arg)
+
+ try:
+ matches = glob(arg, recursive=glob_recursive)
+ except re.error: # pragma: no cover
+ matches = []
+
+ if not matches:
+ out.append(arg)
+ else:
+ out.extend(matches)
+
+ return out
diff --git a/typer/_completion_classes.py b/typer/_completion_classes.py
index 8548fb4d6a..cfae02c9bf 100644
--- a/typer/_completion_classes.py
+++ b/typer/_completion_classes.py
@@ -4,11 +4,9 @@
import sys
from typing import Any
-import click
-import click.parser
-import click.shell_completion
-from click.shell_completion import split_arg_string as click_split_arg_string
-
+from . import _click
+from ._click.shell_completion import CompletionItem, ShellComplete, add_completion_class
+from ._click.shell_completion import split_arg_string as click_split_arg_string
from ._completion_shared import (
COMPLETION_SCRIPT_BASH,
COMPLETION_SCRIPT_FISH,
@@ -27,7 +25,7 @@ def _sanitize_help_text(text: str) -> str:
return rich_utils.rich_render_text(text)
-class BashComplete(click.shell_completion.BashComplete):
+class BashComplete(ShellComplete):
name = Shells.bash.value
source_template = COMPLETION_SCRIPT_BASH
@@ -50,7 +48,7 @@ def get_completion_args(self) -> tuple[list[str], str]:
return args, incomplete
- def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
+ def format_completion(self, item: CompletionItem) -> str:
# TODO: Explore replicating the new behavior from Click, with item types and
# triggering completion for files and directories
# return f"{item.type},{item.value}"
@@ -62,8 +60,42 @@ def complete(self) -> str:
out = [self.format_completion(item) for item in completions]
return "\n".join(out)
+ @staticmethod
+ def _check_version() -> None:
+ import shutil
+ import subprocess
+
+ bash_exe = shutil.which("bash")
+
+ if bash_exe is None:
+ match = None # pragma: no cover
+ else:
+ output = subprocess.run(
+ [bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'],
+ stdout=subprocess.PIPE,
+ )
+ match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())
+
+ if match is not None:
+ major, minor = match.groups()
+
+ if major < "4" or major == "4" and minor < "4":
+ _click.utils.echo(
+ "Shell completion is not supported for Bash versions older than 4.4.",
+ err=True,
+ )
+ else:
+ _click.utils.echo(
+ "Couldn't detect Bash version, shell completion is not supported.",
+ err=True,
+ ) # pragma: no cover
+
+ def source(self) -> str:
+ self._check_version()
+ return super().source()
+
-class ZshComplete(click.shell_completion.ZshComplete):
+class ZshComplete(ShellComplete):
name = Shells.zsh.value
source_template = COMPLETION_SCRIPT_ZSH
@@ -85,7 +117,7 @@ def get_completion_args(self) -> tuple[list[str], str]:
incomplete = ""
return args, incomplete
- def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
+ def format_completion(self, item: CompletionItem) -> str:
def escape(s: str) -> str:
return (
s.replace('"', '""')
@@ -114,7 +146,7 @@ def complete(self) -> str:
return "_files"
-class FishComplete(click.shell_completion.FishComplete):
+class FishComplete(ShellComplete):
name = Shells.fish.value
source_template = COMPLETION_SCRIPT_FISH
@@ -136,7 +168,7 @@ def get_completion_args(self) -> tuple[list[str], str]:
incomplete = ""
return args, incomplete
- def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
+ def format_completion(self, item: CompletionItem) -> str:
# TODO: Explore replicating the new behavior from Click, pay attention to
# the difference with and without formatted help
# if item.help:
@@ -167,7 +199,7 @@ def complete(self) -> str:
return "" # pragma: no cover
-class PowerShellComplete(click.shell_completion.ShellComplete):
+class PowerShellComplete(ShellComplete):
name = Shells.powershell.value
source_template = COMPLETION_SCRIPT_POWER_SHELL
@@ -185,15 +217,13 @@ def get_completion_args(self) -> tuple[list[str], str]:
args = cwords[1:-1] if incomplete else cwords[1:]
return args, incomplete
- def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
+ def format_completion(self, item: CompletionItem) -> str:
return f"{item.value}:::{_sanitize_help_text(item.help) if item.help else ' '}"
def completion_init() -> None:
- click.shell_completion.add_completion_class(BashComplete, Shells.bash.value)
- click.shell_completion.add_completion_class(ZshComplete, Shells.zsh.value)
- click.shell_completion.add_completion_class(FishComplete, Shells.fish.value)
- click.shell_completion.add_completion_class(
- PowerShellComplete, Shells.powershell.value
- )
- click.shell_completion.add_completion_class(PowerShellComplete, Shells.pwsh.value)
+ add_completion_class(BashComplete, Shells.bash.value)
+ add_completion_class(ZshComplete, Shells.zsh.value)
+ add_completion_class(FishComplete, Shells.fish.value)
+ add_completion_class(PowerShellComplete, Shells.powershell.value)
+ add_completion_class(PowerShellComplete, Shells.pwsh.value)
diff --git a/typer/_completion_shared.py b/typer/_completion_shared.py
index 5a81dcf68c..8d2c19715c 100644
--- a/typer/_completion_shared.py
+++ b/typer/_completion_shared.py
@@ -4,9 +4,11 @@
from enum import Enum
from pathlib import Path
-import click
import shellingham
+from . import _click
+from ._click.globals import get_current_context
+
class Shells(str, Enum):
bash = "bash"
@@ -78,8 +80,8 @@ def get_completion_script(*, prog_name: str, complete_var: str, shell: str) -> s
cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
script = _completion_scripts.get(shell)
if script is None:
- click.echo(f"Shell {shell} not supported.", err=True)
- raise click.exceptions.Exit(1)
+ _click.echo(f"Shell {shell} not supported.", err=True)
+ raise _click.exceptions.Exit(1)
return (
script
% {
@@ -172,8 +174,8 @@ def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path
stdout=subprocess.PIPE,
)
if result.returncode != 0: # pragma: no cover
- click.echo("Couldn't get PowerShell user profile", err=True)
- raise click.exceptions.Exit(result.returncode)
+ _click.echo("Couldn't get PowerShell user profile", err=True)
+ raise _click.exceptions.Exit(result.returncode)
path_str = ""
if isinstance(result.stdout, str): # pragma: no cover
path_str = result.stdout
@@ -185,8 +187,8 @@ def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path
except UnicodeDecodeError: # pragma: no cover
pass
if not path_str: # pragma: no cover
- click.echo("Couldn't decode the path automatically", err=True)
- raise click.exceptions.Exit(1)
+ _click.echo("Couldn't decode the path automatically", err=True)
+ raise _click.exceptions.Exit(1)
path_obj = Path(path_str.strip())
parent_dir: Path = path_obj.parent
parent_dir.mkdir(parents=True, exist_ok=True)
@@ -203,7 +205,7 @@ def install(
prog_name: str | None = None,
complete_var: str | None = None,
) -> tuple[str, Path]:
- prog_name = prog_name or click.get_current_context().find_root().info_name
+ prog_name = prog_name or get_current_context().find_root().info_name
assert prog_name
if complete_var is None:
complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
@@ -231,8 +233,8 @@ def install(
)
return shell, installed_path
else:
- click.echo(f"Shell {shell} is not supported.")
- raise click.exceptions.Exit(1)
+ _click.echo(f"Shell {shell} is not supported.")
+ raise _click.exceptions.Exit(1)
def _get_shell_name() -> str | None:
diff --git a/typer/_types.py b/typer/_types.py
index dc9fc63220..09b38afb3f 100644
--- a/typer/_types.py
+++ b/typer/_types.py
@@ -1,21 +1,42 @@
+from collections.abc import Iterable, Mapping, Sequence
from enum import Enum
-from typing import TypeVar
+from typing import Any, Generic, TypeVar
-import click
+from . import _click
+from ._click import types
+from ._click.shell_completion import CompletionItem
ParamTypeValue = TypeVar("ParamTypeValue")
-class TyperChoice(click.Choice[ParamTypeValue]):
+class TyperChoice(types.ParamType, Generic[ParamTypeValue]):
+ # Code adapted from Click 8.3.1, with Typer using enum values in normalize_choice
+ name = "choice"
+
+ def __init__(
+ self, choices: Iterable[ParamTypeValue], case_sensitive: bool = True
+ ) -> None:
+ self.choices: Sequence[ParamTypeValue] = tuple(choices)
+ self.case_sensitive = case_sensitive
+
+ def _normalized_mapping(
+ self, ctx: _click.Context | None = None
+ ) -> Mapping[ParamTypeValue, str]:
+ """
+ Returns mapping where keys are the original choices and the values are
+ the normalized values that are accepted via the command line.
+ """
+ return {
+ choice: self.normalize_choice(
+ choice=choice,
+ ctx=ctx,
+ )
+ for choice in self.choices
+ }
+
def normalize_choice(
- self, choice: ParamTypeValue, ctx: click.Context | None
+ self, choice: ParamTypeValue, ctx: _click.Context | None
) -> str:
- # Click 8.2.0 added a new method `normalize_choice` to the `Choice` class
- # to support enums, but it uses the enum names, while Typer has always used the
- # enum values.
- # This class overrides that method to maintain the previous behavior.
- # In Click:
- # normed_value = choice.name if isinstance(choice, Enum) else str(choice)
normed_value = str(choice.value) if isinstance(choice, Enum) else str(choice)
if ctx is not None and ctx.token_normalize_func is not None:
@@ -25,3 +46,75 @@ def normalize_choice(
normed_value = normed_value.casefold()
return normed_value
+
+ def get_metavar(self, param: _click.Parameter, ctx: _click.Context) -> str | None:
+ if param.param_type_name == "option" and not param.show_choices: # type: ignore
+ choice_metavars = [
+ types.convert_type(type(choice)).name.upper() for choice in self.choices
+ ]
+ choices_str = "|".join([*dict.fromkeys(choice_metavars)])
+ else:
+ choices_str = "|".join(
+ [str(i) for i in self._normalized_mapping(ctx=ctx).values()]
+ )
+
+ # Use curly braces to indicate a required argument.
+ if param.required and param.param_type_name == "argument":
+ return f"{{{choices_str}}}"
+
+ # Use square braces to indicate an option or optional argument.
+ return f"[{choices_str}]"
+
+ def get_missing_message(
+ self, param: _click.Parameter, ctx: _click.Context | None
+ ) -> str:
+ """Message shown when no choice is passed."""
+ choices = ",\n\t".join(self._normalized_mapping(ctx=ctx).values())
+ return f"Choose from:\n\t{choices}"
+
+ def convert(
+ self, value: Any, param: _click.Parameter | None, ctx: _click.Context | None
+ ) -> ParamTypeValue:
+ """
+ For a given value from the parser, normalize it and find its
+ matching normalized value in the list of choices. Then return the
+ matched "original" choice.
+ """
+ normed_value = self.normalize_choice(choice=value, ctx=ctx)
+ normalized_mapping = self._normalized_mapping(ctx=ctx)
+
+ try:
+ return next(
+ original
+ for original, normalized in normalized_mapping.items()
+ if normalized == normed_value
+ )
+ except StopIteration:
+ self.fail(
+ self.get_invalid_choice_message(value=value, ctx=ctx),
+ param=param,
+ ctx=ctx,
+ )
+
+ def get_invalid_choice_message(self, value: Any, ctx: _click.Context | None) -> str:
+ """Get the error message when the given choice is invalid."""
+ choices_str = ", ".join(map(repr, self._normalized_mapping(ctx=ctx).values()))
+ return f"{value!r} is not one of {choices_str}."
+
+ def __repr__(self) -> str:
+ return f"Choice({list(self.choices)})"
+
+ def shell_complete(
+ self, ctx: _click.Context, param: _click.Parameter, incomplete: str
+ ) -> list[CompletionItem]:
+ """Complete choices that start with the incomplete value."""
+
+ str_choices = map(str, self.choices)
+
+ if self.case_sensitive:
+ matched = (c for c in str_choices if c.startswith(incomplete))
+ else:
+ incomplete = incomplete.lower()
+ matched = (c for c in str_choices if c.lower().startswith(incomplete))
+
+ return [CompletionItem(c) for c in matched]
diff --git a/typer/cli.py b/typer/cli.py
index 2a7d78c3a4..665bcf5a59 100644
--- a/typer/cli.py
+++ b/typer/cli.py
@@ -4,13 +4,12 @@
from pathlib import Path
from typing import Any
-import click
import typer
import typer.core
-from click import Command, Group, Option
-from . import __version__
-from .core import HAS_RICH, MARKUP_MODE_KEY
+from . import __version__, _click
+from ._click import Command
+from .core import HAS_RICH, MARKUP_MODE_KEY, TyperGroup, TyperOption
default_app_names = ("app", "cli", "main")
default_func_names = ("main", "cli", "app")
@@ -31,7 +30,7 @@ def __init__(self) -> None:
state = State()
-def maybe_update_state(ctx: click.Context) -> None:
+def maybe_update_state(ctx: _click.Context) -> None:
path_or_module = ctx.params.get("path_or_module")
if path_or_module:
file_path = Path(path_or_module)
@@ -53,19 +52,19 @@ def maybe_update_state(ctx: click.Context) -> None:
class TyperCLIGroup(typer.core.TyperGroup):
- def list_commands(self, ctx: click.Context) -> list[str]:
+ def list_commands(self, ctx: _click.Context) -> list[str]:
self.maybe_add_run(ctx)
return super().list_commands(ctx)
- def get_command(self, ctx: click.Context, name: str) -> Command | None: # ty: ignore[invalid-method-override]
+ def get_command(self, ctx: _click.Context, name: str) -> Command | None: # ty: ignore[invalid-method-override]
self.maybe_add_run(ctx)
return super().get_command(ctx, name)
- def invoke(self, ctx: click.Context) -> Any:
+ def invoke(self, ctx: _click.Context) -> Any:
self.maybe_add_run(ctx)
return super().invoke(ctx)
- def maybe_add_run(self, ctx: click.Context) -> None:
+ def maybe_add_run(self, ctx: _click.Context) -> None:
maybe_update_state(ctx)
maybe_add_run_to_cli(self)
@@ -138,7 +137,7 @@ def get_typer_from_state() -> typer.Typer | None:
return obj
-def maybe_add_run_to_cli(cli: click.Group) -> None:
+def maybe_add_run_to_cli(cli: TyperGroup) -> None:
if "run" not in cli.commands:
if state.file or state.module:
obj = get_typer_from_state()
@@ -151,7 +150,7 @@ def maybe_add_run_to_cli(cli: click.Group) -> None:
cli.add_command(click_obj)
-def print_version(ctx: click.Context, param: Option, value: bool) -> None:
+def print_version(ctx: _click.Context, param: TyperOption, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
typer.echo(f"Typer version: {__version__}")
@@ -242,7 +241,7 @@ def get_docs_for_click(
docs += "\n"
if obj.epilog:
docs += f"{obj.epilog}\n\n"
- if isinstance(obj, Group):
+ if isinstance(obj, TyperGroup):
group = obj
commands = group.list_commands(ctx)
if commands:
diff --git a/typer/completion.py b/typer/completion.py
index 0d621e411d..f63692ddf3 100644
--- a/typer/completion.py
+++ b/typer/completion.py
@@ -3,8 +3,8 @@
from collections.abc import MutableMapping
from typing import Any
-import click
-
+from . import _click
+from ._click import shell_completion
from ._completion_classes import completion_init
from ._completion_shared import Shells, _get_shell_name, get_completion_script, install
from .models import ParamMeta
@@ -27,19 +27,19 @@ def get_completion_inspect_parameters() -> tuple[ParamMeta, ParamMeta]:
return install_param, show_param
-def install_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
+def install_callback(ctx: _click.Context, param: _click.Parameter, value: Any) -> Any:
if not value or ctx.resilient_parsing:
return value # pragma: no cover
if isinstance(value, str):
shell, path = install(shell=value)
else:
shell, path = install()
- click.secho(f"{shell} completion installed in {path}", fg="green")
- click.echo("Completion will take effect once you restart the terminal")
+ _click.termui.secho(f"{shell} completion installed in {path}", fg="green")
+ _click.echo("Completion will take effect once you restart the terminal")
sys.exit(0)
-def show_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
+def show_callback(ctx: _click.Context, param: _click.Parameter, value: Any) -> Any:
if not value or ctx.resilient_parsing:
return value # pragma: no cover
prog_name = ctx.find_root().info_name
@@ -56,7 +56,7 @@ def show_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any
script_content = get_completion_script(
prog_name=prog_name, complete_var=complete_var, shell=shell
)
- click.echo(script_content)
+ _click.echo(script_content)
sys.exit(0)
@@ -103,17 +103,16 @@ def _install_completion_no_auto_placeholder_function(
# And to add extra error messages, for compatibility with Typer in previous versions
# This is only called in new Command method, only used by Click 8.x+
def shell_complete(
- cli: click.Command,
+ cli: _click.Command,
ctx_args: MutableMapping[str, Any],
prog_name: str,
complete_var: str,
instruction: str,
) -> int:
- import click
- import click.shell_completion
+ from . import _click
if "_" not in instruction:
- click.echo("Invalid completion instruction.", err=True)
+ _click.echo("Invalid completion instruction.", err=True)
return 1
# Click 8 changed the order/style of shell instructions from e.g.
@@ -124,23 +123,23 @@ def shell_complete(
instruction, _, shell = instruction.partition("_")
# Typer override end
- comp_cls = click.shell_completion.get_completion_class(shell)
+ comp_cls = shell_completion.get_completion_class(shell)
if comp_cls is None:
- click.echo(f"Shell {shell} not supported.", err=True)
+ _click.echo(f"Shell {shell} not supported.", err=True)
return 1
comp = comp_cls(cli, ctx_args, prog_name, complete_var)
if instruction == "source":
- click.echo(comp.source())
+ _click.echo(comp.source())
return 0
# Typer override to print the completion help msg with Rich
if instruction == "complete":
- click.echo(comp.complete())
+ _click.echo(comp.complete())
return 0
# Typer override end
- click.echo(f'Completion instruction "{instruction}" not supported.', err=True)
+ _click.echo(f'Completion instruction "{instruction}" not supported.', err=True)
return 1
diff --git a/typer/core.py b/typer/core.py
index 48fee64e34..6868ab4355 100644
--- a/typer/core.py
+++ b/typer/core.py
@@ -2,7 +2,7 @@
import inspect
import os
import sys
-from collections.abc import Callable, MutableMapping, Sequence
+from collections.abc import Callable, Mapping, MutableMapping, Sequence
from difflib import get_close_matches
from enum import Enum
from gettext import gettext as _
@@ -13,13 +13,10 @@
cast,
)
-import click
-import click.core
-import click.formatting
-import click.shell_completion
-import click.types
-import click.utils
-
+from . import _click
+from ._click import types
+from ._click.parser import _OptionParser
+from ._click.shell_completion import CompletionItem
from ._typing import Literal
from .utils import parse_boolean_env_var
@@ -34,7 +31,7 @@
DEFAULT_MARKUP_MODE = None
-# Copy from click.parser._split_opt
+# Copy from _click.parser._split_opt
def _split_opt(opt: str) -> tuple[str, str]:
first = opt[:1]
if first.isalnum():
@@ -45,10 +42,10 @@ def _split_opt(opt: str) -> tuple[str, str]:
def _typer_param_setup_autocompletion_compat(
- self: click.Parameter,
+ self: _click.Parameter,
*,
autocompletion: Callable[
- [click.Context, list[str], str], list[tuple[str, str] | str]
+ [_click.Context, list[str], str], list[tuple[str, str] | str]
]
| None = None,
) -> None:
@@ -65,10 +62,8 @@ def _typer_param_setup_autocompletion_compat(
if autocompletion is not None:
def compat_autocompletion(
- ctx: click.Context, param: click.core.Parameter, incomplete: str
- ) -> list["click.shell_completion.CompletionItem"]:
- from click.shell_completion import CompletionItem
-
+ ctx: _click.Context, param: _click.core.Parameter, incomplete: str
+ ) -> list[CompletionItem]:
out = []
for c in autocompletion(ctx, [], incomplete):
@@ -89,11 +84,11 @@ def compat_autocompletion(
def _get_default_string(
obj: Union["TyperArgument", "TyperOption"],
*,
- ctx: click.Context,
+ ctx: _click.Context,
show_default_is_str: bool,
default_value: list[Any] | tuple[Any, ...] | str | Callable[..., Any] | Any,
) -> str:
- # Extracted from click.core.Option.get_help_record() to be reused by
+ # Extracted from _click.core.Option.get_help_record() to be reused by
# rich_utils avoiding RegEx hacks
if show_default_is_str:
default_string = f"({obj.show_default})"
@@ -112,7 +107,7 @@ def _get_default_string(
# For boolean flags that have distinct True/False opts,
# use the opt without prefix instead of the value.
# Typer override, original commented
- # default_string = click.parser.split_opt(
+ # default_string = _click.parser.split_opt(
# (self.opts if self.default else self.secondary_opts)[0]
# )[1]
if obj.default:
@@ -136,9 +131,9 @@ def _get_default_string(
def _extract_default_help_str(
- obj: Union["TyperArgument", "TyperOption"], *, ctx: click.Context
+ obj: Union["TyperArgument", "TyperOption"], *, ctx: _click.Context
) -> Any | Callable[[], Any] | None:
- # Extracted from click.core.Option.get_help_record() to be reused by
+ # Extracted from _click.core.Option.get_help_record() to be reused by
# rich_utils avoiding RegEx hacks
# Temporarily enable resilient parsing to avoid type casting
# failing for the default. Might be possible to extend this to
@@ -154,7 +149,7 @@ def _extract_default_help_str(
def _main(
- self: click.Command,
+ self: _click.Command,
*,
args: Sequence[str] | None = None,
prog_name: str | None = None,
@@ -164,7 +159,7 @@ def _main(
rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
**extra: Any,
) -> Any:
- # Typer override, duplicated from click.main() to handle custom rich exceptions
+ # Typer override, duplicated from _click.main() to handle custom rich exceptions
# Verify that the environment is configured correctly, or reject
# further execution to avoid a broken script.
if args is None:
@@ -172,12 +167,12 @@ def _main(
# Covered in Click tests
if os.name == "nt" and windows_expand_args: # pragma: no cover
- args = click.utils._expand_args(args)
+ args = _click.utils._expand_args(args)
else:
args = list(args)
if prog_name is None:
- prog_name = click.utils._detect_program_name()
+ prog_name = _click.utils._detect_program_name()
# Process shell completion requests and exit early.
self._main_shell_completion(extra, prog_name, complete_var)
@@ -197,11 +192,11 @@ def _main(
# by its truthiness/falsiness
ctx.exit()
except EOFError as e:
- click.echo(file=sys.stderr)
- raise click.Abort() from e
+ _click.echo(file=sys.stderr)
+ raise _click.exceptions.Abort() from e
except KeyboardInterrupt as e:
- raise click.exceptions.Exit(130) from e
- except click.ClickException as e:
+ raise _click.exceptions.Exit(130) from e
+ except _click.exceptions.ClickException as e:
if not standalone_mode:
raise
# Typer override
@@ -215,12 +210,12 @@ def _main(
sys.exit(e.exit_code)
except OSError as e:
if e.errno == errno.EPIPE:
- sys.stdout = cast(TextIO, click.utils.PacifyFlushWrapper(sys.stdout))
- sys.stderr = cast(TextIO, click.utils.PacifyFlushWrapper(sys.stderr))
+ sys.stdout = cast(TextIO, _click.utils.PacifyFlushWrapper(sys.stdout))
+ sys.stderr = cast(TextIO, _click.utils.PacifyFlushWrapper(sys.stderr))
sys.exit(1)
else:
raise
- except click.exceptions.Exit as e:
+ except _click.exceptions.Exit as e:
if standalone_mode:
sys.exit(e.exit_code)
else:
@@ -233,7 +228,7 @@ def _main(
# `ctx.exit(1)` and to `return 1`, the caller won't be able to
# tell the difference between the two
return e.exit_code
- except click.Abort:
+ except _click.exceptions.Abort:
if not standalone_mode:
raise
# Typer override
@@ -242,19 +237,21 @@ def _main(
rich_utils.rich_abort_error()
else:
- click.echo(_("Aborted!"), file=sys.stderr)
+ _click.echo(_("Aborted!"), file=sys.stderr)
# Typer override end
sys.exit(1)
-class TyperArgument(click.core.Argument):
+class TyperArgument(_click.core.Parameter):
+ param_type_name = "argument"
+
def __init__(
self,
*,
# Parameter
param_decls: list[str],
type: Any | None = None,
- required: bool | None = None,
+ required: bool = False,
default: Any | None = None,
callback: Callable[..., Any] | None = None,
nargs: int | None = None,
@@ -265,8 +262,8 @@ def __init__(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list[CompletionItem] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
@@ -301,10 +298,17 @@ def __init__(
)
_typer_param_setup_autocompletion_compat(self, autocompletion=autocompletion)
+ @property
+ def human_readable_name(self) -> str:
+ if self.metavar is not None:
+ return self.metavar
+ assert self.name is not None, "self.name or self.metavar should be set"
+ return self.name.upper()
+
def _get_default_string(
self,
*,
- ctx: click.Context,
+ ctx: _click.Context,
show_default_is_str: bool,
default_value: list[Any] | tuple[Any, ...] | str | Callable[..., Any] | Any,
) -> str:
@@ -316,12 +320,12 @@ def _get_default_string(
)
def _extract_default_help_str(
- self, *, ctx: click.Context
+ self, *, ctx: _click.Context
) -> Any | Callable[[], Any] | None:
return _extract_default_help_str(self, ctx=ctx)
- def get_help_record(self, ctx: click.Context) -> tuple[str, str] | None:
- # Modified version of click.core.Option.get_help_record()
+ def get_help_record(self, ctx: _click.Context) -> tuple[str, str] | None:
+ # Modified version of _click.core.Option.get_help_record()
# to support Arguments
if self.hidden:
return None
@@ -376,8 +380,8 @@ def get_help_record(self, ctx: click.Context) -> tuple[str, str] | None:
help = f"{help} {extra_str}" if help else f"{extra_str}"
return name, help
- def make_metavar(self, ctx: click.Context) -> str:
- # Modified version of click.core.Argument.make_metavar()
+ def make_metavar(self, ctx: _click.Context) -> str:
+ # Modified version of _click.core.Argument.make_metavar()
# to include Argument name
if self.metavar is not None:
var = self.metavar
@@ -397,15 +401,45 @@ def make_metavar(self, ctx: click.Context) -> str:
def value_is_missing(self, value: Any) -> bool:
return _value_is_missing(self, value)
+ def _parse_decls(
+ self, decls: Sequence[str], expose_value: bool
+ ) -> tuple[str | None, list[str], list[str]]:
+ if not decls:
+ if not expose_value:
+ return None, [], []
+ raise TypeError("Argument is marked as exposed, but does not have a name.")
+ if len(decls) == 1:
+ name = arg = decls[0]
+ name = name.replace("-", "_").lower()
+ else:
+ raise TypeError(
+ "Arguments take exactly one parameter declaration, got"
+ f" {len(decls)}: {decls}."
+ )
+ return name, [arg], []
+
+ def get_usage_pieces(self, ctx: _click.Context) -> list[str]:
+ return [self.make_metavar(ctx)]
+
+ def get_error_hint(self, ctx: _click.Context) -> str:
+ return f"'{self.make_metavar(ctx)}'"
+
+ def add_to_parser(self, parser: _OptionParser, ctx: _click.Context) -> None:
+ parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
+
+
+class TyperOption(_click.Parameter):
+ param_type_name = "option"
+
+ _depr_flag_value: bool | None
-class TyperOption(click.core.Option):
def __init__(
self,
*,
# Parameter
param_decls: list[str],
- type: click.types.ParamType | Any | None = None,
- required: bool | None = None,
+ type: types.ParamType | Any | None = None,
+ required: bool = False,
default: Any | None = None,
callback: Callable[..., Any] | None = None,
nargs: int | None = None,
@@ -416,8 +450,8 @@ def __init__(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list[CompletionItem] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
@@ -438,9 +472,13 @@ def __init__(
# Rich settings
rich_help_panel: str | None = None,
):
+ if help:
+ help = inspect.cleandoc(help)
+
super().__init__(
- param_decls=param_decls,
+ param_decls,
type=type,
+ multiple=multiple,
required=required,
default=default,
callback=callback,
@@ -449,28 +487,246 @@ def __init__(
expose_value=expose_value,
is_eager=is_eager,
envvar=envvar,
- show_default=show_default,
- prompt=prompt,
- confirmation_prompt=confirmation_prompt,
- hide_input=hide_input,
- is_flag=is_flag,
- multiple=multiple,
- count=count,
- allow_from_autoenv=allow_from_autoenv,
- help=help,
- hidden=hidden,
- show_choices=show_choices,
- show_envvar=show_envvar,
- prompt_required=prompt_required,
shell_complete=shell_complete,
)
+
+ if prompt is True:
+ if self.name is None:
+ raise TypeError("'name' is required with 'prompt=True'.")
+
+ prompt_text: str | None = self.name.replace("_", " ").capitalize()
+ elif prompt is False:
+ prompt_text = None
+ else:
+ prompt_text = prompt
+
+ self.prompt = prompt_text
+ self.confirmation_prompt = confirmation_prompt
+ self.prompt_required = prompt_required
+ self.hide_input = hide_input
+ self.hidden = hidden
+
+ # TODO: revisit all of this flag stuff
+ if is_flag and type is None:
+ self.type: types.ParamType = types.BoolParamType()
+
+ self.is_flag: bool = bool(is_flag)
+ self.is_bool_flag: bool = bool(
+ is_flag and isinstance(self.type, types.BoolParamType)
+ )
+
+ if self.is_flag:
+ self._depr_flag_value = True
+ else:
+ self._depr_flag_value = None
+
+ # Counting. TODO: test or remove? Not currently in coverage.
+ self.count = count
+ if count and type is None:
+ self.type = types.IntRange(min=0)
+
+ self.allow_from_autoenv = allow_from_autoenv
+ self.help = help
+ self.show_default = show_default
+ self.show_choices = show_choices
+ self.show_envvar = show_envvar
+
_typer_param_setup_autocompletion_compat(self, autocompletion=autocompletion)
self.rich_help_panel = rich_help_panel
+ def get_error_hint(self, ctx: _click.Context) -> str:
+ result = super().get_error_hint(ctx)
+ if self.show_envvar and self.envvar is not None:
+ result += f" (env var: '{self.envvar}')"
+ return result
+
+ def _parse_decls(
+ self, decls: Sequence[str], expose_value: bool
+ ) -> tuple[str | None, list[str], list[str]]:
+ opts = []
+ secondary_opts = []
+ name = None
+ possible_names = []
+
+ for decl in decls:
+ if decl.isidentifier():
+ if name is not None:
+ raise TypeError(f"Name '{name}' defined twice")
+ name = decl
+ else:
+ split_char = ";" if decl[:1] == "/" else "/"
+ if split_char in decl:
+ first, second = decl.split(split_char, 1)
+ first = first.rstrip()
+ if first:
+ possible_names.append(_split_opt(first))
+ opts.append(first)
+ second = second.lstrip()
+ if second:
+ secondary_opts.append(second.lstrip())
+ if first == second:
+ raise ValueError(
+ f"Boolean option {decl!r} cannot use the"
+ " same flag for true/false."
+ )
+ else:
+ possible_names.append(_split_opt(decl))
+ opts.append(decl)
+
+ if name is None and possible_names:
+ possible_names.sort(key=lambda x: -len(x[0])) # group long options first
+ name = possible_names[0][1].replace("-", "_").lower()
+ if not name.isidentifier():
+ name = None
+
+ return name, opts, secondary_opts
+
+ def add_to_parser(self, parser: _OptionParser, ctx: _click.Context) -> None:
+ if self.multiple:
+ action = "append"
+ elif self.count:
+ action = "count"
+ else:
+ action = "store"
+
+ if self.is_flag:
+ action = f"{action}_const"
+
+ if self.is_bool_flag and self.secondary_opts:
+ parser.add_option(
+ obj=self, opts=self.opts, dest=self.name, action=action, const=True
+ )
+ parser.add_option(
+ obj=self,
+ opts=self.secondary_opts,
+ dest=self.name,
+ action=action,
+ const=False,
+ )
+ else:
+ parser.add_option(
+ obj=self,
+ opts=self.opts,
+ dest=self.name,
+ action=action,
+ const=self._depr_flag_value,
+ )
+ else:
+ parser.add_option(
+ obj=self,
+ opts=self.opts,
+ dest=self.name,
+ action=action,
+ nargs=self.nargs,
+ )
+
+ def prompt_for_value(self, ctx: _click.Context) -> Any:
+ """This is an alternative flow that can be activated in the full
+ value processing if a value does not exist. It will prompt the
+ user until a valid value exists and then returns the processed
+ value as result.
+ """
+ assert self.prompt is not None
+
+ # Calculate the default before prompting anything to lock in the value before
+ # attempting any user interaction.
+ default = self.get_default(ctx)
+
+ # A boolean flag can use a simplified [y/n] confirmation prompt.
+ if self.is_bool_flag:
+ # Nothing prevent you to declare an option that is simultaneously:
+ # 1) auto-detected as a boolean flag,
+ # 2) allowed to prompt, and
+ # 3) still declare a non-boolean default.
+ # This forced casting into a boolean is necessary to align any non-boolean
+ # default to the prompt, which is going to be a [y/n]-style confirmation
+ # because the option is still a boolean flag. That way, instead of [y/n],
+ # we get [Y/n] or [y/N] depending on the truthy value of the default.
+ # Refs: https://github.com/pallets/click/pull/3030#discussion_r2289180249
+ if default is not None:
+ default = bool(default)
+ return _click.termui.confirm(self.prompt, default)
+
+ # If show_default is set to True/False, provide this to `prompt` as well. For
+ # non-bool values of `show_default`, we use `prompt`'s default behavior
+ prompt_kwargs: Any = {}
+ if isinstance(self.show_default, bool):
+ prompt_kwargs["show_default"] = self.show_default
+
+ return _click.termui.prompt(
+ self.prompt,
+ # Use ``None`` to inform the prompt() function to reiterate until a valid
+ # value is provided by the user if we have no default.
+ default=default,
+ type=self.type,
+ hide_input=self.hide_input,
+ show_choices=self.show_choices,
+ confirmation_prompt=self.confirmation_prompt,
+ value_proc=lambda x: self.process_value(ctx, x),
+ **prompt_kwargs,
+ )
+
+ def value_from_envvar(self, ctx: _click.Context) -> Any:
+ # TODO: clean up
+ rv = self.resolve_envvar_value(ctx)
+
+ # Absent environment variable or an empty string is interpreted as unset.
+ if rv is None:
+ return None
+
+ if self.nargs != 1 or self.multiple:
+ return self.type.split_envvar_value(rv)
+
+ return rv
+
+ def resolve_envvar_value(self, ctx: _click.Context) -> str | None:
+ rv = super().resolve_envvar_value(ctx)
+
+ if rv is not None:
+ return rv
+
+ if (
+ self.allow_from_autoenv
+ and ctx.auto_envvar_prefix is not None
+ and self.name is not None
+ ):
+ envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
+ rv = os.environ.get(envvar)
+
+ if rv:
+ return rv
+
+ return None
+
+ def consume_value(
+ self, ctx: _click.Context, opts: Mapping[str, _click.Parameter]
+ ) -> tuple[Any, _click.core.ParameterSource]:
+ """For `Option`, the value can be collected from an interactive prompt
+ if the option is a flag that needs a value (and the `prompt` property is
+ set).
+
+ Additionally, this method handles flag option that are activated without a
+ value, in which case the `flag_value` is returned.
+ """
+ value, source = super().consume_value(ctx, opts)
+
+ # The value wasn't set, or used the param's default, prompt for one to the user
+ # if prompting is enabled.
+ if (
+ source in {None, _click.core.ParameterSource.DEFAULT}
+ and self.prompt is not None
+ and (self.required or self.prompt_required)
+ and not ctx.resilient_parsing
+ ):
+ value = self.prompt_for_value(ctx)
+ source = _click.core.ParameterSource.PROMPT
+
+ return value, source
+
def _get_default_string(
self,
*,
- ctx: click.Context,
+ ctx: _click.Context,
show_default_is_str: bool,
default_value: list[Any] | tuple[Any, ...] | str | Callable[..., Any] | Any,
) -> str:
@@ -482,14 +738,14 @@ def _get_default_string(
)
def _extract_default_help_str(
- self, *, ctx: click.Context
+ self, *, ctx: _click.Context
) -> Any | Callable[[], Any] | None:
return _extract_default_help_str(self, ctx=ctx)
- def make_metavar(self, ctx: click.Context) -> str:
+ def make_metavar(self, ctx: _click.Context) -> str:
return super().make_metavar(ctx=ctx)
- def get_help_record(self, ctx: click.Context) -> tuple[str, str] | None:
+ def get_help_record(self, ctx: _click.Context) -> tuple[str, str] | None:
# Duplicate all of Click's logic only to modify a single line, to allow boolean
# flags with only names for False values as it's currently supported by Typer
# Ref: https://typer.tiangolo.com/tutorial/parameter-types/bool/#only-names-for-false
@@ -501,7 +757,7 @@ def get_help_record(self, ctx: click.Context) -> tuple[str, str] | None:
def _write_opts(opts: Sequence[str]) -> str:
nonlocal any_prefix_is_slash
- rv, any_slashes = click.formatting.join_options(opts)
+ rv, any_slashes = _click.formatting.join_options(opts)
if any_slashes:
any_prefix_is_slash = True
@@ -559,7 +815,7 @@ def _write_opts(opts: Sequence[str]) -> str:
if default_string:
extra.append(_("default: {default}").format(default=default_string))
- if isinstance(self.type, click.types._NumberRangeBase):
+ if isinstance(self.type, types._NumberRangeBase):
range_str = self.type._describe_range()
if range_str:
@@ -588,14 +844,10 @@ def value_is_missing(self, value: Any) -> bool:
return _value_is_missing(self, value)
-def _value_is_missing(param: click.Parameter, value: Any) -> bool:
+def _value_is_missing(param: _click.Parameter, value: Any) -> bool:
if value is None:
return True
- # Click 8.3 and beyond
- # if value is UNSET:
- # return True
-
if (param.nargs != 1 or param.multiple) and value == ():
return True # pragma: no cover
@@ -603,7 +855,7 @@ def _value_is_missing(param: click.Parameter, value: Any) -> bool:
def _typer_format_options(
- self: click.core.Command, *, ctx: click.Context, formatter: click.HelpFormatter
+ self: _click.core.Command, *, ctx: _click.Context, formatter: _click.HelpFormatter
) -> None:
args = []
opts = []
@@ -624,7 +876,7 @@ def _typer_format_options(
def _typer_main_shell_completion(
- self: click.core.Command,
+ self: _click.core.Command,
*,
ctx_args: MutableMapping[str, Any],
prog_name: str,
@@ -644,14 +896,14 @@ def _typer_main_shell_completion(
sys.exit(rv)
-class TyperCommand(click.core.Command):
+class TyperCommand(_click.core.Command):
def __init__(
self,
name: str | None,
*,
context_settings: dict[str, Any] | None = None,
callback: Callable[..., Any] | None = None,
- params: list[click.Parameter] | None = None,
+ params: list[_click.Parameter] | None = None,
help: str | None = None,
epilog: str | None = None,
short_help: str | None = None,
@@ -682,7 +934,7 @@ def __init__(
self.rich_help_panel = rich_help_panel
def format_options(
- self, ctx: click.Context, formatter: click.HelpFormatter
+ self, ctx: _click.Context, formatter: _click.HelpFormatter
) -> None:
_typer_format_options(self, ctx=ctx, formatter=formatter)
@@ -716,7 +968,7 @@ def main(
**extra,
)
- def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
+ def format_help(self, ctx: _click.Context, formatter: _click.HelpFormatter) -> None:
if not HAS_RICH or self.rich_markup_mode is None:
if not hasattr(ctx, "obj") or ctx.obj is None:
ctx.ensure_object(dict)
@@ -732,25 +984,153 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non
)
-class TyperGroup(click.core.Group):
+class TyperGroup(_click.Command):
+ allow_extra_args = True
+ allow_interspersed_args = False
+ command_class: type[_click.Command] | None = None
+ group_class: type["TyperGroup"] | type[type] | None = None
+
def __init__(
self,
*,
name: str | None = None,
- commands: dict[str, click.Command] | Sequence[click.Command] | None = None,
+ commands: dict[str, _click.Command] | Sequence[_click.Command] | None = None,
# Rich settings
rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
rich_help_panel: str | None = None,
suggest_commands: bool = True,
+ # Click settings
+ invoke_without_command: bool = False,
+ no_args_is_help: bool = False,
+ subcommand_metavar: str | None = None,
+ result_callback: Callable[..., Any] | None = None,
**attrs: Any,
) -> None:
- super().__init__(name=name, commands=commands, **attrs)
+ super().__init__(name=name, **attrs)
self.rich_markup_mode: MarkupMode = rich_markup_mode
self.rich_help_panel = rich_help_panel
self.suggest_commands = suggest_commands
+ # copied from Click's init
+ if commands is None:
+ commands = {}
+ elif isinstance(commands, Sequence):
+ commands = {
+ c.name: c
+ for c in commands
+ if isinstance(c, _click.Command) and c.name is not None
+ }
+
+ self.commands = cast(MutableMapping[str, _click.Command], commands)
+ self.no_args_is_help = no_args_is_help
+ self.invoke_without_command = invoke_without_command
+
+ if subcommand_metavar is None:
+ subcommand_metavar = "COMMAND [ARGS]..."
+
+ self.subcommand_metavar = subcommand_metavar
+ self._result_callback = result_callback
+
+ def add_command(self, cmd: _click.Command, name: str | None = None) -> None:
+ name = name or cmd.name
+ if name is None:
+ raise TypeError("Command has no name.")
+ self.commands[name] = cmd
+
+ def get_command(self, ctx: _click.Context, cmd_name: str) -> _click.Command | None:
+ return self.commands.get(cmd_name)
+
+ def collect_usage_pieces(self, ctx: _click.Context) -> list[str]:
+ rv = super().collect_usage_pieces(ctx)
+ rv.append(self.subcommand_metavar)
+ return rv
+
+ def format_commands(
+ self, ctx: _click.Context, formatter: _click.HelpFormatter
+ ) -> None:
+ commands = []
+ for subcommand in self.list_commands(ctx):
+ cmd = self.get_command(ctx, subcommand)
+ if cmd is None or cmd.hidden:
+ continue
+
+ commands.append((subcommand, cmd))
+
+ # allow for 3 times the default spacing
+ if len(commands):
+ limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
+
+ rows = []
+ for subcommand, cmd in commands:
+ assert cmd is not None
+ help = cmd.get_short_help_str(limit)
+ rows.append((subcommand, help))
+
+ if rows:
+ with formatter.section(_("Commands")):
+ formatter.write_dl(rows)
+
+ def parse_args(self, ctx: _click.Context, args: list[str]) -> list[str]:
+ if not args and self.no_args_is_help and not ctx.resilient_parsing:
+ raise _click.exceptions.NoArgsIsHelpError(ctx)
+
+ rest = super().parse_args(ctx, args)
+
+ if rest:
+ ctx._protected_args, ctx.args = rest[:1], rest[1:]
+
+ return ctx.args
+
+ def invoke(self, ctx: _click.Context) -> Any:
+ def _process_result(value: Any) -> Any:
+ if self._result_callback is not None:
+ value = ctx.invoke(self._result_callback, value, **ctx.params)
+ return value
+
+ if not ctx._protected_args:
+ if self.invoke_without_command:
+ # No subcommand was invoked, so the result callback is
+ # invoked with the group return value for regular
+ # groups, or an empty list for chained groups.
+ with ctx:
+ rv = super().invoke(ctx)
+ # return _process_result([] if self.chain else rv)
+ return _process_result(rv)
+ ctx.fail(_("Missing command."))
+
+ # Fetch args back out
+ args = [*ctx._protected_args, *ctx.args]
+ ctx.args = []
+ ctx._protected_args = []
+
+ # Make sure the context is entered so we do not clean up
+ # resources until the result processor has worked.
+ with ctx:
+ cmd_name, cmd, args = self.resolve_command(ctx, args)
+ assert cmd is not None
+ ctx.invoked_subcommand = cmd_name
+ super().invoke(ctx)
+ sub_ctx = cmd.make_context(cmd_name, args, parent=ctx)
+ with sub_ctx:
+ return _process_result(sub_ctx.command.invoke(sub_ctx))
+
+ def shell_complete(
+ self, ctx: _click.Context, incomplete: str
+ ) -> list[CompletionItem]:
+ """Return a list of completions for the incomplete value. Looks
+ at the names of options, subcommands, and chained
+ multi-commands.
+ """
+
+ results = [
+ CompletionItem(name, help=command.get_short_help_str())
+ for name, command in _click.core._complete_visible_commands(ctx, incomplete)
+ ]
+ results.extend(super().shell_complete(ctx, incomplete))
+ return results
+
def format_options(
- self, ctx: click.Context, formatter: click.HelpFormatter
+ self, ctx: _click.Context, formatter: _click.HelpFormatter
) -> None:
_typer_format_options(self, ctx=ctx, formatter=formatter)
self.format_commands(ctx, formatter)
@@ -765,12 +1145,31 @@ def _main_shell_completion(
self, ctx_args=ctx_args, prog_name=prog_name, complete_var=complete_var
)
+ def _click_resolve_command(
+ self, ctx: _click.Context, args: list[str]
+ ) -> tuple[str | None, _click.Command | None, list[str]]:
+ cmd_name = args[0]
+ original_cmd_name = cmd_name
+
+ # Get the command
+ cmd = self.get_command(ctx, cmd_name)
+
+ if cmd is None and ctx.token_normalize_func is not None:
+ cmd_name = ctx.token_normalize_func(cmd_name)
+ cmd = self.get_command(ctx, cmd_name)
+
+ if cmd is None and not ctx.resilient_parsing:
+ if _split_opt(cmd_name)[0]:
+ self.parse_args(ctx, args)
+ ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name))
+ return cmd_name if cmd else None, cmd, args[1:]
+
def resolve_command(
- self, ctx: click.Context, args: list[str]
- ) -> tuple[str | None, click.Command | None, list[str]]:
+ self, ctx: _click.Context, args: list[str]
+ ) -> tuple[str | None, _click.Command | None, list[str]]:
try:
- return super().resolve_command(ctx, args)
- except click.UsageError as e:
+ return self._click_resolve_command(ctx, args)
+ except _click.exceptions.UsageError as e:
if self.suggest_commands:
available_commands = list(self.commands.keys())
if available_commands and args:
@@ -802,7 +1201,7 @@ def main(
**extra,
)
- def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
+ def format_help(self, ctx: _click.Context, formatter: _click.HelpFormatter) -> None:
if not HAS_RICH or self.rich_markup_mode is None:
return super().format_help(ctx, formatter)
from . import rich_utils
@@ -813,8 +1212,6 @@ def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> Non
markup_mode=self.rich_markup_mode,
)
- def list_commands(self, ctx: click.Context) -> list[str]:
- """Returns a list of subcommand names.
- Note that in Click's Group class, these are sorted.
- In Typer, we wish to maintain the original order of creation (cf Issue #933)"""
+ def list_commands(self, ctx: _click.Context) -> list[str]:
+ """Returns a list of subcommand names, maintaining the original order of creation (cf Issue #933)"""
return [n for n, c in self.commands.items()]
diff --git a/typer/main.py b/typer/main.py
index ebcf639a2c..43c7a921f7 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -15,10 +15,12 @@
from typing import Annotated, Any
from uuid import UUID
-import click
from annotated_doc import Doc
from typer._types import TyperChoice
+from . import _click
+from ._click import types
+from ._click.globals import get_current_context
from ._typing import get_args, get_origin, is_literal_type, is_union, literal_values
from .completion import get_completion_inspect_parameters
from .core import (
@@ -73,7 +75,7 @@ def except_hook(
_original_except_hook(exc_type, exc_value, tb)
return
typer_path = os.path.dirname(__file__)
- click_path = os.path.dirname(click.__file__)
+ click_path = os.path.dirname(_click.__file__)
internal_dir_names = [typer_path, click_path]
exc = exc_value
if HAS_RICH:
@@ -107,7 +109,7 @@ def except_hook(
return
-def get_install_completion_arguments() -> tuple[click.Parameter, click.Parameter]:
+def get_install_completion_arguments() -> tuple[_click.Parameter, _click.Parameter]:
install_param, show_param = get_completion_inspect_parameters()
click_install_param, _ = get_click_param(install_param)
click_show_param, _ = get_click_param(show_param)
@@ -1168,7 +1170,7 @@ def get_group(typer_instance: Typer) -> TyperGroup:
return group
-def get_command(typer_instance: Typer) -> click.Command:
+def get_command(typer_instance: Typer) -> _click.Command:
if typer_instance._add_completion:
click_install_param, click_show_param = get_install_completion_arguments()
if (
@@ -1178,7 +1180,7 @@ def get_command(typer_instance: Typer) -> click.Command:
or len(typer_instance.registered_commands) > 1
):
# Create a Group
- click_command: click.Command = get_group(typer_instance)
+ click_command: _click.Command = get_group(typer_instance)
if typer_instance._add_completion:
click_command.params.append(click_install_param)
click_command.params.append(click_show_param)
@@ -1288,7 +1290,7 @@ def get_group_from_info(
assert group_info.typer_instance, (
"A Typer instance is needed to generate a Click Group"
)
- commands: dict[str, click.Command] = {}
+ commands: dict[str, _click.Command] = {}
for command_info in group_info.typer_instance.registered_commands:
command = get_command_from_info(
command_info=command_info,
@@ -1330,7 +1332,6 @@ def get_group_from_info(
invoke_without_command=solved_info.invoke_without_command,
no_args_is_help=solved_info.no_args_is_help,
subcommand_metavar=solved_info.subcommand_metavar,
- chain=solved_info.chain,
result_callback=solved_info.result_callback,
context_settings=solved_info.context_settings,
callback=get_callback(
@@ -1362,14 +1363,14 @@ def get_command_name(name: str) -> str:
def get_params_convertors_ctx_param_name_from_function(
callback: Callable[..., Any] | None,
-) -> tuple[list[click.Argument | click.Option], dict[str, Any], str | None]:
+) -> tuple[list[TyperArgument | TyperOption], dict[str, Any], str | None]:
params = []
convertors = {}
context_param_name = None
if callback:
parameters = get_params_from_function(callback)
for param_name, param in parameters.items():
- if lenient_issubclass(param.annotation, click.Context):
+ if lenient_issubclass(param.annotation, _click.Context):
context_param_name = param_name
continue
click_param, convertor = get_click_param(param)
@@ -1384,7 +1385,7 @@ def get_command_from_info(
*,
pretty_exceptions_short: bool,
rich_markup_mode: MarkupMode,
-) -> click.Command:
+) -> _click.Command:
assert command_info.callback, "A command must have a callback function"
name = command_info.name or get_command_name(command_info.callback.__name__) # ty: ignore
use_help = command_info.help
@@ -1486,7 +1487,7 @@ def internal_convertor(
def get_callback(
*,
callback: Callable[..., Any] | None = None,
- params: Sequence[click.Parameter] = [],
+ params: Sequence[_click.Parameter] = [],
convertors: dict[str, Callable[[str], Any]] | None = None,
context_param_name: str | None = None,
pretty_exceptions_short: bool,
@@ -1510,7 +1511,7 @@ def wrapper(**kwargs: Any) -> Any:
else:
use_params[k] = v
if context_param_name:
- use_params[context_param_name] = click.get_current_context()
+ use_params[context_param_name] = get_current_context()
return callback(**use_params)
update_wrapper(wrapper, callback)
@@ -1519,15 +1520,15 @@ def wrapper(**kwargs: Any) -> Any:
def get_click_type(
*, annotation: Any, parameter_info: ParameterInfo
-) -> click.ParamType:
+) -> types.ParamType:
if parameter_info.click_type is not None:
return parameter_info.click_type
elif parameter_info.parser is not None:
- return click.types.FuncParamType(parameter_info.parser)
+ return types.FuncParamType(parameter_info.parser)
elif annotation is str:
- return click.STRING
+ return types.STRING
elif annotation is int:
if parameter_info.min is not None or parameter_info.max is not None:
min_ = None
@@ -1536,24 +1537,24 @@ def get_click_type(
min_ = int(parameter_info.min)
if parameter_info.max is not None:
max_ = int(parameter_info.max)
- return click.IntRange(min=min_, max=max_, clamp=parameter_info.clamp)
+ return types.IntRange(min=min_, max=max_, clamp=parameter_info.clamp)
else:
- return click.INT
+ return types.INT
elif annotation is float:
if parameter_info.min is not None or parameter_info.max is not None:
- return click.FloatRange(
+ return types.FloatRange(
min=parameter_info.min,
max=parameter_info.max,
clamp=parameter_info.clamp,
)
else:
- return click.FLOAT
+ return types.FLOAT
elif annotation is bool:
- return click.BOOL
+ return types.BOOL
elif annotation == UUID:
- return click.UUID
+ return types.UUID
elif annotation == datetime:
- return click.DateTime(formats=parameter_info.formats)
+ return types.DateTime(formats=parameter_info.formats)
elif (
annotation == Path
or parameter_info.allow_dash
@@ -1571,7 +1572,7 @@ def get_click_type(
path_type=parameter_info.path_type,
)
elif lenient_issubclass(annotation, FileTextWrite):
- return click.File(
+ return types.File(
mode=parameter_info.mode or "w",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
@@ -1579,7 +1580,7 @@ def get_click_type(
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, FileText):
- return click.File(
+ return types.File(
mode=parameter_info.mode or "r",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
@@ -1587,7 +1588,7 @@ def get_click_type(
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, FileBinaryRead):
- return click.File(
+ return types.File(
mode=parameter_info.mode or "rb",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
@@ -1595,7 +1596,7 @@ def get_click_type(
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, FileBinaryWrite):
- return click.File(
+ return types.File(
mode=parameter_info.mode or "wb",
encoding=parameter_info.encoding,
errors=parameter_info.errors,
@@ -1603,17 +1604,12 @@ def get_click_type(
atomic=parameter_info.atomic,
)
elif lenient_issubclass(annotation, Enum):
- # The custom TyperChoice is only needed for Click < 8.2.0, to parse the
- # command line values matching them to the enum values. Click 8.2.0 added
- # support for enum values but reading enum names.
- # Passing here the list of enum values (instead of just the enum) accounts for
- # Click < 8.2.0.
return TyperChoice(
[item.value for item in annotation],
case_sensitive=parameter_info.case_sensitive,
)
elif is_literal_type(annotation):
- return click.Choice(
+ return TyperChoice(
literal_values(annotation),
case_sensitive=parameter_info.case_sensitive,
)
@@ -1626,7 +1622,7 @@ def lenient_issubclass(cls: Any, class_or_tuple: AnyType | tuple[AnyType, ...])
def get_click_param(
param: ParamMeta,
-) -> tuple[click.Argument | click.Option, Any]:
+) -> tuple[TyperArgument | TyperOption, Any]:
# First, find out what will be:
# * ParamInfo (ArgumentInfo or OptionInfo)
# * default_value
@@ -1784,7 +1780,7 @@ def get_click_param(
),
convertor,
)
- raise AssertionError("A click.Parameter should be returned") # pragma: no cover
+ raise AssertionError("A _click.Parameter should be returned") # pragma: no cover
def get_param_callback(
@@ -1800,9 +1796,9 @@ def get_param_callback(
value_name = None
untyped_names: list[str] = []
for param_name, param_sig in parameters.items():
- if lenient_issubclass(param_sig.annotation, click.Context):
+ if lenient_issubclass(param_sig.annotation, _click.Context):
ctx_name = param_name
- elif lenient_issubclass(param_sig.annotation, click.Parameter):
+ elif lenient_issubclass(param_sig.annotation, _click.Parameter):
click_param_name = param_name
else:
untyped_names.append(param_name)
@@ -1817,11 +1813,11 @@ def get_param_callback(
if untyped_names:
click_param_name = untyped_names.pop(0)
if untyped_names:
- raise click.ClickException(
+ raise _click.ClickException(
"Too many CLI parameter callback function parameters"
)
- def wrapper(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
+ def wrapper(ctx: _click.Context, param: _click.Parameter, value: Any) -> Any:
use_params: dict[str, Any] = {}
if ctx_name:
use_params[ctx_name] = ctx
@@ -1851,7 +1847,7 @@ def get_param_completion(
unassigned_params = list(parameters.values())
for param_sig in unassigned_params[:]:
origin = get_origin(param_sig.annotation)
- if lenient_issubclass(param_sig.annotation, click.Context):
+ if lenient_issubclass(param_sig.annotation, _click.Context):
ctx_name = param_sig.name
unassigned_params.remove(param_sig)
elif lenient_issubclass(origin, list):
@@ -1874,11 +1870,11 @@ def get_param_completion(
# Extract value param name first
if unassigned_params:
show_params = " ".join([param.name for param in unassigned_params])
- raise click.ClickException(
+ raise _click.ClickException(
f"Invalid autocompletion callback parameters: {show_params}"
)
- def wrapper(ctx: click.Context, args: list[str], incomplete: str | None) -> Any:
+ def wrapper(ctx: _click.Context, args: list[str], incomplete: str | None) -> Any:
use_params: dict[str, Any] = {}
if ctx_name:
use_params[ctx_name] = ctx
@@ -2010,4 +2006,4 @@ def launch(
return 0
else:
- return click.launch(url, wait=wait, locate=locate)
+ return _click.launch(url, wait=wait, locate=locate)
diff --git a/typer/models.py b/typer/models.py
index 3285a96a24..00385c38ce 100644
--- a/typer/models.py
+++ b/typer/models.py
@@ -1,15 +1,20 @@
import inspect
import io
+import os
+import stat
from collections.abc import Callable, Sequence
from typing import (
TYPE_CHECKING,
Any,
+ ClassVar,
Optional,
TypeVar,
+ cast,
)
-import click
-import click.shell_completion
+from . import _click
+from ._click import types
+from ._click.shell_completion import CompletionItem
if TYPE_CHECKING: # pragma: no cover
from .core import TyperCommand, TyperGroup
@@ -23,7 +28,7 @@
Required = ...
-class Context(click.Context):
+class Context(_click.Context):
"""
The [`Context`](https://click.palletsprojects.com/en/stable/api/#click.Context) has some additional data about the current execution of your program.
When declaring it in a [callback](https://typer.tiangolo.com/tutorial/options/callback-and-context/) function,
@@ -153,7 +158,7 @@ def main(file: Annotated[typer.FileBinaryWrite, typer.Option()]):
pass
-class CallbackParam(click.Parameter):
+class CallbackParam(_click.Parameter):
"""
In a callback function, you can declare a function parameter with type `CallbackParam`
to access the specific Click [`Parameter`](https://click.palletsprojects.com/en/stable/api/#click.Parameter) object.
@@ -286,15 +291,15 @@ def __init__(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
default_factory: Callable[[], Any] | None = None,
# Custom type
parser: Callable[[str], Any] | None = None,
- click_type: click.ParamType | None = None,
+ click_type: types.ParamType | None = None,
# TyperArgument
show_default: bool | str = True,
show_choices: bool = True,
@@ -395,15 +400,15 @@ def __init__(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
default_factory: Callable[[], Any] | None = None,
# Custom type
parser: Callable[[str], Any] | None = None,
- click_type: click.ParamType | None = None,
+ click_type: types.ParamType | None = None,
# Option
show_default: bool | str = True,
prompt: bool | str = False,
@@ -523,15 +528,15 @@ def __init__(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
default_factory: Callable[[], Any] | None = None,
# Custom type
parser: Callable[[str], Any] | None = None,
- click_type: click.ParamType | None = None,
+ click_type: types.ParamType | None = None,
# TyperArgument
show_default: bool | str = True,
show_choices: bool = True,
@@ -640,11 +645,98 @@ def __init__(
self.pretty_exceptions_short = pretty_exceptions_short
-class TyperPath(click.Path):
- # Overwrite Click's behaviour to be compatible with Typer's autocompletion system
+class TyperPath(types.ParamType):
+ # Based originally on code from Click 8.3.1
+ # Partly rewritten and added an override for shell_complete
+
+ envvar_list_splitter: ClassVar[str] = os.path.pathsep
+
+ def __init__(
+ self,
+ exists: bool = False,
+ file_okay: bool = True,
+ dir_okay: bool = True,
+ writable: bool = False,
+ readable: bool = True,
+ resolve_path: bool = False,
+ allow_dash: bool = False,
+ path_type: type[Any] | None = None,
+ ):
+ self.exists = exists
+ self.file_okay = file_okay
+ self.dir_okay = dir_okay
+ self.readable = readable
+ self.writable = writable
+ self.resolve_path = resolve_path
+ self.allow_dash = allow_dash
+ self.type = path_type
+
+ if self.file_okay and not self.dir_okay:
+ self.name = "file"
+ elif self.dir_okay and not self.file_okay:
+ self.name = "directory"
+ else:
+ self.name = "path"
+
+ def coerce_path_result(
+ self, value: str | os.PathLike[str]
+ ) -> str | bytes | os.PathLike[str]:
+ if self.type is not None and not isinstance(value, self.type):
+ if (
+ self.type is str
+ ): # pragma: no cover # TODO: perhaps this branch can't be hit and should be removed
+ return os.fsdecode(value)
+ elif self.type is bytes:
+ return os.fsencode(value)
+ else:
+ return cast("os.PathLike[str]", self.type(value))
+
+ return value
+
+ def convert( # ty: ignore[invalid-method-override]
+ self,
+ value: str | os.PathLike[str],
+ param: _click.Parameter | None,
+ ctx: Context | None, # type: ignore[override]
+ ) -> str | bytes | os.PathLike[str]:
+ rv = value
+
+ is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
+
+ if not is_dash:
+ if self.resolve_path:
+ rv = os.path.realpath(rv)
+
+ try:
+ st = os.stat(rv)
+ except OSError:
+ if not self.exists:
+ return self.coerce_path_result(rv)
+ self.fail(
+ f"{self.name.title()} {_click.utils.format_filename(value)!r} does not exist.",
+ param,
+ ctx,
+ )
+
+ name = self.name.title()
+ loc = repr(_click.utils.format_filename(value))
+ if not self.file_okay and stat.S_ISREG(st.st_mode):
+ self.fail(f"{name} {loc} is a file.", param, ctx)
+
+ if not self.dir_okay and stat.S_ISDIR(st.st_mode):
+ self.fail(f"{name} {loc} is a directory.", param, ctx)
+
+ if self.readable and not os.access(rv, os.R_OK):
+ self.fail(f"{name} {loc} is not readable.", param, ctx)
+
+ if self.writable and not os.access(rv, os.W_OK):
+ self.fail(f"{name} {loc} is not writable.", param, ctx)
+
+ return self.coerce_path_result(rv)
+
def shell_complete(
- self, ctx: click.Context, param: click.Parameter, incomplete: str
- ) -> list[click.shell_completion.CompletionItem]:
+ self, ctx: _click.Context, param: _click.Parameter, incomplete: str
+ ) -> list[CompletionItem]:
"""Return an empty list so that the autocompletion functionality
will work properly from the commandline.
"""
diff --git a/typer/params.py b/typer/params.py
index b325b273c4..833461fa78 100644
--- a/typer/params.py
+++ b/typer/params.py
@@ -1,13 +1,15 @@
from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any, overload
-import click
from annotated_doc import Doc
+from . import _click
+from ._click import types
+from ._click.shell_completion import CompletionItem
from .models import ArgumentInfo, OptionInfo
if TYPE_CHECKING: # pragma: no cover
- import click.shell_completion
+ pass
# Overload for Option created with custom type 'parser'
@@ -24,8 +26,8 @@ def Option(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
@@ -89,14 +91,14 @@ def Option(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
default_factory: Callable[[], Any] | None = None,
# Custom type
- click_type: click.ParamType | None = None,
+ click_type: types.ParamType | None = None,
# Option
show_default: bool | str = True,
prompt: bool | str = False,
@@ -265,8 +267,8 @@ def main(user: Annotated[str, typer.Option(envvar="ME")]):
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Annotated[
Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None,
Doc(
@@ -343,7 +345,7 @@ def main(opt: Annotated[CustomClass, typer.Option(parser=my_parser)] = "Foo"):
),
] = None,
click_type: Annotated[
- click.ParamType | None,
+ types.ParamType | None,
Doc(
"""
Define this parameter to use a [custom Click type](https://click.palletsprojects.com/en/stable/parameters/#implementing-custom-types) in your Typer applications.
@@ -1014,8 +1016,8 @@ def Argument(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
@@ -1070,14 +1072,14 @@ def Argument(
# Note that shell_complete is not fully supported and will be removed in future versions
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None = None,
autocompletion: Callable[..., Any] | None = None,
default_factory: Callable[[], Any] | None = None,
# Custom type
- click_type: click.ParamType | None = None,
+ click_type: types.ParamType | None = None,
# TyperArgument
show_default: bool | str = True,
show_choices: bool = True,
@@ -1219,8 +1221,8 @@ def main(name: Annotated[str, typer.Argument(envvar="ME")]):
# TODO: Remove shell_complete in a future version (after 0.16.0)
shell_complete: Annotated[
Callable[
- [click.Context, click.Parameter, str],
- list["click.shell_completion.CompletionItem"] | list[str],
+ [_click.Context, _click.Parameter, str],
+ list["CompletionItem"] | list[str],
]
| None,
Doc(
@@ -1297,7 +1299,7 @@ def main(arg: Annotated[CustomClass, typer.Argument(parser=my_parser):
),
] = None,
click_type: Annotated[
- click.ParamType | None,
+ types.ParamType | None,
Doc(
"""
Define this parameter to use a [custom Click type](https://click.palletsprojects.com/en/stable/parameters/#implementing-custom-types) in your Typer applications.
diff --git a/typer/rich_utils.py b/typer/rich_utils.py
index 69be631207..e5b106126e 100644
--- a/typer/rich_utils.py
+++ b/typer/rich_utils.py
@@ -8,7 +8,6 @@
from os import getenv
from typing import Any, Literal
-import click
from rich import box
from rich.align import Align
from rich.columns import Columns
@@ -25,6 +24,10 @@
from rich.traceback import Traceback
from typer.models import DeveloperExceptionConfig
+from . import _click
+from ._click import types
+from .core import TyperArgument, TyperGroup, TyperOption
+
# Default styles
STYLE_OPTION = "bold cyan"
STYLE_SWITCH = "bold green"
@@ -184,7 +187,7 @@ def _make_rich_text(
@group()
def _get_help_text(
*,
- obj: click.Command | click.Group,
+ obj: _click.Command | TyperGroup,
markup_mode: MarkupModeStrict,
) -> Iterable[Markdown | Text]:
"""Build primary help text for a click command or group.
@@ -231,8 +234,8 @@ def _get_help_text(
def _get_parameter_help(
*,
- param: click.Option | click.Argument | click.Parameter,
- ctx: click.Context,
+ param: TyperOption | TyperArgument | _click.Parameter,
+ ctx: _click.Context,
markup_mode: MarkupModeStrict,
) -> Columns:
"""Build primary help text for a click option or argument.
@@ -348,8 +351,8 @@ def _make_command_help(
def _print_options_panel(
*,
name: str,
- params: list[click.Option] | list[click.Argument],
- ctx: click.Context,
+ params: list[TyperOption] | list[TyperArgument],
+ ctx: _click.Context,
markup_mode: MarkupModeStrict,
console: Console,
) -> None:
@@ -377,7 +380,7 @@ def _print_options_panel(
metavar_str = param.make_metavar(ctx=ctx)
# Do it ourselves if this is a positional argument
if (
- isinstance(param, click.Argument)
+ isinstance(param, TyperArgument)
and param.name
and metavar_str == param.name.upper()
):
@@ -391,8 +394,8 @@ def _print_options_panel(
# https://github.com/pallets/click/blob/c63c70dabd3f86ca68678b4f00951f78f52d0270/src/click/core.py#L2698-L2706 # noqa: E501
# skip count with default range type
if (
- isinstance(param.type, click.types._NumberRangeBase)
- and isinstance(param, click.Option)
+ isinstance(param.type, types._NumberRangeBase)
+ and isinstance(param, TyperOption)
and not (param.count and param.type.min == 0 and param.type.max is None)
):
range_str = param.type._describe_range()
@@ -459,7 +462,7 @@ def _print_options_panel(
def _print_commands_panel(
*,
name: str,
- commands: list[click.Command],
+ commands: list[_click.Command],
markup_mode: MarkupModeStrict,
console: Console,
cmd_len: int,
@@ -534,8 +537,8 @@ def _print_commands_panel(
def rich_format_help(
*,
- obj: click.Command | click.Group,
- ctx: click.Context,
+ obj: _click.Command | TyperGroup,
+ ctx: _click.Context,
markup_mode: MarkupModeStrict,
) -> None:
"""Print nicely formatted help text using rich.
@@ -568,18 +571,18 @@ def rich_format_help(
(0, 1, 1, 1),
)
)
- panel_to_arguments: defaultdict[str, list[click.Argument]] = defaultdict(list)
- panel_to_options: defaultdict[str, list[click.Option]] = defaultdict(list)
+ panel_to_arguments: defaultdict[str, list[TyperArgument]] = defaultdict(list)
+ panel_to_options: defaultdict[str, list[TyperOption]] = defaultdict(list)
for param in obj.get_params(ctx):
# Skip if option is hidden
if getattr(param, "hidden", False):
continue
- if isinstance(param, click.Argument):
+ if isinstance(param, TyperArgument):
panel_name = (
getattr(param, _RICH_HELP_PANEL_NAME, None) or ARGUMENTS_PANEL_TITLE
)
panel_to_arguments[panel_name].append(param)
- elif isinstance(param, click.Option):
+ elif isinstance(param, TyperOption):
panel_name = (
getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
)
@@ -623,8 +626,8 @@ def rich_format_help(
console=console,
)
- if isinstance(obj, click.Group):
- panel_to_commands: defaultdict[str, list[click.Command]] = defaultdict(list)
+ if isinstance(obj, TyperGroup):
+ panel_to_commands: defaultdict[str, list[_click.Command]] = defaultdict(list)
for command_name in obj.list_commands(ctx):
command = obj.get_command(ctx, command_name)
if command and not command.hidden:
@@ -674,18 +677,18 @@ def rich_format_help(
console.print(Padding(Align(epilogue_text, pad=False), 1))
-def rich_format_error(self: click.ClickException) -> None:
+def rich_format_error(self: _click.ClickException) -> None:
"""Print richly formatted click errors.
Called by custom exception handler to print richly formatted click errors.
- Mimics original click.ClickException.echo() function but with rich formatting.
+ Mimics original _click.ClickException.echo() function but with rich formatting.
"""
# Don't do anything when it's a NoArgsIsHelpError (without importing it, cf. #1278)
if self.__class__.__name__ == "NoArgsIsHelpError":
return
console = _get_rich_console(stderr=True)
- ctx: click.Context | None = getattr(self, "ctx", None)
+ ctx: _click.Context | None = getattr(self, "ctx", None)
if ctx is not None:
console.print(ctx.get_usage())
diff --git a/typer/testing.py b/typer/testing.py
index 09711e66fd..7ecc0e693c 100644
--- a/typer/testing.py
+++ b/typer/testing.py
@@ -1,30 +1,342 @@
-from collections.abc import Mapping, Sequence
-from typing import IO, Any
+import contextlib
+import io
+import os
+import shlex
+import sys
+from collections.abc import Iterator, Mapping, Sequence
+from types import TracebackType
+from typing import IO, TYPE_CHECKING, Any, BinaryIO, cast
-from click.testing import CliRunner as ClickCliRunner # noqa
-from click.testing import Result
from typer.main import Typer
from typer.main import get_command as _get_command
+from . import _click
+from ._click import _compat, formatting, termui, utils
-class CliRunner(ClickCliRunner):
- def invoke( # type: ignore
+if TYPE_CHECKING:
+ from _typeshed import ReadableBuffer
+
+
+def make_input_stream(input: str | bytes | None, charset: str) -> BinaryIO:
+ if input is None:
+ input = b""
+ elif isinstance(input, str):
+ input = input.encode(charset)
+
+ return io.BytesIO(input)
+
+
+class BytesIOCopy(io.BytesIO):
+ """Patch ``io.BytesIO`` to let the written stream be copied to another."""
+
+ def __init__(self, copy_to: io.BytesIO) -> None:
+ super().__init__()
+ self.copy_to = copy_to
+
+ def flush(self) -> None:
+ super().flush()
+ self.copy_to.flush()
+
+ def write(self, b: "ReadableBuffer") -> int:
+ self.copy_to.write(b)
+ return super().write(b)
+
+
+class StreamMixer:
+ """Mixes `` and `` streams.
+
+ The result is available in the ``output`` attribute.
+ """
+
+ def __init__(self) -> None:
+ self.output: io.BytesIO = io.BytesIO()
+ self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output)
+ self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output)
+
+ def __del__(self) -> None:
+ """Guarantee that file-like objects are closed in a predictable order"""
+ self.stderr.close()
+ self.stdout.close()
+ self.output.close()
+
+
+class _NamedTextIOWrapper(io.TextIOWrapper):
+ def __init__(self, buffer: BinaryIO, name: str, mode: str, **kwargs: Any) -> None:
+ super().__init__(buffer, **kwargs)
+ self._name = name
+ self._mode = mode
+
+ @property
+ def name(self) -> str:
+ return self._name # pragma: no cover
+
+ @property
+ def mode(self) -> str:
+ return self._mode # pragma: no cover
+
+
+class Result:
+ """Holds the captured result of an invoked CLI script."""
+
+ def __init__(
+ self,
+ runner: "CliRunner",
+ stdout_bytes: bytes,
+ stderr_bytes: bytes,
+ output_bytes: bytes,
+ return_value: Any,
+ exit_code: int,
+ exception: BaseException | None,
+ exc_info: tuple[type[BaseException], BaseException, TracebackType]
+ | None = None,
+ ):
+ self.runner = runner
+ self.stdout_bytes = stdout_bytes
+ self.stderr_bytes = stderr_bytes
+ self.output_bytes = output_bytes
+ self.return_value = return_value
+ self.exit_code = exit_code
+ self.exception = exception
+ self.exc_info = exc_info
+
+ @property
+ def output(self) -> str:
+ """The terminal output as unicode string, as the user would see it."""
+ return self.output_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ @property
+ def stdout(self) -> str:
+ """The standard output as unicode string."""
+ return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ @property
+ def stderr(self) -> str:
+ """The standard error as unicode string."""
+ return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
+ "\r\n", "\n"
+ )
+
+ def __repr__(self) -> str:
+ exc_str = repr(self.exception) if self.exception else "okay"
+ return f"<{type(self).__name__} {exc_str}>"
+
+
+class CliRunner:
+ """The CLI runner provides functionality to invoke a command line
+ script for unittesting purposes in an isolated environment. This only
+ works in single-threaded systems without any concurrency as it changes the
+ global interpreter state. Based on functionality from Click.
+ """
+
+ def __init__(
+ self,
+ charset: str = "utf-8",
+ env: Mapping[str, str | None] | None = None,
+ ) -> None:
+ self.charset = charset
+ self.env: Mapping[str, str | None] = env or {}
+
+ def get_default_prog_name(self, cli: _click.Command) -> str:
+ """Return the default program name for a command.
+ The default is the `name` attribute or ``"root"`` if not set.
+ """
+ return cli.name or "root"
+
+ def make_env(
+ self, overrides: Mapping[str, str | None] | None = None
+ ) -> Mapping[str, str | None]:
+ """Returns the environment overrides for invoking a script."""
+ rv = dict(self.env)
+ if overrides:
+ rv.update(overrides)
+ return rv
+
+ @contextlib.contextmanager
+ def isolation(
+ self,
+ input: str | bytes | None = None,
+ env: Mapping[str, str | None] | None = None,
+ color: bool = False,
+ ) -> Iterator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]:
+ """A context manager that sets up the isolation for invoking of a
+ command line tool. This sets up `` with the given input data
+ and `os.environ` with the overrides from the given dictionary.
+ """
+ bytes_input = make_input_stream(input, self.charset)
+
+ old_stdin = sys.stdin
+ old_stdout = sys.stdout
+ old_stderr = sys.stderr
+ old_forced_width = formatting.FORCED_WIDTH
+ formatting.FORCED_WIDTH = 80
+
+ env = self.make_env(env)
+
+ stream_mixer = StreamMixer()
+
+ sys.stdin = text_input = _NamedTextIOWrapper(
+ bytes_input, encoding=self.charset, name="", mode="r"
+ )
+
+ sys.stdout = _NamedTextIOWrapper(
+ stream_mixer.stdout, encoding=self.charset, name="", mode="w"
+ )
+
+ sys.stderr = _NamedTextIOWrapper(
+ stream_mixer.stderr,
+ encoding=self.charset,
+ name="",
+ mode="w",
+ errors="backslashreplace",
+ )
+
+ def visible_input(prompt: str | None = None) -> str:
+ sys.stdout.write(prompt or "")
+ try:
+ val = next(text_input).rstrip("\r\n")
+ except StopIteration as e: # pragma: no cover
+ raise EOFError() from e
+ sys.stdout.write(f"{val}\n")
+ sys.stdout.flush()
+ return val
+
+ def hidden_input(prompt: str | None = None) -> str:
+ sys.stdout.write(f"{prompt or ''}\n")
+ sys.stdout.flush()
+ try:
+ return next(text_input).rstrip("\r\n")
+ except StopIteration as e: # pragma: no cover
+ raise EOFError() from e
+
+ def _getchar(echo: bool) -> str:
+ char = sys.stdin.read(1)
+
+ if echo:
+ sys.stdout.write(char)
+
+ sys.stdout.flush()
+ return char
+
+ default_color = color
+
+ def should_strip_ansi(
+ stream: IO[Any] | None = None, color: bool | None = None
+ ) -> bool:
+ if color is None:
+ return not default_color
+ return not color
+
+ old_visible_prompt_func = termui.visible_prompt_func
+ old_hidden_prompt_func = termui.hidden_prompt_func
+ old__getchar_func = termui._getchar
+ old_should_strip_ansi = utils.should_strip_ansi # type: ignore[attr-defined]
+ old__compat_should_strip_ansi = _compat.should_strip_ansi
+ termui.visible_prompt_func = visible_input
+ termui.hidden_prompt_func = hidden_input # ty: ignore[invalid-assignment]
+ termui._getchar = _getchar
+ utils.should_strip_ansi = should_strip_ansi # type: ignore
+ _compat.should_strip_ansi = should_strip_ansi # ty: ignore[invalid-assignment]
+
+ old_env = {}
+ try:
+ for key, value in env.items():
+ old_env[key] = os.environ.get(key)
+ if value is None:
+ try:
+ del os.environ[key]
+ except Exception: # pragma: no cover
+ pass
+ else:
+ os.environ[key] = value
+ yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output)
+ finally:
+ for key, value in old_env.items():
+ if value is None:
+ try:
+ del os.environ[key]
+ except Exception: # pragma: no cover
+ pass
+ else:
+ os.environ[key] = value
+ sys.stdout = old_stdout
+ sys.stderr = old_stderr
+ sys.stdin = old_stdin
+ termui.visible_prompt_func = old_visible_prompt_func
+ termui.hidden_prompt_func = old_hidden_prompt_func
+ termui._getchar = old__getchar_func
+ utils.should_strip_ansi = old_should_strip_ansi # type: ignore[attr-defined]
+ _compat.should_strip_ansi = old__compat_should_strip_ansi
+ formatting.FORCED_WIDTH = old_forced_width
+
+ def invoke(
self,
app: Typer,
args: str | Sequence[str] | None = None,
- input: bytes | str | IO[Any] | None = None,
+ input: bytes | str | None = None,
env: Mapping[str, str | None] | None = None,
catch_exceptions: bool = True,
color: bool = False,
**extra: Any,
) -> Result:
- use_cli = _get_command(app)
- return super().invoke(
- use_cli,
- args=args,
- input=input,
- env=env,
- catch_exceptions=catch_exceptions,
- color=color,
- **extra,
+ cli = _get_command(app)
+ exc_info = None
+
+ with self.isolation(input=input, env=env, color=color) as outstreams:
+ return_value = None
+ exception: BaseException | None = None
+ exit_code = 0
+
+ if isinstance(args, str):
+ args = shlex.split(args)
+
+ try:
+ prog_name = extra.pop("prog_name")
+ except KeyError:
+ prog_name = self.get_default_prog_name(cli)
+
+ try:
+ return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
+ except SystemExit as e:
+ exc_info = sys.exc_info()
+ e_code = cast("int | Any | None", e.code)
+
+ if e_code is None:
+ e_code = 0
+
+ if e_code != 0:
+ exception = e
+
+ if not isinstance(e_code, int):
+ sys.stdout.write(str(e_code))
+ sys.stdout.write("\n")
+ e_code = 1
+
+ exit_code = e_code
+
+ except Exception as e:
+ if not catch_exceptions:
+ raise
+ exception = e
+ exit_code = 1
+ exc_info = sys.exc_info()
+ finally:
+ sys.stdout.flush()
+ sys.stderr.flush()
+ stdout = outstreams[0].getvalue()
+ stderr = outstreams[1].getvalue()
+ output = outstreams[2].getvalue()
+
+ return Result(
+ runner=self,
+ stdout_bytes=stdout,
+ stderr_bytes=stderr,
+ output_bytes=output,
+ return_value=return_value,
+ exit_code=exit_code,
+ exception=exception,
+ exc_info=exc_info, # type: ignore
)
diff --git a/uv.lock b/uv.lock
index 3b258b16bf..beeca3c91d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -39,26 +39,41 @@ wheels = [
]
[[package]]
-name = "babel"
-version = "2.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
-]
-
-[[package]]
-name = "backrefs"
-version = "6.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" },
- { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" },
- { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" },
- { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" },
- { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" },
+name = "ast-serialize"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/9d/912fefab0e30aee6a3af8a62bbea4a81b29afa4ba2c973d31170620a26de/ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b", size = 60689, upload-time = "2026-04-30T23:24:48.104Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/57/a54d4de491d6cdd7a4e4b0952cc3ca9f60dcefa7b5fb48d6d492debe1649/ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14", size = 1182966, upload-time = "2026-04-30T23:23:57.376Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/9e/a5db014bb0f91b209236b57c429389e31290c0093532b8436d577699b2fa/ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22", size = 1171316, upload-time = "2026-04-30T23:23:59.63Z" },
+ { url = "https://files.pythonhosted.org/packages/15/59/fd55133e478c4326f60a11df02573bf7ccb2ac685810b50f1803d0f68053/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f", size = 1232234, upload-time = "2026-04-30T23:24:01.168Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/79/0ca1d26357ecb4a697d74d00b73ef3137f24c140424125393a0de820eb09/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66", size = 1233437, upload-time = "2026-04-30T23:24:03.151Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/7078ec94dd6e124b8e028ac77016a4f13c83fa1c145790f2e68f3816998b/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8", size = 1440188, upload-time = "2026-04-30T23:24:04.717Z" },
+ { url = "https://files.pythonhosted.org/packages/21/16/cca7195ef55a012f8013c3442afa91d287a0a36dcf88b480b262475135b3/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9", size = 1254211, upload-time = "2026-04-30T23:24:06.18Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/0f/f3d4dfae67dee6580534361a6343367d34217e7d25cff858bd1d8f03b8ed/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e", size = 1255973, upload-time = "2026-04-30T23:24:07.772Z" },
+ { url = "https://files.pythonhosted.org/packages/14/41/55fbfe02c42f40fbe3e74eda167d977d555ff720ce1abfa08515236efd88/ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de", size = 1298629, upload-time = "2026-04-30T23:24:09.4Z" },
+ { url = "https://files.pythonhosted.org/packages/28/36/7d2501cacc7989fb8504aa9da2a2022a174200a59d4e6639de4367a57fdd/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df", size = 1408435, upload-time = "2026-04-30T23:24:11.013Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/54e3b469c3fa0bf9cd532fa643d1d33b73303f8d70beac3e366b68dd64b7/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728", size = 1508174, upload-time = "2026-04-30T23:24:12.635Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2a/9b9621865b02c60539e26d9b114a312b4fa46aa703e33e79317174bfea21/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d", size = 1502354, upload-time = "2026-04-30T23:24:14.186Z" },
+ { url = "https://files.pythonhosted.org/packages/34/dd/f138bc5c43b0c414fdd12eefe15677839323078b6e75301ad7f96cd26d45/ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d", size = 1450504, upload-time = "2026-04-30T23:24:16.076Z" },
+ { url = "https://files.pythonhosted.org/packages/68/cf/97ef9e1c315601db74365955c8edd3292e3055500d6317602815dbdf08ae/ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9", size = 1058662, upload-time = "2026-04-30T23:24:17.535Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d6/e2c3483c31580fdb623f92ad38d2f856cde4b9205a3e6bd84760f3de7d82/ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1", size = 1100349, upload-time = "2026-04-30T23:24:18.992Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/89/29abcb1fe18a429cda60c6e0bbd1d6e90499339842a2f548d7567542357e/ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0", size = 1072895, upload-time = "2026-04-30T23:24:20.706Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/93/72abad83966ed6235647c9f956417dc1e17e997696388521910e3d1fa3f4/ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9", size = 1190024, upload-time = "2026-04-30T23:24:22.486Z" },
+ { url = "https://files.pythonhosted.org/packages/85/4f/eb88584b2f0234e581762011208ca203252bf6c98e59b4769daa571f3576/ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc", size = 1178633, upload-time = "2026-04-30T23:24:24.35Z" },
+ { url = "https://files.pythonhosted.org/packages/56/51/cf1ec1ff3e616373d0dcbd5fad502e0029dc541f13ab642259762a7d127f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4", size = 1241351, upload-time = "2026-04-30T23:24:25.987Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/44/68fcf50478cf1093f2d423f034ae06453122c8b415d8e21a44668eca485d/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7", size = 1239582, upload-time = "2026-04-30T23:24:27.808Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/c1/a6c9fa284eceb5fc6f21347e968445a051d7ca2c4d34e6a04314646dbcee/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94", size = 1448853, upload-time = "2026-04-30T23:24:29.534Z" },
+ { url = "https://files.pythonhosted.org/packages/23/5f/8ad3829a09e4e8c5328a53ce7d4711d660944e3e164c5f6abcc2c8f27167/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61", size = 1262204, upload-time = "2026-04-30T23:24:31.482Z" },
+ { url = "https://files.pythonhosted.org/packages/25/13/44aa28d97f10e25247e8576b5f6b2795d4fa1a80acc88acc942c508d06f7/ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0", size = 1266458, upload-time = "2026-04-30T23:24:33.088Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/58/b3a8be3777cd3744324fd5cec0d80d37cd96fc7cbb0fb010e03dff1e870f/ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f", size = 1308700, upload-time = "2026-04-30T23:24:34.657Z" },
+ { url = "https://files.pythonhosted.org/packages/13/03/f8312d6b57f5471a9dc7946f22b8798a1fc296d38c25766223aacadec42c/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a", size = 1416724, upload-time = "2026-04-30T23:24:36.562Z" },
+ { url = "https://files.pythonhosted.org/packages/50/5d/13fc3789a7abac00559da2e2e9f386db4612aa1f84fc53d09bf714c37545/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b", size = 1515441, upload-time = "2026-04-30T23:24:38.018Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/b9/7ab43fc7a23b1f970281093228f5f79bed6edeed7a3e672bde6d7a832a58/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9", size = 1510522, upload-time = "2026-04-30T23:24:39.798Z" },
+ { url = "https://files.pythonhosted.org/packages/56/ec/d75fc2b788d319f1fad77c14156896f31afdfc68af85b505e5bdebcb9592/ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57", size = 1460917, upload-time = "2026-04-30T23:24:41.305Z" },
+ { url = "https://files.pythonhosted.org/packages/95/74/f99c81193a2725911e1911ae567ed27c2f2419332c7f3537366f9d238cac/ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2", size = 1067804, upload-time = "2026-04-30T23:24:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/16/81/76af00c47daa151e89f98ae21fbbcb2840aaa9f5766579c4da76a3c57188/ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549", size = 1105561, upload-time = "2026-04-30T23:24:44.578Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" },
]
[[package]]
@@ -476,6 +491,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547, upload-time = "2018-09-26T16:47:05.609Z" },
]
+[[package]]
+name = "deepmerge"
+version = "2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" },
+]
+
[[package]]
name = "defusedxml"
version = "0.7.1"
@@ -561,15 +585,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
-[[package]]
-name = "hjson"
-version = "3.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/e5/0b56d723a76ca67abadbf7fb71609fb0ea7e6926e94fcca6c65a85b36a0e/hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75", size = 40541, upload-time = "2022-08-13T02:53:01.919Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89", size = 54018, upload-time = "2022-08-13T02:52:59.899Z" },
-]
-
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -600,11 +615,11 @@ wheels = [
[[package]]
name = "idna"
-version = "3.11"
+version = "3.15"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
]
[[package]]
@@ -630,87 +645,87 @@ wheels = [
[[package]]
name = "librt"
-version = "0.8.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" },
- { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" },
- { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" },
- { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" },
- { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" },
- { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" },
- { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" },
- { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" },
- { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" },
- { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" },
- { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" },
- { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" },
- { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" },
- { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" },
- { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" },
- { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" },
- { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" },
- { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" },
- { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" },
- { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" },
- { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" },
- { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" },
- { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" },
- { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" },
- { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" },
- { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
- { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
- { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
- { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" },
- { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" },
- { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" },
- { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" },
- { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" },
- { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" },
- { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" },
- { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" },
- { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" },
- { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" },
- { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" },
- { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" },
- { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" },
- { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" },
- { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" },
- { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" },
- { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" },
- { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" },
- { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" },
- { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" },
- { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" },
- { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" },
- { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" },
- { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" },
- { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" },
- { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" },
- { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" },
- { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" },
- { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" },
- { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" },
- { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" },
- { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" },
- { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" },
- { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" },
- { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" },
- { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" },
- { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" },
- { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" },
- { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" },
- { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" },
- { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" },
- { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" },
- { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" },
- { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" },
- { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" },
- { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" },
- { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" },
- { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" },
+version = "0.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" },
+ { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" },
+ { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" },
+ { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" },
+ { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" },
+ { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" },
+ { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" },
+ { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" },
+ { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" },
+ { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" },
+ { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" },
+ { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" },
+ { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" },
+ { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" },
+ { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" },
+ { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" },
]
[[package]]
@@ -915,73 +930,9 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" },
]
-[[package]]
-name = "mkdocs-macros-plugin"
-version = "1.5.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "hjson" },
- { name = "jinja2" },
- { name = "mkdocs" },
- { name = "packaging" },
- { name = "pathspec" },
- { name = "python-dateutil" },
- { name = "pyyaml" },
- { name = "requests" },
- { name = "super-collections" },
- { name = "termcolor" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/92/15/e6a44839841ebc9c5872fa0e6fad1c3757424e4fe026093b68e9f386d136/mkdocs_macros_plugin-1.5.0.tar.gz", hash = "sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f", size = 37730, upload-time = "2025-11-13T08:08:55.545Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/51/62/9fffba5bb9ed3d31a932ad35038ba9483d59850256ee0fea7f1187173983/mkdocs_macros_plugin-1.5.0-py3-none-any.whl", hash = "sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f", size = 44626, upload-time = "2025-11-13T08:08:53.878Z" },
-]
-
-[[package]]
-name = "mkdocs-material"
-version = "9.7.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "babel" },
- { name = "backrefs" },
- { name = "colorama" },
- { name = "jinja2" },
- { name = "markdown" },
- { name = "mkdocs" },
- { name = "mkdocs-material-extensions" },
- { name = "paginate" },
- { name = "pygments" },
- { name = "pymdown-extensions" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" },
-]
-
-[[package]]
-name = "mkdocs-material-extensions"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
-]
-
-[[package]]
-name = "mkdocs-redirects"
-version = "1.2.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mkdocs" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" },
-]
-
[[package]]
name = "mkdocstrings"
-version = "0.30.1"
+version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
@@ -991,9 +942,9 @@ dependencies = [
{ name = "mkdocs-autorefs" },
{ name = "pymdown-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" },
]
[package.optional-dependencies]
@@ -1018,60 +969,61 @@ wheels = [
[[package]]
name = "mypy"
-version = "1.20.2"
+version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "ast-serialize" },
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" },
- { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" },
- { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" },
- { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" },
- { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" },
- { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" },
- { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" },
- { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" },
- { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" },
- { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" },
- { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" },
- { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" },
- { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" },
- { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" },
- { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" },
- { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" },
- { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" },
- { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" },
- { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" },
- { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" },
- { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" },
- { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" },
- { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" },
- { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" },
- { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" },
- { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" },
- { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" },
- { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" },
- { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" },
- { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" },
- { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" },
- { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" },
- { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" },
- { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" },
- { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" },
- { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" },
- { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" },
- { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" },
- { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" },
- { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" },
- { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" },
- { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" },
- { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" },
+ { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" },
+ { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" },
+ { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" },
+ { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" },
+ { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" },
+ { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" },
+ { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" },
+ { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" },
+ { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" },
+ { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" },
+ { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" },
]
[[package]]
@@ -1092,15 +1044,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
-[[package]]
-name = "paginate"
-version = "0.5.7"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
-]
-
[[package]]
name = "pathspec"
version = "1.0.4"
@@ -1228,26 +1171,26 @@ wheels = [
[[package]]
name = "prek"
-version = "0.3.11"
+version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/60/5b980c70525ca5f0d17942d8eae13b399051aa384413366fe5df229712ea/prek-0.3.11.tar.gz", hash = "sha256:c4cf77848009503c58d80ff216e32af45b63ea49652bb5546748c1ebfd4d9847", size = 433440, upload-time = "2026-04-27T04:22:59.923Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/01/1d2c238c6f226d75881cd7a5532e980f4d524babc3c034d16ad89e88b6e1/prek-0.4.1.tar.gz", hash = "sha256:622a8812bda87cf4ddcae2dab5ccecc55b88d70c677129dbe25e975d923179f0", size = 452606, upload-time = "2026-05-20T04:27:19.259Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ee/2a/3392fa7d1fd1ce538915baa7597e7203bbe888367a8b15bfd51ca74d4714/prek-0.3.11-py3-none-linux_armv6l.whl", hash = "sha256:787e605716cfdc86ec01e7c5cf62799f39c28d49de5e37d75595c8e6248cb0f3", size = 5423112, upload-time = "2026-04-27T04:22:52.659Z" },
- { url = "https://files.pythonhosted.org/packages/a9/b0/3fc653b30b70d6c2714fc56bcfe1c2439437fc38f60b72bc300603ace4cd/prek-0.3.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef1f37187ca52d75ba9c46b53007476c4eab2c3f11bd23defd57a81c62d90442", size = 5801382, upload-time = "2026-04-27T04:23:04.464Z" },
- { url = "https://files.pythonhosted.org/packages/2e/46/39aedc7843c3703f1f43b686622e4f8cd123e03b87a163e5c8f2fbd56cda/prek-0.3.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d0828a1b50447502ea1be3f5a84da474fdca558cd5d76a1a5205169bb808c7", size = 5370817, upload-time = "2026-04-27T04:22:49.277Z" },
- { url = "https://files.pythonhosted.org/packages/15/83/df5f3aeacbdea96a88c4f06c98d3932469711fed4e3bf5b703dd6507abe7/prek-0.3.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bf49464b526ee36d2130baf60ab9580560bfaa60efd2997328e6d6671e209014", size = 5621405, upload-time = "2026-04-27T04:23:10.58Z" },
- { url = "https://files.pythonhosted.org/packages/8f/f2/e32c9720747a327669863a4f92d05b9e6fadb851e903b0d7310a97c956a4/prek-0.3.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac344529f0d34757c7c95f65e66b9f6440a691f826eaf43f503247bd22023558", size = 5339780, upload-time = "2026-04-27T04:22:47.614Z" },
- { url = "https://files.pythonhosted.org/packages/29/2e/0e2f71b63bc2e5372575d5c1574b0666d2f90d30da51ed706a32cbf465a0/prek-0.3.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83672d963f249e2f246d3bec97f8fd2e8032e70da0a7d9acb2fa38af76dd82d2", size = 5735277, upload-time = "2026-04-27T04:23:12.437Z" },
- { url = "https://files.pythonhosted.org/packages/09/46/88abf51ac88eeff1ad2fe7d1797ca1fea43eb1ac1ddb8331463ac5b27ed2/prek-0.3.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef70957195d2896a30dd849e64f88344df7bb51af9c950cf16bd11519e7424b0", size = 6622420, upload-time = "2026-04-27T04:23:05.982Z" },
- { url = "https://files.pythonhosted.org/packages/5d/b6/592028a45b084a68b76c7edef909c789d1c96b26761388f63659beef7166/prek-0.3.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f0cc07d2cdb5fe2882015d5fdafc9af98b4c560d4caa1ae948caeab4341b79", size = 6020038, upload-time = "2026-04-27T04:22:54.367Z" },
- { url = "https://files.pythonhosted.org/packages/ca/f7/e97f55a1645a2e9becffeee28892ad8bb66cd144dabfa4392ea8e2674bbe/prek-0.3.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d7554b436dae2ec97f4351a46817e3561657244307d1c0915f355b859f4fab71", size = 5622539, upload-time = "2026-04-27T04:23:01.314Z" },
- { url = "https://files.pythonhosted.org/packages/41/e2/f3119eef6b621782ad216a86d449609858ea34c57cf4a40fc6dc80556d7e/prek-0.3.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:caba5d635a5b64b7ac64d903f29b043ca5b0d9d9693543a0ef331ade89e6ad3f", size = 5440681, upload-time = "2026-04-27T04:23:07.422Z" },
- { url = "https://files.pythonhosted.org/packages/04/62/22dd4f59a47654faeebe74651182ecc48d436542646cc92723052dfd9a45/prek-0.3.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:c95a63f19dde48e84b70bd63a235670834af15fa4df8b85d8b7894dd5bc419a9", size = 5314773, upload-time = "2026-04-27T04:22:57.912Z" },
- { url = "https://files.pythonhosted.org/packages/bb/94/a8361462acb8d8f5b8505255b95ffbfc2ee0872a79b4e066eb330692f7be/prek-0.3.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7353b45f44a386c676fe96ba72a5ee326b676f789339f405cf6f1d69a1707194", size = 5596208, upload-time = "2026-04-27T04:23:09.08Z" },
- { url = "https://files.pythonhosted.org/packages/04/0c/5f065b86bbeb9977074a055d8a05e90c7201f6c4c7032dada61739b5f8cb/prek-0.3.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:39f4e86176ccbb70c098df6abbc8e36c1d86cb81281abe92fb79dcd572418214", size = 6132833, upload-time = "2026-04-27T04:22:56.054Z" },
- { url = "https://files.pythonhosted.org/packages/19/0c/8ab0ae140201dcee505f58b60abbe56bd05ac96b821a6866f6f90c4d971f/prek-0.3.11-py3-none-win32.whl", hash = "sha256:35d2361049653a3dcf27227b7f1b340c5c42a12c0e0361c4b785921bfd125839", size = 5120856, upload-time = "2026-04-27T04:23:02.752Z" },
- { url = "https://files.pythonhosted.org/packages/57/05/9844c1125d3714f6f6c7b475884128a4b0c6c3ee0cd208ead44ca8174687/prek-0.3.11-py3-none-win_amd64.whl", hash = "sha256:a387689cd2e182f92dbb681151ee5a04f494fe97e95d6d783875da90b950e6d5", size = 5510916, upload-time = "2026-04-27T04:22:45.704Z" },
- { url = "https://files.pythonhosted.org/packages/ff/13/24b0288c553dc8d61f44c4d0746fe9bb1e1bd29d1e70571658536e4c0f72/prek-0.3.11-py3-none-win_arm64.whl", hash = "sha256:e4a8f900378a6657c7eb2fc4b12fa5c934edf209d0a24544539842479ec16e0b", size = 5345988, upload-time = "2026-04-27T04:22:50.918Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/ca/0274343faf2672d649b1e648053d3cb48fdfef7a390b43713d95880ebb67/prek-0.4.1-py3-none-linux_armv6l.whl", hash = "sha256:10e7e78ffe65dfba7d687a8c71b2f473554d1ba60f43c742105da4c0030feed9", size = 5515584, upload-time = "2026-05-20T04:27:29.386Z" },
+ { url = "https://files.pythonhosted.org/packages/37/4e/6a067f530194a6e4141c36463eece92356dfd7f924ffe0cbf456bdca723b/prek-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b25807e0aa57d2118747e127b58e7a1bf41d5d7b3323f5f3f1f3cb10031245cc", size = 5878925, upload-time = "2026-05-20T04:27:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/3d/a334c0f5b88fadca888eadfc1fb3d7f1dc8358b1a534d0987339ecb8eb92/prek-0.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:efa95331c4c171a867c0064c19d8a4abc94a1c1c920c8b8092f2d7d87f4b90a8", size = 5440994, upload-time = "2026-05-20T04:27:40.578Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/3b/fa6eb635495c3576e65d7f42a48b9fdf4926dd052010df506ed98e9f9680/prek-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2d1805123ab5d730629de588bf319ea39e7078b589b3288c95740f1b4780a1d4", size = 5692369, upload-time = "2026-05-20T04:27:23.184Z" },
+ { url = "https://files.pythonhosted.org/packages/70/cb/9d9078723b3facb40289444332ca82bf38c0e1db3b5a907af461aba12324/prek-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:051c442b570b53756225410240577bee1aeace6be52955dfacf45a9783223b36", size = 5430031, upload-time = "2026-05-20T04:27:27.475Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/96/2d8cc6b5425215cd0b610f1dcef3f6f0f23db2a2b85f1a6fca43b7e7fe24/prek-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76663998827a2cbc94f5e209319809655489b5bd1f8e70568a623372e80253f0", size = 5834244, upload-time = "2026-05-20T04:27:44.229Z" },
+ { url = "https://files.pythonhosted.org/packages/59/e0/cce02f3ade48a6d4bffb25e5f0ac28d10928263b0a4f53ecc72954957f4e/prek-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ab3460641762edf128b1ec8e833ce7e9ae015d1268a894560cb90d3393a7527", size = 6711903, upload-time = "2026-05-20T04:27:34.128Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2a/ccd581b6222277a2aa095530844d5bb76db4547042f05a9cb649476bf904/prek-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e69a9c02ead38706a5d2a4ae209dccba08ccb5d0026e1d08e723c66ab964750", size = 6084138, upload-time = "2026-05-20T04:27:46.549Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/b7/6164a7dc6bb4796cfc19445be798302cc7625b62e2bec89ffb4272d7f983/prek-0.4.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:dc744fedf98df8a00a9e3bcd629b163fee5e9f9e22bce66029d9945241586165", size = 5698950, upload-time = "2026-05-20T04:27:36.165Z" },
+ { url = "https://files.pythonhosted.org/packages/96/40/8151d6445a0f41ad60e979db39d8b0c6b074aad919cf5c73233281f0dff1/prek-0.4.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c0877e82c52359d655fe1072b3a5228639184d1d5f03c6803b6530cd6da1ef20", size = 5538662, upload-time = "2026-05-20T04:27:15.045Z" },
+ { url = "https://files.pythonhosted.org/packages/96/d7/1f9892a45bb2dc8a3b4b89eb08f5de1cf745fcd7df9e535463ba4d41cebe/prek-0.4.1-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:60928d1dad45ff3e491d3083a50643cc213aa2d54f1dbd8d702d7193773c020e", size = 5406581, upload-time = "2026-05-20T04:27:21.101Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/b8/94ddac155b502859e4dc7943db99fa7fffecfa3878a2ef11726a8e72fad0/prek-0.4.1-py3-none-musllinux_1_1_i686.whl", hash = "sha256:17ffa9d8dd40791b9b99cafe558c5cc28e78e5be57607b280b15f0dab90264e9", size = 5688880, upload-time = "2026-05-20T04:27:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/fd/e93d3853d1bdc06b281fff2aaf4106e19610fe5187c67c9ff13195f2df59/prek-0.4.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cdf4503a240369f66321213d9c4bc6f925014b64ff7121de9e9920c9b9838ce2", size = 6203536, upload-time = "2026-05-20T04:27:42.366Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/760969d6bfc77e3eba04f6c3801c81076e96a908a6c277c142a4b0f31f4e/prek-0.4.1-py3-none-win32.whl", hash = "sha256:7c515492ef3585e6bcd7b83f1bb1cb131038abc88ed2c843de1e4c3ceb865b19", size = 5208995, upload-time = "2026-05-20T04:27:38.331Z" },
+ { url = "https://files.pythonhosted.org/packages/89/12/d43daf290a73dbc3e1a3eabb9077e45df661923949bee045de67cbe82524/prek-0.4.1-py3-none-win_amd64.whl", hash = "sha256:8fa707971465d8ad021c907e43691aad7bb98942943e61e294ece5f95d9fbc78", size = 5591734, upload-time = "2026-05-20T04:27:12.744Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/36/2ab7647fe1e84bba2baae7f04de241197eed62683fb3085e164de266d111/prek-0.4.1-py3-none-win_arm64.whl", hash = "sha256:5b4a348537924b20e208cbd87ef58e96ec37d691c5bec2969209c40de0ecf72e", size = 5423147, upload-time = "2026-05-20T04:27:17.023Z" },
]
[[package]]
@@ -1261,7 +1204,7 @@ wheels = [
[[package]]
name = "pydantic"
-version = "2.13.3"
+version = "2.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -1269,139 +1212,139 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
-version = "2.46.3"
+version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" },
- { url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" },
- { url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" },
- { url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" },
- { url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" },
- { url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" },
- { url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" },
- { url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" },
- { url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" },
- { url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" },
- { url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" },
- { url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" },
- { url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" },
- { url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" },
- { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" },
- { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" },
- { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" },
- { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" },
- { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" },
- { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" },
- { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" },
- { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" },
- { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" },
- { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" },
- { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" },
- { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" },
- { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" },
- { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" },
- { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" },
- { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" },
- { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" },
- { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" },
- { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" },
- { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" },
- { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" },
- { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" },
- { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" },
- { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" },
- { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" },
- { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" },
- { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" },
- { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" },
- { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" },
- { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" },
- { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" },
- { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" },
- { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" },
- { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" },
- { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" },
- { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" },
- { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" },
- { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" },
- { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" },
- { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" },
- { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" },
- { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" },
- { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" },
- { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" },
- { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" },
- { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
- { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
- { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
- { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
- { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
- { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
- { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
- { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
- { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
- { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
- { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
- { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
- { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
- { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
- { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
- { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
- { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
- { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
- { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
- { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
- { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
- { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
- { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
- { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
- { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
- { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
- { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
- { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
- { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
- { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" },
- { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" },
- { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" },
- { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" },
- { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" },
- { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" },
- { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" },
- { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" },
- { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" },
- { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" },
- { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" },
- { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" },
- { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" },
- { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" },
- { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" },
- { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
+ { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
+ { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
+ { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
+ { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
+ { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
+ { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+ { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+ { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+ { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+ { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+ { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
+ { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
+ { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+ { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
+ { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
]
[[package]]
name = "pydantic-settings"
-version = "2.14.0"
+version = "2.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" },
]
[[package]]
@@ -1445,15 +1388,15 @@ crypto = [
[[package]]
name = "pymdown-extensions"
-version = "10.21.2"
+version = "10.21.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" },
]
[[package]]
@@ -1563,11 +1506,11 @@ wheels = [
[[package]]
name = "python-dotenv"
-version = "1.2.1"
+version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]]
@@ -1685,27 +1628,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.12"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
- { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
- { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
- { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
- { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
- { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
- { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
- { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
- { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
- { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
- { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
- { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
- { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
- { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
- { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
- { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
- { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
+version = "0.15.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" },
+ { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" },
+ { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" },
+ { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" },
]
[[package]]
@@ -1739,18 +1682,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/10/0d23e4953eb7c1e1ad848084b3115f19234f34f907658ed11bed0d826aee/smokeshow-0.5.0-py3-none-any.whl", hash = "sha256:da12a960fc7cb525efc4035a0c3c9363b6217ea7e66bc39b9ed3cd8bed6eeedc", size = 8389, upload-time = "2025-01-07T19:41:49.194Z" },
]
-[[package]]
-name = "super-collections"
-version = "0.6.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "hjson" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e0/de/a0c3d1244912c260638f0f925e190e493ccea37ecaea9bbad7c14413b803/super_collections-0.6.2.tar.gz", hash = "sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a", size = 31315, upload-time = "2025-09-30T00:37:08.067Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/17/43/47c7cf84b3bd74a8631b02d47db356656bb8dff6f2e61a4c749963814d0d/super_collections-0.6.2-py3-none-any.whl", hash = "sha256:291b74d26299e9051d69ad9d89e61b07b6646f86a57a2f5ab3063d206eee9c56", size = 16173, upload-time = "2025-09-30T00:37:07.104Z" },
-]
-
[[package]]
name = "termcolor"
version = "3.3.0"
@@ -1774,75 +1705,81 @@ wheels = [
[[package]]
name = "tomli"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
- { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
- { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
- { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
- { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
- { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
- { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
- { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
- { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
- { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
- { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
- { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
- { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
- { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
- { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
- { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
- { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
- { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
- { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" },
- { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" },
- { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" },
- { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
- { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
- { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" },
- { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" },
- { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" },
- { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" },
- { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" },
- { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" },
- { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
- { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
- { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" },
- { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" },
- { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" },
- { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" },
- { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" },
- { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" },
- { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
- { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
- { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
name = "ty"
-version = "0.0.32"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/85/7e/2aa791c9ae7b8cd5024cd4122e92267f664ca954cea3def3211919fa3c1f/ty-0.0.32.tar.gz", hash = "sha256:8743174c5f920f6700a4a0c9de140109189192ba16226884cd50095b43b8a45c", size = 5522294, upload-time = "2026-04-20T19:29:01.626Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/62/eb/1075dc6a49d7acbe2584ae4d5b410c41b1f177a5adcc567e09eca4c69000/ty-0.0.32-py3-none-linux_armv6l.whl", hash = "sha256:dacbc2f6cd698d488ae7436838ff929570455bf94bfa4d9fe57a630c552aff83", size = 10902959, upload-time = "2026-04-20T19:28:31.907Z" },
- { url = "https://files.pythonhosted.org/packages/33/d2/c35fc8bc66e98d1ee9b0f8ed319bf743e450e1f1e997574b178fab75670f/ty-0.0.32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914bbc4f605ce2a9e2a78982e28fae1d3359a169d141f9dc3b4c7749cd5eca81", size = 10726172, upload-time = "2026-04-20T19:28:44.765Z" },
- { url = "https://files.pythonhosted.org/packages/96/32/c827da3ca480456fb02d8cea68a2609273b6c220fea0be9a4c8d8470b86e/ty-0.0.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4787ac9fe1f86b1f3133f5c6732adbe2df5668b50c679ac6e2d98cd284da812f", size = 10163701, upload-time = "2026-04-20T19:28:27.005Z" },
- { url = "https://files.pythonhosted.org/packages/ba/9e/2734478fbdb90c160cb2813a3916a16a2af5c1e231f87d635f6131d781fb/ty-0.0.32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ea0a728af99fe40dd744cba6441a2404f80b7f4bde17aa6da393810af5ea57", size = 10656220, upload-time = "2026-04-20T19:29:03.814Z" },
- { url = "https://files.pythonhosted.org/packages/44/9f/0007da2d35e424debe7e9f86ffbc1ab7f60983cfbc5f0411324ab2de5292/ty-0.0.32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2850561f9b018ae33d7e5bbfa0ac414d3c518513edcffe43877dc9801446b9c5", size = 10696086, upload-time = "2026-04-20T19:28:46.829Z" },
- { url = "https://files.pythonhosted.org/packages/3b/5e/ce5fd4ec803222ae3e69a76d2a2db2eed55e19f5b131702b9789ef45f93d/ty-0.0.32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5fa2fb3c614349ee211d36476b49d88c5ef79a687cdb91b2872ad023b94d2f8", size = 11184800, upload-time = "2026-04-20T19:28:42.57Z" },
- { url = "https://files.pythonhosted.org/packages/6c/46/ebcf67a5999421331214aac51a7464db42de2be15bbe929c612a3ed0b039/ty-0.0.32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b89969307ab2417d41c9be8059dd79feea577234e1e10d35132f5495e0d42c6", size = 11718718, upload-time = "2026-04-20T19:28:36.433Z" },
- { url = "https://files.pythonhosted.org/packages/18/2c/2141c86ed0ce0962b45cefb658a95e734f59759d47f20afdcd9c732910a1/ty-0.0.32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b59868ede9b1d69a088f0d695df52a0061f95fa7baa1d5e0dc6fc9cf06e1334", size = 11346369, upload-time = "2026-04-20T19:28:48.967Z" },
- { url = "https://files.pythonhosted.org/packages/7a/da/ed6f772339cf29bd9a46def9d6db5084689eb574ee4d150ff704224c1ed8/ty-0.0.32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8300caf35345498e9b9b03e550bba03cee8f5f5f8ab4c83c3b1ff1b7403b7d3a", size = 11280714, upload-time = "2026-04-20T19:28:51.516Z" },
- { url = "https://files.pythonhosted.org/packages/da/9b/c6813987edf4816a40e0c8e408b555f97d3f267c7b3a1688c8bbdf65609c/ty-0.0.32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:583c7094f4574b02f724db924f98b804d1387a0bd9405ecb5e078cc0f47fbcfb", size = 10638806, upload-time = "2026-04-20T19:28:29.651Z" },
- { url = "https://files.pythonhosted.org/packages/4e/d4/0cefcbd2ad0f3d51762ccf58e652ec7da146eb6ae34f87228f6254bbb8be/ty-0.0.32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e44ebe1bb4143a5628bc4db67ac0dfebe14594af671e4ee66f6f2e983da56501", size = 10726106, upload-time = "2026-04-20T19:29:06.3Z" },
- { url = "https://files.pythonhosted.org/packages/32/ad/2c8a97f91f06311f4367400f7d13534bbda2522c73c99a3e4c0757dff9b8/ty-0.0.32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:06f17ada3e069cba6148342ef88e9929156beca8473e8d4f101b68f66c75643e", size = 10872951, upload-time = "2026-04-20T19:28:34.077Z" },
- { url = "https://files.pythonhosted.org/packages/ba/68/42293f9248106dd51875120971a5cc6ea315c2c4dcfb8e59aa063aa0af26/ty-0.0.32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e96e60fa556cec04f15d7ea62d2ceee5982bd389233e961ab9fd42304e278175", size = 11363334, upload-time = "2026-04-20T19:28:54.036Z" },
- { url = "https://files.pythonhosted.org/packages/df/92/be9abf4d3e589ad5023e2ea965b93e204ec856420d46adf73c5c36c04678/ty-0.0.32-py3-none-win32.whl", hash = "sha256:2ff2ebb4986b24aebcf1444db7db5ca41b36086040e95eea9f8fb851c11e805c", size = 10260689, upload-time = "2026-04-20T19:28:56.541Z" },
- { url = "https://files.pythonhosted.org/packages/14/61/dc86acea899349d2579cb8419aecedd83dc504d7d6a10df65eef546c8300/ty-0.0.32-py3-none-win_amd64.whl", hash = "sha256:ba7284a4a954b598c1b31500352b3ec1f89bff533825592b5958848226fdc7ee", size = 11255371, upload-time = "2026-04-20T19:28:39.917Z" },
- { url = "https://files.pythonhosted.org/packages/43/01/beffec56d71ca25b343ede63adb076456b5b3e211f1c066452a44cd120b3/ty-0.0.32-py3-none-win_arm64.whl", hash = "sha256:7e10aadbdbda989a7d567ee6a37f8b98d4d542e31e3b190a2879fd581f75d658", size = 10658087, upload-time = "2026-04-20T19:28:59.286Z" },
+version = "0.0.38"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/3b/45be6b37d5060d6917bf7f1f234c00d360fc5f8b7486f8a96af640e25661/ty-0.0.38.tar.gz", hash = "sha256:fbc8d47f7630457669ab41e333dc093897fdb7ead1ffc94dcf8f30b5d39aa56d", size = 5681218, upload-time = "2026-05-20T00:15:32.781Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/43/ea9b4e57d6a266670dbe34858e92f6093ca054ad1b48f1c82580a72340fb/ty-0.0.38-py3-none-linux_armv6l.whl", hash = "sha256:3501dcf44ca03f813f9cb4fabfdf601adc0ac1337c411405b470530679e37a45", size = 11289326, upload-time = "2026-05-20T00:14:52.371Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ff/24e2f623a1c6b5f5ccf8bf82fccd937033c6a7dba57a4028c7f41270fa4a/ty-0.0.38-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b34b4094b76252c3e8c90762cdd5e8a9f1101534484745ff4b480f71eb38ac2e", size = 11063047, upload-time = "2026-05-20T00:14:42.832Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/41/4f0d910f0acbd20b358eda80a5cd6a8361d27ff5b8e87ab559d3f69f125e/ty-0.0.38-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c518ad33a877677365baab2e21d82cf59ffee789203a15a143f5179ee5a1d3f8", size = 10494436, upload-time = "2026-05-20T00:15:24.425Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d8/da06833422082aa98b169a391f9197e2d73865e96c90b6979ac886b890a2/ty-0.0.38-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9238494722303eccddc6a27eb647948b694eecd6b974910d13b9e6cd46bbeb6a", size = 11000992, upload-time = "2026-05-20T00:14:58.368Z" },
+ { url = "https://files.pythonhosted.org/packages/16/f7/e1172197fb827e6410ca3eb0dc68ef2789f3c70683696f2a0ce5c90764fd/ty-0.0.38-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d91d7336c5d51bf822ac0df512f300584ca4dcca041fc6a6d7df03a8ddbb31", size = 11058583, upload-time = "2026-05-20T00:15:11.314Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/61/7fbaf0c05981e006a8804287819c574dff90a6bf8e96efad7226be0700aa/ty-0.0.38-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65165879814993450710b9349791e4898c65e36b1e14eec554884c06a2f20ff1", size = 11531036, upload-time = "2026-05-20T00:15:14.62Z" },
+ { url = "https://files.pythonhosted.org/packages/49/e3/47c0c64e401d50f925df3e52479d4e7626754b2a9e38201d142fdacd6252/ty-0.0.38-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d61868b8d1c4033bf8088191de953fed245c2f9e1bb9d2d53e5699170b0924c", size = 12129991, upload-time = "2026-05-20T00:14:39.475Z" },
+ { url = "https://files.pythonhosted.org/packages/90/99/2f452d02901bcd7f1b109cf5b848727ce37f372c3406143aa52d1305d40e/ty-0.0.38-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f9a9175548c98dbff7707865738c07c2b1f8e07a09b8c68101baebb5dac59a4", size = 11756167, upload-time = "2026-05-20T00:15:27.526Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/0c/c7e14d111c813e1a20b82e944f1c997c4631a2bb710eaa64fb6b26835e13/ty-0.0.38-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375d3a964c6b4aea2e9237fdb5eb9ed03dc43088986a94209a28a4ea3b62001c", size = 11637099, upload-time = "2026-05-20T00:15:21.261Z" },
+ { url = "https://files.pythonhosted.org/packages/37/de/ab02659dd1ed62898db7db4d37f9937c80854dd45e95093fa0fe10328d82/ty-0.0.38-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cdfd547782c45267aa0b52abad31bd406bf4768c264532ef9e2360cd3c6ce048", size = 11813583, upload-time = "2026-05-20T00:14:45.875Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/57/bd1b5ebf4e71a4295484afac0202df1740b0807762b86744b1bef4534984/ty-0.0.38-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:858bc675b75626470abe4e6c3b3934b853642b04f2ac4d7139fcefea3b48b213", size = 10975405, upload-time = "2026-05-20T00:15:30.354Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/55/0305c78711bbd23922cf291996a08ef9544f4179da98e9a75c14e608f379/ty-0.0.38-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54be4f00432870da42cd74fe145a3362fd248e22d032c74bd807cb45bf068f94", size = 11097551, upload-time = "2026-05-20T00:14:55.179Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/4f/7effe7f9a6ac9719eb7234172c01739c5f888bb47f9acc2ea8da1f4afed3/ty-0.0.38-py3-none-musllinux_1_2_i686.whl", hash = "sha256:494af66a76a86dbf16a3003d3b63b03484aa4c7489dfe11f3ee5413b98b22d60", size = 11214391, upload-time = "2026-05-20T00:15:18.094Z" },
+ { url = "https://files.pythonhosted.org/packages/75/cd/d9fdfec3a74a6ad0209fa5e7113ae29d4f457d0651cfbb813b4c6563e0d4/ty-0.0.38-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3d92527c4be78a5ce6d32e8bb0aa2a6988d4076eddf1294e56fdaf06d1a98e7e", size = 11730871, upload-time = "2026-05-20T00:14:49.219Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/4a/beefade12d109b4f7793d61b04b4478b1ad4d1465a719e7ff55b2d42461a/ty-0.0.38-py3-none-win32.whl", hash = "sha256:36fc5dd5dc09207ff3004b1560a79a3fb8d12456daeec914a7b802a918da654c", size = 10548583, upload-time = "2026-05-20T00:15:07.892Z" },
+ { url = "https://files.pythonhosted.org/packages/15/64/941b205e2e46cc2297c245c64aa7691410b7454fa4d07a6cb3cf59487833/ty-0.0.38-py3-none-win_amd64.whl", hash = "sha256:eef0a8956ba14514076b1a963d13eb32986d9ebad7f0527b3cc01cb68bf35147", size = 11650542, upload-time = "2026-05-20T00:15:01.441Z" },
+ { url = "https://files.pythonhosted.org/packages/59/02/c1c4f9ec4b94d95190636fa13f79c32f65165fbe3a0503882d4df164d2ac/ty-0.0.38-py3-none-win_arm64.whl", hash = "sha256:79abfc8658a026c30b1c955613437dab3ef4b12feca56a3e6df50903cc39e07f", size = 11010307, upload-time = "2026-05-20T00:15:04.567Z" },
]
[[package]]
@@ -1850,7 +1787,7 @@ name = "typer"
source = { editable = "." }
dependencies = [
{ name = "annotated-doc" },
- { name = "click" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich" },
{ name = "shellingham" },
]
@@ -1863,9 +1800,6 @@ dev = [
{ name = "griffe-warnings-deprecated" },
{ name = "markdown-include-variants" },
{ name = "mdx-include" },
- { name = "mkdocs-macros-plugin" },
- { name = "mkdocs-material" },
- { name = "mkdocs-redirects" },
{ name = "mkdocstrings", extra = ["python"] },
{ name = "mypy" },
{ name = "pillow" },
@@ -1879,6 +1813,8 @@ dev = [
{ name = "ruff" },
{ name = "shellingham" },
{ name = "ty" },
+ { name = "zensical" },
+ { name = "zizmor" },
]
docs = [
{ name = "cairosvg" },
@@ -1886,12 +1822,10 @@ docs = [
{ name = "griffe-warnings-deprecated" },
{ name = "markdown-include-variants" },
{ name = "mdx-include" },
- { name = "mkdocs-macros-plugin" },
- { name = "mkdocs-material" },
- { name = "mkdocs-redirects" },
{ name = "mkdocstrings", extra = ["python"] },
{ name = "pillow" },
{ name = "pyyaml" },
+ { name = "zensical" },
]
github-actions = [
{ name = "httpx" },
@@ -1916,7 +1850,7 @@ tests = [
[package.metadata]
requires-dist = [
{ name = "annotated-doc", specifier = ">=0.0.2" },
- { name = "click", specifier = ">=8.2.1" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich", specifier = ">=13.8.0" },
{ name = "shellingham", specifier = ">=1.3.0" },
]
@@ -1929,10 +1863,7 @@ dev = [
{ name = "griffe-warnings-deprecated", specifier = ">=1.1.0" },
{ name = "markdown-include-variants", specifier = ">=0.0.8" },
{ name = "mdx-include", specifier = ">=1.4.1" },
- { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" },
- { name = "mkdocs-material", specifier = ">=9.7.1" },
- { name = "mkdocs-redirects", specifier = ">=1.2.1" },
- { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
+ { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.3" },
{ name = "mypy", specifier = ">=1.19.1" },
{ name = "pillow", specifier = ">=11.3.0" },
{ name = "prek", specifier = ">=0.3.2" },
@@ -1945,6 +1876,8 @@ dev = [
{ name = "ruff", specifier = ">=0.15.0" },
{ name = "shellingham", specifier = ">=1.3.0" },
{ name = "ty", specifier = ">=0.0.25" },
+ { name = "zensical", specifier = ">=0.0.42" },
+ { name = "zizmor", specifier = ">=1.23.1" },
]
docs = [
{ name = "cairosvg", specifier = ">=2.8.2" },
@@ -1952,12 +1885,10 @@ docs = [
{ name = "griffe-warnings-deprecated", specifier = ">=1.1.0" },
{ name = "markdown-include-variants", specifier = ">=0.0.8" },
{ name = "mdx-include", specifier = ">=1.4.1" },
- { name = "mkdocs-macros-plugin", specifier = ">=1.5.0" },
- { name = "mkdocs-material", specifier = ">=9.7.1" },
- { name = "mkdocs-redirects", specifier = ">=1.2.1" },
- { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.1" },
+ { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.3" },
{ name = "pillow", specifier = ">=11.3.0" },
{ name = "pyyaml", specifier = ">=5.3.1" },
+ { name = "zensical", specifier = ">=0.0.42" },
]
github-actions = [
{ name = "httpx", specifier = ">=0.27.0" },
@@ -2002,11 +1933,11 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.6.3"
+version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
@@ -2049,3 +1980,51 @@ sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda5308
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
]
+
+[[package]]
+name = "zensical"
+version = "0.0.43"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "deepmerge" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "pygments" },
+ { name = "pymdown-extensions" },
+ { name = "pyyaml" },
+ { name = "tomli" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d4/85/ec45162e7824a8f879d887ef0774ee65926bf7d1064e2eebccc7eaee3378/zensical-0.0.43.tar.gz", hash = "sha256:dc2d3804ff562795c1024130e0c3ce79736467930729dda314f096d0e35b98c8", size = 3932396, upload-time = "2026-05-19T09:44:07.418Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/c2/55e0709607ae41c266987c3b91a1a9702b37fbbef0d07eddfe5e25c2d823/zensical-0.0.43-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:17c335362b6bac3a50178181694a964f6d9f0c516fc532129ba5a0a5c4103fb6", size = 12706531, upload-time = "2026-05-19T09:43:32.729Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/64/ce8627bc5ea30556162b29b041fe97d6a6aef2a87b51f12def628e4fa608/zensical-0.0.43-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8fe97f185194215f6193af45a17d2b30ebd72c8113e3650f2d7d6767b9c2206", size = 12563012, upload-time = "2026-05-19T09:43:35.962Z" },
+ { url = "https://files.pythonhosted.org/packages/66/d1/533bc9454f0e06b3d9d8bd2e7ac405308c3d4dee6572acab98f0ed6d1c07/zensical-0.0.43-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c4c85978c765b3e7f347e8102dfe1373d4bbe4229d7008b6bdbf352f1fbcd7f", size = 12947599, upload-time = "2026-05-19T09:43:38.754Z" },
+ { url = "https://files.pythonhosted.org/packages/75/a0/94f47d6fb592997be7ab9526938c929f0199adf2637c3c2b2b9b2101b28e/zensical-0.0.43-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90d7c06ffd07b2bdf78bef041d541baba8a3ea51fd2dd84dbdbc5b0229076524", size = 12904911, upload-time = "2026-05-19T09:43:42.434Z" },
+ { url = "https://files.pythonhosted.org/packages/96/fb/1db3ad9a86ff772f74a8bc60ad5b447aa02a158e70f94adacf50bdd5c40f/zensical-0.0.43-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60022f4a6b95e46ec0023f51052fcd491743b3ebd08c0066b22a5cf1e741fecd", size = 13269386, upload-time = "2026-05-19T09:43:45.387Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ee/b24fd0f94885519d851c35615b086d069a1077b0198021a56755395a4633/zensical-0.0.43-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e278eb948a0b7545d50609d713c7c27e366dade4523ff73a311a5d5f136518a", size = 12999364, upload-time = "2026-05-19T09:43:48.549Z" },
+ { url = "https://files.pythonhosted.org/packages/28/78/401ccd7afd9d2690f81b5319b7f1eed05108154ce20e4207053914518c1c/zensical-0.0.43-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b85e5ab99fbda13823e67c43a4be6e5ebda6600602969c6575e143f20ac203fd", size = 13124392, upload-time = "2026-05-19T09:43:50.965Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b3/9af6eba5826b0ef143fc8308bd1e219e221441e307a958e39f824ba9ab53/zensical-0.0.43-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:751385accc92cccfd4560dabed7c423870686ef6ede244a67e5c96286af25e8f", size = 13177538, upload-time = "2026-05-19T09:43:53.964Z" },
+ { url = "https://files.pythonhosted.org/packages/be/6b/cd090bd6659d32692487206469988ee84d41aa6de4cdf9e380f847da90e2/zensical-0.0.43-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:dd3ff5bfa6e65cf3d2550dc639c3da2a3bfa11087b83d57e06623c4c1607d583", size = 13327086, upload-time = "2026-05-19T09:43:56.8Z" },
+ { url = "https://files.pythonhosted.org/packages/79/5b/ac2555354b5a53cb9c2c942811905c47be0b9f5603d3c1328ee8564333eb/zensical-0.0.43-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85055a115b12f49c6ab194dcf04f966fc06b690ed6a8ddddd819929fc5f340e6", size = 13284645, upload-time = "2026-05-19T09:43:59.329Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/c6/1688ec6e5be15e3ab367d7804753291bfbdff3109b06e20c19ce30a7129c/zensical-0.0.43-cp310-abi3-win32.whl", hash = "sha256:8a75ddd4bb3cd3c4a8e71d2ebae44c5611fd636c1d355c6124dd96e2f9c52838", size = 12256740, upload-time = "2026-05-19T09:44:02.102Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/a8/d967e70eac810a7e9eb8c5150d6d02848a1f42260f42977c71debed3cb02/zensical-0.0.43-cp310-abi3-win_amd64.whl", hash = "sha256:03a9d1744a6394ad66c355d6f1de04cfd92efa525b0b94bf6dbf6971c5cd2c6b", size = 12496166, upload-time = "2026-05-19T09:44:04.915Z" },
+]
+
+[[package]]
+name = "zizmor"
+version = "1.25.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/41/8987d546e3101cc76748b2f1b0ccda58e244773ef5124d39e7e749e3d6e4/zizmor-1.25.2.tar.gz", hash = "sha256:f26ffeb16659c8922c7b08203ca5a4f8bf5e1a7e8d190734961c40877cf778ea", size = 517794, upload-time = "2026-05-16T06:28:43.816Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/bd/84108a92ccbfda0d28efc11f382997c7a767b58863bf4a550634b8cf0211/zizmor-1.25.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17cc8cfd9d472e8b11945a869c198d25cfdf4a33f36fa7a1f9674099f5fb509d", size = 9115548, upload-time = "2026-05-16T06:28:33.591Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c0/66453a2553a66286a96ca32d75e3e6bcc94ce7f907cd5f8c2c3fce55315e/zizmor-1.25.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3e301eb4465e2da77857cf01ab4ef0184cf3818e826800b270ab01ae7338977", size = 8665071, upload-time = "2026-05-16T06:28:30.861Z" },
+ { url = "https://files.pythonhosted.org/packages/52/3e/d60939d1cc4907c0d021a7c46362aab5e8045550bb09157d56c070e43568/zizmor-1.25.2-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:cf64374149b567c9373228b76c8e77a389b4071899f84b82c36ee50fab894e79", size = 8842884, upload-time = "2026-05-16T06:28:26.041Z" },
+ { url = "https://files.pythonhosted.org/packages/46/82/f3e8d9b6d941194f2558591b449c106d46a16ea566b95eccff3a83bf6acc/zizmor-1.25.2-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:0beba1601be08bd00c9277e6ed4b026e125b26b379d86d6d98eb708409b3050d", size = 8449741, upload-time = "2026-05-16T06:28:45.424Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/13/445bc98acc2c976d6b8f8ca59b9c09f055adb5ffb3445d99af8ff7efcb4f/zizmor-1.25.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:c4246f1344d8dbeffc044d7bb11b131773a7db7eb57d9073c45942dfd3543a1f", size = 9285184, upload-time = "2026-05-16T06:28:39.21Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/78/fc7717c706bde7531b2fde12003994fbc04c47ab4f91aa6ca9b3b24b30fd/zizmor-1.25.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dbb1b5c85b8de8eaa0227c6620f06c8e4fbd0a4da2086e218bc225c0bef0923d", size = 8886579, upload-time = "2026-05-16T06:28:51.384Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/bc/a46f11377cdc145c625d62d88c30fead56f9d29bc31652069a1a0eaed6c2/zizmor-1.25.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d670a1e2f00b3cd56febd145bc1a0b2c4caf1cbe5dad8128721843fa877e2d2e", size = 8413576, upload-time = "2026-05-16T06:28:36.376Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/3b/0fd93b77171c8f229e8e1304eecc9931bf3009f722c57967d545d9f151b6/zizmor-1.25.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b75c84d7387389f95edadbe859fb2aaf0a360c5b080932cc53e92ae1db6f09ef", size = 9378162, upload-time = "2026-05-16T06:28:41.999Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/3f/dcb85fb9a0d87794847f9043f9db9bb4d274cf4b8077604bc13850c8fdb4/zizmor-1.25.2-py3-none-win32.whl", hash = "sha256:aa9f4c43b499c55339c3ef2e885133c5017cd9a18d76d9335541203cfa5ae1e7", size = 7548509, upload-time = "2026-05-16T06:28:28.828Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/81/1cb088098bd53f9b910098b0c19d06dc587acf328a170ef8afd1cd93b482/zizmor-1.25.2-py3-none-win_amd64.whl", hash = "sha256:af55bd9bd119ea8cbce2a7addc3922503019de32c1fe31106d70b3dc77d77908", size = 8609822, upload-time = "2026-05-16T06:28:48.078Z" },
+]