From f6fa894e435f87435fb7e9d169d27be34919d02e Mon Sep 17 00:00:00 2001 From: robot-piglet Date: Mon, 9 Mar 2026 15:01:52 +0300 Subject: Intermediate changes commit_hash:cc43b6f20066a000a87a4b56d73a1234249e0e8a --- contrib/python/annotated-doc/.dist-info/METADATA | 145 +++ .../annotated-doc/.dist-info/entry_points.txt | 4 + contrib/python/annotated-doc/LICENSE | 21 + contrib/python/annotated-doc/README.md | 109 ++ .../python/annotated-doc/annotated_doc/__init__.py | 3 + contrib/python/annotated-doc/annotated_doc/main.py | 36 + .../python/annotated-doc/annotated_doc/py.typed | 0 contrib/python/annotated-doc/ya.make | 24 + contrib/python/jaraco.text/.dist-info/METADATA | 6 +- .../python/jaraco.text/.yandex_meta/yamaker.yaml | 2 - contrib/python/jaraco.text/jaraco/text/__init__.py | 46 +- .../jaraco.text/jaraco/text/show-newlines.py | 13 +- .../python/jaraco.text/jaraco/text/strip-prefix.py | 5 + .../patches/01-remove-autocommand.patch | 20 - contrib/python/jaraco.text/ya.make | 3 +- contrib/python/shellingham/.dist-info/METADATA | 106 ++ .../python/shellingham/.dist-info/top_level.txt | 1 + contrib/python/shellingham/LICENSE | 13 + contrib/python/shellingham/README.rst | 80 ++ contrib/python/shellingham/shellingham/__init__.py | 23 + contrib/python/shellingham/shellingham/_core.py | 11 + contrib/python/shellingham/shellingham/nt.py | 163 +++ .../shellingham/shellingham/posix/__init__.py | 112 ++ .../python/shellingham/shellingham/posix/_core.py | 3 + .../python/shellingham/shellingham/posix/proc.py | 83 ++ contrib/python/shellingham/shellingham/posix/ps.py | 51 + contrib/python/shellingham/ya.make | 32 + contrib/python/typer-slim/.dist-info/METADATA | 425 +++++++ .../python/typer-slim/.dist-info/entry_points.txt | 4 + contrib/python/typer-slim/LICENSE | 21 + contrib/python/typer-slim/README.md | 386 +++++++ contrib/python/typer-slim/typer/__init__.py | 39 + contrib/python/typer-slim/typer/__main__.py | 3 + .../python/typer-slim/typer/_completion_classes.py | 206 ++++ .../python/typer-slim/typer/_completion_shared.py | 259 +++++ contrib/python/typer-slim/typer/_types.py | 27 + contrib/python/typer-slim/typer/_typing.py | 83 ++ contrib/python/typer-slim/typer/cli.py | 317 ++++++ contrib/python/typer-slim/typer/colors.py | 20 + contrib/python/typer-slim/typer/completion.py | 147 +++ contrib/python/typer-slim/typer/core.py | 840 ++++++++++++++ contrib/python/typer-slim/typer/main.py | 1157 ++++++++++++++++++++ contrib/python/typer-slim/typer/models.py | 541 +++++++++ contrib/python/typer-slim/typer/params.py | 479 ++++++++ contrib/python/typer-slim/typer/py.typed | 0 contrib/python/typer-slim/typer/rich_utils.py | 752 +++++++++++++ contrib/python/typer-slim/typer/testing.py | 30 + contrib/python/typer-slim/typer/utils.py | 190 ++++ contrib/python/typer-slim/ya.make | 47 + contrib/python/typer/.dist-info/METADATA | 412 +++++++ contrib/python/typer/.dist-info/entry_points.txt | 5 + contrib/python/typer/.dist-info/top_level.txt | 1 + contrib/python/typer/.yandex_meta/yamaker.yaml | 4 + contrib/python/typer/LICENSE | 21 + contrib/python/typer/README.md | 375 +++++++ contrib/python/typer/ya.make | 26 + contrib/python/xmltodict/py3/.dist-info/METADATA | 3 +- contrib/python/xmltodict/py3/README.md | 1 + .../python/xmltodict/py3/tests/test_dicttoxml.py | 106 ++ contrib/python/xmltodict/py3/tests/ya.make | 5 +- contrib/python/xmltodict/py3/xmltodict.py | 49 +- contrib/python/xmltodict/py3/ya.make | 2 +- 62 files changed, 8038 insertions(+), 60 deletions(-) create mode 100644 contrib/python/annotated-doc/.dist-info/METADATA create mode 100644 contrib/python/annotated-doc/.dist-info/entry_points.txt create mode 100644 contrib/python/annotated-doc/LICENSE create mode 100644 contrib/python/annotated-doc/README.md create mode 100644 contrib/python/annotated-doc/annotated_doc/__init__.py create mode 100644 contrib/python/annotated-doc/annotated_doc/main.py create mode 100644 contrib/python/annotated-doc/annotated_doc/py.typed create mode 100644 contrib/python/annotated-doc/ya.make delete mode 100644 contrib/python/jaraco.text/patches/01-remove-autocommand.patch create mode 100644 contrib/python/shellingham/.dist-info/METADATA create mode 100644 contrib/python/shellingham/.dist-info/top_level.txt create mode 100644 contrib/python/shellingham/LICENSE create mode 100644 contrib/python/shellingham/README.rst create mode 100644 contrib/python/shellingham/shellingham/__init__.py create mode 100644 contrib/python/shellingham/shellingham/_core.py create mode 100644 contrib/python/shellingham/shellingham/nt.py create mode 100644 contrib/python/shellingham/shellingham/posix/__init__.py create mode 100644 contrib/python/shellingham/shellingham/posix/_core.py create mode 100644 contrib/python/shellingham/shellingham/posix/proc.py create mode 100644 contrib/python/shellingham/shellingham/posix/ps.py create mode 100644 contrib/python/shellingham/ya.make create mode 100644 contrib/python/typer-slim/.dist-info/METADATA create mode 100644 contrib/python/typer-slim/.dist-info/entry_points.txt create mode 100644 contrib/python/typer-slim/LICENSE create mode 100644 contrib/python/typer-slim/README.md create mode 100644 contrib/python/typer-slim/typer/__init__.py create mode 100644 contrib/python/typer-slim/typer/__main__.py create mode 100644 contrib/python/typer-slim/typer/_completion_classes.py create mode 100644 contrib/python/typer-slim/typer/_completion_shared.py create mode 100644 contrib/python/typer-slim/typer/_types.py create mode 100644 contrib/python/typer-slim/typer/_typing.py create mode 100644 contrib/python/typer-slim/typer/cli.py create mode 100644 contrib/python/typer-slim/typer/colors.py create mode 100644 contrib/python/typer-slim/typer/completion.py create mode 100644 contrib/python/typer-slim/typer/core.py create mode 100644 contrib/python/typer-slim/typer/main.py create mode 100644 contrib/python/typer-slim/typer/models.py create mode 100644 contrib/python/typer-slim/typer/params.py create mode 100644 contrib/python/typer-slim/typer/py.typed create mode 100644 contrib/python/typer-slim/typer/rich_utils.py create mode 100644 contrib/python/typer-slim/typer/testing.py create mode 100644 contrib/python/typer-slim/typer/utils.py create mode 100644 contrib/python/typer-slim/ya.make create mode 100644 contrib/python/typer/.dist-info/METADATA create mode 100644 contrib/python/typer/.dist-info/entry_points.txt create mode 100644 contrib/python/typer/.dist-info/top_level.txt create mode 100644 contrib/python/typer/.yandex_meta/yamaker.yaml create mode 100644 contrib/python/typer/LICENSE create mode 100644 contrib/python/typer/README.md create mode 100644 contrib/python/typer/ya.make 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?= +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`. + + + Test + + + Coverage + + + Package version + + + Supported Python versions + + +## 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`. + + + Test + + + Coverage + + + Package version + + + Supported Python versions + + +## 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 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" 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] + + +@functools.singledispatch 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: uranusjr@gmail.com +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 + +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, +) + + +@contextlib.contextmanager +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?= +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 + +

