summaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-03-09 15:01:52 +0300
committerrobot-piglet <[email protected]>2026-03-09 15:19:56 +0300
commitf6fa894e435f87435fb7e9d169d27be34919d02e (patch)
tree9133a7ed545f54d5497f48e35e940a72f108eb2e /contrib/python
parent5e4443cb23a723cb03b9a208a3b481e4c03d393e (diff)
Intermediate changes
commit_hash:cc43b6f20066a000a87a4b56d73a1234249e0e8a
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/annotated-doc/.dist-info/METADATA145
-rw-r--r--contrib/python/annotated-doc/.dist-info/entry_points.txt4
-rw-r--r--contrib/python/annotated-doc/LICENSE21
-rw-r--r--contrib/python/annotated-doc/README.md109
-rw-r--r--contrib/python/annotated-doc/annotated_doc/__init__.py3
-rw-r--r--contrib/python/annotated-doc/annotated_doc/main.py36
-rw-r--r--contrib/python/annotated-doc/annotated_doc/py.typed0
-rw-r--r--contrib/python/annotated-doc/ya.make24
-rw-r--r--contrib/python/jaraco.text/.dist-info/METADATA6
-rw-r--r--contrib/python/jaraco.text/.yandex_meta/yamaker.yaml2
-rw-r--r--contrib/python/jaraco.text/jaraco/text/__init__.py46
-rw-r--r--contrib/python/jaraco.text/jaraco/text/show-newlines.py13
-rw-r--r--contrib/python/jaraco.text/jaraco/text/strip-prefix.py5
-rw-r--r--contrib/python/jaraco.text/patches/01-remove-autocommand.patch20
-rw-r--r--contrib/python/jaraco.text/ya.make3
-rw-r--r--contrib/python/shellingham/.dist-info/METADATA106
-rw-r--r--contrib/python/shellingham/.dist-info/top_level.txt1
-rw-r--r--contrib/python/shellingham/LICENSE13
-rw-r--r--contrib/python/shellingham/README.rst80
-rw-r--r--contrib/python/shellingham/shellingham/__init__.py23
-rw-r--r--contrib/python/shellingham/shellingham/_core.py11
-rw-r--r--contrib/python/shellingham/shellingham/nt.py163
-rw-r--r--contrib/python/shellingham/shellingham/posix/__init__.py112
-rw-r--r--contrib/python/shellingham/shellingham/posix/_core.py3
-rw-r--r--contrib/python/shellingham/shellingham/posix/proc.py83
-rw-r--r--contrib/python/shellingham/shellingham/posix/ps.py51
-rw-r--r--contrib/python/shellingham/ya.make32
-rw-r--r--contrib/python/typer-slim/.dist-info/METADATA425
-rw-r--r--contrib/python/typer-slim/.dist-info/entry_points.txt4
-rw-r--r--contrib/python/typer-slim/LICENSE21
-rw-r--r--contrib/python/typer-slim/README.md386
-rw-r--r--contrib/python/typer-slim/typer/__init__.py39
-rw-r--r--contrib/python/typer-slim/typer/__main__.py3
-rw-r--r--contrib/python/typer-slim/typer/_completion_classes.py206
-rw-r--r--contrib/python/typer-slim/typer/_completion_shared.py259
-rw-r--r--contrib/python/typer-slim/typer/_types.py27
-rw-r--r--contrib/python/typer-slim/typer/_typing.py83
-rw-r--r--contrib/python/typer-slim/typer/cli.py317
-rw-r--r--contrib/python/typer-slim/typer/colors.py20
-rw-r--r--contrib/python/typer-slim/typer/completion.py147
-rw-r--r--contrib/python/typer-slim/typer/core.py840
-rw-r--r--contrib/python/typer-slim/typer/main.py1157
-rw-r--r--contrib/python/typer-slim/typer/models.py541
-rw-r--r--contrib/python/typer-slim/typer/params.py479
-rw-r--r--contrib/python/typer-slim/typer/py.typed0
-rw-r--r--contrib/python/typer-slim/typer/rich_utils.py752
-rw-r--r--contrib/python/typer-slim/typer/testing.py30
-rw-r--r--contrib/python/typer-slim/typer/utils.py190
-rw-r--r--contrib/python/typer-slim/ya.make47
-rw-r--r--contrib/python/typer/.dist-info/METADATA412
-rw-r--r--contrib/python/typer/.dist-info/entry_points.txt5
-rw-r--r--contrib/python/typer/.dist-info/top_level.txt1
-rw-r--r--contrib/python/typer/.yandex_meta/yamaker.yaml4
-rw-r--r--contrib/python/typer/LICENSE21
-rw-r--r--contrib/python/typer/README.md375
-rw-r--r--contrib/python/typer/ya.make26
-rw-r--r--contrib/python/xmltodict/py3/.dist-info/METADATA3
-rw-r--r--contrib/python/xmltodict/py3/README.md1
-rw-r--r--contrib/python/xmltodict/py3/tests/test_dicttoxml.py106
-rw-r--r--contrib/python/xmltodict/py3/tests/ya.make5
-rw-r--r--contrib/python/xmltodict/py3/xmltodict.py49
-rw-r--r--contrib/python/xmltodict/py3/ya.make2
62 files changed, 8038 insertions, 60 deletions
diff --git a/contrib/python/annotated-doc/.dist-info/METADATA b/contrib/python/annotated-doc/.dist-info/METADATA
new file mode 100644
index 00000000000..9bf7a9e8007
--- /dev/null
+++ b/contrib/python/annotated-doc/.dist-info/METADATA
@@ -0,0 +1,145 @@
+Metadata-Version: 2.4
+Name: annotated-doc
+Version: 0.0.4
+Summary: Document parameters, class attributes, return types, and variables inline, with Annotated.
+Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <[email protected]>
+License-Expression: MIT
+License-File: LICENSE
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Topic :: Software Development
+Classifier: Typing :: Typed
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Project-URL: Homepage, https://github.com/fastapi/annotated-doc
+Project-URL: Documentation, https://github.com/fastapi/annotated-doc
+Project-URL: Repository, https://github.com/fastapi/annotated-doc
+Project-URL: Issues, https://github.com/fastapi/annotated-doc/issues
+Project-URL: Changelog, https://github.com/fastapi/annotated-doc/release-notes.md
+Requires-Python: >=3.8
+Description-Content-Type: text/markdown
+
+# Annotated Doc
+
+Document parameters, class attributes, return types, and variables inline, with `Annotated`.
+
+<a href="https://github.com/fastapi/annotated-doc/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
+ <img src="https://github.com/fastapi/annotated-doc/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
+</a>
+<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/annotated-doc" target="_blank">
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/annotated-doc.svg" alt="Coverage">
+</a>
+<a href="https://pypi.org/project/annotated-doc" target="_blank">
+ <img src="https://img.shields.io/pypi/v/annotated-doc?color=%2334D058&label=pypi%20package" alt="Package version">
+</a>
+<a href="https://pypi.org/project/annotated-doc" target="_blank">
+ <img src="https://img.shields.io/pypi/pyversions/annotated-doc.svg?color=%2334D058" alt="Supported Python versions">
+</a>
+
+## Installation
+
+```bash
+pip install annotated-doc
+```
+
+Or with `uv`:
+
+```Python
+uv add annotated-doc
+```
+
+## Usage
+
+Import `Doc` and pass a single literal string with the documentation for the specific parameter, class attribute, return type, or variable.
+
+For example, to document a parameter `name` in a function `hi` you could do:
+
+```Python
+from typing import Annotated
+
+from annotated_doc import Doc
+
+def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
+ print(f"Hi, {name}!")
+```
+
+You can also use it to document class attributes:
+
+```Python
+from typing import Annotated
+
+from annotated_doc import Doc
+
+class User:
+ name: Annotated[str, Doc("The user's name")]
+ age: Annotated[int, Doc("The user's age")]
+```
+
+The same way, you could document return types and variables, or anything that could have a type annotation with `Annotated`.
+
+## Who Uses This
+
+`annotated-doc` was made for:
+
+* [FastAPI](https://fastapi.tiangolo.com/)
+* [Typer](https://typer.tiangolo.com/)
+* [SQLModel](https://sqlmodel.tiangolo.com/)
+* [Asyncer](https://asyncer.tiangolo.com/)
+
+`annotated-doc` is supported by [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc), which powers reference documentation like the one in the [FastAPI Reference](https://fastapi.tiangolo.com/reference/).
+
+## Reasons not to use `annotated-doc`
+
+You are already comfortable with one of the existing docstring formats, like:
+
+* Sphinx
+* numpydoc
+* Google
+* Keras
+
+Your team is already comfortable using them.
+
+You prefer having the documentation about parameters all together in a docstring, separated from the code defining them.
+
+You care about a specific set of users, using one specific editor, and that editor already has support for the specific docstring format you use.
+
+## Reasons to use `annotated-doc`
+
+* No micro-syntax to learn for newcomers, itโ€™s **just Python** syntax.
+* **Editing** would be already fully supported by default by any editor (current or future) supporting Python syntax, including syntax errors, syntax highlighting, etc.
+* **Rendering** would be relatively straightforward to implement by static tools (tools that don't need runtime execution), as the information can be extracted from the AST they normally already create.
+* **Deduplication of information**: the name of a parameter would be defined in a single place, not duplicated inside of a docstring.
+* **Elimination** of the possibility of having **inconsistencies** when removing a parameter or class variable and **forgetting to remove** its documentation.
+* **Minimization** of the probability of adding a new parameter or class variable and **forgetting to add its documentation**.
+* **Elimination** of the possibility of having **inconsistencies** between the **name** of a parameter in the **signature** and the name in the docstring when it is renamed.
+* **Access** to the documentation string for each symbol at **runtime**, including existing (older) Python versions.
+* A more formalized way to document other symbols, like type aliases, that could use Annotated.
+* **Support** for apps using FastAPI, Typer and others.
+* **AI Accessibility**: AI tools will have an easier way understanding each parameter as the distance from documentation to parameter is much closer.
+
+## History
+
+I ([@tiangolo](https://github.com/tiangolo)) originally wanted for this to be part of the Python standard library (in [PEP 727](https://peps.python.org/pep-0727/)), but the proposal was withdrawn as there was a fair amount of negative feedback and opposition.
+
+The conclusion was that this was better done as an external effort, in a third-party library.
+
+So, here it is, with a simpler approach, as a third-party library, in a way that can be used by others, starting with FastAPI and friends.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/contrib/python/annotated-doc/.dist-info/entry_points.txt b/contrib/python/annotated-doc/.dist-info/entry_points.txt
new file mode 100644
index 00000000000..c3ad4726d43
--- /dev/null
+++ b/contrib/python/annotated-doc/.dist-info/entry_points.txt
@@ -0,0 +1,4 @@
+[console_scripts]
+
+[gui_scripts]
+
diff --git a/contrib/python/annotated-doc/LICENSE b/contrib/python/annotated-doc/LICENSE
new file mode 100644
index 00000000000..7a254464cc7
--- /dev/null
+++ b/contrib/python/annotated-doc/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2025 Sebastiรกn Ramรญrez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/contrib/python/annotated-doc/README.md b/contrib/python/annotated-doc/README.md
new file mode 100644
index 00000000000..0698b1e15ee
--- /dev/null
+++ b/contrib/python/annotated-doc/README.md
@@ -0,0 +1,109 @@
+# Annotated Doc
+
+Document parameters, class attributes, return types, and variables inline, with `Annotated`.
+
+<a href="https://github.com/fastapi/annotated-doc/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
+ <img src="https://github.com/fastapi/annotated-doc/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
+</a>
+<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/annotated-doc" target="_blank">
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/annotated-doc.svg" alt="Coverage">
+</a>
+<a href="https://pypi.org/project/annotated-doc" target="_blank">
+ <img src="https://img.shields.io/pypi/v/annotated-doc?color=%2334D058&label=pypi%20package" alt="Package version">
+</a>
+<a href="https://pypi.org/project/annotated-doc" target="_blank">
+ <img src="https://img.shields.io/pypi/pyversions/annotated-doc.svg?color=%2334D058" alt="Supported Python versions">
+</a>
+
+## Installation
+
+```bash
+pip install annotated-doc
+```
+
+Or with `uv`:
+
+```Python
+uv add annotated-doc
+```
+
+## Usage
+
+Import `Doc` and pass a single literal string with the documentation for the specific parameter, class attribute, return type, or variable.
+
+For example, to document a parameter `name` in a function `hi` you could do:
+
+```Python
+from typing import Annotated
+
+from annotated_doc import Doc
+
+def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
+ print(f"Hi, {name}!")
+```
+
+You can also use it to document class attributes:
+
+```Python
+from typing import Annotated
+
+from annotated_doc import Doc
+
+class User:
+ name: Annotated[str, Doc("The user's name")]
+ age: Annotated[int, Doc("The user's age")]
+```
+
+The same way, you could document return types and variables, or anything that could have a type annotation with `Annotated`.
+
+## Who Uses This
+
+`annotated-doc` was made for:
+
+* [FastAPI](https://fastapi.tiangolo.com/)
+* [Typer](https://typer.tiangolo.com/)
+* [SQLModel](https://sqlmodel.tiangolo.com/)
+* [Asyncer](https://asyncer.tiangolo.com/)
+
+`annotated-doc` is supported by [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc), which powers reference documentation like the one in the [FastAPI Reference](https://fastapi.tiangolo.com/reference/).
+
+## Reasons not to use `annotated-doc`
+
+You are already comfortable with one of the existing docstring formats, like:
+
+* Sphinx
+* numpydoc
+* Google
+* Keras
+
+Your team is already comfortable using them.
+
+You prefer having the documentation about parameters all together in a docstring, separated from the code defining them.
+
+You care about a specific set of users, using one specific editor, and that editor already has support for the specific docstring format you use.
+
+## Reasons to use `annotated-doc`
+
+* No micro-syntax to learn for newcomers, itโ€™s **just Python** syntax.
+* **Editing** would be already fully supported by default by any editor (current or future) supporting Python syntax, including syntax errors, syntax highlighting, etc.
+* **Rendering** would be relatively straightforward to implement by static tools (tools that don't need runtime execution), as the information can be extracted from the AST they normally already create.
+* **Deduplication of information**: the name of a parameter would be defined in a single place, not duplicated inside of a docstring.
+* **Elimination** of the possibility of having **inconsistencies** when removing a parameter or class variable and **forgetting to remove** its documentation.
+* **Minimization** of the probability of adding a new parameter or class variable and **forgetting to add its documentation**.
+* **Elimination** of the possibility of having **inconsistencies** between the **name** of a parameter in the **signature** and the name in the docstring when it is renamed.
+* **Access** to the documentation string for each symbol at **runtime**, including existing (older) Python versions.
+* A more formalized way to document other symbols, like type aliases, that could use Annotated.
+* **Support** for apps using FastAPI, Typer and others.
+* **AI Accessibility**: AI tools will have an easier way understanding each parameter as the distance from documentation to parameter is much closer.
+
+## History
+
+I ([@tiangolo](https://github.com/tiangolo)) originally wanted for this to be part of the Python standard library (in [PEP 727](https://peps.python.org/pep-0727/)), but the proposal was withdrawn as there was a fair amount of negative feedback and opposition.
+
+The conclusion was that this was better done as an external effort, in a third-party library.
+
+So, here it is, with a simpler approach, as a third-party library, in a way that can be used by others, starting with FastAPI and friends.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/contrib/python/annotated-doc/annotated_doc/__init__.py b/contrib/python/annotated-doc/annotated_doc/__init__.py
new file mode 100644
index 00000000000..a0152a7d12a
--- /dev/null
+++ b/contrib/python/annotated-doc/annotated_doc/__init__.py
@@ -0,0 +1,3 @@
+from .main import Doc as Doc
+
+__version__ = "0.0.4"
diff --git a/contrib/python/annotated-doc/annotated_doc/main.py b/contrib/python/annotated-doc/annotated_doc/main.py
new file mode 100644
index 00000000000..7063c59e450
--- /dev/null
+++ b/contrib/python/annotated-doc/annotated_doc/main.py
@@ -0,0 +1,36 @@
+class Doc:
+ """Define the documentation of a type annotation using `Annotated`, to be
+ used in class attributes, function and method parameters, return values,
+ and variables.
+
+ The value should be a positional-only string literal to allow static tools
+ like editors and documentation generators to use it.
+
+ This complements docstrings.
+
+ The string value passed is available in the attribute `documentation`.
+
+ Example:
+
+ ```Python
+ from typing import Annotated
+ from annotated_doc import Doc
+
+ def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
+ print(f"Hi, {name}!")
+ ```
+ """
+
+ def __init__(self, documentation: str, /) -> None:
+ self.documentation = documentation
+
+ def __repr__(self) -> str:
+ return f"Doc({self.documentation!r})"
+
+ def __hash__(self) -> int:
+ return hash(self.documentation)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Doc):
+ return NotImplemented
+ return self.documentation == other.documentation
diff --git a/contrib/python/annotated-doc/annotated_doc/py.typed b/contrib/python/annotated-doc/annotated_doc/py.typed
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/contrib/python/annotated-doc/annotated_doc/py.typed
diff --git a/contrib/python/annotated-doc/ya.make b/contrib/python/annotated-doc/ya.make
new file mode 100644
index 00000000000..ad7982ce521
--- /dev/null
+++ b/contrib/python/annotated-doc/ya.make
@@ -0,0 +1,24 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(0.0.4)
+
+LICENSE(MIT)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ annotated_doc/__init__.py
+ annotated_doc/main.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/annotated-doc/
+ .dist-info/METADATA
+ .dist-info/entry_points.txt
+ annotated_doc/py.typed
+)
+
+END()
diff --git a/contrib/python/jaraco.text/.dist-info/METADATA b/contrib/python/jaraco.text/.dist-info/METADATA
index 7fc6760373f..1d21bc04f99 100644
--- a/contrib/python/jaraco.text/.dist-info/METADATA
+++ b/contrib/python/jaraco.text/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: jaraco.text
-Version: 4.1.0
+Version: 4.2.0
Summary: Module for text manipulation
Author-email: "Jason R. Coombs" <[email protected]>
License-Expression: MIT
@@ -12,10 +12,10 @@ Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.9
Description-Content-Type: text/x-rst
License-File: LICENSE
-Requires-Dist: jaraco.functools
Requires-Dist: jaraco.context>=4.1
-Requires-Dist: autocommand
+Requires-Dist: jaraco.functools
Requires-Dist: more_itertools
+Requires-Dist: typer-slim
Provides-Extra: test
Requires-Dist: pytest!=8.1.*,>=6; extra == "test"
Requires-Dist: pathlib2; python_version < "3.10" and extra == "test"
diff --git a/contrib/python/jaraco.text/.yandex_meta/yamaker.yaml b/contrib/python/jaraco.text/.yandex_meta/yamaker.yaml
index bc39e7ce986..ae460b6b4f0 100644
--- a/contrib/python/jaraco.text/.yandex_meta/yamaker.yaml
+++ b/contrib/python/jaraco.text/.yandex_meta/yamaker.yaml
@@ -1,5 +1,3 @@
-requirements:
- - autocommand: null
exclude:
- "jaraco/text/Lorem ipsum.txt"
keep:
diff --git a/contrib/python/jaraco.text/jaraco/text/__init__.py b/contrib/python/jaraco.text/jaraco/text/__init__.py
index 1c215b68983..9ae1f6e0600 100644
--- a/contrib/python/jaraco.text/jaraco/text/__init__.py
+++ b/contrib/python/jaraco.text/jaraco/text/__init__.py
@@ -1,7 +1,9 @@
from __future__ import annotations
import functools
+import io
import itertools
+import os
import re
import sys
import textwrap
@@ -13,6 +15,8 @@ from typing import (
Protocol,
SupportsIndex,
TypeVar,
+ Union,
+ cast,
overload,
)
@@ -25,12 +29,16 @@ else: # pragma: no cover
from importlib.abc import Traversable
if TYPE_CHECKING:
- from _typeshed import FileDescriptorOrPath, SupportsGetItem
+ from _typeshed import (
+ FileDescriptorOrPath,
+ SupportsIter,
+ SupportsNext,
+ )
from typing_extensions import Self, TypeAlias, TypeGuard, Unpack
- _T_co = TypeVar("_T_co", covariant=True)
- # Same as builtins._GetItemIterable from typeshed
- _GetItemIterable: TypeAlias = SupportsGetItem[int, _T_co]
+ Openable: TypeAlias = FileDescriptorOrPath
+else:
+ Openable = Union[str, bytes, os.PathLike, int]
_T = TypeVar("_T")
@@ -655,7 +663,7 @@ def drop_comment(line: str) -> str:
return line.partition(' #')[0]
-def join_continuation(lines: _GetItemIterable[str]) -> Generator[str]:
+def join_continuation(lines: SupportsIter[SupportsNext[str]]) -> Generator[str]:
r"""
Join lines continued by a trailing backslash.
@@ -679,7 +687,7 @@ def join_continuation(lines: _GetItemIterable[str]) -> Generator[str]:
['foo']
"""
lines_ = iter(lines)
- for item in lines_:
+ for item in lines_: # type: ignore[attr-defined] # A bit of a false positive with iteration dunder fallback
while item.endswith('\\'):
try:
item = item[:-2].strip() + next(lines_)
@@ -688,9 +696,15 @@ def join_continuation(lines: _GetItemIterable[str]) -> Generator[str]:
yield item
+# https://docs.python.org/3/library/io.html#io.TextIOBase.newlines
+NewlineSpec: TypeAlias = Union[str, tuple[str, ...], None]
+
+
def read_newlines(
- filename: FileDescriptorOrPath, limit: int | None = 1024
-) -> str | tuple[str, ...] | None:
+ filename: Union[Openable, io.TextIOWrapper], # noqa: UP007 # singledispatch uses the annotation at runtime (python 3.9)
+ limit: int | None = 1024,
+) -> NewlineSpec:
r"""
>>> tmp_path = getfixture('tmp_path')
>>> filename = tmp_path / 'out.txt'
@@ -704,9 +718,21 @@ def read_newlines(
>>> read_newlines(filename)
('\r', '\n', '\r\n')
"""
+ if sys.version_info >= (3, 10):
+ assert isinstance(filename, Openable)
+ else: # pragma: no cover
+ filename = cast(Openable, filename)
with open(filename, encoding='utf-8') as fp:
- fp.read(limit)
- return fp.newlines
+ return read_newlines(fp, limit=limit)
+
+
+@read_newlines.register
+def _(
+ filename: io.TextIOWrapper,
+ limit: Union[int, None] = 1024, # noqa: UP007 # singledispatch uses the annotation at runtime (python 3.9)
+) -> NewlineSpec:
+ filename.read(limit)
+ return filename.newlines
def lines_from(input: Traversable) -> Generator[str]:
diff --git a/contrib/python/jaraco.text/jaraco/text/show-newlines.py b/contrib/python/jaraco.text/jaraco/text/show-newlines.py
index 22811e010c6..2a91f7bc291 100644
--- a/contrib/python/jaraco.text/jaraco/text/show-newlines.py
+++ b/contrib/python/jaraco.text/jaraco/text/show-newlines.py
@@ -1,17 +1,13 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
-
import inflect
+import typer
from more_itertools import always_iterable
import jaraco.text
-if TYPE_CHECKING:
- from _typeshed import FileDescriptorOrPath
-
-def report_newlines(filename: FileDescriptorOrPath) -> None:
+def report_newlines(input: typer.FileText) -> None:
r"""
Report the newlines in the indicated file.
@@ -25,7 +21,7 @@ def report_newlines(filename: FileDescriptorOrPath) -> None:
>>> report_newlines(filename)
newlines are ('\n', '\r\n')
"""
- newlines = jaraco.text.read_newlines(filename)
+ newlines = jaraco.text.read_newlines(input)
count = len(tuple(always_iterable(newlines)))
engine = inflect.engine()
print(
@@ -34,3 +30,6 @@ def report_newlines(filename: FileDescriptorOrPath) -> None:
engine.plural_verb("is", count),
repr(newlines),
)
+
+
+__name__ == '__main__' and typer.run(report_newlines) # type: ignore[func-returns-value]
diff --git a/contrib/python/jaraco.text/jaraco/text/strip-prefix.py b/contrib/python/jaraco.text/jaraco/text/strip-prefix.py
index b683f8d72c5..e43cdd1abee 100644
--- a/contrib/python/jaraco.text/jaraco/text/strip-prefix.py
+++ b/contrib/python/jaraco.text/jaraco/text/strip-prefix.py
@@ -1,5 +1,7 @@
import sys
+import typer
+
from jaraco.text import Stripper
@@ -14,3 +16,6 @@ def strip_prefix() -> None:
123
"""
sys.stdout.writelines(Stripper.strip_prefix(sys.stdin).lines)
+
+
+__name__ == '__main__' and typer.run(strip_prefix) # type: ignore[func-returns-value]
diff --git a/contrib/python/jaraco.text/patches/01-remove-autocommand.patch b/contrib/python/jaraco.text/patches/01-remove-autocommand.patch
deleted file mode 100644
index 1b6d9d463c1..00000000000
--- a/contrib/python/jaraco.text/patches/01-remove-autocommand.patch
+++ /dev/null
@@ -1,20 +0,0 @@
-ะฃ autocommand ะปะธั†ะตะฝะทะธั LGPL-3 ะธ ั‚ั€ะฐะฝะทะธั‚ะธะฒะฝะพ ั‡ะตั€ะตะท jaraco.text ะฟะพะดะถะธะณะฐะตั‚ ะฟั€ะพะฒะตั€ะบัƒ ัƒ Catboost
-
-ะ’ ะั€ะบะธะดะธะธ ัั‚ะธะผ ะฝะธ ะบั‚ะพ ะฝะต ะฟะพะปัŒะทัƒะตั‚ัั, ะฟะพั‚ะพะผัƒ ะฟั€ะพัั‚ะพ ัƒะดะฐะปััŽ
---- contrib/python/jaraco.text/jaraco/text/show-newlines.py (index)
-+++ contrib/python/jaraco.text/jaraco/text/show-newlines.py (working tree)
-@@ -1 +0,0 @@
--import autocommand
-@@ -30,3 +28,0 @@ def report_newlines(filename):
--
--
--autocommand.autocommand(__name__)(report_newlines)
---- contrib/python/jaraco.text/jaraco/text/strip-prefix.py (index)
-+++ contrib/python/jaraco.text/jaraco/text/strip-prefix.py (working tree)
-@@ -3,2 +2,0 @@ import sys
--import autocommand
--
-@@ -19,3 +16,0 @@ def strip_prefix():
--
--
--autocommand.autocommand(__name__)(strip_prefix)
diff --git a/contrib/python/jaraco.text/ya.make b/contrib/python/jaraco.text/ya.make
index f4dea16d4c7..ab74fb5b55b 100644
--- a/contrib/python/jaraco.text/ya.make
+++ b/contrib/python/jaraco.text/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(4.1.0)
+VERSION(4.2.0)
LICENSE(MIT)
@@ -10,6 +10,7 @@ PEERDIR(
contrib/python/jaraco.context
contrib/python/jaraco.functools
contrib/python/more-itertools
+ contrib/python/typer
)
NO_LINT()
diff --git a/contrib/python/shellingham/.dist-info/METADATA b/contrib/python/shellingham/.dist-info/METADATA
new file mode 100644
index 00000000000..52118f1e5c8
--- /dev/null
+++ b/contrib/python/shellingham/.dist-info/METADATA
@@ -0,0 +1,106 @@
+Metadata-Version: 2.1
+Name: shellingham
+Version: 1.5.4
+Summary: Tool to Detect Surrounding Shell
+Home-page: https://github.com/sarugaku/shellingham
+Author: Tzu-ping Chung
+Author-email: [email protected]
+License: ISC License
+Keywords: shell
+Classifier: Development Status :: 3 - Alpha
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: ISC License (ISCL)
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.7
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+
+=============================================
+Shellingham: Tool to Detect Surrounding Shell
+=============================================
+
+.. image:: https://img.shields.io/pypi/v/shellingham.svg
+ :target: https://pypi.org/project/shellingham/
+
+Shellingham detects what shell the current Python executable is running in.
+
+
+Usage
+=====
+
+.. code-block:: python
+
+ >>> import shellingham
+ >>> shellingham.detect_shell()
+ ('bash', '/bin/bash')
+
+``detect_shell`` pokes around the process's running environment to determine
+what shell it is run in. It returns a 2-tuple:
+
+* The shell name, always lowercased.
+* The command used to run the shell.
+
+``ShellDetectionFailure`` is raised if ``detect_shell`` fails to detect the
+surrounding shell.
+
+
+Notes
+=====
+
+* The shell name is always lowercased.
+* On Windows, the shell name is the name of the executable, minus the file
+ extension.
+
+
+Notes for Application Developers
+================================
+
+Remember, your application's user is not necessarily using a shell.
+Shellingham raises ``ShellDetectionFailure`` if there is no shell to detect,
+but *your application should almost never do this to your user*.
+
+A practical approach to this is to wrap ``detect_shell`` in a try block, and
+provide a sane default on failure
+
+.. code-block:: python
+
+ try:
+ shell = shellingham.detect_shell()
+ except shellingham.ShellDetectionFailure:
+ shell = provide_default()
+
+
+There are a few choices for you to choose from.
+
+* The POSIX standard mandates the environment variable ``SHELL`` to refer to
+ "the user's preferred command language interpreter". This is always available
+ (even if the user is not in an interactive session), and likely the correct
+ choice to launch an interactive sub-shell with.
+* A command ``sh`` is almost guaranteed to exist, likely at ``/bin/sh``, since
+ several POSIX tools rely on it. This should be suitable if you want to run a
+ (possibly non-interactive) script.
+* All versions of DOS and Windows have an environment variable ``COMSPEC``.
+ This can always be used to launch a usable command prompt (e.g. `cmd.exe` on
+ Windows).
+
+Here's a simple implementation to provide a default shell
+
+.. code-block:: python
+
+ import os
+
+ def provide_default():
+ if os.name == 'posix':
+ return os.environ['SHELL']
+ elif os.name == 'nt':
+ return os.environ['COMSPEC']
+ raise NotImplementedError(f'OS {os.name!r} support not available')
diff --git a/contrib/python/shellingham/.dist-info/top_level.txt b/contrib/python/shellingham/.dist-info/top_level.txt
new file mode 100644
index 00000000000..d4e44ce0299
--- /dev/null
+++ b/contrib/python/shellingham/.dist-info/top_level.txt
@@ -0,0 +1 @@
+shellingham
diff --git a/contrib/python/shellingham/LICENSE b/contrib/python/shellingham/LICENSE
new file mode 100644
index 00000000000..b9077766e9b
--- /dev/null
+++ b/contrib/python/shellingham/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2018, Tzu-ping Chung <[email protected]>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/contrib/python/shellingham/README.rst b/contrib/python/shellingham/README.rst
new file mode 100644
index 00000000000..81b19463591
--- /dev/null
+++ b/contrib/python/shellingham/README.rst
@@ -0,0 +1,80 @@
+=============================================
+Shellingham: Tool to Detect Surrounding Shell
+=============================================
+
+.. image:: https://img.shields.io/pypi/v/shellingham.svg
+ :target: https://pypi.org/project/shellingham/
+
+Shellingham detects what shell the current Python executable is running in.
+
+
+Usage
+=====
+
+.. code-block:: python
+
+ >>> import shellingham
+ >>> shellingham.detect_shell()
+ ('bash', '/bin/bash')
+
+``detect_shell`` pokes around the process's running environment to determine
+what shell it is run in. It returns a 2-tuple:
+
+* The shell name, always lowercased.
+* The command used to run the shell.
+
+``ShellDetectionFailure`` is raised if ``detect_shell`` fails to detect the
+surrounding shell.
+
+
+Notes
+=====
+
+* The shell name is always lowercased.
+* On Windows, the shell name is the name of the executable, minus the file
+ extension.
+
+
+Notes for Application Developers
+================================
+
+Remember, your application's user is not necessarily using a shell.
+Shellingham raises ``ShellDetectionFailure`` if there is no shell to detect,
+but *your application should almost never do this to your user*.
+
+A practical approach to this is to wrap ``detect_shell`` in a try block, and
+provide a sane default on failure
+
+.. code-block:: python
+
+ try:
+ shell = shellingham.detect_shell()
+ except shellingham.ShellDetectionFailure:
+ shell = provide_default()
+
+
+There are a few choices for you to choose from.
+
+* The POSIX standard mandates the environment variable ``SHELL`` to refer to
+ "the user's preferred command language interpreter". This is always available
+ (even if the user is not in an interactive session), and likely the correct
+ choice to launch an interactive sub-shell with.
+* A command ``sh`` is almost guaranteed to exist, likely at ``/bin/sh``, since
+ several POSIX tools rely on it. This should be suitable if you want to run a
+ (possibly non-interactive) script.
+* All versions of DOS and Windows have an environment variable ``COMSPEC``.
+ This can always be used to launch a usable command prompt (e.g. `cmd.exe` on
+ Windows).
+
+Here's a simple implementation to provide a default shell
+
+.. code-block:: python
+
+ import os
+
+ def provide_default():
+ if os.name == 'posix':
+ return os.environ['SHELL']
+ elif os.name == 'nt':
+ return os.environ['COMSPEC']
+ raise NotImplementedError(f'OS {os.name!r} support not available')
diff --git a/contrib/python/shellingham/shellingham/__init__.py b/contrib/python/shellingham/shellingham/__init__.py
new file mode 100644
index 00000000000..15f7a90cbd0
--- /dev/null
+++ b/contrib/python/shellingham/shellingham/__init__.py
@@ -0,0 +1,23 @@
+import importlib
+import os
+
+from ._core import ShellDetectionFailure
+
+__version__ = "1.5.4"
+
+
+def detect_shell(pid=None, max_depth=10):
+ name = os.name
+ try:
+ impl = importlib.import_module(".{}".format(name), __name__)
+ except ImportError:
+ message = "Shell detection not implemented for {0!r}".format(name)
+ raise RuntimeError(message)
+ try:
+ get_shell = impl.get_shell
+ except AttributeError:
+ raise RuntimeError("get_shell not implemented for {0!r}".format(name))
+ shell = get_shell(pid, max_depth=max_depth)
+ if shell:
+ return shell
+ raise ShellDetectionFailure()
diff --git a/contrib/python/shellingham/shellingham/_core.py b/contrib/python/shellingham/shellingham/_core.py
new file mode 100644
index 00000000000..13b65417c73
--- /dev/null
+++ b/contrib/python/shellingham/shellingham/_core.py
@@ -0,0 +1,11 @@
+SHELL_NAMES = (
+ {"sh", "bash", "dash", "ash"} # Bourne.
+ | {"csh", "tcsh"} # C.
+ | {"ksh", "zsh", "fish"} # Common alternatives.
+ | {"cmd", "powershell", "pwsh"} # Microsoft.
+ | {"elvish", "xonsh", "nu"} # More exotic.
+)
+
+
+class ShellDetectionFailure(EnvironmentError):
+ pass
diff --git a/contrib/python/shellingham/shellingham/nt.py b/contrib/python/shellingham/shellingham/nt.py
new file mode 100644
index 00000000000..389551b223a
--- /dev/null
+++ b/contrib/python/shellingham/shellingham/nt.py
@@ -0,0 +1,163 @@
+import contextlib
+import ctypes
+import os
+
+from ctypes.wintypes import (
+ BOOL,
+ CHAR,
+ DWORD,
+ HANDLE,
+ LONG,
+ LPWSTR,
+ MAX_PATH,
+ PDWORD,
+ ULONG,
+)
+
+from shellingham._core import SHELL_NAMES
+
+
+INVALID_HANDLE_VALUE = HANDLE(-1).value
+ERROR_NO_MORE_FILES = 18
+ERROR_INSUFFICIENT_BUFFER = 122
+TH32CS_SNAPPROCESS = 2
+PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
+
+
+kernel32 = ctypes.windll.kernel32
+
+
+def _check_handle(error_val=0):
+ def check(ret, func, args):
+ if ret == error_val:
+ raise ctypes.WinError()
+ return ret
+
+ return check
+
+
+def _check_expected(expected):
+ def check(ret, func, args):
+ if ret:
+ return True
+ code = ctypes.GetLastError()
+ if code == expected:
+ return False
+ raise ctypes.WinError(code)
+
+ return check
+
+
+class ProcessEntry32(ctypes.Structure):
+ _fields_ = (
+ ("dwSize", DWORD),
+ ("cntUsage", DWORD),
+ ("th32ProcessID", DWORD),
+ ("th32DefaultHeapID", ctypes.POINTER(ULONG)),
+ ("th32ModuleID", DWORD),
+ ("cntThreads", DWORD),
+ ("th32ParentProcessID", DWORD),
+ ("pcPriClassBase", LONG),
+ ("dwFlags", DWORD),
+ ("szExeFile", CHAR * MAX_PATH),
+ )
+
+
+kernel32.CloseHandle.argtypes = [HANDLE]
+kernel32.CloseHandle.restype = BOOL
+
+kernel32.CreateToolhelp32Snapshot.argtypes = [DWORD, DWORD]
+kernel32.CreateToolhelp32Snapshot.restype = HANDLE
+kernel32.CreateToolhelp32Snapshot.errcheck = _check_handle( # type: ignore
+ INVALID_HANDLE_VALUE,
+)
+
+kernel32.Process32First.argtypes = [HANDLE, ctypes.POINTER(ProcessEntry32)]
+kernel32.Process32First.restype = BOOL
+kernel32.Process32First.errcheck = _check_expected( # type: ignore
+ ERROR_NO_MORE_FILES,
+)
+
+kernel32.Process32Next.argtypes = [HANDLE, ctypes.POINTER(ProcessEntry32)]
+kernel32.Process32Next.restype = BOOL
+kernel32.Process32Next.errcheck = _check_expected( # type: ignore
+ ERROR_NO_MORE_FILES,
+)
+
+kernel32.GetCurrentProcessId.argtypes = []
+kernel32.GetCurrentProcessId.restype = DWORD
+
+kernel32.OpenProcess.argtypes = [DWORD, BOOL, DWORD]
+kernel32.OpenProcess.restype = HANDLE
+kernel32.OpenProcess.errcheck = _check_handle( # type: ignore
+ INVALID_HANDLE_VALUE,
+)
+
+kernel32.QueryFullProcessImageNameW.argtypes = [HANDLE, DWORD, LPWSTR, PDWORD]
+kernel32.QueryFullProcessImageNameW.restype = BOOL
+kernel32.QueryFullProcessImageNameW.errcheck = _check_expected( # type: ignore
+ ERROR_INSUFFICIENT_BUFFER,
+)
+
+
+def _handle(f, *args, **kwargs):
+ handle = f(*args, **kwargs)
+ try:
+ yield handle
+ finally:
+ kernel32.CloseHandle(handle)
+
+
+def _iter_processes():
+ f = kernel32.CreateToolhelp32Snapshot
+ with _handle(f, TH32CS_SNAPPROCESS, 0) as snap:
+ entry = ProcessEntry32()
+ entry.dwSize = ctypes.sizeof(entry)
+ ret = kernel32.Process32First(snap, entry)
+ while ret:
+ yield entry
+ ret = kernel32.Process32Next(snap, entry)
+
+
+def _get_full_path(proch):
+ size = DWORD(MAX_PATH)
+ while True:
+ path_buff = ctypes.create_unicode_buffer("", size.value)
+ if kernel32.QueryFullProcessImageNameW(proch, 0, path_buff, size):
+ return path_buff.value
+ size.value *= 2
+
+
+def get_shell(pid=None, max_depth=10):
+ proc_map = {
+ proc.th32ProcessID: (proc.th32ParentProcessID, proc.szExeFile)
+ for proc in _iter_processes()
+ }
+ pid = pid or os.getpid()
+
+ for _ in range(0, max_depth + 1):
+ try:
+ ppid, executable = proc_map[pid]
+ except KeyError: # No such process? Give up.
+ break
+
+ # The executable name would be encoded with the current code page if
+ # we're in ANSI mode (usually). Try to decode it into str/unicode,
+ # replacing invalid characters to be safe (not thoeratically necessary,
+ # I think). Note that we need to use 'mbcs' instead of encoding
+ # settings from sys because this is from the Windows API, not Python
+ # internals (which those settings reflect). (pypa/pipenv#3382)
+ if isinstance(executable, bytes):
+ executable = executable.decode("mbcs", "replace")
+
+ name = executable.rpartition(".")[0].lower()
+ if name not in SHELL_NAMES:
+ pid = ppid
+ continue
+
+ key = PROCESS_QUERY_LIMITED_INFORMATION
+ with _handle(kernel32.OpenProcess, key, 0, pid) as proch:
+ return (name, _get_full_path(proch))
+
+ return None
diff --git a/contrib/python/shellingham/shellingham/posix/__init__.py b/contrib/python/shellingham/shellingham/posix/__init__.py
new file mode 100644
index 00000000000..5bd2070db27
--- /dev/null
+++ b/contrib/python/shellingham/shellingham/posix/__init__.py
@@ -0,0 +1,112 @@
+import os
+import re
+
+from .._core import SHELL_NAMES, ShellDetectionFailure
+from . import proc, ps
+
+# Based on QEMU docs: https://www.qemu.org/docs/master/user/main.html
+QEMU_BIN_REGEX = re.compile(
+ r"""qemu-
+ (alpha
+ |armeb
+ |arm
+ |m68k
+ |cris
+ |i386
+ |x86_64
+ |microblaze
+ |mips
+ |mipsel
+ |mips64
+ |mips64el
+ |mipsn32
+ |mipsn32el
+ |nios2
+ |ppc64
+ |ppc
+ |sh4eb
+ |sh4
+ |sparc
+ |sparc32plus
+ |sparc64
+ )""",
+ re.VERBOSE,
+)
+
+
+def _iter_process_parents(pid, max_depth=10):
+ """Select a way to obtain process information from the system.
+
+ * `/proc` is used if supported.
+ * The system `ps` utility is used as a fallback option.
+ """
+ for impl in (proc, ps):
+ try:
+ iterator = impl.iter_process_parents(pid, max_depth)
+ except EnvironmentError:
+ continue
+ return iterator
+ raise ShellDetectionFailure("compatible proc fs or ps utility is required")
+
+
+def _get_login_shell(proc_cmd):
+ """Form shell information from SHELL environ if possible."""
+ login_shell = os.environ.get("SHELL", "")
+ if login_shell:
+ proc_cmd = login_shell
+ else:
+ proc_cmd = proc_cmd[1:]
+ return (os.path.basename(proc_cmd).lower(), proc_cmd)
+
+
+_INTERPRETER_SHELL_NAMES = [
+ (re.compile(r"^python(\d+(\.\d+)?)?$"), {"xonsh"}),
+]
+
+
+def _get_interpreter_shell(proc_name, proc_args):
+ """Get shell invoked via an interpreter.
+
+ Some shells are implemented on, and invoked with an interpreter, e.g. xonsh
+ is commonly executed with an executable Python script. This detects what
+ script the interpreter is actually running, and check whether that looks
+ like a shell.
+
+ See sarugaku/shellingham#26 for rational.
+ """
+ for pattern, shell_names in _INTERPRETER_SHELL_NAMES:
+ if not pattern.match(proc_name):
+ continue
+ for arg in proc_args:
+ name = os.path.basename(arg).lower()
+ if os.path.isfile(arg) and name in shell_names:
+ return (name, arg)
+ return None
+
+
+def _get_shell(cmd, *args):
+ if cmd.startswith("-"): # Login shell! Let's use this.
+ return _get_login_shell(cmd)
+ name = os.path.basename(cmd).lower()
+ if name == "rosetta" or QEMU_BIN_REGEX.fullmatch(name):
+ # If the current process is Rosetta or QEMU, this likely is a
+ # containerized process. Parse out the actual command instead.
+ cmd = args[0]
+ args = args[1:]
+ name = os.path.basename(cmd).lower()
+ if name in SHELL_NAMES: # Command looks like a shell.
+ return (name, cmd)
+ shell = _get_interpreter_shell(name, args)
+ if shell:
+ return shell
+ return None
+
+
+def get_shell(pid=None, max_depth=10):
+ """Get the shell that the supplied pid or os.getpid() is running in."""
+ pid = str(pid or os.getpid())
+ for proc_args, _, _ in _iter_process_parents(pid, max_depth):
+ shell = _get_shell(*proc_args)
+ if shell:
+ return shell
+ return None
diff --git a/contrib/python/shellingham/shellingham/posix/_core.py b/contrib/python/shellingham/shellingham/posix/_core.py
new file mode 100644
index 00000000000..adc49e6e7a9
--- /dev/null
+++ b/contrib/python/shellingham/shellingham/posix/_core.py
@@ -0,0 +1,3 @@
+import collections
+
+Process = collections.namedtuple("Process", "args pid ppid")
diff --git a/contrib/python/shellingham/shellingham/posix/proc.py b/contrib/python/shellingham/shellingham/posix/proc.py
new file mode 100644
index 00000000000..950f63228e5
--- /dev/null
+++ b/contrib/python/shellingham/shellingham/posix/proc.py
@@ -0,0 +1,83 @@
+import io
+import os
+import re
+import sys
+
+from ._core import Process
+
+# FreeBSD: https://www.freebsd.org/cgi/man.cgi?query=procfs
+# NetBSD: https://man.netbsd.org/NetBSD-9.3-STABLE/mount_procfs.8
+# DragonFlyBSD: https://www.dragonflybsd.org/cgi/web-man?command=procfs
+BSD_STAT_PPID = 2
+
+# See https://docs.kernel.org/filesystems/proc.html
+LINUX_STAT_PPID = 3
+
+STAT_PATTERN = re.compile(r"\(.+\)|\S+")
+
+
+def detect_proc():
+ """Detect /proc filesystem style.
+
+ This checks the /proc/{pid} directory for possible formats. Returns one of
+ the following as str:
+
+ * `stat`: Linux-style, i.e. ``/proc/{pid}/stat``.
+ * `status`: BSD-style, i.e. ``/proc/{pid}/status``.
+ """
+ pid = os.getpid()
+ for name in ("stat", "status"):
+ if os.path.exists(os.path.join("/proc", str(pid), name)):
+ return name
+ raise ProcFormatError("unsupported proc format")
+
+
+def _use_bsd_stat_format():
+ try:
+ return os.uname().sysname.lower() in ("freebsd", "netbsd", "dragonfly")
+ except Exception:
+ return False
+
+
+def _get_ppid(pid, name):
+ path = os.path.join("/proc", str(pid), name)
+ with io.open(path, encoding="ascii", errors="replace") as f:
+ parts = STAT_PATTERN.findall(f.read())
+ # We only care about TTY and PPID -- both are numbers.
+ if _use_bsd_stat_format():
+ return parts[BSD_STAT_PPID]
+ return parts[LINUX_STAT_PPID]
+
+
+def _get_cmdline(pid):
+ path = os.path.join("/proc", str(pid), "cmdline")
+ encoding = sys.getfilesystemencoding() or "utf-8"
+ with io.open(path, encoding=encoding, errors="replace") as f:
+ # XXX: Command line arguments can be arbitrary byte sequences, not
+ # necessarily decodable. For Shellingham's purpose, however, we don't
+ # care. (pypa/pipenv#2820)
+ # cmdline appends an extra NULL at the end, hence the [:-1].
+ return tuple(f.read().split("\0")[:-1])
+
+
+class ProcFormatError(EnvironmentError):
+ pass
+
+
+def iter_process_parents(pid, max_depth=10):
+ """Try to look up the process tree via the /proc interface."""
+ stat_name = detect_proc()
+
+ # Inner generator function so we correctly throw an error eagerly if proc
+ # is not supported, rather than on the first call to the iterator. This
+ # allows the call site detects the correct implementation.
+ def _iter_process_parents(pid, max_depth):
+ for _ in range(max_depth):
+ ppid = _get_ppid(pid, stat_name)
+ args = _get_cmdline(pid)
+ yield Process(args=args, pid=pid, ppid=ppid)
+ if ppid == "0":
+ break
+ pid = ppid
+
+ return _iter_process_parents(pid, max_depth)
diff --git a/contrib/python/shellingham/shellingham/posix/ps.py b/contrib/python/shellingham/shellingham/posix/ps.py
new file mode 100644
index 00000000000..3bc39a74a56
--- /dev/null
+++ b/contrib/python/shellingham/shellingham/posix/ps.py
@@ -0,0 +1,51 @@
+import errno
+import subprocess
+import sys
+
+from ._core import Process
+
+
+class PsNotAvailable(EnvironmentError):
+ pass
+
+
+def iter_process_parents(pid, max_depth=10):
+ """Try to look up the process tree via the output of `ps`."""
+ try:
+ cmd = ["ps", "-ww", "-o", "pid=", "-o", "ppid=", "-o", "args="]
+ output = subprocess.check_output(cmd)
+ except OSError as e: # Python 2-compatible FileNotFoundError.
+ if e.errno != errno.ENOENT:
+ raise
+ raise PsNotAvailable("ps not found")
+ except subprocess.CalledProcessError as e:
+ # `ps` can return 1 if the process list is completely empty.
+ # (sarugaku/shellingham#15)
+ if not e.output.strip():
+ return
+ raise
+ if not isinstance(output, str):
+ encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+ output = output.decode(encoding)
+
+ processes_mapping = {}
+ for line in output.split("\n"):
+ try:
+ _pid, ppid, args = line.strip().split(None, 2)
+ # XXX: This is not right, but we are really out of options.
+ # ps does not offer a sane way to decode the argument display,
+ # and this is "Good Enough" for obtaining shell names. Hopefully
+ # people don't name their shell with a space, or have something
+ # like "/usr/bin/xonsh is uber". (sarugaku/shellingham#14)
+ args = tuple(a.strip() for a in args.split(" "))
+ except ValueError:
+ continue
+ processes_mapping[_pid] = Process(args=args, pid=_pid, ppid=ppid)
+
+ for _ in range(max_depth):
+ try:
+ process = processes_mapping[pid]
+ except KeyError:
+ return
+ yield process
+ pid = process.ppid
diff --git a/contrib/python/shellingham/ya.make b/contrib/python/shellingham/ya.make
new file mode 100644
index 00000000000..0497140cbc2
--- /dev/null
+++ b/contrib/python/shellingham/ya.make
@@ -0,0 +1,32 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(1.5.4)
+
+LICENSE(ISC)
+
+NO_LINT()
+
+NO_CHECK_IMPORTS(
+ shellingham.nt
+)
+
+PY_SRCS(
+ TOP_LEVEL
+ shellingham/__init__.py
+ shellingham/_core.py
+ shellingham/nt.py
+ shellingham/posix/__init__.py
+ shellingham/posix/_core.py
+ shellingham/posix/proc.py
+ shellingham/posix/ps.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/shellingham/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
diff --git a/contrib/python/typer-slim/.dist-info/METADATA b/contrib/python/typer-slim/.dist-info/METADATA
new file mode 100644
index 00000000000..8d9cbe6c372
--- /dev/null
+++ b/contrib/python/typer-slim/.dist-info/METADATA
@@ -0,0 +1,425 @@
+Metadata-Version: 2.4
+Name: typer-slim
+Version: 0.21.1
+Summary: Typer, build great CLIs. Easy to code. Based on Python type hints.
+Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <[email protected]>
+License-Expression: MIT
+License-File: LICENSE
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Topic :: Software Development
+Classifier: Typing :: Typed
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Project-URL: Homepage, https://github.com/fastapi/typer
+Project-URL: Documentation, https://typer.tiangolo.com
+Project-URL: Repository, https://github.com/fastapi/typer
+Project-URL: Issues, https://github.com/fastapi/typer/issues
+Project-URL: Changelog, https://typer.tiangolo.com/release-notes/
+Requires-Python: >=3.9
+Requires-Dist: click>=8.0.0
+Requires-Dist: typing-extensions>=3.7.4.3
+Provides-Extra: standard
+Requires-Dist: shellingham>=1.3.0; extra == "standard"
+Requires-Dist: rich>=10.11.0; extra == "standard"
+Description-Content-Type: text/markdown
+
+<p align="center">
+ <a href="https://typer.tiangolo.com"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg#only-light" alt="Typer"></a>
+
+</p>
+<p align="center">
+ <em>Typer, build great CLIs. Easy to code. Based on Python type hints.</em>
+</p>
+<p align="center">
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank">
+ <img src="https://github.com/fastapi/typer/actions/workflows/test.yml/badge.svg?event=push&branch=master" alt="Test">
+</a>
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3APublish" target="_blank">
+ <img src="https://github.com/fastapi/typer/workflows/Publish/badge.svg" alt="Publish">
+</a>
+<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/typer" target="_blank">
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/typer.svg" alt="Coverage">
+<a href="https://pypi.org/project/typer" target="_blank">
+ <img src="https://img.shields.io/pypi/v/typer?color=%2334D058&label=pypi%20package" alt="Package version">
+</a>
+</p>
+
+---
+
+**Documentation**: <a href="https://typer.tiangolo.com" target="_blank">https://typer.tiangolo.com</a>
+
+**Source Code**: <a href="https://github.com/fastapi/typer" target="_blank">https://github.com/fastapi/typer</a>
+
+---
+
+Typer is a library for building <abbr title="command line interface, programs executed from a terminal">CLI</abbr> applications that users will **love using** and developers will **love creating**. Based on Python type hints.
+
+It's also a command line tool to run scripts, automatically converting them to CLI applications.
+
+The key features are:
+
+* **Intuitive to write**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs.
+* **Easy to use**: It's easy to use for the final users. Automatic help, and automatic completion for all shells.
+* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
+* **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**.
+* **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments.
+* **Run scripts**: Typer includes a `typer` command/program that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally.
+
+## FastAPI of CLIs
+
+**Typer** is <a href="https://fastapi.tiangolo.com" class="external-link" target="_blank">FastAPI</a>'s little sibling, it's the FastAPI of CLIs.
+
+## Installation
+
+Create and activate a <a href="https://typer.tiangolo.com/virtual-environments/" class="external-link" target="_blank">virtual environment</a> and then install **Typer**:
+
+<div class="termy">
+
+```console
+$ pip install typer
+---> 100%
+Successfully installed typer rich shellingham
+```
+
+</div>
+
+## Example
+
+### The absolute minimum
+
+* Create a file `main.py` with:
+
+```Python
+def main(name: str):
+ print(f"Hello {name}")
+```
+
+This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application.
+
+### Run it
+
+Run your application with the `typer` command:
+
+<div class="termy">
+
+```console
+// Run your application
+$ typer main.py run
+
+// You get a nice error, you are missing NAME
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+Try 'typer [PATH_OR_MODULE] run --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ typer main.py run --help
+
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+
+Run the provided Typer app.
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ typer main.py run Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts.
+
+**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command.
+
+## Use Typer in your code
+
+Now let's start using Typer in your own code, update `main.py` with:
+
+```Python
+import typer
+
+
+def main(name: str):
+ print(f"Hello {name}")
+
+
+if __name__ == "__main__":
+ typer.run(main)
+```
+
+Now you could run it with Python directly:
+
+<div class="termy">
+
+```console
+// Run your application
+$ python main.py
+
+// You get a nice error, you are missing NAME
+Usage: main.py [OPTIONS] NAME
+Try 'main.py --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ python main.py --help
+
+Usage: main.py [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ python main.py Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+**Note**: you can also call this same script with the `typer` command, but you don't need to.
+
+## Example upgrade
+
+This was the simplest example possible.
+
+Now let's see one a bit more complex.
+
+### An example with two subcommands
+
+Modify the file `main.py`.
+
+Create a `typer.Typer()` app, and create two subcommands with their parameters.
+
+```Python hl_lines="3 6 11 20"
+import typer
+
+app = typer.Typer()
+
+
+def hello(name: str):
+ print(f"Hello {name}")
+
+
+def goodbye(name: str, formal: bool = False):
+ if formal:
+ print(f"Goodbye Ms. {name}. Have a good day.")
+ else:
+ print(f"Bye {name}!")
+
+
+if __name__ == "__main__":
+ app()
+```
+
+And that will:
+
+* Explicitly create a `typer.Typer` app.
+ * The previous `typer.run` actually creates one implicitly for you.
+* Add two subcommands with `@app.command()`.
+* Execute the `app()` itself, as if it was a function (instead of `typer.run`).
+
+### Run the upgraded example
+
+Check the new help:
+
+<div class="termy">
+
+```console
+$ python main.py --help
+
+ Usage: main.py [OPTIONS] COMMAND [ARGS]...
+
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --install-completion Install completion โ”‚
+โ”‚ for the current โ”‚
+โ”‚ shell. โ”‚
+โ”‚ --show-completion Show completion for โ”‚
+โ”‚ the current shell, โ”‚
+โ”‚ to copy it or โ”‚
+โ”‚ customize the โ”‚
+โ”‚ installation. โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ goodbye โ”‚
+โ”‚ hello โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// When you create a package you get โœจ auto-completion โœจ for free, installed with --install-completion
+
+// You have 2 subcommands (the 2 functions): goodbye and hello
+```
+
+</div>
+
+Now check the help for the `hello` command:
+
+<div class="termy">
+
+```console
+$ python main.py hello --help
+
+ Usage: main.py hello [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+```
+
+</div>
+
+And now check the help for the `goodbye` command:
+
+<div class="termy">
+
+```console
+$ python main.py goodbye --help
+
+ Usage: main.py goodbye [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --formal --no-formal [default: no-formal] โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Automatic --formal and --no-formal for the bool option ๐ŸŽ‰
+```
+
+</div>
+
+Now you can try out the new command line application:
+
+<div class="termy">
+
+```console
+// Use it with the hello command
+
+$ python main.py hello Camila
+
+Hello Camila
+
+// And with the goodbye command
+
+$ python main.py goodbye Camila
+
+Bye Camila!
+
+// And with --formal
+
+$ python main.py goodbye --formal Camila
+
+Goodbye Ms. Camila. Have a good day.
+```
+
+</div>
+
+**Note**: If your app only has one command, by default the command name is **omitted** in usage: `python main.py Camila`. However, when there are multiple commands, you must **explicitly include the command name**: `python main.py hello Camila`. See [One or Multiple Commands](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/) for more details.
+
+### Recap
+
+In summary, you declare **once** the types of parameters (*CLI arguments* and *CLI options*) as function parameters.
+
+You do that with standard modern Python types.
+
+You don't have to learn a new syntax, the methods or classes of a specific library, etc.
+
+Just standard **Python**.
+
+For example, for an `int`:
+
+```Python
+total: int
+```
+
+or for a `bool` flag:
+
+```Python
+force: bool
+```
+
+And similarly for **files**, **paths**, **enums** (choices), etc. And there are tools to create **groups of subcommands**, add metadata, extra **validation**, etc.
+
+**You get**: great editor support, including **completion** and **type checks** everywhere.
+
+**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command.
+
+For a more complete example including more features, see the <a href="https://typer.tiangolo.com/tutorial/">Tutorial - User Guide</a>.
+
+## Dependencies
+
+**Typer** stands on the shoulders of a giant. Its only internal required dependency is <a href="https://click.palletsprojects.com/" class="external-link" target="_blank">Click</a>.
+
+By default it also comes with extra standard dependencies:
+
+* <a href="https://rich.readthedocs.io/en/stable/index.html" class="external-link" target="_blank"><code>rich</code></a>: to show nicely formatted errors automatically.
+* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: to automatically detect the current shell when installing completion.
+ * With `shellingham` you can just use `--install-completion`.
+ * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`.
+
+### `typer-slim`
+
+If you don't want the extra standard optional dependencies, install `typer-slim` instead.
+
+When you install with:
+
+```bash
+pip install typer
+```
+
+...it includes the same code and dependencies as:
+
+```bash
+pip install "typer-slim[standard]"
+```
+
+The `standard` extra dependencies are `rich` and `shellingham`.
+
+**Note**: The `typer` command is only included in the `typer` package.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/contrib/python/typer-slim/.dist-info/entry_points.txt b/contrib/python/typer-slim/.dist-info/entry_points.txt
new file mode 100644
index 00000000000..c3ad4726d43
--- /dev/null
+++ b/contrib/python/typer-slim/.dist-info/entry_points.txt
@@ -0,0 +1,4 @@
+[console_scripts]
+
+[gui_scripts]
+
diff --git a/contrib/python/typer-slim/LICENSE b/contrib/python/typer-slim/LICENSE
new file mode 100644
index 00000000000..a7694736cf3
--- /dev/null
+++ b/contrib/python/typer-slim/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Sebastiรกn Ramรญrez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/contrib/python/typer-slim/README.md b/contrib/python/typer-slim/README.md
new file mode 100644
index 00000000000..32ead017462
--- /dev/null
+++ b/contrib/python/typer-slim/README.md
@@ -0,0 +1,386 @@
+<p align="center">
+ <a href="https://typer.tiangolo.com"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg#only-light" alt="Typer"></a>
+
+</p>
+<p align="center">
+ <em>Typer, build great CLIs. Easy to code. Based on Python type hints.</em>
+</p>
+<p align="center">
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank">
+ <img src="https://github.com/fastapi/typer/actions/workflows/test.yml/badge.svg?event=push&branch=master" alt="Test">
+</a>
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3APublish" target="_blank">
+ <img src="https://github.com/fastapi/typer/workflows/Publish/badge.svg" alt="Publish">
+</a>
+<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/typer" target="_blank">
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/typer.svg" alt="Coverage">
+<a href="https://pypi.org/project/typer" target="_blank">
+ <img src="https://img.shields.io/pypi/v/typer?color=%2334D058&label=pypi%20package" alt="Package version">
+</a>
+</p>
+
+---
+
+**Documentation**: <a href="https://typer.tiangolo.com" target="_blank">https://typer.tiangolo.com</a>
+
+**Source Code**: <a href="https://github.com/fastapi/typer" target="_blank">https://github.com/fastapi/typer</a>
+
+---
+
+Typer is a library for building <abbr title="command line interface, programs executed from a terminal">CLI</abbr> applications that users will **love using** and developers will **love creating**. Based on Python type hints.
+
+It's also a command line tool to run scripts, automatically converting them to CLI applications.
+
+The key features are:
+
+* **Intuitive to write**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs.
+* **Easy to use**: It's easy to use for the final users. Automatic help, and automatic completion for all shells.
+* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
+* **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**.
+* **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments.
+* **Run scripts**: Typer includes a `typer` command/program that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally.
+
+## FastAPI of CLIs
+
+**Typer** is <a href="https://fastapi.tiangolo.com" class="external-link" target="_blank">FastAPI</a>'s little sibling, it's the FastAPI of CLIs.
+
+## Installation
+
+Create and activate a <a href="https://typer.tiangolo.com/virtual-environments/" class="external-link" target="_blank">virtual environment</a> and then install **Typer**:
+
+<div class="termy">
+
+```console
+$ pip install typer
+---> 100%
+Successfully installed typer rich shellingham
+```
+
+</div>
+
+## Example
+
+### The absolute minimum
+
+* Create a file `main.py` with:
+
+```Python
+def main(name: str):
+ print(f"Hello {name}")
+```
+
+This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application.
+
+### Run it
+
+Run your application with the `typer` command:
+
+<div class="termy">
+
+```console
+// Run your application
+$ typer main.py run
+
+// You get a nice error, you are missing NAME
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+Try 'typer [PATH_OR_MODULE] run --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ typer main.py run --help
+
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+
+Run the provided Typer app.
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ typer main.py run Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts.
+
+**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command.
+
+## Use Typer in your code
+
+Now let's start using Typer in your own code, update `main.py` with:
+
+```Python
+import typer
+
+
+def main(name: str):
+ print(f"Hello {name}")
+
+
+if __name__ == "__main__":
+ typer.run(main)
+```
+
+Now you could run it with Python directly:
+
+<div class="termy">
+
+```console
+// Run your application
+$ python main.py
+
+// You get a nice error, you are missing NAME
+Usage: main.py [OPTIONS] NAME
+Try 'main.py --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ python main.py --help
+
+Usage: main.py [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ python main.py Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+**Note**: you can also call this same script with the `typer` command, but you don't need to.
+
+## Example upgrade
+
+This was the simplest example possible.
+
+Now let's see one a bit more complex.
+
+### An example with two subcommands
+
+Modify the file `main.py`.
+
+Create a `typer.Typer()` app, and create two subcommands with their parameters.
+
+```Python hl_lines="3 6 11 20"
+import typer
+
+app = typer.Typer()
+
+
+def hello(name: str):
+ print(f"Hello {name}")
+
+
+def goodbye(name: str, formal: bool = False):
+ if formal:
+ print(f"Goodbye Ms. {name}. Have a good day.")
+ else:
+ print(f"Bye {name}!")
+
+
+if __name__ == "__main__":
+ app()
+```
+
+And that will:
+
+* Explicitly create a `typer.Typer` app.
+ * The previous `typer.run` actually creates one implicitly for you.
+* Add two subcommands with `@app.command()`.
+* Execute the `app()` itself, as if it was a function (instead of `typer.run`).
+
+### Run the upgraded example
+
+Check the new help:
+
+<div class="termy">
+
+```console
+$ python main.py --help
+
+ Usage: main.py [OPTIONS] COMMAND [ARGS]...
+
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --install-completion Install completion โ”‚
+โ”‚ for the current โ”‚
+โ”‚ shell. โ”‚
+โ”‚ --show-completion Show completion for โ”‚
+โ”‚ the current shell, โ”‚
+โ”‚ to copy it or โ”‚
+โ”‚ customize the โ”‚
+โ”‚ installation. โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ goodbye โ”‚
+โ”‚ hello โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// When you create a package you get โœจ auto-completion โœจ for free, installed with --install-completion
+
+// You have 2 subcommands (the 2 functions): goodbye and hello
+```
+
+</div>
+
+Now check the help for the `hello` command:
+
+<div class="termy">
+
+```console
+$ python main.py hello --help
+
+ Usage: main.py hello [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+```
+
+</div>
+
+And now check the help for the `goodbye` command:
+
+<div class="termy">
+
+```console
+$ python main.py goodbye --help
+
+ Usage: main.py goodbye [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --formal --no-formal [default: no-formal] โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Automatic --formal and --no-formal for the bool option ๐ŸŽ‰
+```
+
+</div>
+
+Now you can try out the new command line application:
+
+<div class="termy">
+
+```console
+// Use it with the hello command
+
+$ python main.py hello Camila
+
+Hello Camila
+
+// And with the goodbye command
+
+$ python main.py goodbye Camila
+
+Bye Camila!
+
+// And with --formal
+
+$ python main.py goodbye --formal Camila
+
+Goodbye Ms. Camila. Have a good day.
+```
+
+</div>
+
+**Note**: If your app only has one command, by default the command name is **omitted** in usage: `python main.py Camila`. However, when there are multiple commands, you must **explicitly include the command name**: `python main.py hello Camila`. See [One or Multiple Commands](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/) for more details.
+
+### Recap
+
+In summary, you declare **once** the types of parameters (*CLI arguments* and *CLI options*) as function parameters.
+
+You do that with standard modern Python types.
+
+You don't have to learn a new syntax, the methods or classes of a specific library, etc.
+
+Just standard **Python**.
+
+For example, for an `int`:
+
+```Python
+total: int
+```
+
+or for a `bool` flag:
+
+```Python
+force: bool
+```
+
+And similarly for **files**, **paths**, **enums** (choices), etc. And there are tools to create **groups of subcommands**, add metadata, extra **validation**, etc.
+
+**You get**: great editor support, including **completion** and **type checks** everywhere.
+
+**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command.
+
+For a more complete example including more features, see the <a href="https://typer.tiangolo.com/tutorial/">Tutorial - User Guide</a>.
+
+## Dependencies
+
+**Typer** stands on the shoulders of a giant. Its only internal required dependency is <a href="https://click.palletsprojects.com/" class="external-link" target="_blank">Click</a>.
+
+By default it also comes with extra standard dependencies:
+
+* <a href="https://rich.readthedocs.io/en/stable/index.html" class="external-link" target="_blank"><code>rich</code></a>: to show nicely formatted errors automatically.
+* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: to automatically detect the current shell when installing completion.
+ * With `shellingham` you can just use `--install-completion`.
+ * Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`.
+
+### `typer-slim`
+
+If you don't want the extra standard optional dependencies, install `typer-slim` instead.
+
+When you install with:
+
+```bash
+pip install typer
+```
+
+...it includes the same code and dependencies as:
+
+```bash
+pip install "typer-slim[standard]"
+```
+
+The `standard` extra dependencies are `rich` and `shellingham`.
+
+**Note**: The `typer` command is only included in the `typer` package.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/contrib/python/typer-slim/typer/__init__.py b/contrib/python/typer-slim/typer/__init__.py
new file mode 100644
index 00000000000..603725b28cf
--- /dev/null
+++ b/contrib/python/typer-slim/typer/__init__.py
@@ -0,0 +1,39 @@
+"""Typer, build great CLIs. Easy to code. Based on Python type hints."""
+
+__version__ = "0.21.1"
+
+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 .main import Typer as Typer
+from .main import launch as launch
+from .main import run as run
+from .models import CallbackParam as CallbackParam
+from .models import Context as Context
+from .models import FileBinaryRead as FileBinaryRead
+from .models import FileBinaryWrite as FileBinaryWrite
+from .models import FileText as FileText
+from .models import FileTextWrite as FileTextWrite
+from .params import Argument as Argument
+from .params import Option as Option
diff --git a/contrib/python/typer-slim/typer/__main__.py b/contrib/python/typer-slim/typer/__main__.py
new file mode 100644
index 00000000000..4e28416e104
--- /dev/null
+++ b/contrib/python/typer-slim/typer/__main__.py
@@ -0,0 +1,3 @@
+from .cli import main
+
+main()
diff --git a/contrib/python/typer-slim/typer/_completion_classes.py b/contrib/python/typer-slim/typer/_completion_classes.py
new file mode 100644
index 00000000000..d08c76ac5ef
--- /dev/null
+++ b/contrib/python/typer-slim/typer/_completion_classes.py
@@ -0,0 +1,206 @@
+import importlib.util
+import os
+import re
+import sys
+from typing import Any
+
+import click
+import click.parser
+import click.shell_completion
+
+from ._completion_shared import (
+ COMPLETION_SCRIPT_BASH,
+ COMPLETION_SCRIPT_FISH,
+ COMPLETION_SCRIPT_POWER_SHELL,
+ COMPLETION_SCRIPT_ZSH,
+ Shells,
+)
+
+try:
+ from click.shell_completion import split_arg_string as click_split_arg_string
+except ImportError: # pragma: no cover
+ # TODO: when removing support for Click < 8.2, remove this import
+ from click.parser import ( # type: ignore[no-redef]
+ split_arg_string as click_split_arg_string,
+ )
+
+
+def _sanitize_help_text(text: str) -> str:
+ """Sanitizes the help text by removing rich tags"""
+ if not importlib.util.find_spec("rich"):
+ return text
+ from . import rich_utils
+
+ return rich_utils.rich_render_text(text)
+
+
+class BashComplete(click.shell_completion.BashComplete):
+ name = Shells.bash.value
+ source_template = COMPLETION_SCRIPT_BASH
+
+ def source_vars(self) -> dict[str, Any]:
+ return {
+ "complete_func": self.func_name,
+ "autocomplete_var": self.complete_var,
+ "prog_name": self.prog_name,
+ }
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ cwords = click_split_arg_string(os.environ["COMP_WORDS"])
+ cword = int(os.environ["COMP_CWORD"])
+ args = cwords[1:cword]
+
+ try:
+ incomplete = cwords[cword]
+ except IndexError:
+ incomplete = ""
+
+ return args, incomplete
+
+ def format_completion(self, item: click.shell_completion.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}"
+ return f"{item.value}"
+
+ def complete(self) -> str:
+ 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)
+
+
+class ZshComplete(click.shell_completion.ZshComplete):
+ name = Shells.zsh.value
+ source_template = COMPLETION_SCRIPT_ZSH
+
+ def source_vars(self) -> dict[str, Any]:
+ return {
+ "complete_func": self.func_name,
+ "autocomplete_var": self.complete_var,
+ "prog_name": self.prog_name,
+ }
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
+ cwords = click_split_arg_string(completion_args)
+ args = cwords[1:]
+ if args and not completion_args.endswith(" "):
+ incomplete = args[-1]
+ args = args[:-1]
+ else:
+ incomplete = ""
+ return args, incomplete
+
+ def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
+ def escape(s: str) -> str:
+ return (
+ s.replace('"', '""')
+ .replace("'", "''")
+ .replace("$", "\\$")
+ .replace("`", "\\`")
+ .replace(":", r"\\:")
+ )
+
+ # TODO: Explore replicating the new behavior from Click, pay attention to
+ # the difference with and without escape
+ # return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}"
+ if item.help:
+ return f'"{escape(item.value)}":"{_sanitize_help_text(escape(item.help))}"'
+ else:
+ return f'"{escape(item.value)}"'
+
+ def complete(self) -> str:
+ args, incomplete = self.get_completion_args()
+ completions = self.get_completions(args, incomplete)
+ res = [self.format_completion(item) for item in completions]
+ if res:
+ args_str = "\n".join(res)
+ return f"_arguments '*: :(({args_str}))'"
+ else:
+ return "_files"
+
+
+class FishComplete(click.shell_completion.FishComplete):
+ name = Shells.fish.value
+ source_template = COMPLETION_SCRIPT_FISH
+
+ def source_vars(self) -> dict[str, Any]:
+ return {
+ "complete_func": self.func_name,
+ "autocomplete_var": self.complete_var,
+ "prog_name": self.prog_name,
+ }
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
+ cwords = click_split_arg_string(completion_args)
+ args = cwords[1:]
+ if args and not completion_args.endswith(" "):
+ incomplete = args[-1]
+ args = args[:-1]
+ else:
+ incomplete = ""
+ return args, incomplete
+
+ def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
+ # TODO: Explore replicating the new behavior from Click, pay attention to
+ # the difference with and without formatted help
+ # if item.help:
+ # return f"{item.type},{item.value}\t{item.help}"
+
+ # return f"{item.type},{item.value}
+ if item.help:
+ formatted_help = re.sub(r"\s", " ", item.help)
+ return f"{item.value}\t{_sanitize_help_text(formatted_help)}"
+ else:
+ return f"{item.value}"
+
+ def complete(self) -> str:
+ complete_action = os.getenv("_TYPER_COMPLETE_FISH_ACTION", "")
+ args, incomplete = self.get_completion_args()
+ completions = self.get_completions(args, incomplete)
+ show_args = [self.format_completion(item) for item in completions]
+ if complete_action == "get-args":
+ if show_args:
+ return "\n".join(show_args)
+ elif complete_action == "is-args":
+ if show_args:
+ # Activate complete args (no files)
+ sys.exit(0)
+ else:
+ # Deactivate complete args (allow files)
+ sys.exit(1)
+ return "" # pragma: no cover
+
+
+class PowerShellComplete(click.shell_completion.ShellComplete):
+ name = Shells.powershell.value
+ source_template = COMPLETION_SCRIPT_POWER_SHELL
+
+ def source_vars(self) -> dict[str, Any]:
+ return {
+ "complete_func": self.func_name,
+ "autocomplete_var": self.complete_var,
+ "prog_name": self.prog_name,
+ }
+
+ def get_completion_args(self) -> tuple[list[str], str]:
+ completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
+ incomplete = os.getenv("_TYPER_COMPLETE_WORD_TO_COMPLETE", "")
+ cwords = click_split_arg_string(completion_args)
+ args = cwords[1:-1] if incomplete else cwords[1:]
+ return args, incomplete
+
+ def format_completion(self, item: click.shell_completion.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)
diff --git a/contrib/python/typer-slim/typer/_completion_shared.py b/contrib/python/typer-slim/typer/_completion_shared.py
new file mode 100644
index 00000000000..4c4d79dda7a
--- /dev/null
+++ b/contrib/python/typer-slim/typer/_completion_shared.py
@@ -0,0 +1,259 @@
+import os
+import re
+import subprocess
+from enum import Enum
+from pathlib import Path
+from typing import Optional, Union
+
+import click
+from typer.core import HAS_SHELLINGHAM
+
+if HAS_SHELLINGHAM:
+ import shellingham
+
+
+class Shells(str, Enum):
+ bash = "bash"
+ zsh = "zsh"
+ fish = "fish"
+ powershell = "powershell"
+ pwsh = "pwsh"
+
+
+COMPLETION_SCRIPT_BASH = """
+%(complete_func)s() {
+ local IFS=$'\n'
+ COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
+ COMP_CWORD=$COMP_CWORD \\
+ %(autocomplete_var)s=complete_bash $1 ) )
+ return 0
+}
+
+complete -o default -F %(complete_func)s %(prog_name)s
+"""
+
+COMPLETION_SCRIPT_ZSH = """
+#compdef %(prog_name)s
+
+%(complete_func)s() {
+ eval $(env _TYPER_COMPLETE_ARGS="${words[1,$CURRENT]}" %(autocomplete_var)s=complete_zsh %(prog_name)s)
+}
+
+compdef %(complete_func)s %(prog_name)s
+"""
+
+COMPLETION_SCRIPT_FISH = 'complete --command %(prog_name)s --no-files --arguments "(env %(autocomplete_var)s=complete_fish _TYPER_COMPLETE_FISH_ACTION=get-args _TYPER_COMPLETE_ARGS=(commandline -cp) %(prog_name)s)" --condition "env %(autocomplete_var)s=complete_fish _TYPER_COMPLETE_FISH_ACTION=is-args _TYPER_COMPLETE_ARGS=(commandline -cp) %(prog_name)s"'
+
+COMPLETION_SCRIPT_POWER_SHELL = """
+Import-Module PSReadLine
+Set-PSReadLineKeyHandler -Chord Tab -Function MenuComplete
+$scriptblock = {
+ param($wordToComplete, $commandAst, $cursorPosition)
+ $Env:%(autocomplete_var)s = "complete_powershell"
+ $Env:_TYPER_COMPLETE_ARGS = $commandAst.ToString()
+ $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = $wordToComplete
+ %(prog_name)s | ForEach-Object {
+ $commandArray = $_ -Split ":::"
+ $command = $commandArray[0]
+ $helpString = $commandArray[1]
+ [System.Management.Automation.CompletionResult]::new(
+ $command, $command, 'ParameterValue', $helpString)
+ }
+ $Env:%(autocomplete_var)s = ""
+ $Env:_TYPER_COMPLETE_ARGS = ""
+ $Env:_TYPER_COMPLETE_WORD_TO_COMPLETE = ""
+}
+Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock $scriptblock
+"""
+
+_completion_scripts = {
+ "bash": COMPLETION_SCRIPT_BASH,
+ "zsh": COMPLETION_SCRIPT_ZSH,
+ "fish": COMPLETION_SCRIPT_FISH,
+ "powershell": COMPLETION_SCRIPT_POWER_SHELL,
+ "pwsh": COMPLETION_SCRIPT_POWER_SHELL,
+}
+
+# TODO: Probably refactor this, copied from Click 7.x
+_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")
+
+
+def get_completion_script(*, prog_name: str, complete_var: str, shell: str) -> str:
+ 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)
+ return (
+ script
+ % {
+ "complete_func": f"_{cf_name}_completion",
+ "prog_name": prog_name,
+ "autocomplete_var": complete_var,
+ }
+ ).strip()
+
+
+def install_bash(*, prog_name: str, complete_var: str, shell: str) -> Path:
+ # Ref: https://github.com/scop/bash-completion#faq
+ # It seems bash-completion is the official completion system for bash:
+ # Ref: https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html
+ # But installing in the locations from the docs doesn't seem to have effect
+ completion_path = Path.home() / ".bash_completions" / f"{prog_name}.sh"
+ rc_path = Path.home() / ".bashrc"
+ rc_path.parent.mkdir(parents=True, exist_ok=True)
+ rc_content = ""
+ if rc_path.is_file():
+ rc_content = rc_path.read_text()
+ completion_init_lines = [f"source '{completion_path}'"]
+ for line in completion_init_lines:
+ if line not in rc_content: # pragma: no cover
+ rc_content += f"\n{line}"
+ rc_content += "\n"
+ rc_path.write_text(rc_content)
+ # Install completion
+ completion_path.parent.mkdir(parents=True, exist_ok=True)
+ script_content = get_completion_script(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ completion_path.write_text(script_content)
+ return completion_path
+
+
+def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path:
+ # Setup Zsh and load ~/.zfunc
+ zshrc_path = Path.home() / ".zshrc"
+ zshrc_path.parent.mkdir(parents=True, exist_ok=True)
+ zshrc_content = ""
+ if zshrc_path.is_file():
+ zshrc_content = zshrc_path.read_text()
+ completion_line = "fpath+=~/.zfunc; autoload -Uz compinit; compinit"
+ if completion_line not in zshrc_content:
+ zshrc_content += f"\n{completion_line}\n"
+ style_line = "zstyle ':completion:*' menu select"
+ # TODO: consider setting the style only for the current program
+ # style_line = f"zstyle ':completion:*:*:{prog_name}:*' menu select"
+ # Install zstyle completion config only if the user doesn't have a customization
+ if "zstyle" not in zshrc_content:
+ zshrc_content += f"\n{style_line}\n"
+ zshrc_content = f"{zshrc_content.strip()}\n"
+ zshrc_path.write_text(zshrc_content)
+ # Install completion under ~/.zfunc/
+ path_obj = Path.home() / f".zfunc/_{prog_name}"
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
+ script_content = get_completion_script(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ path_obj.write_text(script_content)
+ return path_obj
+
+
+def install_fish(*, prog_name: str, complete_var: str, shell: str) -> Path:
+ path_obj = Path.home() / f".config/fish/completions/{prog_name}.fish"
+ parent_dir: Path = path_obj.parent
+ parent_dir.mkdir(parents=True, exist_ok=True)
+ script_content = get_completion_script(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ path_obj.write_text(f"{script_content}\n")
+ return path_obj
+
+
+def install_powershell(*, prog_name: str, complete_var: str, shell: str) -> Path:
+ subprocess.run(
+ [
+ shell,
+ "-Command",
+ "Set-ExecutionPolicy",
+ "Unrestricted",
+ "-Scope",
+ "CurrentUser",
+ ]
+ )
+ result = subprocess.run(
+ [shell, "-NoProfile", "-Command", "echo", "$profile"],
+ check=True,
+ 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)
+ path_str = ""
+ if isinstance(result.stdout, str): # pragma: no cover
+ path_str = result.stdout
+ if isinstance(result.stdout, bytes):
+ for encoding in ["windows-1252", "utf8", "cp850"]:
+ try:
+ path_str = result.stdout.decode(encoding)
+ break
+ 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)
+ path_obj = Path(path_str.strip())
+ parent_dir: Path = path_obj.parent
+ parent_dir.mkdir(parents=True, exist_ok=True)
+ script_content = get_completion_script(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ with path_obj.open(mode="a") as f:
+ f.write(f"{script_content}\n")
+ return path_obj
+
+
+def install(
+ shell: Optional[str] = None,
+ prog_name: Optional[str] = None,
+ complete_var: Optional[str] = None,
+) -> tuple[str, Path]:
+ prog_name = prog_name or click.get_current_context().find_root().info_name
+ assert prog_name
+ if complete_var is None:
+ complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
+ test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION")
+ if shell is None and not test_disable_detection:
+ shell = _get_shell_name()
+ if shell == "bash":
+ installed_path = install_bash(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ return shell, installed_path
+ elif shell == "zsh":
+ installed_path = install_zsh(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ return shell, installed_path
+ elif shell == "fish":
+ installed_path = install_fish(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ return shell, installed_path
+ elif shell in {"powershell", "pwsh"}:
+ installed_path = install_powershell(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ return shell, installed_path
+ else:
+ click.echo(f"Shell {shell} is not supported.")
+ raise click.exceptions.Exit(1)
+
+
+def _get_shell_name() -> Union[str, None]:
+ """Get the current shell name, if available.
+
+ The name will always be lowercase. If the shell cannot be detected, None is
+ returned.
+ """
+ name: Union[str, None] # N.B. shellingham is untyped
+ if HAS_SHELLINGHAM:
+ try:
+ # N.B. detect_shell returns a tuple of (shell name, shell command).
+ # We only need the name.
+ name, _cmd = shellingham.detect_shell() # noqa: TID251
+ except shellingham.ShellDetectionFailure: # pragma: no cover
+ name = None
+ else:
+ name = None # pragma: no cover
+
+ return name
diff --git a/contrib/python/typer-slim/typer/_types.py b/contrib/python/typer-slim/typer/_types.py
new file mode 100644
index 00000000000..045e36b8156
--- /dev/null
+++ b/contrib/python/typer-slim/typer/_types.py
@@ -0,0 +1,27 @@
+from enum import Enum
+from typing import Generic, TypeVar, Union
+
+import click
+
+ParamTypeValue = TypeVar("ParamTypeValue")
+
+
+class TyperChoice(click.Choice, Generic[ParamTypeValue]): # type: ignore[type-arg]
+ def normalize_choice(
+ self, choice: ParamTypeValue, ctx: Union[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:
+ normed_value = ctx.token_normalize_func(normed_value)
+
+ if not self.case_sensitive:
+ normed_value = normed_value.casefold()
+
+ return normed_value
diff --git a/contrib/python/typer-slim/typer/_typing.py b/contrib/python/typer-slim/typer/_typing.py
new file mode 100644
index 00000000000..15d0e91b656
--- /dev/null
+++ b/contrib/python/typer-slim/typer/_typing.py
@@ -0,0 +1,83 @@
+# Copied from pydantic 1.9.2 (the latest version to support python 3.6.)
+# https://github.com/pydantic/pydantic/blob/v1.9.2/pydantic/typing.py
+# Reduced drastically to only include Typer-specific 3.9+ functionality
+# mypy: ignore-errors
+
+import sys
+from typing import (
+ Annotated,
+ Any,
+ Callable,
+ Literal,
+ Optional,
+ Union,
+ get_args,
+ get_origin,
+ get_type_hints,
+)
+
+if sys.version_info < (3, 10):
+
+ def is_union(tp: Optional[type[Any]]) -> bool:
+ return tp is Union
+
+else:
+ import types
+
+ def is_union(tp: Optional[type[Any]]) -> bool:
+ return tp is Union or tp is types.UnionType # noqa: E721
+
+
+__all__ = (
+ "NoneType",
+ "is_none_type",
+ "is_callable_type",
+ "is_literal_type",
+ "all_literal_values",
+ "is_union",
+ "Annotated",
+ "Literal",
+ "get_args",
+ "get_origin",
+ "get_type_hints",
+)
+
+
+NoneType = None.__class__
+
+
+NONE_TYPES: tuple[Any, Any, Any] = (None, NoneType, Literal[None])
+
+
+def is_none_type(type_: Any) -> bool:
+ for none_type in NONE_TYPES:
+ if type_ is none_type:
+ return True
+ return False
+
+
+def is_callable_type(type_: type[Any]) -> bool:
+ return type_ is Callable or get_origin(type_) is Callable
+
+
+def is_literal_type(type_: type[Any]) -> bool:
+ import typing_extensions
+
+ return get_origin(type_) in (Literal, typing_extensions.Literal)
+
+
+def literal_values(type_: type[Any]) -> tuple[Any, ...]:
+ return get_args(type_)
+
+
+def all_literal_values(type_: type[Any]) -> tuple[Any, ...]:
+ """
+ This method is used to retrieve all Literal values as
+ Literal can be used recursively (see https://www.python.org/dev/peps/pep-0586)
+ e.g. `Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]`
+ """
+ if not is_literal_type(type_):
+ return (type_,)
+
+ values = literal_values(type_)
+ return tuple(x for value in values for x in all_literal_values(value))
diff --git a/contrib/python/typer-slim/typer/cli.py b/contrib/python/typer-slim/typer/cli.py
new file mode 100644
index 00000000000..ef83bf87a02
--- /dev/null
+++ b/contrib/python/typer-slim/typer/cli.py
@@ -0,0 +1,317 @@
+import importlib.util
+import re
+import sys
+from pathlib import Path
+from typing import Any, Optional
+
+import click
+import typer
+import typer.core
+from click import Command, Group, Option
+
+from . import __version__
+from .core import HAS_RICH, MARKUP_MODE_KEY
+
+default_app_names = ("app", "cli", "main")
+default_func_names = ("main", "cli", "app")
+
+app = typer.Typer()
+utils_app = typer.Typer(help="Extra utility commands for Typer apps.")
+app.add_typer(utils_app, name="utils")
+
+
+class State:
+ def __init__(self) -> None:
+ self.app: Optional[str] = None
+ self.func: Optional[str] = None
+ self.file: Optional[Path] = None
+ self.module: Optional[str] = None
+
+
+state = State()
+
+
+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)
+ if file_path.exists() and file_path.is_file():
+ state.file = file_path
+ else:
+ if not re.fullmatch(r"[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*", path_or_module):
+ typer.echo(
+ f"Not a valid file or Python module: {path_or_module}", err=True
+ )
+ sys.exit(1)
+ state.module = path_or_module
+ app_name = ctx.params.get("app")
+ if app_name:
+ state.app = app_name
+ func_name = ctx.params.get("func")
+ if func_name:
+ state.func = func_name
+
+
+class TyperCLIGroup(typer.core.TyperGroup):
+ 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) -> Optional[Command]:
+ self.maybe_add_run(ctx)
+ return super().get_command(ctx, name)
+
+ 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:
+ maybe_update_state(ctx)
+ maybe_add_run_to_cli(self)
+
+
+def get_typer_from_module(module: Any) -> Optional[typer.Typer]:
+ # Try to get defined app
+ if state.app:
+ obj = getattr(module, state.app, None)
+ if not isinstance(obj, typer.Typer):
+ typer.echo(f"Not a Typer object: --app {state.app}", err=True)
+ sys.exit(1)
+ return obj
+ # Try to get defined function
+ if state.func:
+ func_obj = getattr(module, state.func, None)
+ if not callable(func_obj):
+ typer.echo(f"Not a function: --func {state.func}", err=True)
+ sys.exit(1)
+ sub_app = typer.Typer()
+ sub_app.command()(func_obj)
+ return sub_app
+ # Iterate and get a default object to use as CLI
+ local_names = dir(module)
+ local_names_set = set(local_names)
+ # Try to get a default Typer app
+ for name in default_app_names:
+ if name in local_names_set:
+ obj = getattr(module, name, None)
+ if isinstance(obj, typer.Typer):
+ return obj
+ # Try to get any Typer app
+ for name in local_names_set - set(default_app_names):
+ obj = getattr(module, name)
+ if isinstance(obj, typer.Typer):
+ return obj
+ # Try to get a default function
+ for func_name in default_func_names:
+ func_obj = getattr(module, func_name, None)
+ if callable(func_obj):
+ sub_app = typer.Typer()
+ sub_app.command()(func_obj)
+ return sub_app
+ # Try to get any func app
+ for func_name in local_names_set - set(default_func_names):
+ func_obj = getattr(module, func_name)
+ if callable(func_obj):
+ sub_app = typer.Typer()
+ sub_app.command()(func_obj)
+ return sub_app
+ return None
+
+
+def get_typer_from_state() -> Optional[typer.Typer]:
+ spec = None
+ if state.file:
+ module_name = state.file.name
+ spec = importlib.util.spec_from_file_location(module_name, str(state.file))
+ elif state.module:
+ spec = importlib.util.find_spec(state.module)
+ if spec is None:
+ if state.file:
+ typer.echo(f"Could not import as Python file: {state.file}", err=True)
+ else:
+ typer.echo(f"Could not import as Python module: {state.module}", err=True)
+ sys.exit(1)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module) # type: ignore
+ obj = get_typer_from_module(module)
+ return obj
+
+
+def maybe_add_run_to_cli(cli: click.Group) -> None:
+ if "run" not in cli.commands:
+ if state.file or state.module:
+ obj = get_typer_from_state()
+ if obj:
+ obj._add_completion = False
+ click_obj = typer.main.get_command(obj)
+ click_obj.name = "run"
+ if not click_obj.help:
+ click_obj.help = "Run the provided Typer app."
+ cli.add_command(click_obj)
+
+
+def print_version(ctx: click.Context, param: Option, value: bool) -> None:
+ if not value or ctx.resilient_parsing:
+ return
+ typer.echo(f"Typer version: {__version__}")
+ raise typer.Exit()
+
+
[email protected](cls=TyperCLIGroup, no_args_is_help=True)
+def callback(
+ ctx: typer.Context,
+ *,
+ path_or_module: str = typer.Argument(None),
+ app: str = typer.Option(None, help="The typer app object/variable to use."),
+ func: str = typer.Option(None, help="The function to convert to Typer."),
+ version: bool = typer.Option(
+ False,
+ "--version",
+ help="Print version and exit.",
+ callback=print_version,
+ ),
+) -> None:
+ """
+ Run Typer scripts with completion, without having to create a package.
+
+ You probably want to install completion for the typer command:
+
+ $ typer --install-completion
+
+ https://typer.tiangolo.com/
+ """
+ maybe_update_state(ctx)
+
+
+def get_docs_for_click(
+ *,
+ obj: Command,
+ ctx: typer.Context,
+ indent: int = 0,
+ name: str = "",
+ call_prefix: str = "",
+ title: Optional[str] = None,
+) -> str:
+ docs = "#" * (1 + indent)
+ command_name = name or obj.name
+ if call_prefix:
+ command_name = f"{call_prefix} {command_name}"
+ if not title:
+ title = f"`{command_name}`" if command_name else "CLI"
+ docs += f" {title}\n\n"
+ rich_markup_mode = None
+ if hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
+ rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None)
+ to_parse: bool = bool(HAS_RICH and (rich_markup_mode == "rich"))
+ if obj.help:
+ docs += f"{_parse_html(to_parse, obj.help)}\n\n"
+ usage_pieces = obj.collect_usage_pieces(ctx)
+ if usage_pieces:
+ docs += "**Usage**:\n\n"
+ docs += "```console\n"
+ docs += "$ "
+ if command_name:
+ docs += f"{command_name} "
+ docs += f"{' '.join(usage_pieces)}\n"
+ docs += "```\n\n"
+ args = []
+ opts = []
+ for param in obj.get_params(ctx):
+ rv = param.get_help_record(ctx)
+ if rv is not None:
+ if param.param_type_name == "argument":
+ args.append(rv)
+ elif param.param_type_name == "option":
+ opts.append(rv)
+ if args:
+ docs += "**Arguments**:\n\n"
+ for arg_name, arg_help in args:
+ docs += f"* `{arg_name}`"
+ if arg_help:
+ docs += f": {_parse_html(to_parse, arg_help)}"
+ docs += "\n"
+ docs += "\n"
+ if opts:
+ docs += "**Options**:\n\n"
+ for opt_name, opt_help in opts:
+ docs += f"* `{opt_name}`"
+ if opt_help:
+ docs += f": {_parse_html(to_parse, opt_help)}"
+ docs += "\n"
+ docs += "\n"
+ if obj.epilog:
+ docs += f"{obj.epilog}\n\n"
+ if isinstance(obj, Group):
+ group = obj
+ commands = group.list_commands(ctx)
+ if commands:
+ docs += "**Commands**:\n\n"
+ for command in commands:
+ command_obj = group.get_command(ctx, command)
+ assert command_obj
+ docs += f"* `{command_obj.name}`"
+ command_help = command_obj.get_short_help_str()
+ if command_help:
+ docs += f": {_parse_html(to_parse, command_help)}"
+ docs += "\n"
+ docs += "\n"
+ for command in commands:
+ command_obj = group.get_command(ctx, command)
+ assert command_obj
+ use_prefix = ""
+ if command_name:
+ use_prefix += f"{command_name}"
+ docs += get_docs_for_click(
+ obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix
+ )
+ return docs
+
+
+def _parse_html(to_parse: bool, input_text: str) -> str:
+ if not to_parse:
+ return input_text
+ from . import rich_utils
+
+ return rich_utils.rich_to_html(input_text)
+
+
+@utils_app.command()
+def docs(
+ ctx: typer.Context,
+ name: str = typer.Option("", help="The name of the CLI program to use in docs."),
+ output: Optional[Path] = typer.Option(
+ None,
+ help="An output file to write docs to, like README.md.",
+ file_okay=True,
+ dir_okay=False,
+ ),
+ title: Optional[str] = typer.Option(
+ None,
+ help="The title for the documentation page. If not provided, the name of "
+ "the program is used.",
+ ),
+) -> None:
+ """
+ Generate Markdown docs for a Typer app.
+ """
+ typer_obj = get_typer_from_state()
+ if not typer_obj:
+ typer.echo("No Typer app found", err=True)
+ raise typer.Abort()
+ if hasattr(typer_obj, "rich_markup_mode"):
+ if not hasattr(ctx, "obj") or ctx.obj is None:
+ ctx.ensure_object(dict)
+ if isinstance(ctx.obj, dict):
+ ctx.obj[MARKUP_MODE_KEY] = typer_obj.rich_markup_mode
+ click_obj = typer.main.get_command(typer_obj)
+ docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title)
+ clean_docs = f"{docs.strip()}\n"
+ if output:
+ output.write_text(clean_docs)
+ typer.echo(f"Docs saved to: {output}")
+ else:
+ typer.echo(clean_docs)
+
+
+def main() -> Any:
+ return app()
diff --git a/contrib/python/typer-slim/typer/colors.py b/contrib/python/typer-slim/typer/colors.py
new file mode 100644
index 00000000000..54e7b166cb1
--- /dev/null
+++ b/contrib/python/typer-slim/typer/colors.py
@@ -0,0 +1,20 @@
+# Variable names to colors, just for completion
+BLACK = "black"
+RED = "red"
+GREEN = "green"
+YELLOW = "yellow"
+BLUE = "blue"
+MAGENTA = "magenta"
+CYAN = "cyan"
+WHITE = "white"
+
+RESET = "reset"
+
+BRIGHT_BLACK = "bright_black"
+BRIGHT_RED = "bright_red"
+BRIGHT_GREEN = "bright_green"
+BRIGHT_YELLOW = "bright_yellow"
+BRIGHT_BLUE = "bright_blue"
+BRIGHT_MAGENTA = "bright_magenta"
+BRIGHT_CYAN = "bright_cyan"
+BRIGHT_WHITE = "bright_white"
diff --git a/contrib/python/typer-slim/typer/completion.py b/contrib/python/typer-slim/typer/completion.py
new file mode 100644
index 00000000000..db87f83e3f3
--- /dev/null
+++ b/contrib/python/typer-slim/typer/completion.py
@@ -0,0 +1,147 @@
+import os
+import sys
+from collections.abc import MutableMapping
+from typing import Any
+
+import click
+
+from ._completion_classes import completion_init
+from ._completion_shared import Shells, _get_shell_name, get_completion_script, install
+from .core import HAS_SHELLINGHAM
+from .models import ParamMeta
+from .params import Option
+from .utils import get_params_from_function
+
+_click_patched = False
+
+
+def get_completion_inspect_parameters() -> tuple[ParamMeta, ParamMeta]:
+ completion_init()
+ test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION")
+ if HAS_SHELLINGHAM and not test_disable_detection:
+ parameters = get_params_from_function(_install_completion_placeholder_function)
+ else:
+ parameters = get_params_from_function(
+ _install_completion_no_auto_placeholder_function
+ )
+ install_param, show_param = parameters.values()
+ return install_param, show_param
+
+
+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")
+ sys.exit(0)
+
+
+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
+ assert prog_name
+ complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
+ shell = ""
+ test_disable_detection = os.getenv("_TYPER_COMPLETE_TEST_DISABLE_SHELL_DETECTION")
+ if isinstance(value, str):
+ shell = value
+ elif not test_disable_detection:
+ detected_shell = _get_shell_name()
+ if detected_shell is not None:
+ shell = detected_shell
+ script_content = get_completion_script(
+ prog_name=prog_name, complete_var=complete_var, shell=shell
+ )
+ click.echo(script_content)
+ sys.exit(0)
+
+
+# Create a fake command function to extract the completion parameters
+def _install_completion_placeholder_function(
+ install_completion: bool = Option(
+ None,
+ "--install-completion",
+ callback=install_callback,
+ expose_value=False,
+ help="Install completion for the current shell.",
+ ),
+ show_completion: bool = Option(
+ None,
+ "--show-completion",
+ callback=show_callback,
+ expose_value=False,
+ help="Show completion for the current shell, to copy it or customize the installation.",
+ ),
+) -> Any:
+ pass # pragma: no cover
+
+
+def _install_completion_no_auto_placeholder_function(
+ install_completion: Shells = Option(
+ None,
+ callback=install_callback,
+ expose_value=False,
+ help="Install completion for the specified shell.",
+ ),
+ show_completion: Shells = Option(
+ None,
+ callback=show_callback,
+ expose_value=False,
+ help="Show completion for the specified shell, to copy it or customize the installation.",
+ ),
+) -> Any:
+ pass # pragma: no cover
+
+
+# Re-implement Click's shell_complete to add error message with:
+# Invalid completion instruction
+# To use 7.x instruction style for compatibility
+# 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,
+ ctx_args: MutableMapping[str, Any],
+ prog_name: str,
+ complete_var: str,
+ instruction: str,
+) -> int:
+ import click
+ import click.shell_completion
+
+ if "_" not in instruction:
+ click.echo("Invalid completion instruction.", err=True)
+ return 1
+
+ # Click 8 changed the order/style of shell instructions from e.g.
+ # source_bash to bash_source
+ # Typer override to preserve the old style for compatibility
+ # Original in Click 8.x commented:
+ # shell, _, instruction = instruction.partition("_")
+ instruction, _, shell = instruction.partition("_")
+ # Typer override end
+
+ comp_cls = click.shell_completion.get_completion_class(shell)
+
+ if comp_cls is None:
+ 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())
+ return 0
+
+ # Typer override to print the completion help msg with Rich
+ if instruction == "complete":
+ click.echo(comp.complete())
+ return 0
+ # Typer override end
+
+ click.echo(f'Completion instruction "{instruction}" not supported.', err=True)
+ return 1
diff --git a/contrib/python/typer-slim/typer/core.py b/contrib/python/typer-slim/typer/core.py
new file mode 100644
index 00000000000..d0d888ccf0d
--- /dev/null
+++ b/contrib/python/typer-slim/typer/core.py
@@ -0,0 +1,840 @@
+import errno
+import importlib.util
+import inspect
+import os
+import sys
+from collections.abc import MutableMapping, Sequence
+from difflib import get_close_matches
+from enum import Enum
+from gettext import gettext as _
+from typing import (
+ Any,
+ Callable,
+ Optional,
+ TextIO,
+ Union,
+ cast,
+)
+
+import click
+import click.core
+import click.formatting
+import click.shell_completion
+import click.types
+import click.utils
+
+from ._typing import Literal
+
+MarkupMode = Literal["markdown", "rich", None]
+MARKUP_MODE_KEY = "TYPER_RICH_MARKUP_MODE"
+
+HAS_RICH = importlib.util.find_spec("rich") is not None
+HAS_SHELLINGHAM = importlib.util.find_spec("shellingham") is not None
+
+if HAS_RICH:
+ DEFAULT_MARKUP_MODE: MarkupMode = "rich"
+else: # pragma: no cover
+ DEFAULT_MARKUP_MODE = None
+
+
+# Copy from click.parser._split_opt
+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 _typer_param_setup_autocompletion_compat(
+ self: click.Parameter,
+ *,
+ autocompletion: Optional[
+ Callable[[click.Context, list[str], str], list[Union[tuple[str, str], str]]]
+ ] = None,
+) -> None:
+ if self._custom_shell_complete is not None:
+ import warnings
+
+ warnings.warn(
+ "In Typer, only the parameter 'autocompletion' is supported. "
+ "The support for 'shell_complete' is deprecated and will be removed in upcoming versions. ",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ 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
+
+ out = []
+
+ for c in autocompletion(ctx, [], incomplete):
+ if isinstance(c, tuple):
+ use_completion = CompletionItem(c[0], help=c[1])
+ else:
+ assert isinstance(c, str)
+ use_completion = CompletionItem(c)
+
+ if use_completion.value.startswith(incomplete):
+ out.append(use_completion)
+
+ return out
+
+ self._custom_shell_complete = compat_autocompletion
+
+
+def _get_default_string(
+ obj: Union["TyperArgument", "TyperOption"],
+ *,
+ ctx: click.Context,
+ show_default_is_str: bool,
+ default_value: Union[list[Any], tuple[Any, ...], str, Callable[..., Any], Any],
+) -> str:
+ # 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})"
+ elif isinstance(default_value, (list, tuple)):
+ default_string = ", ".join(
+ _get_default_string(
+ obj, ctx=ctx, show_default_is_str=show_default_is_str, default_value=d
+ )
+ for d in default_value
+ )
+ elif isinstance(default_value, Enum):
+ default_string = str(default_value.value)
+ elif inspect.isfunction(default_value):
+ default_string = _("(dynamic)")
+ elif isinstance(obj, TyperOption) and obj.is_bool_flag and obj.secondary_opts:
+ # 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(
+ # (self.opts if self.default else self.secondary_opts)[0]
+ # )[1]
+ if obj.default:
+ if obj.opts:
+ default_string = _split_opt(obj.opts[0])[1]
+ else:
+ default_string = str(default_value)
+ else:
+ default_string = _split_opt(obj.secondary_opts[0])[1]
+ # Typer override end
+ elif (
+ isinstance(obj, TyperOption)
+ and obj.is_bool_flag
+ and not obj.secondary_opts
+ and not default_value
+ ):
+ default_string = ""
+ else:
+ default_string = str(default_value)
+ return default_string
+
+
+def _extract_default_help_str(
+ obj: Union["TyperArgument", "TyperOption"], *, ctx: click.Context
+) -> Optional[Union[Any, Callable[[], Any]]]:
+ # 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
+ # help formatting in general.
+ resilient = ctx.resilient_parsing
+ ctx.resilient_parsing = True
+
+ try:
+ default_value = obj.get_default(ctx, call=False)
+ finally:
+ ctx.resilient_parsing = resilient
+ return default_value
+
+
+def _main(
+ self: click.Command,
+ *,
+ args: Optional[Sequence[str]] = None,
+ prog_name: Optional[str] = None,
+ complete_var: Optional[str] = None,
+ standalone_mode: bool = True,
+ windows_expand_args: bool = True,
+ rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
+ **extra: Any,
+) -> Any:
+ # 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:
+ args = sys.argv[1:]
+
+ # Covered in Click tests
+ if os.name == "nt" and windows_expand_args: # pragma: no cover
+ args = click.utils._expand_args(args)
+ else:
+ args = list(args)
+
+ if prog_name is None:
+ prog_name = click.utils._detect_program_name()
+
+ # Process shell completion requests and exit early.
+ self._main_shell_completion(extra, prog_name, complete_var)
+
+ try:
+ try:
+ with self.make_context(prog_name, args, **extra) as ctx:
+ rv = self.invoke(ctx)
+ if not standalone_mode:
+ return rv
+ # it's not safe to `ctx.exit(rv)` here!
+ # note that `rv` may actually contain data like "1" which
+ # has obvious effects
+ # more subtle case: `rv=[None, None]` can come out of
+ # chained commands which all returned `None` -- so it's not
+ # even always obvious that `rv` indicates success/failure
+ # by its truthiness/falsiness
+ ctx.exit()
+ except EOFError as e:
+ click.echo(file=sys.stderr)
+ raise click.Abort() from e
+ except KeyboardInterrupt as e:
+ raise click.exceptions.Exit(130) from e
+ except click.ClickException as e:
+ if not standalone_mode:
+ raise
+ # Typer override
+ if HAS_RICH and rich_markup_mode is not None:
+ from . import rich_utils
+
+ rich_utils.rich_format_error(e)
+ else:
+ e.show()
+ # Typer override end
+ 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.exit(1)
+ else:
+ raise
+ except click.exceptions.Exit as e:
+ if standalone_mode:
+ sys.exit(e.exit_code)
+ else:
+ # in non-standalone mode, return the exit code
+ # note that this is only reached if `self.invoke` above raises
+ # an Exit explicitly -- thus bypassing the check there which
+ # would return its result
+ # the results of non-standalone execution may therefore be
+ # somewhat ambiguous: if there are codepaths which lead to
+ # `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:
+ if not standalone_mode:
+ raise
+ # Typer override
+ if HAS_RICH and rich_markup_mode is not None:
+ from . import rich_utils
+
+ rich_utils.rich_abort_error()
+ else:
+ click.echo(_("Aborted!"), file=sys.stderr)
+ # Typer override end
+ sys.exit(1)
+
+
+class TyperArgument(click.core.Argument):
+ def __init__(
+ self,
+ *,
+ # Parameter
+ param_decls: list[str],
+ type: Optional[Any] = None,
+ required: Optional[bool] = None,
+ default: Optional[Any] = None,
+ callback: Optional[Callable[..., Any]] = None,
+ nargs: Optional[int] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ # TyperArgument
+ show_default: Union[bool, str] = True,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+ ):
+ self.help = help
+ self.show_default = show_default
+ self.show_choices = show_choices
+ self.show_envvar = show_envvar
+ self.hidden = hidden
+ self.rich_help_panel = rich_help_panel
+
+ super().__init__(
+ param_decls=param_decls,
+ type=type,
+ required=required,
+ default=default,
+ callback=callback,
+ nargs=nargs,
+ metavar=metavar,
+ expose_value=expose_value,
+ is_eager=is_eager,
+ envvar=envvar,
+ shell_complete=shell_complete,
+ )
+ _typer_param_setup_autocompletion_compat(self, autocompletion=autocompletion)
+
+ def _get_default_string(
+ self,
+ *,
+ ctx: click.Context,
+ show_default_is_str: bool,
+ default_value: Union[list[Any], tuple[Any, ...], str, Callable[..., Any], Any],
+ ) -> str:
+ return _get_default_string(
+ self,
+ ctx=ctx,
+ show_default_is_str=show_default_is_str,
+ default_value=default_value,
+ )
+
+ def _extract_default_help_str(
+ self, *, ctx: click.Context
+ ) -> Optional[Union[Any, Callable[[], Any]]]:
+ return _extract_default_help_str(self, ctx=ctx)
+
+ def get_help_record(self, ctx: click.Context) -> Optional[tuple[str, str]]:
+ # Modified version of click.core.Option.get_help_record()
+ # to support Arguments
+ if self.hidden:
+ return None
+ name = self.make_metavar(ctx=ctx)
+ help = self.help or ""
+ extra = []
+ if self.show_envvar:
+ envvar = self.envvar
+ # allow_from_autoenv is currently not supported in Typer for CLI Arguments
+ if envvar is not None:
+ var_str = (
+ ", ".join(str(d) for d in envvar)
+ if isinstance(envvar, (list, tuple))
+ else envvar
+ )
+ extra.append(f"env var: {var_str}")
+
+ # Typer override:
+ # Extracted to _extract_default_help_str() to allow re-using it in rich_utils
+ default_value = self._extract_default_help_str(ctx=ctx)
+ # Typer override end
+
+ show_default_is_str = isinstance(self.show_default, str)
+
+ if show_default_is_str or (
+ default_value is not None and (self.show_default or ctx.show_default)
+ ):
+ # Typer override:
+ # Extracted to _get_default_string() to allow re-using it in rich_utils
+ default_string = self._get_default_string(
+ ctx=ctx,
+ show_default_is_str=show_default_is_str,
+ default_value=default_value,
+ )
+ # Typer override end
+ if default_string:
+ extra.append(_("default: {default}").format(default=default_string))
+ if self.required:
+ extra.append(_("required"))
+ if extra:
+ extra_str = "; ".join(extra)
+ extra_str = f"[{extra_str}]"
+ rich_markup_mode = None
+ if hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
+ rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None)
+ if HAS_RICH and rich_markup_mode == "rich":
+ # This is needed for when we want to export to HTML
+ from . import rich_utils
+
+ extra_str = rich_utils.escape_before_html_export(extra_str)
+
+ help = f"{help} {extra_str}" if help else f"{extra_str}"
+ return name, help
+
+ def make_metavar(self, ctx: Union[click.Context, None] = None) -> str:
+ # Modified version of click.core.Argument.make_metavar()
+ # to include Argument name
+ if self.metavar is not None:
+ var = self.metavar
+ if not self.required and not var.startswith("["):
+ var = f"[{var}]"
+ return var
+ var = (self.name or "").upper()
+ if not self.required:
+ var = f"[{var}]"
+ # TODO: When deprecating Click < 8.2, remove this
+ signature = inspect.signature(self.type.get_metavar)
+ if "ctx" in signature.parameters:
+ # Click >= 8.2
+ type_var = self.type.get_metavar(self, ctx=ctx) # type: ignore[arg-type]
+ else:
+ # Click < 8.2
+ type_var = self.type.get_metavar(self) # type: ignore[call-arg]
+ # TODO: /When deprecating Click < 8.2, remove this, uncomment the line below
+ # type_var = self.type.get_metavar(self, ctx=ctx)
+ if type_var:
+ var += f":{type_var}"
+ if self.nargs != 1:
+ var += "..."
+ return var
+
+ def value_is_missing(self, value: Any) -> bool:
+ return _value_is_missing(self, value)
+
+
+class TyperOption(click.core.Option):
+ def __init__(
+ self,
+ *,
+ # Parameter
+ param_decls: list[str],
+ type: Optional[Union[click.types.ParamType, Any]] = None,
+ required: Optional[bool] = None,
+ default: Optional[Any] = None,
+ callback: Optional[Callable[..., Any]] = None,
+ nargs: Optional[int] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ # Option
+ show_default: Union[bool, str] = False,
+ prompt: Union[bool, str] = False,
+ confirmation_prompt: Union[bool, str] = False,
+ prompt_required: bool = True,
+ hide_input: bool = False,
+ is_flag: Optional[bool] = None,
+ multiple: bool = False,
+ count: bool = False,
+ allow_from_autoenv: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ show_choices: bool = True,
+ show_envvar: bool = False,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+ ):
+ super().__init__(
+ param_decls=param_decls,
+ type=type,
+ required=required,
+ default=default,
+ callback=callback,
+ nargs=nargs,
+ metavar=metavar,
+ 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,
+ )
+ _typer_param_setup_autocompletion_compat(self, autocompletion=autocompletion)
+ self.rich_help_panel = rich_help_panel
+
+ def _get_default_string(
+ self,
+ *,
+ ctx: click.Context,
+ show_default_is_str: bool,
+ default_value: Union[list[Any], tuple[Any, ...], str, Callable[..., Any], Any],
+ ) -> str:
+ return _get_default_string(
+ self,
+ ctx=ctx,
+ show_default_is_str=show_default_is_str,
+ default_value=default_value,
+ )
+
+ def _extract_default_help_str(
+ self, *, ctx: click.Context
+ ) -> Optional[Union[Any, Callable[[], Any]]]:
+ return _extract_default_help_str(self, ctx=ctx)
+
+ def make_metavar(self, ctx: Union[click.Context, None] = None) -> str:
+ signature = inspect.signature(super().make_metavar)
+ if "ctx" in signature.parameters:
+ # Click >= 8.2
+ return super().make_metavar(ctx=ctx) # type: ignore[arg-type]
+ # Click < 8.2
+ return super().make_metavar() # type: ignore[call-arg]
+
+ def get_help_record(self, ctx: click.Context) -> Optional[tuple[str, str]]:
+ # 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
+ if self.hidden:
+ return None
+
+ any_prefix_is_slash = False
+
+ def _write_opts(opts: Sequence[str]) -> str:
+ nonlocal any_prefix_is_slash
+
+ rv, any_slashes = click.formatting.join_options(opts)
+
+ if any_slashes:
+ any_prefix_is_slash = True
+
+ if not self.is_flag and not self.count:
+ rv += f" {self.make_metavar(ctx=ctx)}"
+
+ return rv
+
+ rv = [_write_opts(self.opts)]
+
+ if self.secondary_opts:
+ rv.append(_write_opts(self.secondary_opts))
+
+ help = self.help or ""
+ extra = []
+
+ if self.show_envvar:
+ envvar = self.envvar
+
+ if envvar is None:
+ 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()}"
+
+ if envvar is not None:
+ var_str = (
+ envvar
+ if isinstance(envvar, str)
+ else ", ".join(str(d) for d in envvar)
+ )
+ extra.append(_("env var: {var}").format(var=var_str))
+
+ # Typer override:
+ # Extracted to _extract_default() to allow re-using it in rich_utils
+ default_value = self._extract_default_help_str(ctx=ctx)
+ # Typer override end
+
+ show_default_is_str = isinstance(self.show_default, str)
+
+ if show_default_is_str or (
+ default_value is not None and (self.show_default or ctx.show_default)
+ ):
+ # Typer override:
+ # Extracted to _get_default_string() to allow re-using it in rich_utils
+ default_string = self._get_default_string(
+ ctx=ctx,
+ show_default_is_str=show_default_is_str,
+ default_value=default_value,
+ )
+ # Typer override end
+ if default_string:
+ extra.append(_("default: {default}").format(default=default_string))
+
+ if isinstance(self.type, click.types._NumberRangeBase):
+ range_str = self.type._describe_range()
+
+ if range_str:
+ extra.append(range_str)
+
+ if self.required:
+ extra.append(_("required"))
+
+ if extra:
+ extra_str = "; ".join(extra)
+ extra_str = f"[{extra_str}]"
+ rich_markup_mode = None
+ if hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
+ rich_markup_mode = ctx.obj.get(MARKUP_MODE_KEY, None)
+ if HAS_RICH and rich_markup_mode == "rich":
+ # This is needed for when we want to export to HTML
+ from . import rich_utils
+
+ extra_str = rich_utils.escape_before_html_export(extra_str)
+
+ help = f"{help} {extra_str}" if help else f"{extra_str}"
+
+ return ("; " if any_prefix_is_slash else " / ").join(rv), help
+
+ def value_is_missing(self, value: Any) -> bool:
+ return _value_is_missing(self, value)
+
+
+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
+
+ return False
+
+
+def _typer_format_options(
+ self: click.core.Command, *, ctx: click.Context, formatter: click.HelpFormatter
+) -> None:
+ args = []
+ opts = []
+ for param in self.get_params(ctx):
+ rv = param.get_help_record(ctx)
+ if rv is not None:
+ if param.param_type_name == "argument":
+ args.append(rv)
+ elif param.param_type_name == "option":
+ opts.append(rv)
+
+ if args:
+ with formatter.section(_("Arguments")):
+ formatter.write_dl(args)
+ if opts:
+ with formatter.section(_("Options")):
+ formatter.write_dl(opts)
+
+
+def _typer_main_shell_completion(
+ self: click.core.Command,
+ *,
+ ctx_args: MutableMapping[str, Any],
+ prog_name: str,
+ complete_var: Optional[str] = None,
+) -> None:
+ if complete_var is None:
+ complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
+
+ instruction = os.environ.get(complete_var)
+
+ if not instruction:
+ return
+
+ from .completion import shell_complete
+
+ rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction)
+ sys.exit(rv)
+
+
+class TyperCommand(click.core.Command):
+ def __init__(
+ self,
+ name: Optional[str],
+ *,
+ context_settings: Optional[dict[str, Any]] = None,
+ callback: Optional[Callable[..., Any]] = None,
+ params: Optional[list[click.Parameter]] = None,
+ help: Optional[str] = None,
+ epilog: Optional[str] = None,
+ short_help: Optional[str] = None,
+ options_metavar: Optional[str] = "[OPTIONS]",
+ add_help_option: bool = True,
+ no_args_is_help: bool = False,
+ hidden: bool = False,
+ deprecated: bool = False,
+ # Rich settings
+ rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
+ rich_help_panel: Union[str, None] = None,
+ ) -> None:
+ super().__init__(
+ name=name,
+ context_settings=context_settings,
+ callback=callback,
+ params=params,
+ help=help,
+ epilog=epilog,
+ short_help=short_help,
+ options_metavar=options_metavar,
+ add_help_option=add_help_option,
+ no_args_is_help=no_args_is_help,
+ hidden=hidden,
+ deprecated=deprecated,
+ )
+ self.rich_markup_mode: MarkupMode = rich_markup_mode
+ self.rich_help_panel = rich_help_panel
+
+ def format_options(
+ self, ctx: click.Context, formatter: click.HelpFormatter
+ ) -> None:
+ _typer_format_options(self, ctx=ctx, formatter=formatter)
+
+ def _main_shell_completion(
+ self,
+ ctx_args: MutableMapping[str, Any],
+ prog_name: str,
+ complete_var: Optional[str] = None,
+ ) -> None:
+ _typer_main_shell_completion(
+ self, ctx_args=ctx_args, prog_name=prog_name, complete_var=complete_var
+ )
+
+ def main(
+ self,
+ args: Optional[Sequence[str]] = None,
+ prog_name: Optional[str] = None,
+ complete_var: Optional[str] = None,
+ standalone_mode: bool = True,
+ windows_expand_args: bool = True,
+ **extra: Any,
+ ) -> Any:
+ return _main(
+ self,
+ args=args,
+ prog_name=prog_name,
+ complete_var=complete_var,
+ standalone_mode=standalone_mode,
+ windows_expand_args=windows_expand_args,
+ rich_markup_mode=self.rich_markup_mode,
+ **extra,
+ )
+
+ 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)
+ if isinstance(ctx.obj, dict):
+ ctx.obj[MARKUP_MODE_KEY] = self.rich_markup_mode
+ return super().format_help(ctx, formatter)
+ from . import rich_utils
+
+ return rich_utils.rich_format_help(
+ obj=self,
+ ctx=ctx,
+ markup_mode=self.rich_markup_mode,
+ )
+
+
+class TyperGroup(click.core.Group):
+ def __init__(
+ self,
+ *,
+ name: Optional[str] = None,
+ commands: Optional[
+ Union[dict[str, click.Command], Sequence[click.Command]]
+ ] = None,
+ # Rich settings
+ rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
+ rich_help_panel: Union[str, None] = None,
+ suggest_commands: bool = True,
+ **attrs: Any,
+ ) -> None:
+ super().__init__(name=name, commands=commands, **attrs)
+ self.rich_markup_mode: MarkupMode = rich_markup_mode
+ self.rich_help_panel = rich_help_panel
+ self.suggest_commands = suggest_commands
+
+ def format_options(
+ self, ctx: click.Context, formatter: click.HelpFormatter
+ ) -> None:
+ _typer_format_options(self, ctx=ctx, formatter=formatter)
+ self.format_commands(ctx, formatter)
+
+ def _main_shell_completion(
+ self,
+ ctx_args: MutableMapping[str, Any],
+ prog_name: str,
+ complete_var: Optional[str] = None,
+ ) -> None:
+ _typer_main_shell_completion(
+ self, ctx_args=ctx_args, prog_name=prog_name, complete_var=complete_var
+ )
+
+ def resolve_command(
+ self, ctx: click.Context, args: list[str]
+ ) -> tuple[Optional[str], Optional[click.Command], list[str]]:
+ try:
+ return super().resolve_command(ctx, args)
+ except click.UsageError as e:
+ if self.suggest_commands:
+ available_commands = list(self.commands.keys())
+ if available_commands and args:
+ typo = args[0]
+ matches = get_close_matches(typo, available_commands)
+ if matches:
+ suggestions = ", ".join(f"{m!r}" for m in matches)
+ message = e.message.rstrip(".")
+ e.message = f"{message}. Did you mean {suggestions}?"
+ raise
+
+ def main(
+ self,
+ args: Optional[Sequence[str]] = None,
+ prog_name: Optional[str] = None,
+ complete_var: Optional[str] = None,
+ standalone_mode: bool = True,
+ windows_expand_args: bool = True,
+ **extra: Any,
+ ) -> Any:
+ return _main(
+ self,
+ args=args,
+ prog_name=prog_name,
+ complete_var=complete_var,
+ standalone_mode=standalone_mode,
+ windows_expand_args=windows_expand_args,
+ rich_markup_mode=self.rich_markup_mode,
+ **extra,
+ )
+
+ 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
+
+ return rich_utils.rich_format_help(
+ obj=self,
+ ctx=ctx,
+ 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)"""
+ return [n for n, c in self.commands.items()]
diff --git a/contrib/python/typer-slim/typer/main.py b/contrib/python/typer-slim/typer/main.py
new file mode 100644
index 00000000000..e8c6b9e4297
--- /dev/null
+++ b/contrib/python/typer-slim/typer/main.py
@@ -0,0 +1,1157 @@
+import inspect
+import os
+import platform
+import shutil
+import subprocess
+import sys
+import traceback
+from collections.abc import Sequence
+from datetime import datetime
+from enum import Enum
+from functools import update_wrapper
+from pathlib import Path
+from traceback import FrameSummary, StackSummary
+from types import TracebackType
+from typing import Any, Callable, Optional, Union
+from uuid import UUID
+
+import click
+from typer._types import TyperChoice
+
+from ._typing import get_args, get_origin, is_literal_type, is_union, literal_values
+from .completion import get_completion_inspect_parameters
+from .core import (
+ DEFAULT_MARKUP_MODE,
+ HAS_RICH,
+ MarkupMode,
+ TyperArgument,
+ TyperCommand,
+ TyperGroup,
+ TyperOption,
+)
+from .models import (
+ AnyType,
+ ArgumentInfo,
+ CommandFunctionType,
+ CommandInfo,
+ Default,
+ DefaultPlaceholder,
+ DeveloperExceptionConfig,
+ FileBinaryRead,
+ FileBinaryWrite,
+ FileText,
+ FileTextWrite,
+ NoneType,
+ OptionInfo,
+ ParameterInfo,
+ ParamMeta,
+ Required,
+ TyperInfo,
+ TyperPath,
+)
+from .utils import get_params_from_function
+
+_original_except_hook = sys.excepthook
+_typer_developer_exception_attr_name = "__typer_developer_exception__"
+
+
+def except_hook(
+ exc_type: type[BaseException], exc_value: BaseException, tb: Optional[TracebackType]
+) -> None:
+ exception_config: Union[DeveloperExceptionConfig, None] = getattr(
+ exc_value, _typer_developer_exception_attr_name, None
+ )
+ standard_traceback = os.getenv(
+ "TYPER_STANDARD_TRACEBACK", os.getenv("_TYPER_STANDARD_TRACEBACK")
+ )
+ if (
+ standard_traceback
+ or not exception_config
+ or not exception_config.pretty_exceptions_enable
+ ):
+ _original_except_hook(exc_type, exc_value, tb)
+ return
+ typer_path = os.path.dirname(__file__)
+ click_path = os.path.dirname(click.__file__)
+ internal_dir_names = [typer_path, click_path]
+ exc = exc_value
+ if HAS_RICH:
+ from . import rich_utils
+
+ rich_tb = rich_utils.get_traceback(exc, exception_config, internal_dir_names)
+ console_stderr = rich_utils._get_rich_console(stderr=True)
+ console_stderr.print(rich_tb)
+ return
+ tb_exc = traceback.TracebackException.from_exception(exc)
+ stack: list[FrameSummary] = []
+ for frame in tb_exc.stack:
+ if any(frame.filename.startswith(path) for path in internal_dir_names):
+ if not exception_config.pretty_exceptions_short:
+ # Hide the line for internal libraries, Typer and Click
+ stack.append(
+ traceback.FrameSummary(
+ filename=frame.filename,
+ lineno=frame.lineno,
+ name=frame.name,
+ line="",
+ )
+ )
+ else:
+ stack.append(frame)
+ # Type ignore ref: https://github.com/python/typeshed/pull/8244
+ final_stack_summary = StackSummary.from_list(stack)
+ tb_exc.stack = final_stack_summary
+ for line in tb_exc.format():
+ print(line, file=sys.stderr)
+ return
+
+
+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)
+ return click_install_param, click_show_param
+
+
+class Typer:
+ def __init__(
+ self,
+ *,
+ name: Optional[str] = Default(None),
+ cls: Optional[type[TyperGroup]] = Default(None),
+ invoke_without_command: bool = Default(False),
+ no_args_is_help: bool = Default(False),
+ subcommand_metavar: Optional[str] = Default(None),
+ chain: bool = Default(False),
+ result_callback: Optional[Callable[..., Any]] = Default(None),
+ # Command
+ context_settings: Optional[dict[Any, Any]] = Default(None),
+ callback: Optional[Callable[..., Any]] = Default(None),
+ help: Optional[str] = Default(None),
+ epilog: Optional[str] = Default(None),
+ short_help: Optional[str] = Default(None),
+ options_metavar: str = Default("[OPTIONS]"),
+ add_help_option: bool = Default(True),
+ hidden: bool = Default(False),
+ deprecated: bool = Default(False),
+ add_completion: bool = True,
+ # Rich settings
+ rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
+ rich_help_panel: Union[str, None] = Default(None),
+ suggest_commands: bool = True,
+ pretty_exceptions_enable: bool = True,
+ pretty_exceptions_show_locals: bool = True,
+ pretty_exceptions_short: bool = True,
+ ):
+ self._add_completion = add_completion
+ self.rich_markup_mode: MarkupMode = rich_markup_mode
+ self.rich_help_panel = rich_help_panel
+ self.suggest_commands = suggest_commands
+ self.pretty_exceptions_enable = pretty_exceptions_enable
+ self.pretty_exceptions_show_locals = pretty_exceptions_show_locals
+ self.pretty_exceptions_short = pretty_exceptions_short
+ self.info = TyperInfo(
+ name=name,
+ cls=cls,
+ invoke_without_command=invoke_without_command,
+ no_args_is_help=no_args_is_help,
+ subcommand_metavar=subcommand_metavar,
+ chain=chain,
+ result_callback=result_callback,
+ context_settings=context_settings,
+ callback=callback,
+ help=help,
+ epilog=epilog,
+ short_help=short_help,
+ options_metavar=options_metavar,
+ add_help_option=add_help_option,
+ hidden=hidden,
+ deprecated=deprecated,
+ )
+ self.registered_groups: list[TyperInfo] = []
+ self.registered_commands: list[CommandInfo] = []
+ self.registered_callback: Optional[TyperInfo] = None
+
+ def callback(
+ self,
+ *,
+ cls: Optional[type[TyperGroup]] = Default(None),
+ invoke_without_command: bool = Default(False),
+ no_args_is_help: bool = Default(False),
+ subcommand_metavar: Optional[str] = Default(None),
+ chain: bool = Default(False),
+ result_callback: Optional[Callable[..., Any]] = Default(None),
+ # Command
+ context_settings: Optional[dict[Any, Any]] = Default(None),
+ help: Optional[str] = Default(None),
+ epilog: Optional[str] = Default(None),
+ short_help: Optional[str] = Default(None),
+ options_metavar: Optional[str] = Default(None),
+ add_help_option: bool = Default(True),
+ hidden: bool = Default(False),
+ deprecated: bool = Default(False),
+ # Rich settings
+ rich_help_panel: Union[str, None] = Default(None),
+ ) -> Callable[[CommandFunctionType], CommandFunctionType]:
+ def decorator(f: CommandFunctionType) -> CommandFunctionType:
+ self.registered_callback = TyperInfo(
+ cls=cls,
+ invoke_without_command=invoke_without_command,
+ no_args_is_help=no_args_is_help,
+ subcommand_metavar=subcommand_metavar,
+ chain=chain,
+ result_callback=result_callback,
+ context_settings=context_settings,
+ callback=f,
+ help=help,
+ epilog=epilog,
+ short_help=short_help,
+ options_metavar=(
+ options_metavar or self._info_val_str("options_metavar")
+ ),
+ add_help_option=add_help_option,
+ hidden=hidden,
+ deprecated=deprecated,
+ rich_help_panel=rich_help_panel,
+ )
+ return f
+
+ return decorator
+
+ def command(
+ self,
+ name: Optional[str] = None,
+ *,
+ cls: Optional[type[TyperCommand]] = None,
+ context_settings: Optional[dict[Any, Any]] = None,
+ help: Optional[str] = None,
+ epilog: Optional[str] = None,
+ short_help: Optional[str] = None,
+ options_metavar: Optional[str] = None,
+ add_help_option: bool = True,
+ no_args_is_help: bool = False,
+ hidden: bool = False,
+ deprecated: bool = False,
+ # Rich settings
+ rich_help_panel: Union[str, None] = Default(None),
+ ) -> Callable[[CommandFunctionType], CommandFunctionType]:
+ if cls is None:
+ cls = TyperCommand
+
+ def decorator(f: CommandFunctionType) -> CommandFunctionType:
+ self.registered_commands.append(
+ CommandInfo(
+ name=name,
+ cls=cls,
+ context_settings=context_settings,
+ callback=f,
+ help=help,
+ epilog=epilog,
+ short_help=short_help,
+ options_metavar=(
+ options_metavar or self._info_val_str("options_metavar")
+ ),
+ add_help_option=add_help_option,
+ no_args_is_help=no_args_is_help,
+ hidden=hidden,
+ deprecated=deprecated,
+ # Rich settings
+ rich_help_panel=rich_help_panel,
+ )
+ )
+ return f
+
+ return decorator
+
+ def add_typer(
+ self,
+ typer_instance: "Typer",
+ *,
+ name: Optional[str] = Default(None),
+ cls: Optional[type[TyperGroup]] = Default(None),
+ invoke_without_command: bool = Default(False),
+ no_args_is_help: bool = Default(False),
+ subcommand_metavar: Optional[str] = Default(None),
+ chain: bool = Default(False),
+ result_callback: Optional[Callable[..., Any]] = Default(None),
+ # Command
+ context_settings: Optional[dict[Any, Any]] = Default(None),
+ callback: Optional[Callable[..., Any]] = Default(None),
+ help: Optional[str] = Default(None),
+ epilog: Optional[str] = Default(None),
+ short_help: Optional[str] = Default(None),
+ options_metavar: Optional[str] = Default(None),
+ add_help_option: bool = Default(True),
+ hidden: bool = Default(False),
+ deprecated: bool = Default(False),
+ # Rich settings
+ rich_help_panel: Union[str, None] = Default(None),
+ ) -> None:
+ self.registered_groups.append(
+ TyperInfo(
+ typer_instance,
+ name=name,
+ cls=cls,
+ invoke_without_command=invoke_without_command,
+ no_args_is_help=no_args_is_help,
+ subcommand_metavar=subcommand_metavar,
+ chain=chain,
+ result_callback=result_callback,
+ context_settings=context_settings,
+ callback=callback,
+ help=help,
+ epilog=epilog,
+ short_help=short_help,
+ options_metavar=(
+ options_metavar or self._info_val_str("options_metavar")
+ ),
+ add_help_option=add_help_option,
+ hidden=hidden,
+ deprecated=deprecated,
+ rich_help_panel=rich_help_panel,
+ )
+ )
+
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
+ if sys.excepthook != except_hook:
+ sys.excepthook = except_hook
+ try:
+ return get_command(self)(*args, **kwargs)
+ except Exception as e:
+ # Set a custom attribute to tell the hook to show nice exceptions for user
+ # code. An alternative/first implementation was a custom exception with
+ # raise custom_exc from e
+ # but that means the last error shown is the custom exception, not the
+ # actual error. This trick improves developer experience by showing the
+ # actual error last.
+ setattr(
+ e,
+ _typer_developer_exception_attr_name,
+ DeveloperExceptionConfig(
+ pretty_exceptions_enable=self.pretty_exceptions_enable,
+ pretty_exceptions_show_locals=self.pretty_exceptions_show_locals,
+ pretty_exceptions_short=self.pretty_exceptions_short,
+ ),
+ )
+ raise e
+
+ def _info_val_str(self, name: str) -> str:
+ val = getattr(self.info, name)
+ val_str = val.value if isinstance(val, DefaultPlaceholder) else val
+ assert isinstance(val_str, str)
+ return val_str
+
+
+def get_group(typer_instance: Typer) -> TyperGroup:
+ group = get_group_from_info(
+ TyperInfo(typer_instance),
+ pretty_exceptions_short=typer_instance.pretty_exceptions_short,
+ rich_markup_mode=typer_instance.rich_markup_mode,
+ suggest_commands=typer_instance.suggest_commands,
+ )
+ return group
+
+
+def get_command(typer_instance: Typer) -> click.Command:
+ if typer_instance._add_completion:
+ click_install_param, click_show_param = get_install_completion_arguments()
+ if (
+ typer_instance.registered_callback
+ or typer_instance.info.callback
+ or typer_instance.registered_groups
+ or len(typer_instance.registered_commands) > 1
+ ):
+ # Create a Group
+ 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)
+ return click_command
+ elif len(typer_instance.registered_commands) == 1:
+ # Create a single Command
+ single_command = typer_instance.registered_commands[0]
+
+ if not single_command.context_settings and not isinstance(
+ typer_instance.info.context_settings, DefaultPlaceholder
+ ):
+ single_command.context_settings = typer_instance.info.context_settings
+
+ click_command = get_command_from_info(
+ single_command,
+ pretty_exceptions_short=typer_instance.pretty_exceptions_short,
+ rich_markup_mode=typer_instance.rich_markup_mode,
+ )
+ if typer_instance._add_completion:
+ click_command.params.append(click_install_param)
+ click_command.params.append(click_show_param)
+ return click_command
+ raise RuntimeError(
+ "Could not get a command for this Typer instance"
+ ) # pragma: no cover
+
+
+def solve_typer_info_help(typer_info: TyperInfo) -> str:
+ # Priority 1: Explicit value was set in app.add_typer()
+ if not isinstance(typer_info.help, DefaultPlaceholder):
+ return inspect.cleandoc(typer_info.help or "")
+ # Priority 2: Explicit value was set in sub_app.callback()
+ try:
+ callback_help = typer_info.typer_instance.registered_callback.help
+ if not isinstance(callback_help, DefaultPlaceholder):
+ return inspect.cleandoc(callback_help or "")
+ except AttributeError:
+ pass
+ # Priority 3: Explicit value was set in sub_app = typer.Typer()
+ try:
+ instance_help = typer_info.typer_instance.info.help
+ if not isinstance(instance_help, DefaultPlaceholder):
+ return inspect.cleandoc(instance_help or "")
+ except AttributeError:
+ pass
+ # Priority 4: Implicit inference from callback docstring in app.add_typer()
+ if typer_info.callback:
+ doc = inspect.getdoc(typer_info.callback)
+ if doc:
+ return doc
+ # Priority 5: Implicit inference from callback docstring in @app.callback()
+ try:
+ callback = typer_info.typer_instance.registered_callback.callback
+ if not isinstance(callback, DefaultPlaceholder):
+ doc = inspect.getdoc(callback or "")
+ if doc:
+ return doc
+ except AttributeError:
+ pass
+ # Priority 6: Implicit inference from callback docstring in typer.Typer()
+ try:
+ instance_callback = typer_info.typer_instance.info.callback
+ if not isinstance(instance_callback, DefaultPlaceholder):
+ doc = inspect.getdoc(instance_callback)
+ if doc:
+ return doc
+ except AttributeError:
+ pass
+ # Value not set, use the default
+ return typer_info.help.value
+
+
+def solve_typer_info_defaults(typer_info: TyperInfo) -> TyperInfo:
+ values: dict[str, Any] = {}
+ for name, value in typer_info.__dict__.items():
+ # Priority 1: Value was set in app.add_typer()
+ if not isinstance(value, DefaultPlaceholder):
+ values[name] = value
+ continue
+ # Priority 2: Value was set in @subapp.callback()
+ try:
+ callback_value = getattr(
+ typer_info.typer_instance.registered_callback, # type: ignore
+ name,
+ )
+ if not isinstance(callback_value, DefaultPlaceholder):
+ values[name] = callback_value
+ continue
+ except AttributeError:
+ pass
+ # Priority 3: Value set in subapp = typer.Typer()
+ try:
+ instance_value = getattr(
+ typer_info.typer_instance.info, # type: ignore
+ name,
+ )
+ if not isinstance(instance_value, DefaultPlaceholder):
+ values[name] = instance_value
+ continue
+ except AttributeError:
+ pass
+ # Value not set, use the default
+ values[name] = value.value
+ values["help"] = solve_typer_info_help(typer_info)
+ return TyperInfo(**values)
+
+
+def get_group_from_info(
+ group_info: TyperInfo,
+ *,
+ pretty_exceptions_short: bool,
+ suggest_commands: bool,
+ rich_markup_mode: MarkupMode,
+) -> TyperGroup:
+ assert group_info.typer_instance, (
+ "A Typer instance is needed to generate a Click Group"
+ )
+ commands: dict[str, click.Command] = {}
+ for command_info in group_info.typer_instance.registered_commands:
+ command = get_command_from_info(
+ command_info=command_info,
+ pretty_exceptions_short=pretty_exceptions_short,
+ rich_markup_mode=rich_markup_mode,
+ )
+ if command.name:
+ commands[command.name] = command
+ for sub_group_info in group_info.typer_instance.registered_groups:
+ sub_group = get_group_from_info(
+ sub_group_info,
+ pretty_exceptions_short=pretty_exceptions_short,
+ rich_markup_mode=rich_markup_mode,
+ suggest_commands=suggest_commands,
+ )
+ if sub_group.name:
+ commands[sub_group.name] = sub_group
+ else:
+ if sub_group.callback:
+ import warnings
+
+ warnings.warn(
+ "The 'callback' parameter is not supported by Typer when using `add_typer` without a name",
+ stacklevel=5,
+ )
+ for sub_command_name, sub_command in sub_group.commands.items():
+ commands[sub_command_name] = sub_command
+ solved_info = solve_typer_info_defaults(group_info)
+ (
+ params,
+ convertors,
+ context_param_name,
+ ) = get_params_convertors_ctx_param_name_from_function(solved_info.callback)
+ cls = solved_info.cls or TyperGroup
+ assert issubclass(cls, TyperGroup), f"{cls} should be a subclass of {TyperGroup}"
+ group = cls(
+ name=solved_info.name or "",
+ commands=commands,
+ 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(
+ callback=solved_info.callback,
+ params=params,
+ convertors=convertors,
+ context_param_name=context_param_name,
+ pretty_exceptions_short=pretty_exceptions_short,
+ ),
+ params=params,
+ help=solved_info.help,
+ epilog=solved_info.epilog,
+ short_help=solved_info.short_help,
+ options_metavar=solved_info.options_metavar,
+ add_help_option=solved_info.add_help_option,
+ hidden=solved_info.hidden,
+ deprecated=solved_info.deprecated,
+ rich_markup_mode=rich_markup_mode,
+ # Rich settings
+ rich_help_panel=solved_info.rich_help_panel,
+ suggest_commands=suggest_commands,
+ )
+ return group
+
+
+def get_command_name(name: str) -> str:
+ return name.lower().replace("_", "-")
+
+
+def get_params_convertors_ctx_param_name_from_function(
+ callback: Optional[Callable[..., Any]],
+) -> tuple[list[Union[click.Argument, click.Option]], dict[str, Any], Optional[str]]:
+ 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):
+ context_param_name = param_name
+ continue
+ click_param, convertor = get_click_param(param)
+ if convertor:
+ convertors[param_name] = convertor
+ params.append(click_param)
+ return params, convertors, context_param_name
+
+
+def get_command_from_info(
+ command_info: CommandInfo,
+ *,
+ pretty_exceptions_short: bool,
+ rich_markup_mode: MarkupMode,
+) -> 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__)
+ use_help = command_info.help
+ if use_help is None:
+ use_help = inspect.getdoc(command_info.callback)
+ else:
+ use_help = inspect.cleandoc(use_help)
+ (
+ params,
+ convertors,
+ context_param_name,
+ ) = get_params_convertors_ctx_param_name_from_function(command_info.callback)
+ cls = command_info.cls or TyperCommand
+ command = cls(
+ name=name,
+ context_settings=command_info.context_settings,
+ callback=get_callback(
+ callback=command_info.callback,
+ params=params,
+ convertors=convertors,
+ context_param_name=context_param_name,
+ pretty_exceptions_short=pretty_exceptions_short,
+ ),
+ params=params, # type: ignore
+ help=use_help,
+ epilog=command_info.epilog,
+ short_help=command_info.short_help,
+ options_metavar=command_info.options_metavar,
+ add_help_option=command_info.add_help_option,
+ no_args_is_help=command_info.no_args_is_help,
+ hidden=command_info.hidden,
+ deprecated=command_info.deprecated,
+ rich_markup_mode=rich_markup_mode,
+ # Rich settings
+ rich_help_panel=command_info.rich_help_panel,
+ )
+ return command
+
+
+def determine_type_convertor(type_: Any) -> Optional[Callable[[Any], Any]]:
+ convertor: Optional[Callable[[Any], Any]] = None
+ if lenient_issubclass(type_, Path):
+ convertor = param_path_convertor
+ if lenient_issubclass(type_, Enum):
+ convertor = generate_enum_convertor(type_)
+ return convertor
+
+
+def param_path_convertor(value: Optional[str] = None) -> Optional[Path]:
+ if value is not None:
+ # allow returning any subclass of Path created by an annotated parser without converting
+ # it back to a Path
+ return value if isinstance(value, Path) else Path(value)
+ return None
+
+
+def generate_enum_convertor(enum: type[Enum]) -> Callable[[Any], Any]:
+ val_map = {str(val.value): val for val in enum}
+
+ def convertor(value: Any) -> Any:
+ if value is not None:
+ val = str(value)
+ if val in val_map:
+ key = val_map[val]
+ return enum(key)
+
+ return convertor
+
+
+def generate_list_convertor(
+ convertor: Optional[Callable[[Any], Any]], default_value: Optional[Any]
+) -> Callable[[Optional[Sequence[Any]]], Optional[list[Any]]]:
+ def internal_convertor(value: Optional[Sequence[Any]]) -> Optional[list[Any]]:
+ if (value is None) or (default_value is None and len(value) == 0):
+ return None
+ return [convertor(v) if convertor else v for v in value]
+
+ return internal_convertor
+
+
+def generate_tuple_convertor(
+ types: Sequence[Any],
+) -> Callable[[Optional[tuple[Any, ...]]], Optional[tuple[Any, ...]]]:
+ convertors = [determine_type_convertor(type_) for type_ in types]
+
+ def internal_convertor(
+ param_args: Optional[tuple[Any, ...]],
+ ) -> Optional[tuple[Any, ...]]:
+ if param_args is None:
+ return None
+ return tuple(
+ convertor(arg) if convertor else arg
+ for (convertor, arg) in zip(convertors, param_args)
+ )
+
+ return internal_convertor
+
+
+def get_callback(
+ *,
+ callback: Optional[Callable[..., Any]] = None,
+ params: Sequence[click.Parameter] = [],
+ convertors: Optional[dict[str, Callable[[str], Any]]] = None,
+ context_param_name: Optional[str] = None,
+ pretty_exceptions_short: bool,
+) -> Optional[Callable[..., Any]]:
+ use_convertors = convertors or {}
+ if not callback:
+ return None
+ parameters = get_params_from_function(callback)
+ use_params: dict[str, Any] = {}
+ for param_name in parameters:
+ use_params[param_name] = None
+ for param in params:
+ if param.name:
+ use_params[param.name] = param.default
+
+ def wrapper(**kwargs: Any) -> Any:
+ _rich_traceback_guard = pretty_exceptions_short # noqa: F841
+ for k, v in kwargs.items():
+ if k in use_convertors:
+ use_params[k] = use_convertors[k](v)
+ else:
+ use_params[k] = v
+ if context_param_name:
+ use_params[context_param_name] = click.get_current_context()
+ return callback(**use_params)
+
+ update_wrapper(wrapper, callback)
+ return wrapper
+
+
+def get_click_type(
+ *, annotation: Any, parameter_info: ParameterInfo
+) -> click.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)
+
+ elif annotation is str:
+ return click.STRING
+ elif annotation is int:
+ if parameter_info.min is not None or parameter_info.max is not None:
+ min_ = None
+ max_ = None
+ if parameter_info.min is not None:
+ 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)
+ else:
+ return click.INT
+ elif annotation is float:
+ if parameter_info.min is not None or parameter_info.max is not None:
+ return click.FloatRange(
+ min=parameter_info.min,
+ max=parameter_info.max,
+ clamp=parameter_info.clamp,
+ )
+ else:
+ return click.FLOAT
+ elif annotation is bool:
+ return click.BOOL
+ elif annotation == UUID:
+ return click.UUID
+ elif annotation == datetime:
+ return click.DateTime(formats=parameter_info.formats)
+ elif (
+ annotation == Path
+ or parameter_info.allow_dash
+ or parameter_info.path_type
+ or parameter_info.resolve_path
+ ):
+ return TyperPath(
+ exists=parameter_info.exists,
+ file_okay=parameter_info.file_okay,
+ dir_okay=parameter_info.dir_okay,
+ writable=parameter_info.writable,
+ readable=parameter_info.readable,
+ resolve_path=parameter_info.resolve_path,
+ allow_dash=parameter_info.allow_dash,
+ path_type=parameter_info.path_type,
+ )
+ elif lenient_issubclass(annotation, FileTextWrite):
+ return click.File(
+ mode=parameter_info.mode or "w",
+ encoding=parameter_info.encoding,
+ errors=parameter_info.errors,
+ lazy=parameter_info.lazy,
+ atomic=parameter_info.atomic,
+ )
+ elif lenient_issubclass(annotation, FileText):
+ return click.File(
+ mode=parameter_info.mode or "r",
+ encoding=parameter_info.encoding,
+ errors=parameter_info.errors,
+ lazy=parameter_info.lazy,
+ atomic=parameter_info.atomic,
+ )
+ elif lenient_issubclass(annotation, FileBinaryRead):
+ return click.File(
+ mode=parameter_info.mode or "rb",
+ encoding=parameter_info.encoding,
+ errors=parameter_info.errors,
+ lazy=parameter_info.lazy,
+ atomic=parameter_info.atomic,
+ )
+ elif lenient_issubclass(annotation, FileBinaryWrite):
+ return click.File(
+ mode=parameter_info.mode or "wb",
+ encoding=parameter_info.encoding,
+ errors=parameter_info.errors,
+ lazy=parameter_info.lazy,
+ 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(
+ literal_values(annotation),
+ case_sensitive=parameter_info.case_sensitive,
+ )
+ raise RuntimeError(f"Type not yet supported: {annotation}") # pragma: no cover
+
+
+def lenient_issubclass(
+ cls: Any, class_or_tuple: Union[AnyType, tuple[AnyType, ...]]
+) -> bool:
+ return isinstance(cls, type) and issubclass(cls, class_or_tuple)
+
+
+def get_click_param(
+ param: ParamMeta,
+) -> tuple[Union[click.Argument, click.Option], Any]:
+ # First, find out what will be:
+ # * ParamInfo (ArgumentInfo or OptionInfo)
+ # * default_value
+ # * required
+ default_value = None
+ required = False
+ if isinstance(param.default, ParameterInfo):
+ parameter_info = param.default
+ if parameter_info.default == Required:
+ required = True
+ else:
+ default_value = parameter_info.default
+ elif param.default == Required or param.default is param.empty:
+ required = True
+ parameter_info = ArgumentInfo()
+ else:
+ default_value = param.default
+ parameter_info = OptionInfo()
+ annotation: Any
+ if param.annotation is not param.empty:
+ annotation = param.annotation
+ else:
+ annotation = str
+ main_type = annotation
+ is_list = False
+ is_tuple = False
+ parameter_type: Any = None
+ is_flag = None
+ origin = get_origin(main_type)
+
+ if origin is not None:
+ # Handle SomeType | None and Optional[SomeType]
+ if is_union(origin):
+ types = []
+ for type_ in get_args(main_type):
+ if type_ is NoneType:
+ continue
+ types.append(type_)
+ assert len(types) == 1, "Typer Currently doesn't support Union types"
+ main_type = types[0]
+ origin = get_origin(main_type)
+ # Handle Tuples and Lists
+ if lenient_issubclass(origin, list):
+ main_type = get_args(main_type)[0]
+ assert not get_origin(main_type), (
+ "List types with complex sub-types are not currently supported"
+ )
+ is_list = True
+ elif lenient_issubclass(origin, tuple):
+ types = []
+ for type_ in get_args(main_type):
+ assert not get_origin(type_), (
+ "Tuple types with complex sub-types are not currently supported"
+ )
+ types.append(
+ get_click_type(annotation=type_, parameter_info=parameter_info)
+ )
+ parameter_type = tuple(types)
+ is_tuple = True
+ if parameter_type is None:
+ parameter_type = get_click_type(
+ annotation=main_type, parameter_info=parameter_info
+ )
+ convertor = determine_type_convertor(main_type)
+ if is_list:
+ convertor = generate_list_convertor(
+ convertor=convertor, default_value=default_value
+ )
+ if is_tuple:
+ convertor = generate_tuple_convertor(get_args(main_type))
+ if isinstance(parameter_info, OptionInfo):
+ if main_type is bool:
+ is_flag = True
+ # Click doesn't accept a flag of type bool, only None, and then it sets it
+ # to bool internally
+ parameter_type = None
+ default_option_name = get_command_name(param.name)
+ if is_flag:
+ default_option_declaration = (
+ f"--{default_option_name}/--no-{default_option_name}"
+ )
+ else:
+ default_option_declaration = f"--{default_option_name}"
+ param_decls = [param.name]
+ if parameter_info.param_decls:
+ param_decls.extend(parameter_info.param_decls)
+ else:
+ param_decls.append(default_option_declaration)
+ return (
+ TyperOption(
+ # Option
+ param_decls=param_decls,
+ show_default=parameter_info.show_default,
+ prompt=parameter_info.prompt,
+ confirmation_prompt=parameter_info.confirmation_prompt,
+ prompt_required=parameter_info.prompt_required,
+ hide_input=parameter_info.hide_input,
+ is_flag=is_flag,
+ multiple=is_list,
+ count=parameter_info.count,
+ allow_from_autoenv=parameter_info.allow_from_autoenv,
+ type=parameter_type,
+ help=parameter_info.help,
+ hidden=parameter_info.hidden,
+ show_choices=parameter_info.show_choices,
+ show_envvar=parameter_info.show_envvar,
+ # Parameter
+ required=required,
+ default=default_value,
+ callback=get_param_callback(
+ callback=parameter_info.callback, convertor=convertor
+ ),
+ metavar=parameter_info.metavar,
+ expose_value=parameter_info.expose_value,
+ is_eager=parameter_info.is_eager,
+ envvar=parameter_info.envvar,
+ shell_complete=parameter_info.shell_complete,
+ autocompletion=get_param_completion(parameter_info.autocompletion),
+ # Rich settings
+ rich_help_panel=parameter_info.rich_help_panel,
+ ),
+ convertor,
+ )
+ elif isinstance(parameter_info, ArgumentInfo):
+ param_decls = [param.name]
+ nargs = None
+ if is_list:
+ nargs = -1
+ return (
+ TyperArgument(
+ # Argument
+ param_decls=param_decls,
+ type=parameter_type,
+ required=required,
+ nargs=nargs,
+ # TyperArgument
+ show_default=parameter_info.show_default,
+ show_choices=parameter_info.show_choices,
+ show_envvar=parameter_info.show_envvar,
+ help=parameter_info.help,
+ hidden=parameter_info.hidden,
+ # Parameter
+ default=default_value,
+ callback=get_param_callback(
+ callback=parameter_info.callback, convertor=convertor
+ ),
+ metavar=parameter_info.metavar,
+ expose_value=parameter_info.expose_value,
+ is_eager=parameter_info.is_eager,
+ envvar=parameter_info.envvar,
+ shell_complete=parameter_info.shell_complete,
+ autocompletion=get_param_completion(parameter_info.autocompletion),
+ # Rich settings
+ rich_help_panel=parameter_info.rich_help_panel,
+ ),
+ convertor,
+ )
+ raise AssertionError("A click.Parameter should be returned") # pragma: no cover
+
+
+def get_param_callback(
+ *,
+ callback: Optional[Callable[..., Any]] = None,
+ convertor: Optional[Callable[..., Any]] = None,
+) -> Optional[Callable[..., Any]]:
+ if not callback:
+ return None
+ parameters = get_params_from_function(callback)
+ ctx_name = None
+ click_param_name = None
+ value_name = None
+ untyped_names: list[str] = []
+ for param_name, param_sig in parameters.items():
+ if lenient_issubclass(param_sig.annotation, click.Context):
+ ctx_name = param_name
+ elif lenient_issubclass(param_sig.annotation, click.Parameter):
+ click_param_name = param_name
+ else:
+ untyped_names.append(param_name)
+ # Extract value param name first
+ if untyped_names:
+ value_name = untyped_names.pop()
+ # If context and Click param were not typed (old/Click callback style) extract them
+ if untyped_names:
+ if ctx_name is None:
+ ctx_name = untyped_names.pop(0)
+ if click_param_name is None:
+ if untyped_names:
+ click_param_name = untyped_names.pop(0)
+ if untyped_names:
+ raise click.ClickException(
+ "Too many CLI parameter callback function parameters"
+ )
+
+ def wrapper(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
+ use_params: dict[str, Any] = {}
+ if ctx_name:
+ use_params[ctx_name] = ctx
+ if click_param_name:
+ use_params[click_param_name] = param
+ if value_name:
+ if convertor:
+ use_value = convertor(value)
+ else:
+ use_value = value
+ use_params[value_name] = use_value
+ return callback(**use_params)
+
+ update_wrapper(wrapper, callback)
+ return wrapper
+
+
+def get_param_completion(
+ callback: Optional[Callable[..., Any]] = None,
+) -> Optional[Callable[..., Any]]:
+ if not callback:
+ return None
+ parameters = get_params_from_function(callback)
+ ctx_name = None
+ args_name = None
+ incomplete_name = None
+ 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):
+ ctx_name = param_sig.name
+ unassigned_params.remove(param_sig)
+ elif lenient_issubclass(origin, list):
+ args_name = param_sig.name
+ unassigned_params.remove(param_sig)
+ elif lenient_issubclass(param_sig.annotation, str):
+ incomplete_name = param_sig.name
+ unassigned_params.remove(param_sig)
+ # If there are still unassigned parameters (not typed), extract by name
+ for param_sig in unassigned_params[:]:
+ if ctx_name is None and param_sig.name == "ctx":
+ ctx_name = param_sig.name
+ unassigned_params.remove(param_sig)
+ elif args_name is None and param_sig.name == "args":
+ args_name = param_sig.name
+ unassigned_params.remove(param_sig)
+ elif incomplete_name is None and param_sig.name == "incomplete":
+ incomplete_name = param_sig.name
+ unassigned_params.remove(param_sig)
+ # Extract value param name first
+ if unassigned_params:
+ show_params = " ".join([param.name for param in unassigned_params])
+ raise click.ClickException(
+ f"Invalid autocompletion callback parameters: {show_params}"
+ )
+
+ def wrapper(ctx: click.Context, args: list[str], incomplete: Optional[str]) -> Any:
+ use_params: dict[str, Any] = {}
+ if ctx_name:
+ use_params[ctx_name] = ctx
+ if args_name:
+ use_params[args_name] = args
+ if incomplete_name:
+ use_params[incomplete_name] = incomplete
+ return callback(**use_params)
+
+ update_wrapper(wrapper, callback)
+ return wrapper
+
+
+def run(function: Callable[..., Any]) -> None:
+ app = Typer(add_completion=False)
+ app.command()(function)
+ app()
+
+
+def _is_macos() -> bool:
+ return platform.system() == "Darwin"
+
+
+def _is_linux_or_bsd() -> bool:
+ if platform.system() == "Linux":
+ return True
+
+ return "BSD" in platform.system()
+
+
+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.
+
+ This function handles url in different operating systems separately:
+ - On macOS (Darwin), it uses the 'open' command.
+ - On Linux and BSD, it uses 'xdg-open' if available.
+ - On Windows (and other OSes), it uses the standard webbrowser module.
+
+ The function avoids, when possible, using the webbrowser module on Linux and macOS
+ to prevent spammy terminal messages from some browsers (e.g., Chrome).
+
+ Examples::
+
+ typer.launch("https://typer.tiangolo.com/")
+ typer.launch("/my/downloaded/file", locate=True)
+
+ :param url: URL or filename of the thing to launch.
+ :param wait: Wait for the program to exit before returning. This
+ only works if the launched program blocks. In particular,
+ ``xdg-open`` on Linux does not block.
+ :param locate: if this is set to `True` then instead of launching the
+ application associated with the URL it will attempt to
+ launch a file manager with the file located. This
+ might have weird effects if the URL does not point to
+ the filesystem.
+ """
+
+ if url.startswith("http://") or url.startswith("https://"):
+ if _is_macos():
+ return subprocess.Popen(
+ ["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
+ ).wait()
+
+ has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None
+
+ if has_xdg_open:
+ return subprocess.Popen(
+ ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
+ ).wait()
+
+ import webbrowser
+
+ webbrowser.open(url)
+
+ return 0
+
+ else:
+ return click.launch(url)
diff --git a/contrib/python/typer-slim/typer/models.py b/contrib/python/typer-slim/typer/models.py
new file mode 100644
index 00000000000..78d1a5354d5
--- /dev/null
+++ b/contrib/python/typer-slim/typer/models.py
@@ -0,0 +1,541 @@
+import inspect
+import io
+from collections.abc import Sequence
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Optional,
+ TypeVar,
+ Union,
+)
+
+import click
+import click.shell_completion
+
+if TYPE_CHECKING: # pragma: no cover
+ from .core import TyperCommand, TyperGroup
+ from .main import Typer
+
+
+NoneType = type(None)
+
+AnyType = type[Any]
+
+Required = ...
+
+
+class Context(click.Context):
+ pass
+
+
+class FileText(io.TextIOWrapper):
+ pass
+
+
+class FileTextWrite(FileText):
+ pass
+
+
+class FileBinaryRead(io.BufferedReader):
+ pass
+
+
+class FileBinaryWrite(io.BufferedWriter):
+ pass
+
+
+class CallbackParam(click.Parameter):
+ pass
+
+
+class DefaultPlaceholder:
+ """
+ You shouldn't use this class directly.
+
+ It's used internally to recognize when a default value has been overwritten, even
+ if the new value is `None`.
+ """
+
+ def __init__(self, value: Any):
+ self.value = value
+
+ def __bool__(self) -> bool:
+ return bool(self.value)
+
+
+DefaultType = TypeVar("DefaultType")
+
+CommandFunctionType = TypeVar("CommandFunctionType", bound=Callable[..., Any])
+
+
+def Default(value: DefaultType) -> DefaultType:
+ """
+ You shouldn't use this function directly.
+
+ It's used internally to recognize when a default value has been overwritten, even
+ if the new value is `None`.
+ """
+ return DefaultPlaceholder(value) # type: ignore
+
+
+class CommandInfo:
+ def __init__(
+ self,
+ name: Optional[str] = None,
+ *,
+ cls: Optional[type["TyperCommand"]] = None,
+ context_settings: Optional[dict[Any, Any]] = None,
+ callback: Optional[Callable[..., Any]] = None,
+ help: Optional[str] = None,
+ epilog: Optional[str] = None,
+ short_help: Optional[str] = None,
+ options_metavar: str = "[OPTIONS]",
+ add_help_option: bool = True,
+ no_args_is_help: bool = False,
+ hidden: bool = False,
+ deprecated: bool = False,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+ ):
+ self.name = name
+ self.cls = cls
+ self.context_settings = context_settings
+ self.callback = callback
+ self.help = help
+ self.epilog = epilog
+ self.short_help = short_help
+ self.options_metavar = options_metavar
+ self.add_help_option = add_help_option
+ self.no_args_is_help = no_args_is_help
+ self.hidden = hidden
+ self.deprecated = deprecated
+ # Rich settings
+ self.rich_help_panel = rich_help_panel
+
+
+class TyperInfo:
+ def __init__(
+ self,
+ typer_instance: Optional["Typer"] = Default(None),
+ *,
+ name: Optional[str] = Default(None),
+ cls: Optional[type["TyperGroup"]] = Default(None),
+ invoke_without_command: bool = Default(False),
+ no_args_is_help: bool = Default(False),
+ subcommand_metavar: Optional[str] = Default(None),
+ chain: bool = Default(False),
+ result_callback: Optional[Callable[..., Any]] = Default(None),
+ # Command
+ context_settings: Optional[dict[Any, Any]] = Default(None),
+ callback: Optional[Callable[..., Any]] = Default(None),
+ help: Optional[str] = Default(None),
+ epilog: Optional[str] = Default(None),
+ short_help: Optional[str] = Default(None),
+ options_metavar: str = Default("[OPTIONS]"),
+ add_help_option: bool = Default(True),
+ hidden: bool = Default(False),
+ deprecated: bool = Default(False),
+ # Rich settings
+ rich_help_panel: Union[str, None] = Default(None),
+ ):
+ self.typer_instance = typer_instance
+ self.name = name
+ self.cls = cls
+ self.invoke_without_command = invoke_without_command
+ self.no_args_is_help = no_args_is_help
+ self.subcommand_metavar = subcommand_metavar
+ self.chain = chain
+ self.result_callback = result_callback
+ self.context_settings = context_settings
+ self.callback = callback
+ self.help = help
+ self.epilog = epilog
+ self.short_help = short_help
+ self.options_metavar = options_metavar
+ self.add_help_option = add_help_option
+ self.hidden = hidden
+ self.deprecated = deprecated
+ self.rich_help_panel = rich_help_panel
+
+
+class ParameterInfo:
+ def __init__(
+ self,
+ *,
+ default: Optional[Any] = None,
+ param_decls: Optional[Sequence[str]] = None,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ parser: Optional[Callable[[str], Any]] = None,
+ click_type: Optional[click.ParamType] = None,
+ # TyperArgument
+ show_default: Union[bool, str] = True,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+ ):
+ # Check if user has provided multiple custom parsers
+ if parser and click_type:
+ raise ValueError(
+ "Multiple custom type parsers provided. "
+ "`parser` and `click_type` may not both be provided."
+ )
+
+ self.default = default
+ self.param_decls = param_decls
+ self.callback = callback
+ self.metavar = metavar
+ self.expose_value = expose_value
+ self.is_eager = is_eager
+ self.envvar = envvar
+ self.shell_complete = shell_complete
+ self.autocompletion = autocompletion
+ self.default_factory = default_factory
+ # Custom type
+ self.parser = parser
+ self.click_type = click_type
+ # TyperArgument
+ self.show_default = show_default
+ self.show_choices = show_choices
+ self.show_envvar = show_envvar
+ self.help = help
+ self.hidden = hidden
+ # Choice
+ self.case_sensitive = case_sensitive
+ # Numbers
+ self.min = min
+ self.max = max
+ self.clamp = clamp
+ # DateTime
+ self.formats = formats
+ # File
+ self.mode = mode
+ self.encoding = encoding
+ self.errors = errors
+ self.lazy = lazy
+ self.atomic = atomic
+ # Path
+ self.exists = exists
+ self.file_okay = file_okay
+ self.dir_okay = dir_okay
+ self.writable = writable
+ self.readable = readable
+ self.resolve_path = resolve_path
+ self.allow_dash = allow_dash
+ self.path_type = path_type
+ # Rich settings
+ self.rich_help_panel = rich_help_panel
+
+
+class OptionInfo(ParameterInfo):
+ def __init__(
+ self,
+ *,
+ # ParameterInfo
+ default: Optional[Any] = None,
+ param_decls: Optional[Sequence[str]] = None,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ parser: Optional[Callable[[str], Any]] = None,
+ click_type: Optional[click.ParamType] = None,
+ # Option
+ show_default: Union[bool, str] = True,
+ prompt: Union[bool, str] = False,
+ confirmation_prompt: bool = False,
+ prompt_required: bool = True,
+ hide_input: bool = False,
+ # TODO: remove is_flag and flag_value in a future release
+ is_flag: Optional[bool] = None,
+ flag_value: Optional[Any] = None,
+ count: bool = False,
+ allow_from_autoenv: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+ ):
+ super().__init__(
+ default=default,
+ param_decls=param_decls,
+ callback=callback,
+ metavar=metavar,
+ expose_value=expose_value,
+ is_eager=is_eager,
+ envvar=envvar,
+ shell_complete=shell_complete,
+ autocompletion=autocompletion,
+ default_factory=default_factory,
+ # Custom type
+ parser=parser,
+ click_type=click_type,
+ # TyperArgument
+ show_default=show_default,
+ show_choices=show_choices,
+ show_envvar=show_envvar,
+ help=help,
+ hidden=hidden,
+ # Choice
+ case_sensitive=case_sensitive,
+ # Numbers
+ min=min,
+ max=max,
+ clamp=clamp,
+ # DateTime
+ formats=formats,
+ # File
+ mode=mode,
+ encoding=encoding,
+ errors=errors,
+ lazy=lazy,
+ atomic=atomic,
+ # Path
+ exists=exists,
+ file_okay=file_okay,
+ dir_okay=dir_okay,
+ writable=writable,
+ readable=readable,
+ resolve_path=resolve_path,
+ allow_dash=allow_dash,
+ path_type=path_type,
+ # Rich settings
+ rich_help_panel=rich_help_panel,
+ )
+ if is_flag is not None or flag_value is not None:
+ import warnings
+
+ warnings.warn(
+ "The 'is_flag' and 'flag_value' parameters are not supported by Typer "
+ "and will be removed entirely in a future release.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.prompt = prompt
+ self.confirmation_prompt = confirmation_prompt
+ self.prompt_required = prompt_required
+ self.hide_input = hide_input
+ self.count = count
+ self.allow_from_autoenv = allow_from_autoenv
+
+
+class ArgumentInfo(ParameterInfo):
+ def __init__(
+ self,
+ *,
+ # ParameterInfo
+ default: Optional[Any] = None,
+ param_decls: Optional[Sequence[str]] = None,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ parser: Optional[Callable[[str], Any]] = None,
+ click_type: Optional[click.ParamType] = None,
+ # TyperArgument
+ show_default: Union[bool, str] = True,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+ ):
+ super().__init__(
+ default=default,
+ param_decls=param_decls,
+ callback=callback,
+ metavar=metavar,
+ expose_value=expose_value,
+ is_eager=is_eager,
+ envvar=envvar,
+ shell_complete=shell_complete,
+ autocompletion=autocompletion,
+ default_factory=default_factory,
+ # Custom type
+ parser=parser,
+ click_type=click_type,
+ # TyperArgument
+ show_default=show_default,
+ show_choices=show_choices,
+ show_envvar=show_envvar,
+ help=help,
+ hidden=hidden,
+ # Choice
+ case_sensitive=case_sensitive,
+ # Numbers
+ min=min,
+ max=max,
+ clamp=clamp,
+ # DateTime
+ formats=formats,
+ # File
+ mode=mode,
+ encoding=encoding,
+ errors=errors,
+ lazy=lazy,
+ atomic=atomic,
+ # Path
+ exists=exists,
+ file_okay=file_okay,
+ dir_okay=dir_okay,
+ writable=writable,
+ readable=readable,
+ resolve_path=resolve_path,
+ allow_dash=allow_dash,
+ path_type=path_type,
+ # Rich settings
+ rich_help_panel=rich_help_panel,
+ )
+
+
+class ParamMeta:
+ empty = inspect.Parameter.empty
+
+ def __init__(
+ self,
+ *,
+ name: str,
+ default: Any = inspect.Parameter.empty,
+ annotation: Any = inspect.Parameter.empty,
+ ) -> None:
+ self.name = name
+ self.default = default
+ self.annotation = annotation
+
+
+class DeveloperExceptionConfig:
+ def __init__(
+ self,
+ *,
+ pretty_exceptions_enable: bool = True,
+ pretty_exceptions_show_locals: bool = True,
+ pretty_exceptions_short: bool = True,
+ ) -> None:
+ self.pretty_exceptions_enable = pretty_exceptions_enable
+ self.pretty_exceptions_show_locals = pretty_exceptions_show_locals
+ self.pretty_exceptions_short = pretty_exceptions_short
+
+
+class TyperPath(click.Path):
+ # Overwrite Click's behaviour to be compatible with Typer's autocompletion system
+ def shell_complete(
+ self, ctx: click.Context, param: click.Parameter, incomplete: str
+ ) -> list[click.shell_completion.CompletionItem]:
+ """Return an empty list so that the autocompletion functionality
+ will work properly from the commandline.
+ """
+ return []
diff --git a/contrib/python/typer-slim/typer/params.py b/contrib/python/typer-slim/typer/params.py
new file mode 100644
index 00000000000..2a03c03e71b
--- /dev/null
+++ b/contrib/python/typer-slim/typer/params.py
@@ -0,0 +1,479 @@
+from typing import TYPE_CHECKING, Any, Callable, Optional, Union, overload
+
+import click
+
+from .models import ArgumentInfo, OptionInfo
+
+if TYPE_CHECKING: # pragma: no cover
+ import click.shell_completion
+
+
+# Overload for Option created with custom type 'parser'
+@overload
+def Option(
+ # Parameter
+ default: Optional[Any] = ...,
+ *param_decls: str,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ parser: Optional[Callable[[str], Any]] = None,
+ # Option
+ show_default: Union[bool, str] = True,
+ prompt: Union[bool, str] = False,
+ confirmation_prompt: bool = False,
+ prompt_required: bool = True,
+ hide_input: bool = False,
+ # TODO: remove is_flag and flag_value in a future release
+ is_flag: Optional[bool] = None,
+ flag_value: Optional[Any] = None,
+ count: bool = False,
+ allow_from_autoenv: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+) -> Any: ...
+
+
+# Overload for Option created with custom type 'click_type'
+@overload
+def Option(
+ # Parameter
+ default: Optional[Any] = ...,
+ *param_decls: str,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ click_type: Optional[click.ParamType] = None,
+ # Option
+ show_default: Union[bool, str] = True,
+ prompt: Union[bool, str] = False,
+ confirmation_prompt: bool = False,
+ prompt_required: bool = True,
+ hide_input: bool = False,
+ # TODO: remove is_flag and flag_value in a future release
+ is_flag: Optional[bool] = None,
+ flag_value: Optional[Any] = None,
+ count: bool = False,
+ allow_from_autoenv: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+) -> Any: ...
+
+
+def Option(
+ # Parameter
+ default: Optional[Any] = ...,
+ *param_decls: str,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ parser: Optional[Callable[[str], Any]] = None,
+ click_type: Optional[click.ParamType] = None,
+ # Option
+ show_default: Union[bool, str] = True,
+ prompt: Union[bool, str] = False,
+ confirmation_prompt: bool = False,
+ prompt_required: bool = True,
+ hide_input: bool = False,
+ # TODO: remove is_flag and flag_value in a future release
+ is_flag: Optional[bool] = None,
+ flag_value: Optional[Any] = None,
+ count: bool = False,
+ allow_from_autoenv: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+) -> Any:
+ return OptionInfo(
+ # Parameter
+ default=default,
+ param_decls=param_decls,
+ callback=callback,
+ metavar=metavar,
+ expose_value=expose_value,
+ is_eager=is_eager,
+ envvar=envvar,
+ shell_complete=shell_complete,
+ autocompletion=autocompletion,
+ default_factory=default_factory,
+ # Custom type
+ parser=parser,
+ click_type=click_type,
+ # Option
+ show_default=show_default,
+ prompt=prompt,
+ confirmation_prompt=confirmation_prompt,
+ prompt_required=prompt_required,
+ hide_input=hide_input,
+ is_flag=is_flag,
+ flag_value=flag_value,
+ count=count,
+ allow_from_autoenv=allow_from_autoenv,
+ help=help,
+ hidden=hidden,
+ show_choices=show_choices,
+ show_envvar=show_envvar,
+ # Choice
+ case_sensitive=case_sensitive,
+ # Numbers
+ min=min,
+ max=max,
+ clamp=clamp,
+ # DateTime
+ formats=formats,
+ # File
+ mode=mode,
+ encoding=encoding,
+ errors=errors,
+ lazy=lazy,
+ atomic=atomic,
+ # Path
+ exists=exists,
+ file_okay=file_okay,
+ dir_okay=dir_okay,
+ writable=writable,
+ readable=readable,
+ resolve_path=resolve_path,
+ allow_dash=allow_dash,
+ path_type=path_type,
+ # Rich settings
+ rich_help_panel=rich_help_panel,
+ )
+
+
+# Overload for Argument created with custom type 'parser'
+@overload
+def Argument(
+ # Parameter
+ default: Optional[Any] = ...,
+ *,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ parser: Optional[Callable[[str], Any]] = None,
+ # TyperArgument
+ show_default: Union[bool, str] = True,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+) -> Any: ...
+
+
+# Overload for Argument created with custom type 'click_type'
+@overload
+def Argument(
+ # Parameter
+ default: Optional[Any] = ...,
+ *,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ click_type: Optional[click.ParamType] = None,
+ # TyperArgument
+ show_default: Union[bool, str] = True,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+) -> Any: ...
+
+
+def Argument(
+ # Parameter
+ default: Optional[Any] = ...,
+ *,
+ callback: Optional[Callable[..., Any]] = None,
+ metavar: Optional[str] = None,
+ expose_value: bool = True,
+ is_eager: bool = False,
+ envvar: Optional[Union[str, list[str]]] = None,
+ # 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: Optional[
+ Callable[
+ [click.Context, click.Parameter, str],
+ Union[list["click.shell_completion.CompletionItem"], list[str]],
+ ]
+ ] = None,
+ autocompletion: Optional[Callable[..., Any]] = None,
+ default_factory: Optional[Callable[[], Any]] = None,
+ # Custom type
+ parser: Optional[Callable[[str], Any]] = None,
+ click_type: Optional[click.ParamType] = None,
+ # TyperArgument
+ show_default: Union[bool, str] = True,
+ show_choices: bool = True,
+ show_envvar: bool = True,
+ help: Optional[str] = None,
+ hidden: bool = False,
+ # Choice
+ case_sensitive: bool = True,
+ # Numbers
+ min: Optional[Union[int, float]] = None,
+ max: Optional[Union[int, float]] = None,
+ clamp: bool = False,
+ # DateTime
+ formats: Optional[list[str]] = None,
+ # File
+ mode: Optional[str] = None,
+ encoding: Optional[str] = None,
+ errors: Optional[str] = "strict",
+ lazy: Optional[bool] = None,
+ atomic: bool = False,
+ # Path
+ 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: Union[None, type[str], type[bytes]] = None,
+ # Rich settings
+ rich_help_panel: Union[str, None] = None,
+) -> Any:
+ return ArgumentInfo(
+ # Parameter
+ default=default,
+ # Arguments can only have one param declaration
+ # it will be generated from the param name
+ param_decls=None,
+ callback=callback,
+ metavar=metavar,
+ expose_value=expose_value,
+ is_eager=is_eager,
+ envvar=envvar,
+ shell_complete=shell_complete,
+ autocompletion=autocompletion,
+ default_factory=default_factory,
+ # Custom type
+ parser=parser,
+ click_type=click_type,
+ # TyperArgument
+ show_default=show_default,
+ show_choices=show_choices,
+ show_envvar=show_envvar,
+ help=help,
+ hidden=hidden,
+ # Choice
+ case_sensitive=case_sensitive,
+ # Numbers
+ min=min,
+ max=max,
+ clamp=clamp,
+ # DateTime
+ formats=formats,
+ # File
+ mode=mode,
+ encoding=encoding,
+ errors=errors,
+ lazy=lazy,
+ atomic=atomic,
+ # Path
+ exists=exists,
+ file_okay=file_okay,
+ dir_okay=dir_okay,
+ writable=writable,
+ readable=readable,
+ resolve_path=resolve_path,
+ allow_dash=allow_dash,
+ path_type=path_type,
+ # Rich settings
+ rich_help_panel=rich_help_panel,
+ )
diff --git a/contrib/python/typer-slim/typer/py.typed b/contrib/python/typer-slim/typer/py.typed
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/contrib/python/typer-slim/typer/py.typed
diff --git a/contrib/python/typer-slim/typer/rich_utils.py b/contrib/python/typer-slim/typer/rich_utils.py
new file mode 100644
index 00000000000..ad110cb8d69
--- /dev/null
+++ b/contrib/python/typer-slim/typer/rich_utils.py
@@ -0,0 +1,752 @@
+# Extracted and modified from https://github.com/ewels/rich-click
+
+import inspect
+import io
+from collections import defaultdict
+from collections.abc import Iterable
+from gettext import gettext as _
+from os import getenv
+from typing import Any, Literal, Optional, Union
+
+import click
+from rich import box
+from rich.align import Align
+from rich.columns import Columns
+from rich.console import Console, RenderableType, group
+from rich.emoji import Emoji
+from rich.highlighter import RegexHighlighter
+from rich.markdown import Markdown
+from rich.markup import escape
+from rich.padding import Padding
+from rich.panel import Panel
+from rich.table import Table
+from rich.text import Text
+from rich.theme import Theme
+from rich.traceback import Traceback
+from typer.models import DeveloperExceptionConfig
+
+# Default styles
+STYLE_OPTION = "bold cyan"
+STYLE_SWITCH = "bold green"
+STYLE_NEGATIVE_OPTION = "bold magenta"
+STYLE_NEGATIVE_SWITCH = "bold red"
+STYLE_METAVAR = "bold yellow"
+STYLE_METAVAR_SEPARATOR = "dim"
+STYLE_USAGE = "yellow"
+STYLE_USAGE_COMMAND = "bold"
+STYLE_DEPRECATED = "red"
+STYLE_DEPRECATED_COMMAND = "dim"
+STYLE_HELPTEXT_FIRST_LINE = ""
+STYLE_HELPTEXT = "dim"
+STYLE_OPTION_HELP = ""
+STYLE_OPTION_DEFAULT = "dim"
+STYLE_OPTION_ENVVAR = "dim yellow"
+STYLE_REQUIRED_SHORT = "red"
+STYLE_REQUIRED_LONG = "dim red"
+STYLE_OPTIONS_PANEL_BORDER = "dim"
+ALIGN_OPTIONS_PANEL: Literal["left", "center", "right"] = "left"
+STYLE_OPTIONS_TABLE_SHOW_LINES = False
+STYLE_OPTIONS_TABLE_LEADING = 0
+STYLE_OPTIONS_TABLE_PAD_EDGE = False
+STYLE_OPTIONS_TABLE_PADDING = (0, 1)
+STYLE_OPTIONS_TABLE_BOX = ""
+STYLE_OPTIONS_TABLE_ROW_STYLES = None
+STYLE_OPTIONS_TABLE_BORDER_STYLE = None
+STYLE_COMMANDS_PANEL_BORDER = "dim"
+ALIGN_COMMANDS_PANEL: Literal["left", "center", "right"] = "left"
+STYLE_COMMANDS_TABLE_SHOW_LINES = False
+STYLE_COMMANDS_TABLE_LEADING = 0
+STYLE_COMMANDS_TABLE_PAD_EDGE = False
+STYLE_COMMANDS_TABLE_PADDING = (0, 1)
+STYLE_COMMANDS_TABLE_BOX = ""
+STYLE_COMMANDS_TABLE_ROW_STYLES = None
+STYLE_COMMANDS_TABLE_BORDER_STYLE = None
+STYLE_COMMANDS_TABLE_FIRST_COLUMN = "bold cyan"
+STYLE_ERRORS_PANEL_BORDER = "red"
+ALIGN_ERRORS_PANEL: Literal["left", "center", "right"] = "left"
+STYLE_ERRORS_SUGGESTION = "dim"
+STYLE_ABORTED = "red"
+_TERMINAL_WIDTH = getenv("TERMINAL_WIDTH")
+MAX_WIDTH = int(_TERMINAL_WIDTH) if _TERMINAL_WIDTH else None
+COLOR_SYSTEM: Optional[Literal["auto", "standard", "256", "truecolor", "windows"]] = (
+ "auto" # Set to None to disable colors
+)
+_TYPER_FORCE_DISABLE_TERMINAL = getenv("_TYPER_FORCE_DISABLE_TERMINAL")
+FORCE_TERMINAL = (
+ True
+ if getenv("GITHUB_ACTIONS") or getenv("FORCE_COLOR") or getenv("PY_COLORS")
+ else None
+)
+if _TYPER_FORCE_DISABLE_TERMINAL:
+ FORCE_TERMINAL = False
+
+# Fixed strings
+DEPRECATED_STRING = _("(deprecated) ")
+DEFAULT_STRING = _("[default: {}]")
+ENVVAR_STRING = _("[env var: {}]")
+REQUIRED_SHORT_STRING = "*"
+REQUIRED_LONG_STRING = _("[required]")
+RANGE_STRING = " [{}]"
+ARGUMENTS_PANEL_TITLE = _("Arguments")
+OPTIONS_PANEL_TITLE = _("Options")
+COMMANDS_PANEL_TITLE = _("Commands")
+ERRORS_PANEL_TITLE = _("Error")
+ABORTED_TEXT = _("Aborted.")
+RICH_HELP = _("Try [blue]'{command_path} {help_option}'[/] for help.")
+
+MARKUP_MODE_MARKDOWN = "markdown"
+MARKUP_MODE_RICH = "rich"
+_RICH_HELP_PANEL_NAME = "rich_help_panel"
+
+MarkupModeStrict = Literal["markdown", "rich"]
+
+
+# Rich regex highlighter
+class OptionHighlighter(RegexHighlighter):
+ """Highlights our special options."""
+
+ highlights = [
+ r"(^|\W)(?P<switch>\-\w+)(?![a-zA-Z0-9])",
+ r"(^|\W)(?P<option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
+ r"(?P<metavar>\<[^\>]+\>)",
+ r"(?P<usage>Usage: )",
+ ]
+
+
+class NegativeOptionHighlighter(RegexHighlighter):
+ highlights = [
+ r"(^|\W)(?P<negative_switch>\-\w+)(?![a-zA-Z0-9])",
+ r"(^|\W)(?P<negative_option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
+ ]
+
+
+highlighter = OptionHighlighter()
+negative_highlighter = NegativeOptionHighlighter()
+
+
+def _get_rich_console(stderr: bool = False) -> Console:
+ return Console(
+ theme=Theme(
+ {
+ "option": STYLE_OPTION,
+ "switch": STYLE_SWITCH,
+ "negative_option": STYLE_NEGATIVE_OPTION,
+ "negative_switch": STYLE_NEGATIVE_SWITCH,
+ "metavar": STYLE_METAVAR,
+ "metavar_sep": STYLE_METAVAR_SEPARATOR,
+ "usage": STYLE_USAGE,
+ },
+ ),
+ highlighter=highlighter,
+ color_system=COLOR_SYSTEM,
+ force_terminal=FORCE_TERMINAL,
+ width=MAX_WIDTH,
+ stderr=stderr,
+ )
+
+
+def _make_rich_text(
+ *, text: str, style: str = "", markup_mode: MarkupModeStrict
+) -> Union[Markdown, Text]:
+ """Take a string, remove indentations, and return styled text.
+
+ If `markup_mode` is `"rich"`, the text is parsed for Rich markup strings.
+ If `markup_mode` is `"markdown"`, parse as Markdown.
+ """
+ # Remove indentations from input text
+ text = inspect.cleandoc(text)
+ if markup_mode == MARKUP_MODE_MARKDOWN:
+ text = Emoji.replace(text)
+ return Markdown(text, style=style)
+ else:
+ assert markup_mode == MARKUP_MODE_RICH
+ return highlighter(Text.from_markup(text, style=style))
+
+
+@group()
+def _get_help_text(
+ *,
+ obj: Union[click.Command, click.Group],
+ markup_mode: MarkupModeStrict,
+) -> Iterable[Union[Markdown, Text]]:
+ """Build primary help text for a click command or group.
+
+ Returns the prose help text for a command or group, rendered either as a
+ Rich Text object or as Markdown.
+ If the command is marked as deprecated, the deprecated string will be prepended.
+ """
+ # Prepend deprecated status
+ if obj.deprecated:
+ yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)
+
+ # Fetch and dedent the help text
+ help_text = inspect.cleandoc(obj.help or "")
+
+ # Trim off anything that comes after \f on its own line
+ help_text = help_text.partition("\f")[0]
+
+ # Get the first paragraph
+ first_line, *remaining_paragraphs = help_text.split("\n\n")
+
+ # Remove single linebreaks
+ if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"):
+ first_line = first_line.replace("\n", " ")
+ yield _make_rich_text(
+ text=first_line.strip(),
+ style=STYLE_HELPTEXT_FIRST_LINE,
+ markup_mode=markup_mode,
+ )
+
+ # Get remaining lines, remove single line breaks and format as dim
+ if remaining_paragraphs:
+ # Add a newline inbetween the header and the remaining paragraphs
+ yield Text("")
+ # Join with double linebreaks for markdown and Rich markup
+ remaining_lines = "\n\n".join(remaining_paragraphs)
+
+ yield _make_rich_text(
+ text=remaining_lines,
+ style=STYLE_HELPTEXT,
+ markup_mode=markup_mode,
+ )
+
+
+def _get_parameter_help(
+ *,
+ param: Union[click.Option, click.Argument, click.Parameter],
+ ctx: click.Context,
+ markup_mode: MarkupModeStrict,
+) -> Columns:
+ """Build primary help text for a click option or argument.
+
+ Returns the prose help text for an option or argument, rendered either
+ as a Rich Text object or as Markdown.
+ Additional elements are appended to show the default and required status if
+ applicable.
+ """
+ # import here to avoid cyclic imports
+ from .core import TyperArgument, TyperOption
+
+ items: list[Union[Text, Markdown]] = []
+
+ # Get the environment variable first
+
+ envvar = getattr(param, "envvar", None)
+ var_str = ""
+ # https://github.com/pallets/click/blob/0aec1168ac591e159baf6f61026d6ae322c53aaf/src/click/core.py#L2720-L2726
+ if envvar is None:
+ if (
+ getattr(param, "allow_from_autoenv", None)
+ and getattr(ctx, "auto_envvar_prefix", None) is not None
+ and param.name is not None
+ ):
+ envvar = f"{ctx.auto_envvar_prefix}_{param.name.upper()}"
+ if envvar is not None:
+ var_str = (
+ envvar if isinstance(envvar, str) else ", ".join(str(d) for d in envvar)
+ )
+
+ # Main help text
+ help_value: Union[str, None] = getattr(param, "help", None)
+ if help_value:
+ paragraphs = help_value.split("\n\n")
+ # Remove single linebreaks
+ if markup_mode != MARKUP_MODE_MARKDOWN:
+ paragraphs = [
+ x.replace("\n", " ").strip()
+ if not x.startswith("\b")
+ else "{}\n".format(x.strip("\b\n"))
+ for x in paragraphs
+ ]
+ items.append(
+ _make_rich_text(
+ text="\n".join(paragraphs).strip(),
+ style=STYLE_OPTION_HELP,
+ markup_mode=markup_mode,
+ )
+ )
+
+ # Environment variable AFTER help text
+ if envvar and getattr(param, "show_envvar", None):
+ items.append(Text(ENVVAR_STRING.format(var_str), style=STYLE_OPTION_ENVVAR))
+
+ # Default value
+ # This uses Typer's specific param._get_default_string
+ if isinstance(param, (TyperOption, TyperArgument)):
+ default_value = param._extract_default_help_str(ctx=ctx)
+ show_default_is_str = isinstance(param.show_default, str)
+ if show_default_is_str or (
+ default_value is not None and (param.show_default or ctx.show_default)
+ ):
+ default_str = param._get_default_string(
+ ctx=ctx,
+ show_default_is_str=show_default_is_str,
+ default_value=default_value,
+ )
+ if default_str:
+ items.append(
+ Text(
+ DEFAULT_STRING.format(default_str),
+ style=STYLE_OPTION_DEFAULT,
+ )
+ )
+
+ # Required?
+ if param.required:
+ items.append(Text(REQUIRED_LONG_STRING, style=STYLE_REQUIRED_LONG))
+
+ # Use Columns - this allows us to group different renderable types
+ # (Text, Markdown) onto a single line.
+ return Columns(items)
+
+
+def _make_command_help(
+ *,
+ help_text: str,
+ markup_mode: MarkupModeStrict,
+) -> Union[Text, Markdown]:
+ """Build cli help text for a click group command.
+
+ That is, when calling help on groups with multiple subcommands
+ (not the main help text when calling the subcommand help).
+
+ Returns the first paragraph of help text for a command, rendered either as a
+ Rich Text object or as Markdown.
+ Ignores single newlines as paragraph markers, looks for double only.
+ """
+ paragraphs = inspect.cleandoc(help_text).split("\n\n")
+ # Remove single linebreaks
+ if markup_mode != MARKUP_MODE_RICH and not paragraphs[0].startswith("\b"):
+ paragraphs[0] = paragraphs[0].replace("\n", " ")
+ elif paragraphs[0].startswith("\b"):
+ paragraphs[0] = paragraphs[0].replace("\b\n", "")
+ return _make_rich_text(
+ text=paragraphs[0].strip(),
+ style=STYLE_OPTION_HELP,
+ markup_mode=markup_mode,
+ )
+
+
+def _print_options_panel(
+ *,
+ name: str,
+ params: Union[list[click.Option], list[click.Argument]],
+ ctx: click.Context,
+ markup_mode: MarkupModeStrict,
+ console: Console,
+) -> None:
+ options_rows: list[list[RenderableType]] = []
+ required_rows: list[Union[str, Text]] = []
+ for param in params:
+ # Short and long form
+ opt_long_strs = []
+ opt_short_strs = []
+ secondary_opt_long_strs = []
+ secondary_opt_short_strs = []
+ for opt_str in param.opts:
+ if "--" in opt_str:
+ opt_long_strs.append(opt_str)
+ else:
+ opt_short_strs.append(opt_str)
+ for opt_str in param.secondary_opts:
+ if "--" in opt_str:
+ secondary_opt_long_strs.append(opt_str)
+ else:
+ secondary_opt_short_strs.append(opt_str)
+
+ # Column for a metavar, if we have one
+ metavar = Text(style=STYLE_METAVAR, overflow="fold")
+ # TODO: when deprecating Click < 8.2, make ctx required
+ signature = inspect.signature(param.make_metavar)
+ if "ctx" in signature.parameters:
+ metavar_str = param.make_metavar(ctx=ctx)
+ else:
+ # Click < 8.2
+ metavar_str = param.make_metavar() # type: ignore[call-arg]
+
+ # Do it ourselves if this is a positional argument
+ if (
+ isinstance(param, click.Argument)
+ and param.name
+ and metavar_str == param.name.upper()
+ ):
+ metavar_str = param.type.name.upper()
+
+ # Skip booleans and choices (handled above)
+ if metavar_str != "BOOLEAN":
+ metavar.append(metavar_str)
+
+ # Range - from
+ # 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)
+ and not (param.count and param.type.min == 0 and param.type.max is None)
+ ):
+ range_str = param.type._describe_range()
+ if range_str:
+ metavar.append(RANGE_STRING.format(range_str))
+
+ # Required asterisk
+ required: Union[str, Text] = ""
+ if param.required:
+ required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT)
+
+ # Highlighter to make [ | ] and <> dim
+ class MetavarHighlighter(RegexHighlighter):
+ highlights = [
+ r"^(?P<metavar_sep>(\[|<))",
+ r"(?P<metavar_sep>\|)",
+ r"(?P<metavar_sep>(\]|>)$)",
+ ]
+
+ metavar_highlighter = MetavarHighlighter()
+
+ required_rows.append(required)
+ options_rows.append(
+ [
+ highlighter(",".join(opt_long_strs)),
+ highlighter(",".join(opt_short_strs)),
+ negative_highlighter(",".join(secondary_opt_long_strs)),
+ negative_highlighter(",".join(secondary_opt_short_strs)),
+ metavar_highlighter(metavar),
+ _get_parameter_help(
+ param=param,
+ ctx=ctx,
+ markup_mode=markup_mode,
+ ),
+ ]
+ )
+ rows_with_required: list[list[RenderableType]] = []
+ if any(required_rows):
+ for required, row in zip(required_rows, options_rows):
+ rows_with_required.append([required, *row])
+ else:
+ rows_with_required = options_rows
+ if options_rows:
+ t_styles: dict[str, Any] = {
+ "show_lines": STYLE_OPTIONS_TABLE_SHOW_LINES,
+ "leading": STYLE_OPTIONS_TABLE_LEADING,
+ "box": STYLE_OPTIONS_TABLE_BOX,
+ "border_style": STYLE_OPTIONS_TABLE_BORDER_STYLE,
+ "row_styles": STYLE_OPTIONS_TABLE_ROW_STYLES,
+ "pad_edge": STYLE_OPTIONS_TABLE_PAD_EDGE,
+ "padding": STYLE_OPTIONS_TABLE_PADDING,
+ }
+ box_style = getattr(box, t_styles.pop("box"), None)
+
+ options_table = Table(
+ highlight=True,
+ show_header=False,
+ expand=True,
+ box=box_style,
+ **t_styles,
+ )
+ for row in rows_with_required:
+ options_table.add_row(*row)
+ console.print(
+ Panel(
+ options_table,
+ border_style=STYLE_OPTIONS_PANEL_BORDER,
+ title=name,
+ title_align=ALIGN_OPTIONS_PANEL,
+ )
+ )
+
+
+def _print_commands_panel(
+ *,
+ name: str,
+ commands: list[click.Command],
+ markup_mode: MarkupModeStrict,
+ console: Console,
+ cmd_len: int,
+) -> None:
+ t_styles: dict[str, Any] = {
+ "show_lines": STYLE_COMMANDS_TABLE_SHOW_LINES,
+ "leading": STYLE_COMMANDS_TABLE_LEADING,
+ "box": STYLE_COMMANDS_TABLE_BOX,
+ "border_style": STYLE_COMMANDS_TABLE_BORDER_STYLE,
+ "row_styles": STYLE_COMMANDS_TABLE_ROW_STYLES,
+ "pad_edge": STYLE_COMMANDS_TABLE_PAD_EDGE,
+ "padding": STYLE_COMMANDS_TABLE_PADDING,
+ }
+ box_style = getattr(box, t_styles.pop("box"), None)
+
+ commands_table = Table(
+ highlight=False,
+ show_header=False,
+ expand=True,
+ box=box_style,
+ **t_styles,
+ )
+ # Define formatting in first column, as commands don't match highlighter
+ # regex
+ commands_table.add_column(
+ style=STYLE_COMMANDS_TABLE_FIRST_COLUMN,
+ no_wrap=True,
+ width=cmd_len,
+ )
+
+ # A big ratio makes the description column be greedy and take all the space
+ # available instead of allowing the command column to grow and misalign with
+ # other panels.
+ commands_table.add_column("Description", justify="left", no_wrap=False, ratio=10)
+ rows: list[list[Union[RenderableType, None]]] = []
+ deprecated_rows: list[Union[RenderableType, None]] = []
+ for command in commands:
+ helptext = command.short_help or command.help or ""
+ command_name = command.name or ""
+ if command.deprecated:
+ command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND)
+ deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED))
+ else:
+ command_name_text = Text(command_name)
+ deprecated_rows.append(None)
+ rows.append(
+ [
+ command_name_text,
+ _make_command_help(
+ help_text=helptext,
+ markup_mode=markup_mode,
+ ),
+ ]
+ )
+ rows_with_deprecated = rows
+ if any(deprecated_rows):
+ rows_with_deprecated = []
+ for row, deprecated_text in zip(rows, deprecated_rows):
+ rows_with_deprecated.append([*row, deprecated_text])
+ for row in rows_with_deprecated:
+ commands_table.add_row(*row)
+ if commands_table.row_count:
+ console.print(
+ Panel(
+ commands_table,
+ border_style=STYLE_COMMANDS_PANEL_BORDER,
+ title=name,
+ title_align=ALIGN_COMMANDS_PANEL,
+ )
+ )
+
+
+def rich_format_help(
+ *,
+ obj: Union[click.Command, click.Group],
+ ctx: click.Context,
+ markup_mode: MarkupModeStrict,
+) -> None:
+ """Print nicely formatted help text using rich.
+
+ Based on original code from rich-cli, by @willmcgugan.
+ https://github.com/Textualize/rich-cli/blob/8a2767c7a340715fc6fbf4930ace717b9b2fc5e5/src/rich_cli/__main__.py#L162-L236
+
+ Replacement for the click function format_help().
+ Takes a command or group and builds the help text output.
+ """
+ console = _get_rich_console()
+
+ # Print usage
+ console.print(
+ Padding(highlighter(obj.get_usage(ctx)), 1), style=STYLE_USAGE_COMMAND
+ )
+
+ # Print command / group help if we have some
+ if obj.help:
+ # Print with some padding
+ console.print(
+ Padding(
+ Align(
+ _get_help_text(
+ obj=obj,
+ markup_mode=markup_mode,
+ ),
+ pad=False,
+ ),
+ (0, 1, 1, 1),
+ )
+ )
+ panel_to_arguments: defaultdict[str, list[click.Argument]] = defaultdict(list)
+ panel_to_options: defaultdict[str, list[click.Option]] = 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):
+ 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):
+ panel_name = (
+ getattr(param, _RICH_HELP_PANEL_NAME, None) or OPTIONS_PANEL_TITLE
+ )
+ panel_to_options[panel_name].append(param)
+ default_arguments = panel_to_arguments.get(ARGUMENTS_PANEL_TITLE, [])
+ _print_options_panel(
+ name=ARGUMENTS_PANEL_TITLE,
+ params=default_arguments,
+ ctx=ctx,
+ markup_mode=markup_mode,
+ console=console,
+ )
+ for panel_name, arguments in panel_to_arguments.items():
+ if panel_name == ARGUMENTS_PANEL_TITLE:
+ # Already printed above
+ continue
+ _print_options_panel(
+ name=panel_name,
+ params=arguments,
+ ctx=ctx,
+ markup_mode=markup_mode,
+ console=console,
+ )
+ default_options = panel_to_options.get(OPTIONS_PANEL_TITLE, [])
+ _print_options_panel(
+ name=OPTIONS_PANEL_TITLE,
+ params=default_options,
+ ctx=ctx,
+ markup_mode=markup_mode,
+ console=console,
+ )
+ for panel_name, options in panel_to_options.items():
+ if panel_name == OPTIONS_PANEL_TITLE:
+ # Already printed above
+ continue
+ _print_options_panel(
+ name=panel_name,
+ params=options,
+ ctx=ctx,
+ markup_mode=markup_mode,
+ console=console,
+ )
+
+ if isinstance(obj, click.Group):
+ 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:
+ panel_name = (
+ getattr(command, _RICH_HELP_PANEL_NAME, None)
+ or COMMANDS_PANEL_TITLE
+ )
+ panel_to_commands[panel_name].append(command)
+
+ # Identify the longest command name in all panels
+ max_cmd_len = max(
+ [
+ len(command.name or "")
+ for commands in panel_to_commands.values()
+ for command in commands
+ ],
+ default=0,
+ )
+
+ # Print each command group panel
+ default_commands = panel_to_commands.get(COMMANDS_PANEL_TITLE, [])
+ _print_commands_panel(
+ name=COMMANDS_PANEL_TITLE,
+ commands=default_commands,
+ markup_mode=markup_mode,
+ console=console,
+ cmd_len=max_cmd_len,
+ )
+ for panel_name, commands in panel_to_commands.items():
+ if panel_name == COMMANDS_PANEL_TITLE:
+ # Already printed above
+ continue
+ _print_commands_panel(
+ name=panel_name,
+ commands=commands,
+ markup_mode=markup_mode,
+ console=console,
+ cmd_len=max_cmd_len,
+ )
+
+ # Epilogue if we have it
+ if obj.epilog:
+ # Remove single linebreaks, replace double with single
+ lines = obj.epilog.split("\n\n")
+ epilogue = "\n".join([x.replace("\n", " ").strip() for x in lines])
+ epilogue_text = _make_rich_text(text=epilogue, markup_mode=markup_mode)
+ console.print(Padding(Align(epilogue_text, pad=False), 1))
+
+
+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.
+ """
+ # 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: Union[click.Context, None] = getattr(self, "ctx", None)
+ if ctx is not None:
+ console.print(ctx.get_usage())
+
+ if ctx is not None and ctx.command.get_help_option(ctx) is not None:
+ console.print(
+ RICH_HELP.format(
+ command_path=ctx.command_path, help_option=ctx.help_option_names[0]
+ ),
+ style=STYLE_ERRORS_SUGGESTION,
+ )
+
+ console.print(
+ Panel(
+ highlighter(self.format_message()),
+ border_style=STYLE_ERRORS_PANEL_BORDER,
+ title=ERRORS_PANEL_TITLE,
+ title_align=ALIGN_ERRORS_PANEL,
+ )
+ )
+
+
+def rich_abort_error() -> None:
+ """Print richly formatted abort error."""
+ console = _get_rich_console(stderr=True)
+ console.print(ABORTED_TEXT, style=STYLE_ABORTED)
+
+
+def escape_before_html_export(input_text: str) -> str:
+ """Ensure that the input string can be used for HTML export."""
+ return escape(input_text).strip()
+
+
+def rich_to_html(input_text: str) -> str:
+ """Print the HTML version of a rich-formatted input string.
+
+ This function does not provide a full HTML page, but can be used to insert
+ HTML-formatted text spans into a markdown file.
+ """
+ console = Console(record=True, highlight=False, file=io.StringIO())
+
+ console.print(input_text, overflow="ignore", crop=False)
+
+ return console.export_html(inline_styles=True, code_format="{code}").strip()
+
+
+def rich_render_text(text: str) -> str:
+ """Remove rich tags and render a pure text representation"""
+ console = _get_rich_console()
+ return "".join(segment.text for segment in console.render(text)).rstrip("\n")
+
+
+def get_traceback(
+ exc: BaseException,
+ exception_config: DeveloperExceptionConfig,
+ internal_dir_names: list[str],
+) -> Traceback:
+ rich_tb = Traceback.from_exception(
+ type(exc),
+ exc,
+ exc.__traceback__,
+ show_locals=exception_config.pretty_exceptions_show_locals,
+ suppress=internal_dir_names,
+ width=MAX_WIDTH,
+ )
+ return rich_tb
diff --git a/contrib/python/typer-slim/typer/testing.py b/contrib/python/typer-slim/typer/testing.py
new file mode 100644
index 00000000000..61f337b7892
--- /dev/null
+++ b/contrib/python/typer-slim/typer/testing.py
@@ -0,0 +1,30 @@
+from collections.abc import Mapping, Sequence
+from typing import IO, Any, Optional, Union
+
+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
+
+
+class CliRunner(ClickCliRunner):
+ def invoke( # type: ignore
+ self,
+ app: Typer,
+ args: Optional[Union[str, Sequence[str]]] = None,
+ input: Optional[Union[bytes, str, IO[Any]]] = None,
+ env: Optional[Mapping[str, Optional[str]]] = 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,
+ )
diff --git a/contrib/python/typer-slim/typer/utils.py b/contrib/python/typer-slim/typer/utils.py
new file mode 100644
index 00000000000..568639bade3
--- /dev/null
+++ b/contrib/python/typer-slim/typer/utils.py
@@ -0,0 +1,190 @@
+import inspect
+import sys
+from copy import copy
+from typing import Any, Callable, cast
+
+from ._typing import Annotated, get_args, get_origin, get_type_hints
+from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta
+
+
+def _param_type_to_user_string(param_type: type[ParameterInfo]) -> str:
+ # Render a `ParameterInfo` subclass for use in error messages.
+ # User code doesn't call `*Info` directly, so errors should present the classes how
+ # they were (probably) defined in the user code.
+ if param_type is OptionInfo:
+ return "`Option`"
+ elif param_type is ArgumentInfo:
+ return "`Argument`"
+ # This line shouldn't be reachable during normal use.
+ return f"`{param_type.__name__}`" # pragma: no cover
+
+
+class AnnotatedParamWithDefaultValueError(Exception):
+ argument_name: str
+ param_type: type[ParameterInfo]
+
+ def __init__(self, argument_name: str, param_type: type[ParameterInfo]):
+ self.argument_name = argument_name
+ self.param_type = param_type
+
+ def __str__(self) -> str:
+ param_type_str = _param_type_to_user_string(self.param_type)
+ return (
+ f"{param_type_str} default value cannot be set in `Annotated`"
+ f" for {self.argument_name!r}. Set the default value with `=` instead."
+ )
+
+
+class MixedAnnotatedAndDefaultStyleError(Exception):
+ argument_name: str
+ annotated_param_type: type[ParameterInfo]
+ default_param_type: type[ParameterInfo]
+
+ def __init__(
+ self,
+ argument_name: str,
+ annotated_param_type: type[ParameterInfo],
+ default_param_type: type[ParameterInfo],
+ ):
+ self.argument_name = argument_name
+ self.annotated_param_type = annotated_param_type
+ self.default_param_type = default_param_type
+
+ def __str__(self) -> str:
+ annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type)
+ default_param_type_str = _param_type_to_user_string(self.default_param_type)
+ msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and"
+ if self.annotated_param_type is self.default_param_type:
+ msg += " default value"
+ else:
+ msg += f" {default_param_type_str} as a default value"
+ msg += f" together for {self.argument_name!r}"
+ return msg
+
+
+class MultipleTyperAnnotationsError(Exception):
+ argument_name: str
+
+ def __init__(self, argument_name: str):
+ self.argument_name = argument_name
+
+ def __str__(self) -> str:
+ return (
+ "Cannot specify multiple `Annotated` Typer arguments"
+ f" for {self.argument_name!r}"
+ )
+
+
+class DefaultFactoryAndDefaultValueError(Exception):
+ argument_name: str
+ param_type: type[ParameterInfo]
+
+ def __init__(self, argument_name: str, param_type: type[ParameterInfo]):
+ self.argument_name = argument_name
+ self.param_type = param_type
+
+ def __str__(self) -> str:
+ param_type_str = _param_type_to_user_string(self.param_type)
+ return (
+ "Cannot specify `default_factory` and a default value together"
+ f" for {param_type_str}"
+ )
+
+
+def _split_annotation_from_typer_annotations(
+ base_annotation: type[Any],
+) -> tuple[type[Any], list[ParameterInfo]]:
+ if get_origin(base_annotation) is not Annotated:
+ return base_annotation, []
+ base_annotation, *maybe_typer_annotations = get_args(base_annotation)
+ return base_annotation, [
+ annotation
+ for annotation in maybe_typer_annotations
+ if isinstance(annotation, ParameterInfo)
+ ]
+
+
+def get_params_from_function(func: Callable[..., Any]) -> dict[str, ParamMeta]:
+ if sys.version_info >= (3, 10):
+ signature = inspect.signature(func, eval_str=True)
+ else:
+ signature = inspect.signature(func)
+
+ type_hints = get_type_hints(func)
+ params = {}
+ for param in signature.parameters.values():
+ annotation, typer_annotations = _split_annotation_from_typer_annotations(
+ param.annotation,
+ )
+ if len(typer_annotations) > 1:
+ raise MultipleTyperAnnotationsError(param.name)
+
+ default = param.default
+ if typer_annotations:
+ # It's something like `my_param: Annotated[str, Argument()]`
+ [parameter_info] = typer_annotations
+
+ # Forbid `my_param: Annotated[str, Argument()] = Argument("...")`
+ if isinstance(param.default, ParameterInfo):
+ raise MixedAnnotatedAndDefaultStyleError(
+ argument_name=param.name,
+ annotated_param_type=type(parameter_info),
+ default_param_type=type(param.default),
+ )
+
+ parameter_info = copy(parameter_info)
+
+ # When used as a default, `Option` takes a default value and option names
+ # as positional arguments:
+ # `Option(some_value, "--some-argument", "-s")`
+ # When used in `Annotated` (ie, what this is handling), `Option` just takes
+ # option names as positional arguments:
+ # `Option("--some-argument", "-s")`
+ # In this case, the `default` attribute of `parameter_info` is actually
+ # meant to be the first item of `param_decls`.
+ if (
+ isinstance(parameter_info, OptionInfo)
+ and parameter_info.default is not ...
+ ):
+ parameter_info.param_decls = (
+ cast(str, parameter_info.default),
+ *(parameter_info.param_decls or ()),
+ )
+ parameter_info.default = ...
+
+ # Forbid `my_param: Annotated[str, Argument('some-default')]`
+ if parameter_info.default is not ...:
+ raise AnnotatedParamWithDefaultValueError(
+ param_type=type(parameter_info),
+ argument_name=param.name,
+ )
+ if param.default is not param.empty:
+ # Put the parameter's default (set by `=`) into `parameter_info`, where
+ # typer can find it.
+ parameter_info.default = param.default
+
+ default = parameter_info
+ elif param.name in type_hints:
+ # Resolve forward references.
+ annotation = type_hints[param.name]
+
+ if isinstance(default, ParameterInfo):
+ parameter_info = copy(default)
+ # Click supports `default` as either
+ # - an actual value; or
+ # - a factory function (returning a default value.)
+ # The two are not interchangeable for static typing, so typer allows
+ # specifying `default_factory`. Move the `default_factory` into `default`
+ # so click can find it.
+ if parameter_info.default is ... and parameter_info.default_factory:
+ parameter_info.default = parameter_info.default_factory
+ elif parameter_info.default_factory:
+ raise DefaultFactoryAndDefaultValueError(
+ argument_name=param.name, param_type=type(parameter_info)
+ )
+ default = parameter_info
+
+ params[param.name] = ParamMeta(
+ name=param.name, default=default, annotation=annotation
+ )
+ return params
diff --git a/contrib/python/typer-slim/ya.make b/contrib/python/typer-slim/ya.make
new file mode 100644
index 00000000000..ebd3c585432
--- /dev/null
+++ b/contrib/python/typer-slim/ya.make
@@ -0,0 +1,47 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(0.21.1)
+
+LICENSE(MIT)
+
+PEERDIR(
+ contrib/python/click
+ contrib/python/typing-extensions
+)
+
+NO_LINT()
+
+NO_CHECK_IMPORTS(
+ typer.rich_utils
+)
+
+PY_SRCS(
+ TOP_LEVEL
+ typer/__init__.py
+ typer/__main__.py
+ typer/_completion_classes.py
+ typer/_completion_shared.py
+ typer/_types.py
+ typer/_typing.py
+ typer/cli.py
+ typer/colors.py
+ typer/completion.py
+ typer/core.py
+ typer/main.py
+ typer/models.py
+ typer/params.py
+ typer/rich_utils.py
+ typer/testing.py
+ typer/utils.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/typer-slim/
+ .dist-info/METADATA
+ .dist-info/entry_points.txt
+ typer/py.typed
+)
+
+END()
diff --git a/contrib/python/typer/.dist-info/METADATA b/contrib/python/typer/.dist-info/METADATA
new file mode 100644
index 00000000000..8fc14ea2b68
--- /dev/null
+++ b/contrib/python/typer/.dist-info/METADATA
@@ -0,0 +1,412 @@
+Metadata-Version: 2.4
+Name: typer
+Version: 0.24.1
+Summary: Typer, build great CLIs. Easy to code. Based on Python type hints.
+Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <[email protected]>
+License-Expression: MIT
+License-File: LICENSE
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Topic :: Software Development
+Classifier: Typing :: Typed
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Project-URL: Homepage, https://github.com/fastapi/typer
+Project-URL: Documentation, https://typer.tiangolo.com
+Project-URL: Repository, https://github.com/fastapi/typer
+Project-URL: Issues, https://github.com/fastapi/typer/issues
+Project-URL: Changelog, https://typer.tiangolo.com/release-notes/
+Requires-Python: >=3.10
+Requires-Dist: click>=8.2.1
+Requires-Dist: shellingham>=1.3.0
+Requires-Dist: rich>=12.3.0
+Requires-Dist: annotated-doc>=0.0.2
+Description-Content-Type: text/markdown
+
+<p align="center">
+ <a href="https://typer.tiangolo.com"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg#only-light" alt="Typer"></a>
+
+</p>
+<p align="center">
+ <em>Typer, build great CLIs. Easy to code. Based on Python type hints.</em>
+</p>
+<p align="center">
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank">
+ <img src="https://github.com/fastapi/typer/actions/workflows/test.yml/badge.svg?event=push&branch=master" alt="Test">
+</a>
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3APublish" target="_blank">
+ <img src="https://github.com/fastapi/typer/workflows/Publish/badge.svg" alt="Publish">
+</a>
+<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/typer" target="_blank">
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/typer.svg" alt="Coverage">
+<a href="https://pypi.org/project/typer" target="_blank">
+ <img src="https://img.shields.io/pypi/v/typer?color=%2334D058&label=pypi%20package" alt="Package version">
+</a>
+</p>
+
+---
+
+**Documentation**: <a href="https://typer.tiangolo.com" target="_blank">https://typer.tiangolo.com</a>
+
+**Source Code**: <a href="https://github.com/fastapi/typer" target="_blank">https://github.com/fastapi/typer</a>
+
+---
+
+Typer is a library for building <abbr title="command line interface, programs executed from a terminal">CLI</abbr> applications that users will **love using** and developers will **love creating**. Based on Python type hints.
+
+It's also a command line tool to run scripts, automatically converting them to CLI applications.
+
+The key features are:
+
+* **Intuitive to write**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs.
+* **Easy to use**: It's easy to use for the final users. Automatic help, and automatic completion for all shells.
+* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
+* **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**.
+* **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments.
+* **Run scripts**: Typer includes a `typer` command/program that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally.
+
+## 2026 February - Typer developer survey
+
+Help us define Typer's future by filling the <a href="https://forms.gle/nYvutPrVkmBQZLas7" class="external-link" target="_blank">Typer developer survey</a>. โœจ
+
+## FastAPI of CLIs
+
+**Typer** is <a href="https://fastapi.tiangolo.com" class="external-link" target="_blank">FastAPI</a>'s little sibling, it's the FastAPI of CLIs.
+
+## Installation
+
+Create and activate a <a href="https://typer.tiangolo.com/virtual-environments/" class="external-link" target="_blank">virtual environment</a> and then install **Typer**:
+
+<div class="termy">
+
+```console
+$ pip install typer
+---> 100%
+Successfully installed typer rich shellingham
+```
+
+</div>
+
+## Example
+
+### The absolute minimum
+
+* Create a file `main.py` with:
+
+```Python
+def main(name: str):
+ print(f"Hello {name}")
+```
+
+This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application.
+
+### Run it
+
+Run your application with the `typer` command:
+
+<div class="termy">
+
+```console
+// Run your application
+$ typer main.py run
+
+// You get a nice error, you are missing NAME
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+Try 'typer [PATH_OR_MODULE] run --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ typer main.py run --help
+
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+
+Run the provided Typer app.
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ typer main.py run Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts.
+
+**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command.
+
+## Use Typer in your code
+
+Now let's start using Typer in your own code, update `main.py` with:
+
+```Python
+import typer
+
+
+def main(name: str):
+ print(f"Hello {name}")
+
+
+if __name__ == "__main__":
+ typer.run(main)
+```
+
+Now you could run it with Python directly:
+
+<div class="termy">
+
+```console
+// Run your application
+$ python main.py
+
+// You get a nice error, you are missing NAME
+Usage: main.py [OPTIONS] NAME
+Try 'main.py --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ python main.py --help
+
+Usage: main.py [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ python main.py Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+**Note**: you can also call this same script with the `typer` command, but you don't need to.
+
+## Example upgrade
+
+This was the simplest example possible.
+
+Now let's see one a bit more complex.
+
+### An example with two subcommands
+
+Modify the file `main.py`.
+
+Create a `typer.Typer()` app, and create two subcommands with their parameters.
+
+```Python hl_lines="3 6 11 20"
+import typer
+
+app = typer.Typer()
+
+
+def hello(name: str):
+ print(f"Hello {name}")
+
+
+def goodbye(name: str, formal: bool = False):
+ if formal:
+ print(f"Goodbye Ms. {name}. Have a good day.")
+ else:
+ print(f"Bye {name}!")
+
+
+if __name__ == "__main__":
+ app()
+```
+
+And that will:
+
+* Explicitly create a `typer.Typer` app.
+ * The previous `typer.run` actually creates one implicitly for you.
+* Add two subcommands with `@app.command()`.
+* Execute the `app()` itself, as if it was a function (instead of `typer.run`).
+
+### Run the upgraded example
+
+Check the new help:
+
+<div class="termy">
+
+```console
+$ python main.py --help
+
+ Usage: main.py [OPTIONS] COMMAND [ARGS]...
+
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --install-completion Install completion โ”‚
+โ”‚ for the current โ”‚
+โ”‚ shell. โ”‚
+โ”‚ --show-completion Show completion for โ”‚
+โ”‚ the current shell, โ”‚
+โ”‚ to copy it or โ”‚
+โ”‚ customize the โ”‚
+โ”‚ installation. โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ goodbye โ”‚
+โ”‚ hello โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// When you create a package you get โœจ auto-completion โœจ for free, installed with --install-completion
+
+// You have 2 subcommands (the 2 functions): goodbye and hello
+```
+
+</div>
+
+Now check the help for the `hello` command:
+
+<div class="termy">
+
+```console
+$ python main.py hello --help
+
+ Usage: main.py hello [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+```
+
+</div>
+
+And now check the help for the `goodbye` command:
+
+<div class="termy">
+
+```console
+$ python main.py goodbye --help
+
+ Usage: main.py goodbye [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --formal --no-formal [default: no-formal] โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Automatic --formal and --no-formal for the bool option ๐ŸŽ‰
+```
+
+</div>
+
+Now you can try out the new command line application:
+
+<div class="termy">
+
+```console
+// Use it with the hello command
+
+$ python main.py hello Camila
+
+Hello Camila
+
+// And with the goodbye command
+
+$ python main.py goodbye Camila
+
+Bye Camila!
+
+// And with --formal
+
+$ python main.py goodbye --formal Camila
+
+Goodbye Ms. Camila. Have a good day.
+```
+
+</div>
+
+**Note**: If your app only has one command, by default the command name is **omitted** in usage: `python main.py Camila`. However, when there are multiple commands, you must **explicitly include the command name**: `python main.py hello Camila`. See [One or Multiple Commands](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/) for more details.
+
+### Recap
+
+In summary, you declare **once** the types of parameters (*CLI arguments* and *CLI options*) as function parameters.
+
+You do that with standard modern Python types.
+
+You don't have to learn a new syntax, the methods or classes of a specific library, etc.
+
+Just standard **Python**.
+
+For example, for an `int`:
+
+```Python
+total: int
+```
+
+or for a `bool` flag:
+
+```Python
+force: bool
+```
+
+And similarly for **files**, **paths**, **enums** (choices), etc. And there are tools to create **groups of subcommands**, add metadata, extra **validation**, etc.
+
+**You get**: great editor support, including **completion** and **type checks** everywhere.
+
+**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command.
+
+For a more complete example including more features, see the <a href="https://typer.tiangolo.com/tutorial/">Tutorial - User Guide</a>.
+
+## Dependencies
+
+**Typer** stands on the shoulders of giants. It has three required dependencies:
+
+* <a href="https://click.palletsprojects.com/" class="external-link" target="_blank">Click</a>: a popular tool for building CLIs in Python. Typer is based on it.
+* <a href="https://rich.readthedocs.io/en/stable/index.html" class="external-link" target="_blank"><code>rich</code></a>: to show nicely formatted errors automatically.
+* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: to automatically detect the current shell when installing completion.
+
+### `typer-slim`
+
+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`.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/contrib/python/typer/.dist-info/entry_points.txt b/contrib/python/typer/.dist-info/entry_points.txt
new file mode 100644
index 00000000000..ca44c05a071
--- /dev/null
+++ b/contrib/python/typer/.dist-info/entry_points.txt
@@ -0,0 +1,5 @@
+[console_scripts]
+typer = typer.cli:main
+
+[gui_scripts]
+
diff --git a/contrib/python/typer/.dist-info/top_level.txt b/contrib/python/typer/.dist-info/top_level.txt
new file mode 100644
index 00000000000..ec184f68004
--- /dev/null
+++ b/contrib/python/typer/.dist-info/top_level.txt
@@ -0,0 +1 @@
+typer
diff --git a/contrib/python/typer/.yandex_meta/yamaker.yaml b/contrib/python/typer/.yandex_meta/yamaker.yaml
new file mode 100644
index 00000000000..15c0c41de02
--- /dev/null
+++ b/contrib/python/typer/.yandex_meta/yamaker.yaml
@@ -0,0 +1,4 @@
+requirements:
+ - typer-slim
+exclude:
+ - typer/*
diff --git a/contrib/python/typer/LICENSE b/contrib/python/typer/LICENSE
new file mode 100644
index 00000000000..a7694736cf3
--- /dev/null
+++ b/contrib/python/typer/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Sebastiรกn Ramรญrez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/contrib/python/typer/README.md b/contrib/python/typer/README.md
new file mode 100644
index 00000000000..8e203e31e1a
--- /dev/null
+++ b/contrib/python/typer/README.md
@@ -0,0 +1,375 @@
+<p align="center">
+ <a href="https://typer.tiangolo.com"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg#only-light" alt="Typer"></a>
+
+</p>
+<p align="center">
+ <em>Typer, build great CLIs. Easy to code. Based on Python type hints.</em>
+</p>
+<p align="center">
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank">
+ <img src="https://github.com/fastapi/typer/actions/workflows/test.yml/badge.svg?event=push&branch=master" alt="Test">
+</a>
+<a href="https://github.com/fastapi/typer/actions?query=workflow%3APublish" target="_blank">
+ <img src="https://github.com/fastapi/typer/workflows/Publish/badge.svg" alt="Publish">
+</a>
+<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/typer" target="_blank">
+ <img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/typer.svg" alt="Coverage">
+<a href="https://pypi.org/project/typer" target="_blank">
+ <img src="https://img.shields.io/pypi/v/typer?color=%2334D058&label=pypi%20package" alt="Package version">
+</a>
+</p>
+
+---
+
+**Documentation**: <a href="https://typer.tiangolo.com" target="_blank">https://typer.tiangolo.com</a>
+
+**Source Code**: <a href="https://github.com/fastapi/typer" target="_blank">https://github.com/fastapi/typer</a>
+
+---
+
+Typer is a library for building <abbr title="command line interface, programs executed from a terminal">CLI</abbr> applications that users will **love using** and developers will **love creating**. Based on Python type hints.
+
+It's also a command line tool to run scripts, automatically converting them to CLI applications.
+
+The key features are:
+
+* **Intuitive to write**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs.
+* **Easy to use**: It's easy to use for the final users. Automatic help, and automatic completion for all shells.
+* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
+* **Start simple**: The simplest example adds only 2 lines of code to your app: **1 import, 1 function call**.
+* **Grow large**: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments.
+* **Run scripts**: Typer includes a `typer` command/program that you can use to run scripts, automatically converting them to CLIs, even if they don't use Typer internally.
+
+## 2026 February - Typer developer survey
+
+Help us define Typer's future by filling the <a href="https://forms.gle/nYvutPrVkmBQZLas7" class="external-link" target="_blank">Typer developer survey</a>. โœจ
+
+## FastAPI of CLIs
+
+**Typer** is <a href="https://fastapi.tiangolo.com" class="external-link" target="_blank">FastAPI</a>'s little sibling, it's the FastAPI of CLIs.
+
+## Installation
+
+Create and activate a <a href="https://typer.tiangolo.com/virtual-environments/" class="external-link" target="_blank">virtual environment</a> and then install **Typer**:
+
+<div class="termy">
+
+```console
+$ pip install typer
+---> 100%
+Successfully installed typer rich shellingham
+```
+
+</div>
+
+## Example
+
+### The absolute minimum
+
+* Create a file `main.py` with:
+
+```Python
+def main(name: str):
+ print(f"Hello {name}")
+```
+
+This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application.
+
+### Run it
+
+Run your application with the `typer` command:
+
+<div class="termy">
+
+```console
+// Run your application
+$ typer main.py run
+
+// You get a nice error, you are missing NAME
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+Try 'typer [PATH_OR_MODULE] run --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ typer main.py run --help
+
+Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME
+
+Run the provided Typer app.
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ typer main.py run Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+This is the simplest use case, not even using Typer internally, but it can already be quite useful for simple scripts.
+
+**Note**: auto-completion works when you create a Python package and run it with `--install-completion` or when you use the `typer` command.
+
+## Use Typer in your code
+
+Now let's start using Typer in your own code, update `main.py` with:
+
+```Python
+import typer
+
+
+def main(name: str):
+ print(f"Hello {name}")
+
+
+if __name__ == "__main__":
+ typer.run(main)
+```
+
+Now you could run it with Python directly:
+
+<div class="termy">
+
+```console
+// Run your application
+$ python main.py
+
+// You get a nice error, you are missing NAME
+Usage: main.py [OPTIONS] NAME
+Try 'main.py --help' for help.
+โ•ญโ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ Missing argument 'NAME'. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+
+// You get a --help for free
+$ python main.py --help
+
+Usage: main.py [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] |
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Now pass the NAME argument
+$ python main.py Camila
+
+Hello Camila
+
+// It works! ๐ŸŽ‰
+```
+
+</div>
+
+**Note**: you can also call this same script with the `typer` command, but you don't need to.
+
+## Example upgrade
+
+This was the simplest example possible.
+
+Now let's see one a bit more complex.
+
+### An example with two subcommands
+
+Modify the file `main.py`.
+
+Create a `typer.Typer()` app, and create two subcommands with their parameters.
+
+```Python hl_lines="3 6 11 20"
+import typer
+
+app = typer.Typer()
+
+
+def hello(name: str):
+ print(f"Hello {name}")
+
+
+def goodbye(name: str, formal: bool = False):
+ if formal:
+ print(f"Goodbye Ms. {name}. Have a good day.")
+ else:
+ print(f"Bye {name}!")
+
+
+if __name__ == "__main__":
+ app()
+```
+
+And that will:
+
+* Explicitly create a `typer.Typer` app.
+ * The previous `typer.run` actually creates one implicitly for you.
+* Add two subcommands with `@app.command()`.
+* Execute the `app()` itself, as if it was a function (instead of `typer.run`).
+
+### Run the upgraded example
+
+Check the new help:
+
+<div class="termy">
+
+```console
+$ python main.py --help
+
+ Usage: main.py [OPTIONS] COMMAND [ARGS]...
+
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --install-completion Install completion โ”‚
+โ”‚ for the current โ”‚
+โ”‚ shell. โ”‚
+โ”‚ --show-completion Show completion for โ”‚
+โ”‚ the current shell, โ”‚
+โ”‚ to copy it or โ”‚
+โ”‚ customize the โ”‚
+โ”‚ installation. โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ goodbye โ”‚
+โ”‚ hello โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// When you create a package you get โœจ auto-completion โœจ for free, installed with --install-completion
+
+// You have 2 subcommands (the 2 functions): goodbye and hello
+```
+
+</div>
+
+Now check the help for the `hello` command:
+
+<div class="termy">
+
+```console
+$ python main.py hello --help
+
+ Usage: main.py hello [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --help Show this message and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+```
+
+</div>
+
+And now check the help for the `goodbye` command:
+
+<div class="termy">
+
+```console
+$ python main.py goodbye --help
+
+ Usage: main.py goodbye [OPTIONS] NAME
+
+โ•ญโ”€ Arguments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ * name TEXT [default: None] [required] โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+โ•ญโ”€ Options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
+โ”‚ --formal --no-formal [default: no-formal] โ”‚
+โ”‚ --help Show this message โ”‚
+โ”‚ and exit. โ”‚
+โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
+
+// Automatic --formal and --no-formal for the bool option ๐ŸŽ‰
+```
+
+</div>
+
+Now you can try out the new command line application:
+
+<div class="termy">
+
+```console
+// Use it with the hello command
+
+$ python main.py hello Camila
+
+Hello Camila
+
+// And with the goodbye command
+
+$ python main.py goodbye Camila
+
+Bye Camila!
+
+// And with --formal
+
+$ python main.py goodbye --formal Camila
+
+Goodbye Ms. Camila. Have a good day.
+```
+
+</div>
+
+**Note**: If your app only has one command, by default the command name is **omitted** in usage: `python main.py Camila`. However, when there are multiple commands, you must **explicitly include the command name**: `python main.py hello Camila`. See [One or Multiple Commands](https://typer.tiangolo.com/tutorial/commands/one-or-multiple/) for more details.
+
+### Recap
+
+In summary, you declare **once** the types of parameters (*CLI arguments* and *CLI options*) as function parameters.
+
+You do that with standard modern Python types.
+
+You don't have to learn a new syntax, the methods or classes of a specific library, etc.
+
+Just standard **Python**.
+
+For example, for an `int`:
+
+```Python
+total: int
+```
+
+or for a `bool` flag:
+
+```Python
+force: bool
+```
+
+And similarly for **files**, **paths**, **enums** (choices), etc. And there are tools to create **groups of subcommands**, add metadata, extra **validation**, etc.
+
+**You get**: great editor support, including **completion** and **type checks** everywhere.
+
+**Your users get**: automatic **`--help`**, **auto-completion** in their terminal (Bash, Zsh, Fish, PowerShell) when they install your package or when using the `typer` command.
+
+For a more complete example including more features, see the <a href="https://typer.tiangolo.com/tutorial/">Tutorial - User Guide</a>.
+
+## Dependencies
+
+**Typer** stands on the shoulders of giants. It has three required dependencies:
+
+* <a href="https://click.palletsprojects.com/" class="external-link" target="_blank">Click</a>: a popular tool for building CLIs in Python. Typer is based on it.
+* <a href="https://rich.readthedocs.io/en/stable/index.html" class="external-link" target="_blank"><code>rich</code></a>: to show nicely formatted errors automatically.
+* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: to automatically detect the current shell when installing completion.
+
+### `typer-slim`
+
+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`.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/contrib/python/typer/ya.make b/contrib/python/typer/ya.make
new file mode 100644
index 00000000000..48ea26ec6f6
--- /dev/null
+++ b/contrib/python/typer/ya.make
@@ -0,0 +1,26 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(0.24.1)
+
+LICENSE(MIT)
+
+PEERDIR(
+ contrib/python/annotated-doc
+ contrib/python/click
+ contrib/python/rich
+ contrib/python/shellingham
+ contrib/python/typer-slim
+)
+
+NO_LINT()
+
+RESOURCE_FILES(
+ PREFIX contrib/python/typer/
+ .dist-info/METADATA
+ .dist-info/entry_points.txt
+ .dist-info/top_level.txt
+)
+
+END()
diff --git a/contrib/python/xmltodict/py3/.dist-info/METADATA b/contrib/python/xmltodict/py3/.dist-info/METADATA
index 00cc7dab64f..d0be5149ca6 100644
--- a/contrib/python/xmltodict/py3/.dist-info/METADATA
+++ b/contrib/python/xmltodict/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: xmltodict
-Version: 1.0.3
+Version: 1.0.4
Summary: Makes working with XML feel like you are working with JSON
Author: Martin Blech
License-Expression: MIT
@@ -255,6 +255,7 @@ Convert a Python dictionary back into XML.
- `input_dict`: Dictionary to convert to XML.
- `output=None`: File-like object to write XML to; returns string if None.
- `encoding='utf-8'`: Encoding of the output XML.
+- `bytes_errors='replace'`: Error handler used when decoding byte values during unparse (for example `'replace'`, `'strict'`, `'ignore'`).
- `full_document=True`: Include XML declaration if True.
- `short_empty_elements=False`: Use short tags for empty elements (`<tag/>`).
- `attr_prefix='@'`: Prefix for dictionary keys representing attributes.
diff --git a/contrib/python/xmltodict/py3/README.md b/contrib/python/xmltodict/py3/README.md
index ff62bf49b61..80a0501974e 100644
--- a/contrib/python/xmltodict/py3/README.md
+++ b/contrib/python/xmltodict/py3/README.md
@@ -228,6 +228,7 @@ Convert a Python dictionary back into XML.
- `input_dict`: Dictionary to convert to XML.
- `output=None`: File-like object to write XML to; returns string if None.
- `encoding='utf-8'`: Encoding of the output XML.
+- `bytes_errors='replace'`: Error handler used when decoding byte values during unparse (for example `'replace'`, `'strict'`, `'ignore'`).
- `full_document=True`: Include XML declaration if True.
- `short_empty_elements=False`: Use short tags for empty elements (`<tag/>`).
- `attr_prefix='@'`: Prefix for dictionary keys representing attributes.
diff --git a/contrib/python/xmltodict/py3/tests/test_dicttoxml.py b/contrib/python/xmltodict/py3/tests/test_dicttoxml.py
index e67600e27de..873fa9075cb 100644
--- a/contrib/python/xmltodict/py3/tests/test_dicttoxml.py
+++ b/contrib/python/xmltodict/py3/tests/test_dicttoxml.py
@@ -178,6 +178,29 @@ def test_unparse_with_multiple_top_level_comments():
assert xml == "<!--t1--><!--t2--><a>1</a>"
+def test_unparse_with_bytes_comment_uses_output_encoding():
+ obj = {"#comment": b"caf\xe9", "a": "1"}
+ xml = _strip(unparse(obj, full_document=True, encoding="iso-8859-1"))
+ assert xml == "<!--caf\xe9--><a>1</a>"
+
+
+def test_unparse_invalid_bytes_comment_replaced_by_default():
+ obj = {"#comment": b"\xff", "a": "1"}
+ xml = _strip(unparse(obj, full_document=True, encoding="utf-8"))
+ assert xml == "<!--\ufffd--><a>1</a>"
+
+
+def test_unparse_rejects_invalid_bytes_comment_for_encoding_with_strict():
+ obj = {"#comment": b"\xff", "a": "1"}
+ with pytest.raises(UnicodeDecodeError, match="'utf-8' codec can't decode byte 0xff"):
+ unparse(
+ obj,
+ full_document=True,
+ encoding="utf-8",
+ bytes_errors="strict",
+ )
+
+
def test_unparse_rejects_comment_with_double_hyphen():
obj = {"#comment": "bad--comment", "a": "1"}
with pytest.raises(ValueError, match="cannot contain '--'"):
@@ -298,6 +321,39 @@ xmlns:b="http://b.com/"><x a:attr="val">1</x><a:y>2</a:y><b:z>3</b:z></root>'''
assert xml == expected_xml
+def test_xmlns_values_use_consistent_boolean_coercion():
+ xml = unparse({"root": {"@xmlns": {"a": True}}}, full_document=False)
+ assert xml == '<root xmlns:a="true"></root>'
+
+
+def test_xmlns_values_decode_bytes_with_output_encoding():
+ xml = unparse(
+ {"root": {"@xmlns": {"a": b"http://ex\xe9.com/"}}},
+ full_document=False,
+ encoding="iso-8859-1",
+ )
+ assert xml == '<root xmlns:a="http://ex\xe9.com/"></root>'
+
+
+def test_xmlns_values_replace_invalid_bytes_by_default():
+ xml = unparse(
+ {"root": {"@xmlns": {"a": b"\xff"}}},
+ full_document=False,
+ encoding="utf-8",
+ )
+ assert xml == '<root xmlns:a="\ufffd"></root>'
+
+
+def test_xmlns_values_reject_invalid_bytes_with_strict():
+ with pytest.raises(UnicodeDecodeError, match="'utf-8' codec can't decode byte 0xff"):
+ unparse(
+ {"root": {"@xmlns": {"a": b"\xff"}}},
+ full_document=False,
+ encoding="utf-8",
+ bytes_errors="strict",
+ )
+
+
def test_boolean_unparse():
expected_xml = '<?xml version="1.0" encoding="utf-8"?>\n<x>true</x>'
xml = unparse(dict(x=True))
@@ -307,6 +363,56 @@ def test_boolean_unparse():
xml = unparse(dict(x=False))
assert xml == expected_xml
+ expected_xml = '<?xml version="1.0" encoding="utf-8"?>\n<x attr="true"></x>'
+ xml = unparse({'x': {'@attr': True}})
+ assert xml == expected_xml
+
+ expected_xml = '<?xml version="1.0" encoding="utf-8"?>\n<x attr="false"></x>'
+ xml = unparse({'x': {'@attr': False}})
+ assert xml == expected_xml
+
+
+def test_unparse_bytes_in_attributes_and_cdata_use_output_encoding():
+ xml = unparse({"x": {"@attr": b"caf\xe9", "#text": b"caf\xe9"}}, full_document=False, encoding="iso-8859-1")
+ assert xml == '<x attr="caf\xe9">caf\xe9</x>'
+
+
+def test_unparse_bytes_text_node_uses_output_encoding():
+ xml = unparse({"x": b"caf\xe9"}, full_document=False, encoding="iso-8859-1")
+ assert xml == "<x>caf\xe9</x>"
+
+
+def test_unparse_bytes_text_node_with_expand_iter_uses_output_encoding():
+ xml = unparse({"x": b"caf\xe9"}, full_document=False, encoding="iso-8859-1", expand_iter="item")
+ assert xml == "<x>caf\xe9</x>"
+
+
+def test_unparse_invalid_bytes_in_attributes_and_cdata_replaced_by_default():
+ xml = unparse({"x": {"@attr": b"\xff", "#text": b"\xff"}}, full_document=False, encoding="utf-8")
+ assert xml == '<x attr="\ufffd">\ufffd</x>'
+
+
+def test_unparse_invalid_bytes_text_node_replaced_by_default():
+ xml = unparse({"x": b"\xff"}, full_document=False, encoding="utf-8")
+ assert xml == "<x>\ufffd</x>"
+
+
+def test_unparse_rejects_invalid_bytes_in_attributes_and_cdata_for_encoding_with_strict():
+ with pytest.raises(UnicodeDecodeError, match="'utf-8' codec can't decode byte 0xff"):
+ unparse({"x": {"@attr": b"\xff"}}, full_document=False, encoding="utf-8", bytes_errors="strict")
+ with pytest.raises(UnicodeDecodeError, match="'utf-8' codec can't decode byte 0xff"):
+ unparse({"x": {"#text": b"\xff"}}, full_document=False, encoding="utf-8", bytes_errors="strict")
+
+
+def test_unparse_rejects_invalid_bytes_text_node_for_encoding_with_strict():
+ with pytest.raises(UnicodeDecodeError, match="'utf-8' codec can't decode byte 0xff"):
+ unparse({"x": b"\xff"}, full_document=False, encoding="utf-8", bytes_errors="strict")
+
+
+def test_unparse_rejects_invalid_bytes_errors_handler():
+ with pytest.raises(ValueError, match="Invalid bytes_errors handler: nope"):
+ unparse({"x": {"@attr": b"\xff"}}, full_document=False, bytes_errors="nope")
+
def test_rejects_tag_name_with_angle_brackets():
# Minimal guard: disallow '<' or '>' to prevent breaking tag context
diff --git a/contrib/python/xmltodict/py3/tests/ya.make b/contrib/python/xmltodict/py3/tests/ya.make
index f70781b60ec..b54a107ca88 100644
--- a/contrib/python/xmltodict/py3/tests/ya.make
+++ b/contrib/python/xmltodict/py3/tests/ya.make
@@ -4,10 +4,7 @@ PEERDIR(
contrib/python/xmltodict
)
-TEST_SRCS(
- test_dicttoxml.py
- test_xmltodict.py
-)
+ALL_PYTEST_SRCS(RECURSIVE)
NO_LINT()
diff --git a/contrib/python/xmltodict/py3/xmltodict.py b/contrib/python/xmltodict/py3/xmltodict.py
index 6bdf947590a..6ad4e45916d 100644
--- a/contrib/python/xmltodict/py3/xmltodict.py
+++ b/contrib/python/xmltodict/py3/xmltodict.py
@@ -6,6 +6,7 @@ from xml.sax.saxutils import XMLGenerator, escape
from xml.sax.xmlreader import AttributesImpl
from io import StringIO
from inspect import isgenerator
+import codecs
class ParsingInterrupted(Exception):
pass
@@ -369,15 +370,17 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False,
return handler.item
-def _convert_value_to_string(value):
+def _convert_value_to_string(value, encoding='utf-8', bytes_errors='replace'):
"""Convert a value to its string representation for XML output.
Handles boolean values consistently by converting them to lowercase.
"""
- if isinstance(value, (str, bytes)):
+ if isinstance(value, str):
return value
if isinstance(value, bool):
return "true" if value else "false"
+ if isinstance(value, (bytes, bytearray, memoryview)):
+ return bytes(value).decode(encoding, errors=bytes_errors)
return str(value)
@@ -448,6 +451,8 @@ def _emit(key, value, content_handler,
namespaces=None,
full_document=True,
expand_iter=None,
+ encoding='utf-8',
+ bytes_errors='replace',
comment_key='#comment'):
if isinstance(key, str) and key == comment_key:
comments_list = value if isinstance(value, list) else [value]
@@ -456,7 +461,9 @@ def _emit(key, value, content_handler,
for comment_text in comments_list:
if comment_text is None:
continue
- comment_text = _convert_value_to_string(comment_text)
+ comment_text = _convert_value_to_string(
+ comment_text, encoding=encoding, bytes_errors=bytes_errors
+ )
if not comment_text:
continue
if pretty:
@@ -474,7 +481,7 @@ def _emit(key, value, content_handler,
key, value = result
# Minimal validation to avoid breaking out of tag context
_validate_name(key, "element")
- if not hasattr(value, '__iter__') or isinstance(value, (str, dict)):
+ if not hasattr(value, '__iter__') or isinstance(value, (str, bytes, bytearray, memoryview, dict)):
value = [value]
for index, v in enumerate(value):
if full_document and depth == 0 and index > 0:
@@ -482,10 +489,10 @@ def _emit(key, value, content_handler,
if v is None:
v = {}
elif not isinstance(v, (dict, str)):
- if expand_iter and hasattr(v, '__iter__'):
+ if expand_iter and hasattr(v, '__iter__') and not isinstance(v, (bytes, bytearray, memoryview)):
v = {expand_iter: v}
else:
- v = _convert_value_to_string(v)
+ v = _convert_value_to_string(v, encoding=encoding, bytes_errors=bytes_errors)
if isinstance(v, str):
v = {cdata_key: v}
cdata = None
@@ -496,7 +503,7 @@ def _emit(key, value, content_handler,
if iv is None:
cdata = None
else:
- cdata = _convert_value_to_string(iv)
+ cdata = _convert_value_to_string(iv, encoding=encoding, bytes_errors=bytes_errors)
continue
if isinstance(ik, str) and ik.startswith(attr_prefix):
ik = _process_namespace(ik, namespaces, namespace_separator,
@@ -505,12 +512,14 @@ def _emit(key, value, content_handler,
for k, v in iv.items():
_validate_name(k, "attribute")
attr = 'xmlns{}'.format(f':{k}' if k else '')
- attrs[attr] = '' if v is None else str(v)
+ attrs[attr] = '' if v is None else _convert_value_to_string(
+ v, encoding=encoding, bytes_errors=bytes_errors
+ )
continue
if iv is None:
iv = ''
elif not isinstance(iv, str):
- iv = str(iv)
+ iv = _convert_value_to_string(iv, encoding=encoding, bytes_errors=bytes_errors)
attr_name = ik[len(attr_prefix) :]
_validate_name(attr_name, "attribute")
attrs[attr_name] = iv
@@ -530,7 +539,8 @@ def _emit(key, value, content_handler,
attr_prefix, cdata_key, depth+1, preprocessor,
pretty, newl, indent, namespaces=namespaces,
namespace_separator=namespace_separator,
- expand_iter=expand_iter, comment_key=comment_key)
+ expand_iter=expand_iter, encoding=encoding,
+ bytes_errors=bytes_errors, comment_key=comment_key)
if cdata is not None:
content_handler.characters(cdata)
if pretty and children:
@@ -565,8 +575,16 @@ def unparse(input_dict, output=None, encoding='utf-8', full_document=True,
The `pretty` parameter (default=`False`) enables pretty-printing. In this
mode, lines are terminated with `'\n'` and indented with `'\t'`, but this
can be customized with the `newl` and `indent` parameters.
+ The `bytes_errors` parameter controls decoding errors for byte values and
+ defaults to `'replace'`.
"""
+ bytes_errors = kwargs.pop('bytes_errors', 'replace')
+ try:
+ codecs.lookup_error(bytes_errors)
+ except LookupError as exc:
+ raise ValueError(f"Invalid bytes_errors handler: {bytes_errors}") from exc
+
must_return = False
if output is None:
output = StringIO()
@@ -581,7 +599,16 @@ def unparse(input_dict, output=None, encoding='utf-8', full_document=True,
for key, value in input_dict.items():
if key != comment_key and full_document and seen_root:
raise ValueError("Document must have exactly one root.")
- _emit(key, value, content_handler, full_document=full_document, comment_key=comment_key, **kwargs)
+ _emit(
+ key,
+ value,
+ content_handler,
+ full_document=full_document,
+ encoding=encoding,
+ bytes_errors=bytes_errors,
+ comment_key=comment_key,
+ **kwargs,
+ )
if key != comment_key:
seen_root = True
if full_document and not seen_root:
diff --git a/contrib/python/xmltodict/py3/ya.make b/contrib/python/xmltodict/py3/ya.make
index a26f3f5f3ee..2bf0720ef05 100644
--- a/contrib/python/xmltodict/py3/ya.make
+++ b/contrib/python/xmltodict/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(1.0.3)
+VERSION(1.0.4)
LICENSE(MIT)