+ Typer + +

+

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

+

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

+ +--- + +**Documentation**: https://typer.tiangolo.com + +**Source Code**: https://github.com/fastapi/typer + +--- + +Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python type hints. + +It's also a command line tool to run scripts, automatically converting them to CLI applications. + +The key features are: + +* **Intuitive to write**: Great editor support. Completion 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 FastAPI's little sibling, it's the FastAPI of CLIs. + +## Installation + +Create and activate a virtual environment and then install **Typer**: + +
+ +```console +$ pip install typer +---> 100% +Successfully installed typer rich shellingham +``` + +
+ +## Example + +### The absolute minimum + +* Create a file `main.py` with: + +```Python +def main(name: str): + print(f"Hello {name}") +``` + +This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application. + +### Run it + +Run your application with the `typer` command: + +
+ +```console +// Run your application +$ typer main.py run + +// You get a nice error, you are missing NAME +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME +Try 'typer [PATH_OR_MODULE] run --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'NAME'. │ +╰───────────────────────────────────────────────────╯ + + +// You get a --help for free +$ typer main.py run --help + +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME + +Run the provided Typer app. + +╭─ Arguments ───────────────────────────────────────╮ +│ * name 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! 🎉 +``` + +
+ +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: + +
+ +```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! 🎉 +``` + +
+ +**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() + + +@app.command() +def hello(name: str): + print(f"Hello {name}") + + +@app.command() +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: + +
+ +```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 +``` + +
+ +Now check the help for the `hello` command: + +
+ +```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. │ +╰───────────────────────────────────────────────────╯ +``` + +
+ +And now check the help for the `goodbye` command: + +
+ +```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 🎉 +``` + +
+ +Now you can try out the new command line application: + +
+ +```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. +``` + +
+ +**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 Tutorial - User Guide. + +## Dependencies + +**Typer** stands on the shoulders of a giant. Its only internal required dependency is Click. + +By default it also comes with extra standard dependencies: + +* rich: to show nicely formatted errors automatically. +* shellingham: 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 @@ +

+ Typer + +

+

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

+

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

+ +--- + +**Documentation**: https://typer.tiangolo.com + +**Source Code**: https://github.com/fastapi/typer + +--- + +Typer is a library for building CLI applications that users will **love using** and developers will **love creating**. Based on Python type hints. + +It's also a command line tool to run scripts, automatically converting them to CLI applications. + +The key features are: + +* **Intuitive to write**: Great editor support. Completion 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 FastAPI's little sibling, it's the FastAPI of CLIs. + +## Installation + +Create and activate a virtual environment and then install **Typer**: + +
+ +```console +$ pip install typer +---> 100% +Successfully installed typer rich shellingham +``` + +
+ +## Example + +### The absolute minimum + +* Create a file `main.py` with: + +```Python +def main(name: str): + print(f"Hello {name}") +``` + +This script doesn't even use Typer internally. But you can use the `typer` command to run it as a CLI application. + +### Run it + +Run your application with the `typer` command: + +
+ +```console +// Run your application +$ typer main.py run + +// You get a nice error, you are missing NAME +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME +Try 'typer [PATH_OR_MODULE] run --help' for help. +╭─ Error ───────────────────────────────────────────╮ +│ Missing argument 'NAME'. │ +╰───────────────────────────────────────────────────╯ + + +// You get a --help for free +$ typer main.py run --help + +Usage: typer [PATH_OR_MODULE] run [OPTIONS] NAME + +Run the provided Typer app. + +╭─ Arguments ───────────────────────────────────────╮ +│ * name 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! 🎉 +``` + +
+ +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: + +
+ +```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! 🎉 +``` + +
+ +**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() + + +@app.command() +def hello(name: str): + print(f"Hello {name}") + + +@app.command() +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: + +
+ +```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 +``` + +
+ +Now check the help for the `hello` command: + +
+ +```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. │ +╰───────────────────────────────────────────────────╯ +``` + +
+ +And now check the help for the `goodbye` command: + +
+ +```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 🎉 +``` + +
+ +Now you can try out the new command line application: + +
+ +```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. +``` + +
+ +**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 Tutorial - User Guide. + +## Dependencies + +**Typer** stands on the shoulders of a giant. Its only internal required dependency is Click. + +By default it also comes with extra standard dependencies: + +* rich: to show nicely formatted errors automatically. +* shellingham: 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() + + +@app.callback(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 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\-\w+)(?![a-zA-Z0-9])", + r"(^|\W)(?P