Add files using upload-large-folder tool
Browse files- env/lib/python3.13/site-packages/anyio/__init__.py +111 -0
- env/lib/python3.13/site-packages/anyio/from_thread.py +570 -0
- env/lib/python3.13/site-packages/anyio/functools.py +347 -0
- env/lib/python3.13/site-packages/anyio/lowlevel.py +195 -0
- env/lib/python3.13/site-packages/anyio/py.typed +0 -0
- env/lib/python3.13/site-packages/anyio/pytest_plugin.py +302 -0
- env/lib/python3.13/site-packages/anyio/to_interpreter.py +246 -0
- env/lib/python3.13/site-packages/anyio/to_process.py +264 -0
- env/lib/python3.13/site-packages/anyio/to_thread.py +74 -0
- env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/INSTALLER +1 -0
- env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/METADATA +78 -0
- env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/RECORD +14 -0
- env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/WHEEL +5 -0
- env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/top_level.txt +1 -0
- env/lib/python3.13/site-packages/click/__init__.py +123 -0
- env/lib/python3.13/site-packages/click/_compat.py +622 -0
- env/lib/python3.13/site-packages/click/_termui_impl.py +852 -0
- env/lib/python3.13/site-packages/click/_textwrap.py +51 -0
- env/lib/python3.13/site-packages/click/_utils.py +36 -0
- env/lib/python3.13/site-packages/click/_winconsole.py +296 -0
- env/lib/python3.13/site-packages/click/core.py +0 -0
- env/lib/python3.13/site-packages/click/decorators.py +551 -0
- env/lib/python3.13/site-packages/click/exceptions.py +308 -0
- env/lib/python3.13/site-packages/click/formatting.py +301 -0
- env/lib/python3.13/site-packages/click/globals.py +67 -0
- env/lib/python3.13/site-packages/click/parser.py +532 -0
- env/lib/python3.13/site-packages/click/py.typed +0 -0
- env/lib/python3.13/site-packages/click/shell_completion.py +667 -0
- env/lib/python3.13/site-packages/click/termui.py +883 -0
- env/lib/python3.13/site-packages/click/testing.py +577 -0
- env/lib/python3.13/site-packages/click/types.py +1209 -0
- env/lib/python3.13/site-packages/click/utils.py +627 -0
- env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/INSTALLER +1 -0
- env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/LICENSE +13 -0
- env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/METADATA +106 -0
- env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/RECORD +21 -0
- env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/WHEEL +6 -0
- env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/top_level.txt +1 -0
- env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/zip-safe +1 -0
- env/lib/python3.13/site-packages/tqdm-4.67.1.dist-info/INSTALLER +1 -0
- env/lib/python3.13/site-packages/tqdm-4.67.1.dist-info/RECORD +74 -0
- env/lib/python3.13/site-packages/tqdm-4.67.1.dist-info/WHEEL +5 -0
- env/lib/python3.13/site-packages/typer/__main__.py +3 -0
- env/lib/python3.13/site-packages/typer/_completion_classes.py +211 -0
- env/lib/python3.13/site-packages/typer/_typing.py +105 -0
- env/lib/python3.13/site-packages/typer/cli.py +308 -0
- env/lib/python3.13/site-packages/typer/colors.py +20 -0
- env/lib/python3.13/site-packages/typer/models.py +544 -0
- env/lib/python3.13/site-packages/typer/params.py +479 -0
- env/lib/python3.13/site-packages/typer/utils.py +190 -0
env/lib/python3.13/site-packages/anyio/__init__.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from ._core._contextmanagers import AsyncContextManagerMixin as AsyncContextManagerMixin
|
| 4 |
+
from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin
|
| 5 |
+
from ._core._eventloop import current_time as current_time
|
| 6 |
+
from ._core._eventloop import get_all_backends as get_all_backends
|
| 7 |
+
from ._core._eventloop import get_available_backends as get_available_backends
|
| 8 |
+
from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class
|
| 9 |
+
from ._core._eventloop import run as run
|
| 10 |
+
from ._core._eventloop import sleep as sleep
|
| 11 |
+
from ._core._eventloop import sleep_forever as sleep_forever
|
| 12 |
+
from ._core._eventloop import sleep_until as sleep_until
|
| 13 |
+
from ._core._exceptions import BrokenResourceError as BrokenResourceError
|
| 14 |
+
from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter
|
| 15 |
+
from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess
|
| 16 |
+
from ._core._exceptions import BusyResourceError as BusyResourceError
|
| 17 |
+
from ._core._exceptions import ClosedResourceError as ClosedResourceError
|
| 18 |
+
from ._core._exceptions import ConnectionFailed as ConnectionFailed
|
| 19 |
+
from ._core._exceptions import DelimiterNotFound as DelimiterNotFound
|
| 20 |
+
from ._core._exceptions import EndOfStream as EndOfStream
|
| 21 |
+
from ._core._exceptions import IncompleteRead as IncompleteRead
|
| 22 |
+
from ._core._exceptions import NoEventLoopError as NoEventLoopError
|
| 23 |
+
from ._core._exceptions import RunFinishedError as RunFinishedError
|
| 24 |
+
from ._core._exceptions import TypedAttributeLookupError as TypedAttributeLookupError
|
| 25 |
+
from ._core._exceptions import WouldBlock as WouldBlock
|
| 26 |
+
from ._core._fileio import AsyncFile as AsyncFile
|
| 27 |
+
from ._core._fileio import Path as Path
|
| 28 |
+
from ._core._fileio import open_file as open_file
|
| 29 |
+
from ._core._fileio import wrap_file as wrap_file
|
| 30 |
+
from ._core._resources import aclose_forcefully as aclose_forcefully
|
| 31 |
+
from ._core._signals import open_signal_receiver as open_signal_receiver
|
| 32 |
+
from ._core._sockets import TCPConnectable as TCPConnectable
|
| 33 |
+
from ._core._sockets import UNIXConnectable as UNIXConnectable
|
| 34 |
+
from ._core._sockets import as_connectable as as_connectable
|
| 35 |
+
from ._core._sockets import connect_tcp as connect_tcp
|
| 36 |
+
from ._core._sockets import connect_unix as connect_unix
|
| 37 |
+
from ._core._sockets import create_connected_udp_socket as create_connected_udp_socket
|
| 38 |
+
from ._core._sockets import (
|
| 39 |
+
create_connected_unix_datagram_socket as create_connected_unix_datagram_socket,
|
| 40 |
+
)
|
| 41 |
+
from ._core._sockets import create_tcp_listener as create_tcp_listener
|
| 42 |
+
from ._core._sockets import create_udp_socket as create_udp_socket
|
| 43 |
+
from ._core._sockets import create_unix_datagram_socket as create_unix_datagram_socket
|
| 44 |
+
from ._core._sockets import create_unix_listener as create_unix_listener
|
| 45 |
+
from ._core._sockets import getaddrinfo as getaddrinfo
|
| 46 |
+
from ._core._sockets import getnameinfo as getnameinfo
|
| 47 |
+
from ._core._sockets import notify_closing as notify_closing
|
| 48 |
+
from ._core._sockets import wait_readable as wait_readable
|
| 49 |
+
from ._core._sockets import wait_socket_readable as wait_socket_readable
|
| 50 |
+
from ._core._sockets import wait_socket_writable as wait_socket_writable
|
| 51 |
+
from ._core._sockets import wait_writable as wait_writable
|
| 52 |
+
from ._core._streams import create_memory_object_stream as create_memory_object_stream
|
| 53 |
+
from ._core._subprocesses import open_process as open_process
|
| 54 |
+
from ._core._subprocesses import run_process as run_process
|
| 55 |
+
from ._core._synchronization import CapacityLimiter as CapacityLimiter
|
| 56 |
+
from ._core._synchronization import (
|
| 57 |
+
CapacityLimiterStatistics as CapacityLimiterStatistics,
|
| 58 |
+
)
|
| 59 |
+
from ._core._synchronization import Condition as Condition
|
| 60 |
+
from ._core._synchronization import ConditionStatistics as ConditionStatistics
|
| 61 |
+
from ._core._synchronization import Event as Event
|
| 62 |
+
from ._core._synchronization import EventStatistics as EventStatistics
|
| 63 |
+
from ._core._synchronization import Lock as Lock
|
| 64 |
+
from ._core._synchronization import LockStatistics as LockStatistics
|
| 65 |
+
from ._core._synchronization import ResourceGuard as ResourceGuard
|
| 66 |
+
from ._core._synchronization import Semaphore as Semaphore
|
| 67 |
+
from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics
|
| 68 |
+
from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED
|
| 69 |
+
from ._core._tasks import CancelScope as CancelScope
|
| 70 |
+
from ._core._tasks import create_task_group as create_task_group
|
| 71 |
+
from ._core._tasks import current_effective_deadline as current_effective_deadline
|
| 72 |
+
from ._core._tasks import fail_after as fail_after
|
| 73 |
+
from ._core._tasks import move_on_after as move_on_after
|
| 74 |
+
from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile
|
| 75 |
+
from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile
|
| 76 |
+
from ._core._tempfile import TemporaryDirectory as TemporaryDirectory
|
| 77 |
+
from ._core._tempfile import TemporaryFile as TemporaryFile
|
| 78 |
+
from ._core._tempfile import gettempdir as gettempdir
|
| 79 |
+
from ._core._tempfile import gettempdirb as gettempdirb
|
| 80 |
+
from ._core._tempfile import mkdtemp as mkdtemp
|
| 81 |
+
from ._core._tempfile import mkstemp as mkstemp
|
| 82 |
+
from ._core._testing import TaskInfo as TaskInfo
|
| 83 |
+
from ._core._testing import get_current_task as get_current_task
|
| 84 |
+
from ._core._testing import get_running_tasks as get_running_tasks
|
| 85 |
+
from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked
|
| 86 |
+
from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider
|
| 87 |
+
from ._core._typedattr import TypedAttributeSet as TypedAttributeSet
|
| 88 |
+
from ._core._typedattr import typed_attribute as typed_attribute
|
| 89 |
+
|
| 90 |
+
# Re-export imports so they look like they live directly in this package
|
| 91 |
+
for __value in list(locals().values()):
|
| 92 |
+
if getattr(__value, "__module__", "").startswith("anyio."):
|
| 93 |
+
__value.__module__ = __name__
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
del __value
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def __getattr__(attr: str) -> type[BrokenWorkerInterpreter]:
|
| 100 |
+
"""Support deprecated aliases."""
|
| 101 |
+
if attr == "BrokenWorkerIntepreter":
|
| 102 |
+
import warnings
|
| 103 |
+
|
| 104 |
+
warnings.warn(
|
| 105 |
+
"The 'BrokenWorkerIntepreter' alias is deprecated, use 'BrokenWorkerInterpreter' instead.",
|
| 106 |
+
DeprecationWarning,
|
| 107 |
+
stacklevel=2,
|
| 108 |
+
)
|
| 109 |
+
return BrokenWorkerInterpreter
|
| 110 |
+
|
| 111 |
+
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
|
env/lib/python3.13/site-packages/anyio/from_thread.py
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
__all__ = (
|
| 4 |
+
"BlockingPortal",
|
| 5 |
+
"BlockingPortalProvider",
|
| 6 |
+
"check_cancelled",
|
| 7 |
+
"run",
|
| 8 |
+
"run_sync",
|
| 9 |
+
"start_blocking_portal",
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
import sys
|
| 13 |
+
from collections.abc import Awaitable, Callable, Generator
|
| 14 |
+
from concurrent.futures import Future
|
| 15 |
+
from contextlib import (
|
| 16 |
+
AbstractAsyncContextManager,
|
| 17 |
+
AbstractContextManager,
|
| 18 |
+
contextmanager,
|
| 19 |
+
)
|
| 20 |
+
from dataclasses import dataclass, field
|
| 21 |
+
from inspect import isawaitable
|
| 22 |
+
from threading import Lock, Thread, current_thread, get_ident
|
| 23 |
+
from types import TracebackType
|
| 24 |
+
from typing import (
|
| 25 |
+
Any,
|
| 26 |
+
Generic,
|
| 27 |
+
TypeVar,
|
| 28 |
+
cast,
|
| 29 |
+
overload,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
from ._core._eventloop import (
|
| 33 |
+
get_async_backend,
|
| 34 |
+
get_cancelled_exc_class,
|
| 35 |
+
threadlocals,
|
| 36 |
+
)
|
| 37 |
+
from ._core._eventloop import run as run_eventloop
|
| 38 |
+
from ._core._exceptions import NoEventLoopError
|
| 39 |
+
from ._core._synchronization import Event
|
| 40 |
+
from ._core._tasks import CancelScope, create_task_group
|
| 41 |
+
from .abc._tasks import TaskStatus
|
| 42 |
+
from .lowlevel import EventLoopToken
|
| 43 |
+
|
| 44 |
+
if sys.version_info >= (3, 11):
|
| 45 |
+
from typing import TypeVarTuple, Unpack
|
| 46 |
+
else:
|
| 47 |
+
from typing_extensions import TypeVarTuple, Unpack
|
| 48 |
+
|
| 49 |
+
T_Retval = TypeVar("T_Retval")
|
| 50 |
+
T_co = TypeVar("T_co", covariant=True)
|
| 51 |
+
PosArgsT = TypeVarTuple("PosArgsT")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _token_or_error(token: EventLoopToken | None) -> EventLoopToken:
|
| 55 |
+
if token is not None:
|
| 56 |
+
return token
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
return threadlocals.current_token
|
| 60 |
+
except AttributeError:
|
| 61 |
+
raise NoEventLoopError(
|
| 62 |
+
"Not running inside an AnyIO worker thread, and no event loop token was "
|
| 63 |
+
"provided"
|
| 64 |
+
) from None
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def run(
|
| 68 |
+
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
| 69 |
+
*args: Unpack[PosArgsT],
|
| 70 |
+
token: EventLoopToken | None = None,
|
| 71 |
+
) -> T_Retval:
|
| 72 |
+
"""
|
| 73 |
+
Call a coroutine function from a worker thread.
|
| 74 |
+
|
| 75 |
+
:param func: a coroutine function
|
| 76 |
+
:param args: positional arguments for the callable
|
| 77 |
+
:param token: an event loop token to use to get back to the event loop thread
|
| 78 |
+
(required if calling this function from outside an AnyIO worker thread)
|
| 79 |
+
:return: the return value of the coroutine function
|
| 80 |
+
:raises MissingTokenError: if no token was provided and called from outside an
|
| 81 |
+
AnyIO worker thread
|
| 82 |
+
:raises RunFinishedError: if the event loop tied to ``token`` is no longer running
|
| 83 |
+
|
| 84 |
+
.. versionchanged:: 4.11.0
|
| 85 |
+
Added the ``token`` parameter.
|
| 86 |
+
|
| 87 |
+
"""
|
| 88 |
+
explicit_token = token is not None
|
| 89 |
+
token = _token_or_error(token)
|
| 90 |
+
return token.backend_class.run_async_from_thread(
|
| 91 |
+
func, args, token=token.native_token if explicit_token else None
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def run_sync(
|
| 96 |
+
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
| 97 |
+
*args: Unpack[PosArgsT],
|
| 98 |
+
token: EventLoopToken | None = None,
|
| 99 |
+
) -> T_Retval:
|
| 100 |
+
"""
|
| 101 |
+
Call a function in the event loop thread from a worker thread.
|
| 102 |
+
|
| 103 |
+
:param func: a callable
|
| 104 |
+
:param args: positional arguments for the callable
|
| 105 |
+
:param token: an event loop token to use to get back to the event loop thread
|
| 106 |
+
(required if calling this function from outside an AnyIO worker thread)
|
| 107 |
+
:return: the return value of the callable
|
| 108 |
+
:raises MissingTokenError: if no token was provided and called from outside an
|
| 109 |
+
AnyIO worker thread
|
| 110 |
+
:raises RunFinishedError: if the event loop tied to ``token`` is no longer running
|
| 111 |
+
|
| 112 |
+
.. versionchanged:: 4.11.0
|
| 113 |
+
Added the ``token`` parameter.
|
| 114 |
+
|
| 115 |
+
"""
|
| 116 |
+
explicit_token = token is not None
|
| 117 |
+
token = _token_or_error(token)
|
| 118 |
+
return token.backend_class.run_sync_from_thread(
|
| 119 |
+
func, args, token=token.native_token if explicit_token else None
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class _BlockingAsyncContextManager(Generic[T_co], AbstractContextManager):
|
| 124 |
+
_enter_future: Future[T_co]
|
| 125 |
+
_exit_future: Future[bool | None]
|
| 126 |
+
_exit_event: Event
|
| 127 |
+
_exit_exc_info: tuple[
|
| 128 |
+
type[BaseException] | None, BaseException | None, TracebackType | None
|
| 129 |
+
] = (None, None, None)
|
| 130 |
+
|
| 131 |
+
def __init__(
|
| 132 |
+
self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal
|
| 133 |
+
):
|
| 134 |
+
self._async_cm = async_cm
|
| 135 |
+
self._portal = portal
|
| 136 |
+
|
| 137 |
+
async def run_async_cm(self) -> bool | None:
|
| 138 |
+
try:
|
| 139 |
+
self._exit_event = Event()
|
| 140 |
+
value = await self._async_cm.__aenter__()
|
| 141 |
+
except BaseException as exc:
|
| 142 |
+
self._enter_future.set_exception(exc)
|
| 143 |
+
raise
|
| 144 |
+
else:
|
| 145 |
+
self._enter_future.set_result(value)
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
# Wait for the sync context manager to exit.
|
| 149 |
+
# This next statement can raise `get_cancelled_exc_class()` if
|
| 150 |
+
# something went wrong in a task group in this async context
|
| 151 |
+
# manager.
|
| 152 |
+
await self._exit_event.wait()
|
| 153 |
+
finally:
|
| 154 |
+
# In case of cancellation, it could be that we end up here before
|
| 155 |
+
# `_BlockingAsyncContextManager.__exit__` is called, and an
|
| 156 |
+
# `_exit_exc_info` has been set.
|
| 157 |
+
result = await self._async_cm.__aexit__(*self._exit_exc_info)
|
| 158 |
+
|
| 159 |
+
return result
|
| 160 |
+
|
| 161 |
+
def __enter__(self) -> T_co:
|
| 162 |
+
self._enter_future = Future()
|
| 163 |
+
self._exit_future = self._portal.start_task_soon(self.run_async_cm)
|
| 164 |
+
return self._enter_future.result()
|
| 165 |
+
|
| 166 |
+
def __exit__(
|
| 167 |
+
self,
|
| 168 |
+
__exc_type: type[BaseException] | None,
|
| 169 |
+
__exc_value: BaseException | None,
|
| 170 |
+
__traceback: TracebackType | None,
|
| 171 |
+
) -> bool | None:
|
| 172 |
+
self._exit_exc_info = __exc_type, __exc_value, __traceback
|
| 173 |
+
self._portal.call(self._exit_event.set)
|
| 174 |
+
return self._exit_future.result()
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class _BlockingPortalTaskStatus(TaskStatus):
|
| 178 |
+
def __init__(self, future: Future):
|
| 179 |
+
self._future = future
|
| 180 |
+
|
| 181 |
+
def started(self, value: object = None) -> None:
|
| 182 |
+
self._future.set_result(value)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class BlockingPortal:
|
| 186 |
+
"""An object that lets external threads run code in an asynchronous event loop."""
|
| 187 |
+
|
| 188 |
+
def __new__(cls) -> BlockingPortal:
|
| 189 |
+
return get_async_backend().create_blocking_portal()
|
| 190 |
+
|
| 191 |
+
def __init__(self) -> None:
|
| 192 |
+
self._event_loop_thread_id: int | None = get_ident()
|
| 193 |
+
self._stop_event = Event()
|
| 194 |
+
self._task_group = create_task_group()
|
| 195 |
+
self._cancelled_exc_class = get_cancelled_exc_class()
|
| 196 |
+
|
| 197 |
+
async def __aenter__(self) -> BlockingPortal:
|
| 198 |
+
await self._task_group.__aenter__()
|
| 199 |
+
return self
|
| 200 |
+
|
| 201 |
+
async def __aexit__(
|
| 202 |
+
self,
|
| 203 |
+
exc_type: type[BaseException] | None,
|
| 204 |
+
exc_val: BaseException | None,
|
| 205 |
+
exc_tb: TracebackType | None,
|
| 206 |
+
) -> bool:
|
| 207 |
+
await self.stop()
|
| 208 |
+
return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
|
| 209 |
+
|
| 210 |
+
def _check_running(self) -> None:
|
| 211 |
+
if self._event_loop_thread_id is None:
|
| 212 |
+
raise RuntimeError("This portal is not running")
|
| 213 |
+
if self._event_loop_thread_id == get_ident():
|
| 214 |
+
raise RuntimeError(
|
| 215 |
+
"This method cannot be called from the event loop thread"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
async def sleep_until_stopped(self) -> None:
|
| 219 |
+
"""Sleep until :meth:`stop` is called."""
|
| 220 |
+
await self._stop_event.wait()
|
| 221 |
+
|
| 222 |
+
async def stop(self, cancel_remaining: bool = False) -> None:
|
| 223 |
+
"""
|
| 224 |
+
Signal the portal to shut down.
|
| 225 |
+
|
| 226 |
+
This marks the portal as no longer accepting new calls and exits from
|
| 227 |
+
:meth:`sleep_until_stopped`.
|
| 228 |
+
|
| 229 |
+
:param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False``
|
| 230 |
+
to let them finish before returning
|
| 231 |
+
|
| 232 |
+
"""
|
| 233 |
+
self._event_loop_thread_id = None
|
| 234 |
+
self._stop_event.set()
|
| 235 |
+
if cancel_remaining:
|
| 236 |
+
self._task_group.cancel_scope.cancel("the blocking portal is shutting down")
|
| 237 |
+
|
| 238 |
+
async def _call_func(
|
| 239 |
+
self,
|
| 240 |
+
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
| 241 |
+
args: tuple[Unpack[PosArgsT]],
|
| 242 |
+
kwargs: dict[str, Any],
|
| 243 |
+
future: Future[T_Retval],
|
| 244 |
+
) -> None:
|
| 245 |
+
def callback(f: Future[T_Retval]) -> None:
|
| 246 |
+
if f.cancelled():
|
| 247 |
+
if self._event_loop_thread_id == get_ident():
|
| 248 |
+
scope.cancel("the future was cancelled")
|
| 249 |
+
elif self._event_loop_thread_id is not None:
|
| 250 |
+
self.call(scope.cancel, "the future was cancelled")
|
| 251 |
+
|
| 252 |
+
try:
|
| 253 |
+
retval_or_awaitable = func(*args, **kwargs)
|
| 254 |
+
if isawaitable(retval_or_awaitable):
|
| 255 |
+
with CancelScope() as scope:
|
| 256 |
+
future.add_done_callback(callback)
|
| 257 |
+
retval = await retval_or_awaitable
|
| 258 |
+
else:
|
| 259 |
+
retval = retval_or_awaitable
|
| 260 |
+
except self._cancelled_exc_class:
|
| 261 |
+
future.cancel()
|
| 262 |
+
future.set_running_or_notify_cancel()
|
| 263 |
+
except BaseException as exc:
|
| 264 |
+
if not future.cancelled():
|
| 265 |
+
future.set_exception(exc)
|
| 266 |
+
|
| 267 |
+
# Let base exceptions fall through
|
| 268 |
+
if not isinstance(exc, Exception):
|
| 269 |
+
raise
|
| 270 |
+
else:
|
| 271 |
+
if not future.cancelled():
|
| 272 |
+
future.set_result(retval)
|
| 273 |
+
finally:
|
| 274 |
+
scope = None # type: ignore[assignment]
|
| 275 |
+
|
| 276 |
+
def _spawn_task_from_thread(
|
| 277 |
+
self,
|
| 278 |
+
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
| 279 |
+
args: tuple[Unpack[PosArgsT]],
|
| 280 |
+
kwargs: dict[str, Any],
|
| 281 |
+
name: object,
|
| 282 |
+
future: Future[T_Retval],
|
| 283 |
+
) -> None:
|
| 284 |
+
"""
|
| 285 |
+
Spawn a new task using the given callable.
|
| 286 |
+
|
| 287 |
+
Implementers must ensure that the future is resolved when the task finishes.
|
| 288 |
+
|
| 289 |
+
:param func: a callable
|
| 290 |
+
:param args: positional arguments to be passed to the callable
|
| 291 |
+
:param kwargs: keyword arguments to be passed to the callable
|
| 292 |
+
:param name: name of the task (will be coerced to a string if not ``None``)
|
| 293 |
+
:param future: a future that will resolve to the return value of the callable,
|
| 294 |
+
or the exception raised during its execution
|
| 295 |
+
|
| 296 |
+
"""
|
| 297 |
+
raise NotImplementedError
|
| 298 |
+
|
| 299 |
+
@overload
|
| 300 |
+
def call(
|
| 301 |
+
self,
|
| 302 |
+
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
| 303 |
+
*args: Unpack[PosArgsT],
|
| 304 |
+
) -> T_Retval: ...
|
| 305 |
+
|
| 306 |
+
@overload
|
| 307 |
+
def call(
|
| 308 |
+
self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT]
|
| 309 |
+
) -> T_Retval: ...
|
| 310 |
+
|
| 311 |
+
def call(
|
| 312 |
+
self,
|
| 313 |
+
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
| 314 |
+
*args: Unpack[PosArgsT],
|
| 315 |
+
) -> T_Retval:
|
| 316 |
+
"""
|
| 317 |
+
Call the given function in the event loop thread.
|
| 318 |
+
|
| 319 |
+
If the callable returns a coroutine object, it is awaited on.
|
| 320 |
+
|
| 321 |
+
:param func: any callable
|
| 322 |
+
:raises RuntimeError: if the portal is not running or if this method is called
|
| 323 |
+
from within the event loop thread
|
| 324 |
+
|
| 325 |
+
"""
|
| 326 |
+
return cast(T_Retval, self.start_task_soon(func, *args).result())
|
| 327 |
+
|
| 328 |
+
@overload
|
| 329 |
+
def start_task_soon(
|
| 330 |
+
self,
|
| 331 |
+
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
|
| 332 |
+
*args: Unpack[PosArgsT],
|
| 333 |
+
name: object = None,
|
| 334 |
+
) -> Future[T_Retval]: ...
|
| 335 |
+
|
| 336 |
+
@overload
|
| 337 |
+
def start_task_soon(
|
| 338 |
+
self,
|
| 339 |
+
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
| 340 |
+
*args: Unpack[PosArgsT],
|
| 341 |
+
name: object = None,
|
| 342 |
+
) -> Future[T_Retval]: ...
|
| 343 |
+
|
| 344 |
+
def start_task_soon(
|
| 345 |
+
self,
|
| 346 |
+
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
|
| 347 |
+
*args: Unpack[PosArgsT],
|
| 348 |
+
name: object = None,
|
| 349 |
+
) -> Future[T_Retval]:
|
| 350 |
+
"""
|
| 351 |
+
Start a task in the portal's task group.
|
| 352 |
+
|
| 353 |
+
The task will be run inside a cancel scope which can be cancelled by cancelling
|
| 354 |
+
the returned future.
|
| 355 |
+
|
| 356 |
+
:param func: the target function
|
| 357 |
+
:param args: positional arguments passed to ``func``
|
| 358 |
+
:param name: name of the task (will be coerced to a string if not ``None``)
|
| 359 |
+
:return: a future that resolves with the return value of the callable if the
|
| 360 |
+
task completes successfully, or with the exception raised in the task
|
| 361 |
+
:raises RuntimeError: if the portal is not running or if this method is called
|
| 362 |
+
from within the event loop thread
|
| 363 |
+
:rtype: concurrent.futures.Future[T_Retval]
|
| 364 |
+
|
| 365 |
+
.. versionadded:: 3.0
|
| 366 |
+
|
| 367 |
+
"""
|
| 368 |
+
self._check_running()
|
| 369 |
+
f: Future[T_Retval] = Future()
|
| 370 |
+
self._spawn_task_from_thread(func, args, {}, name, f)
|
| 371 |
+
return f
|
| 372 |
+
|
| 373 |
+
def start_task(
|
| 374 |
+
self,
|
| 375 |
+
func: Callable[..., Awaitable[T_Retval]],
|
| 376 |
+
*args: object,
|
| 377 |
+
name: object = None,
|
| 378 |
+
) -> tuple[Future[T_Retval], Any]:
|
| 379 |
+
"""
|
| 380 |
+
Start a task in the portal's task group and wait until it signals for readiness.
|
| 381 |
+
|
| 382 |
+
This method works the same way as :meth:`.abc.TaskGroup.start`.
|
| 383 |
+
|
| 384 |
+
:param func: the target function
|
| 385 |
+
:param args: positional arguments passed to ``func``
|
| 386 |
+
:param name: name of the task (will be coerced to a string if not ``None``)
|
| 387 |
+
:return: a tuple of (future, task_status_value) where the ``task_status_value``
|
| 388 |
+
is the value passed to ``task_status.started()`` from within the target
|
| 389 |
+
function
|
| 390 |
+
:rtype: tuple[concurrent.futures.Future[T_Retval], Any]
|
| 391 |
+
|
| 392 |
+
.. versionadded:: 3.0
|
| 393 |
+
|
| 394 |
+
"""
|
| 395 |
+
|
| 396 |
+
def task_done(future: Future[T_Retval]) -> None:
|
| 397 |
+
if not task_status_future.done():
|
| 398 |
+
if future.cancelled():
|
| 399 |
+
task_status_future.cancel()
|
| 400 |
+
elif future.exception():
|
| 401 |
+
task_status_future.set_exception(future.exception())
|
| 402 |
+
else:
|
| 403 |
+
exc = RuntimeError(
|
| 404 |
+
"Task exited without calling task_status.started()"
|
| 405 |
+
)
|
| 406 |
+
task_status_future.set_exception(exc)
|
| 407 |
+
|
| 408 |
+
self._check_running()
|
| 409 |
+
task_status_future: Future = Future()
|
| 410 |
+
task_status = _BlockingPortalTaskStatus(task_status_future)
|
| 411 |
+
f: Future = Future()
|
| 412 |
+
f.add_done_callback(task_done)
|
| 413 |
+
self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f)
|
| 414 |
+
return f, task_status_future.result()
|
| 415 |
+
|
| 416 |
+
def wrap_async_context_manager(
|
| 417 |
+
self, cm: AbstractAsyncContextManager[T_co]
|
| 418 |
+
) -> AbstractContextManager[T_co]:
|
| 419 |
+
"""
|
| 420 |
+
Wrap an async context manager as a synchronous context manager via this portal.
|
| 421 |
+
|
| 422 |
+
Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping
|
| 423 |
+
in the middle until the synchronous context manager exits.
|
| 424 |
+
|
| 425 |
+
:param cm: an asynchronous context manager
|
| 426 |
+
:return: a synchronous context manager
|
| 427 |
+
|
| 428 |
+
.. versionadded:: 2.1
|
| 429 |
+
|
| 430 |
+
"""
|
| 431 |
+
return _BlockingAsyncContextManager(cm, self)
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
@dataclass
|
| 435 |
+
class BlockingPortalProvider:
|
| 436 |
+
"""
|
| 437 |
+
A manager for a blocking portal. Used as a context manager. The first thread to
|
| 438 |
+
enter this context manager causes a blocking portal to be started with the specific
|
| 439 |
+
parameters, and the last thread to exit causes the portal to be shut down. Thus,
|
| 440 |
+
there will be exactly one blocking portal running in this context as long as at
|
| 441 |
+
least one thread has entered this context manager.
|
| 442 |
+
|
| 443 |
+
The parameters are the same as for :func:`~anyio.run`.
|
| 444 |
+
|
| 445 |
+
:param backend: name of the backend
|
| 446 |
+
:param backend_options: backend options
|
| 447 |
+
|
| 448 |
+
.. versionadded:: 4.4
|
| 449 |
+
"""
|
| 450 |
+
|
| 451 |
+
backend: str = "asyncio"
|
| 452 |
+
backend_options: dict[str, Any] | None = None
|
| 453 |
+
_lock: Lock = field(init=False, default_factory=Lock)
|
| 454 |
+
_leases: int = field(init=False, default=0)
|
| 455 |
+
_portal: BlockingPortal = field(init=False)
|
| 456 |
+
_portal_cm: AbstractContextManager[BlockingPortal] | None = field(
|
| 457 |
+
init=False, default=None
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
def __enter__(self) -> BlockingPortal:
|
| 461 |
+
with self._lock:
|
| 462 |
+
if self._portal_cm is None:
|
| 463 |
+
self._portal_cm = start_blocking_portal(
|
| 464 |
+
self.backend, self.backend_options
|
| 465 |
+
)
|
| 466 |
+
self._portal = self._portal_cm.__enter__()
|
| 467 |
+
|
| 468 |
+
self._leases += 1
|
| 469 |
+
return self._portal
|
| 470 |
+
|
| 471 |
+
def __exit__(
|
| 472 |
+
self,
|
| 473 |
+
exc_type: type[BaseException] | None,
|
| 474 |
+
exc_val: BaseException | None,
|
| 475 |
+
exc_tb: TracebackType | None,
|
| 476 |
+
) -> None:
|
| 477 |
+
portal_cm: AbstractContextManager[BlockingPortal] | None = None
|
| 478 |
+
with self._lock:
|
| 479 |
+
assert self._portal_cm
|
| 480 |
+
assert self._leases > 0
|
| 481 |
+
self._leases -= 1
|
| 482 |
+
if not self._leases:
|
| 483 |
+
portal_cm = self._portal_cm
|
| 484 |
+
self._portal_cm = None
|
| 485 |
+
del self._portal
|
| 486 |
+
|
| 487 |
+
if portal_cm:
|
| 488 |
+
portal_cm.__exit__(None, None, None)
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
@contextmanager
|
| 492 |
+
def start_blocking_portal(
|
| 493 |
+
backend: str = "asyncio",
|
| 494 |
+
backend_options: dict[str, Any] | None = None,
|
| 495 |
+
*,
|
| 496 |
+
name: str | None = None,
|
| 497 |
+
) -> Generator[BlockingPortal, Any, None]:
|
| 498 |
+
"""
|
| 499 |
+
Start a new event loop in a new thread and run a blocking portal in its main task.
|
| 500 |
+
|
| 501 |
+
The parameters are the same as for :func:`~anyio.run`.
|
| 502 |
+
|
| 503 |
+
:param backend: name of the backend
|
| 504 |
+
:param backend_options: backend options
|
| 505 |
+
:param name: name of the thread
|
| 506 |
+
:return: a context manager that yields a blocking portal
|
| 507 |
+
|
| 508 |
+
.. versionchanged:: 3.0
|
| 509 |
+
Usage as a context manager is now required.
|
| 510 |
+
|
| 511 |
+
"""
|
| 512 |
+
|
| 513 |
+
async def run_portal() -> None:
|
| 514 |
+
async with BlockingPortal() as portal_:
|
| 515 |
+
if name is None:
|
| 516 |
+
current_thread().name = f"{backend}-portal-{id(portal_):x}"
|
| 517 |
+
|
| 518 |
+
future.set_result(portal_)
|
| 519 |
+
await portal_.sleep_until_stopped()
|
| 520 |
+
|
| 521 |
+
def run_blocking_portal() -> None:
|
| 522 |
+
if future.set_running_or_notify_cancel():
|
| 523 |
+
try:
|
| 524 |
+
run_eventloop(
|
| 525 |
+
run_portal, backend=backend, backend_options=backend_options
|
| 526 |
+
)
|
| 527 |
+
except BaseException as exc:
|
| 528 |
+
if not future.done():
|
| 529 |
+
future.set_exception(exc)
|
| 530 |
+
|
| 531 |
+
future: Future[BlockingPortal] = Future()
|
| 532 |
+
thread = Thread(target=run_blocking_portal, daemon=True, name=name)
|
| 533 |
+
thread.start()
|
| 534 |
+
try:
|
| 535 |
+
cancel_remaining_tasks = False
|
| 536 |
+
portal = future.result()
|
| 537 |
+
try:
|
| 538 |
+
yield portal
|
| 539 |
+
except BaseException:
|
| 540 |
+
cancel_remaining_tasks = True
|
| 541 |
+
raise
|
| 542 |
+
finally:
|
| 543 |
+
try:
|
| 544 |
+
portal.call(portal.stop, cancel_remaining_tasks)
|
| 545 |
+
except RuntimeError:
|
| 546 |
+
pass
|
| 547 |
+
finally:
|
| 548 |
+
thread.join()
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def check_cancelled() -> None:
|
| 552 |
+
"""
|
| 553 |
+
Check if the cancel scope of the host task's running the current worker thread has
|
| 554 |
+
been cancelled.
|
| 555 |
+
|
| 556 |
+
If the host task's current cancel scope has indeed been cancelled, the
|
| 557 |
+
backend-specific cancellation exception will be raised.
|
| 558 |
+
|
| 559 |
+
:raises RuntimeError: if the current thread was not spawned by
|
| 560 |
+
:func:`.to_thread.run_sync`
|
| 561 |
+
|
| 562 |
+
"""
|
| 563 |
+
try:
|
| 564 |
+
token: EventLoopToken = threadlocals.current_token
|
| 565 |
+
except AttributeError:
|
| 566 |
+
raise NoEventLoopError(
|
| 567 |
+
"This function can only be called inside an AnyIO worker thread"
|
| 568 |
+
) from None
|
| 569 |
+
|
| 570 |
+
token.backend_class.check_cancelled()
|
env/lib/python3.13/site-packages/anyio/functools.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
__all__ = (
|
| 4 |
+
"AsyncCacheInfo",
|
| 5 |
+
"AsyncCacheParameters",
|
| 6 |
+
"AsyncLRUCacheWrapper",
|
| 7 |
+
"cache",
|
| 8 |
+
"lru_cache",
|
| 9 |
+
"reduce",
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
import functools
|
| 13 |
+
import sys
|
| 14 |
+
from collections import OrderedDict
|
| 15 |
+
from collections.abc import (
|
| 16 |
+
AsyncIterable,
|
| 17 |
+
Awaitable,
|
| 18 |
+
Callable,
|
| 19 |
+
Coroutine,
|
| 20 |
+
Hashable,
|
| 21 |
+
Iterable,
|
| 22 |
+
)
|
| 23 |
+
from functools import update_wrapper
|
| 24 |
+
from inspect import iscoroutinefunction
|
| 25 |
+
from typing import (
|
| 26 |
+
Any,
|
| 27 |
+
Generic,
|
| 28 |
+
NamedTuple,
|
| 29 |
+
TypedDict,
|
| 30 |
+
TypeVar,
|
| 31 |
+
cast,
|
| 32 |
+
final,
|
| 33 |
+
overload,
|
| 34 |
+
)
|
| 35 |
+
from weakref import WeakKeyDictionary
|
| 36 |
+
|
| 37 |
+
from ._core._synchronization import Lock
|
| 38 |
+
from .lowlevel import RunVar, checkpoint
|
| 39 |
+
|
| 40 |
+
if sys.version_info >= (3, 11):
|
| 41 |
+
from typing import ParamSpec
|
| 42 |
+
else:
|
| 43 |
+
from typing_extensions import ParamSpec
|
| 44 |
+
|
| 45 |
+
T = TypeVar("T")
|
| 46 |
+
S = TypeVar("S")
|
| 47 |
+
P = ParamSpec("P")
|
| 48 |
+
lru_cache_items: RunVar[
|
| 49 |
+
WeakKeyDictionary[
|
| 50 |
+
AsyncLRUCacheWrapper[Any, Any],
|
| 51 |
+
OrderedDict[Hashable, tuple[_InitialMissingType, Lock] | tuple[Any, None]],
|
| 52 |
+
]
|
| 53 |
+
] = RunVar("lru_cache_items")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class _InitialMissingType:
|
| 57 |
+
pass
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
initial_missing: _InitialMissingType = _InitialMissingType()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class AsyncCacheInfo(NamedTuple):
|
| 64 |
+
hits: int
|
| 65 |
+
misses: int
|
| 66 |
+
maxsize: int | None
|
| 67 |
+
currsize: int
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class AsyncCacheParameters(TypedDict):
|
| 71 |
+
maxsize: int | None
|
| 72 |
+
typed: bool
|
| 73 |
+
always_checkpoint: bool
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@final
|
| 77 |
+
class AsyncLRUCacheWrapper(Generic[P, T]):
|
| 78 |
+
def __init__(
|
| 79 |
+
self,
|
| 80 |
+
func: Callable[..., Awaitable[T]],
|
| 81 |
+
maxsize: int | None,
|
| 82 |
+
typed: bool,
|
| 83 |
+
always_checkpoint: bool,
|
| 84 |
+
):
|
| 85 |
+
self.__wrapped__ = func
|
| 86 |
+
self._hits: int = 0
|
| 87 |
+
self._misses: int = 0
|
| 88 |
+
self._maxsize = max(maxsize, 0) if maxsize is not None else None
|
| 89 |
+
self._currsize: int = 0
|
| 90 |
+
self._typed = typed
|
| 91 |
+
self._always_checkpoint = always_checkpoint
|
| 92 |
+
update_wrapper(self, func)
|
| 93 |
+
|
| 94 |
+
def cache_info(self) -> AsyncCacheInfo:
|
| 95 |
+
return AsyncCacheInfo(self._hits, self._misses, self._maxsize, self._currsize)
|
| 96 |
+
|
| 97 |
+
def cache_parameters(self) -> AsyncCacheParameters:
|
| 98 |
+
return {
|
| 99 |
+
"maxsize": self._maxsize,
|
| 100 |
+
"typed": self._typed,
|
| 101 |
+
"always_checkpoint": self._always_checkpoint,
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def cache_clear(self) -> None:
|
| 105 |
+
if cache := lru_cache_items.get(None):
|
| 106 |
+
cache.pop(self, None)
|
| 107 |
+
self._hits = self._misses = self._currsize = 0
|
| 108 |
+
|
| 109 |
+
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
| 110 |
+
# Easy case first: if maxsize == 0, no caching is done
|
| 111 |
+
if self._maxsize == 0:
|
| 112 |
+
value = await self.__wrapped__(*args, **kwargs)
|
| 113 |
+
self._misses += 1
|
| 114 |
+
return value
|
| 115 |
+
|
| 116 |
+
# The key is constructed as a flat tuple to avoid memory overhead
|
| 117 |
+
key: tuple[Any, ...] = args
|
| 118 |
+
if kwargs:
|
| 119 |
+
# initial_missing is used as a separator
|
| 120 |
+
key += (initial_missing,) + sum(kwargs.items(), ())
|
| 121 |
+
|
| 122 |
+
if self._typed:
|
| 123 |
+
key += tuple(type(arg) for arg in args)
|
| 124 |
+
if kwargs:
|
| 125 |
+
key += (initial_missing,) + tuple(type(val) for val in kwargs.values())
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
cache = lru_cache_items.get()
|
| 129 |
+
except LookupError:
|
| 130 |
+
cache = WeakKeyDictionary()
|
| 131 |
+
lru_cache_items.set(cache)
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
cache_entry = cache[self]
|
| 135 |
+
except KeyError:
|
| 136 |
+
cache_entry = cache[self] = OrderedDict()
|
| 137 |
+
|
| 138 |
+
cached_value: T | _InitialMissingType
|
| 139 |
+
try:
|
| 140 |
+
cached_value, lock = cache_entry[key]
|
| 141 |
+
except KeyError:
|
| 142 |
+
# We're the first task to call this function
|
| 143 |
+
cached_value, lock = (
|
| 144 |
+
initial_missing,
|
| 145 |
+
Lock(fast_acquire=not self._always_checkpoint),
|
| 146 |
+
)
|
| 147 |
+
cache_entry[key] = cached_value, lock
|
| 148 |
+
|
| 149 |
+
if lock is None:
|
| 150 |
+
# The value was already cached
|
| 151 |
+
self._hits += 1
|
| 152 |
+
cache_entry.move_to_end(key)
|
| 153 |
+
if self._always_checkpoint:
|
| 154 |
+
await checkpoint()
|
| 155 |
+
|
| 156 |
+
return cast(T, cached_value)
|
| 157 |
+
|
| 158 |
+
async with lock:
|
| 159 |
+
# Check if another task filled the cache while we acquired the lock
|
| 160 |
+
if (cached_value := cache_entry[key][0]) is initial_missing:
|
| 161 |
+
self._misses += 1
|
| 162 |
+
if self._maxsize is not None and self._currsize >= self._maxsize:
|
| 163 |
+
cache_entry.popitem(last=False)
|
| 164 |
+
else:
|
| 165 |
+
self._currsize += 1
|
| 166 |
+
|
| 167 |
+
value = await self.__wrapped__(*args, **kwargs)
|
| 168 |
+
cache_entry[key] = value, None
|
| 169 |
+
else:
|
| 170 |
+
# Another task filled the cache while we were waiting for the lock
|
| 171 |
+
self._hits += 1
|
| 172 |
+
cache_entry.move_to_end(key)
|
| 173 |
+
value = cast(T, cached_value)
|
| 174 |
+
|
| 175 |
+
return value
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class _LRUCacheWrapper(Generic[T]):
|
| 179 |
+
def __init__(self, maxsize: int | None, typed: bool, always_checkpoint: bool):
|
| 180 |
+
self._maxsize = maxsize
|
| 181 |
+
self._typed = typed
|
| 182 |
+
self._always_checkpoint = always_checkpoint
|
| 183 |
+
|
| 184 |
+
@overload
|
| 185 |
+
def __call__( # type: ignore[overload-overlap]
|
| 186 |
+
self, func: Callable[P, Coroutine[Any, Any, T]], /
|
| 187 |
+
) -> AsyncLRUCacheWrapper[P, T]: ...
|
| 188 |
+
|
| 189 |
+
@overload
|
| 190 |
+
def __call__(
|
| 191 |
+
self, func: Callable[..., T], /
|
| 192 |
+
) -> functools._lru_cache_wrapper[T]: ...
|
| 193 |
+
|
| 194 |
+
def __call__(
|
| 195 |
+
self, f: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T], /
|
| 196 |
+
) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]:
|
| 197 |
+
if iscoroutinefunction(f):
|
| 198 |
+
return AsyncLRUCacheWrapper(
|
| 199 |
+
f, self._maxsize, self._typed, self._always_checkpoint
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
return functools.lru_cache(maxsize=self._maxsize, typed=self._typed)(f) # type: ignore[arg-type]
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
@overload
|
| 206 |
+
def cache( # type: ignore[overload-overlap]
|
| 207 |
+
func: Callable[P, Coroutine[Any, Any, T]], /
|
| 208 |
+
) -> AsyncLRUCacheWrapper[P, T]: ...
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@overload
|
| 212 |
+
def cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def cache(
|
| 216 |
+
func: Callable[..., T] | Callable[P, Coroutine[Any, Any, T]], /
|
| 217 |
+
) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]:
|
| 218 |
+
"""
|
| 219 |
+
A convenient shortcut for :func:`lru_cache` with ``maxsize=None``.
|
| 220 |
+
|
| 221 |
+
This is the asynchronous equivalent to :func:`functools.cache`.
|
| 222 |
+
|
| 223 |
+
"""
|
| 224 |
+
return lru_cache(maxsize=None)(func)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@overload
|
| 228 |
+
def lru_cache(
|
| 229 |
+
*, maxsize: int | None = ..., typed: bool = ..., always_checkpoint: bool = ...
|
| 230 |
+
) -> _LRUCacheWrapper[Any]: ...
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
@overload
|
| 234 |
+
def lru_cache( # type: ignore[overload-overlap]
|
| 235 |
+
func: Callable[P, Coroutine[Any, Any, T]], /
|
| 236 |
+
) -> AsyncLRUCacheWrapper[P, T]: ...
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@overload
|
| 240 |
+
def lru_cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def lru_cache(
|
| 244 |
+
func: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T] | None = None,
|
| 245 |
+
/,
|
| 246 |
+
*,
|
| 247 |
+
maxsize: int | None = 128,
|
| 248 |
+
typed: bool = False,
|
| 249 |
+
always_checkpoint: bool = False,
|
| 250 |
+
) -> (
|
| 251 |
+
AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T] | _LRUCacheWrapper[Any]
|
| 252 |
+
):
|
| 253 |
+
"""
|
| 254 |
+
An asynchronous version of :func:`functools.lru_cache`.
|
| 255 |
+
|
| 256 |
+
If a synchronous function is passed, the standard library
|
| 257 |
+
:func:`functools.lru_cache` is applied instead.
|
| 258 |
+
|
| 259 |
+
:param always_checkpoint: if ``True``, every call to the cached function will be
|
| 260 |
+
guaranteed to yield control to the event loop at least once
|
| 261 |
+
|
| 262 |
+
.. note:: Caches and locks are managed on a per-event loop basis.
|
| 263 |
+
|
| 264 |
+
"""
|
| 265 |
+
if func is None:
|
| 266 |
+
return _LRUCacheWrapper[Any](maxsize, typed, always_checkpoint)
|
| 267 |
+
|
| 268 |
+
if not callable(func):
|
| 269 |
+
raise TypeError("the first argument must be callable")
|
| 270 |
+
|
| 271 |
+
return _LRUCacheWrapper[T](maxsize, typed, always_checkpoint)(func)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
@overload
|
| 275 |
+
async def reduce(
|
| 276 |
+
function: Callable[[T, S], Awaitable[T]],
|
| 277 |
+
iterable: Iterable[S] | AsyncIterable[S],
|
| 278 |
+
/,
|
| 279 |
+
initial: T,
|
| 280 |
+
) -> T: ...
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
@overload
|
| 284 |
+
async def reduce(
|
| 285 |
+
function: Callable[[T, T], Awaitable[T]],
|
| 286 |
+
iterable: Iterable[T] | AsyncIterable[T],
|
| 287 |
+
/,
|
| 288 |
+
) -> T: ...
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
async def reduce( # type: ignore[misc]
|
| 292 |
+
function: Callable[[T, T], Awaitable[T]] | Callable[[T, S], Awaitable[T]],
|
| 293 |
+
iterable: Iterable[T] | Iterable[S] | AsyncIterable[T] | AsyncIterable[S],
|
| 294 |
+
/,
|
| 295 |
+
initial: T | _InitialMissingType = initial_missing,
|
| 296 |
+
) -> T:
|
| 297 |
+
"""
|
| 298 |
+
Asynchronous version of :func:`functools.reduce`.
|
| 299 |
+
|
| 300 |
+
:param function: a coroutine function that takes two arguments: the accumulated
|
| 301 |
+
value and the next element from the iterable
|
| 302 |
+
:param iterable: an iterable or async iterable
|
| 303 |
+
:param initial: the initial value (if missing, the first element of the iterable is
|
| 304 |
+
used as the initial value)
|
| 305 |
+
|
| 306 |
+
"""
|
| 307 |
+
element: Any
|
| 308 |
+
function_called = False
|
| 309 |
+
if isinstance(iterable, AsyncIterable):
|
| 310 |
+
async_it = iterable.__aiter__()
|
| 311 |
+
if initial is initial_missing:
|
| 312 |
+
try:
|
| 313 |
+
value = cast(T, await async_it.__anext__())
|
| 314 |
+
except StopAsyncIteration:
|
| 315 |
+
raise TypeError(
|
| 316 |
+
"reduce() of empty sequence with no initial value"
|
| 317 |
+
) from None
|
| 318 |
+
else:
|
| 319 |
+
value = cast(T, initial)
|
| 320 |
+
|
| 321 |
+
async for element in async_it:
|
| 322 |
+
value = await function(value, element)
|
| 323 |
+
function_called = True
|
| 324 |
+
elif isinstance(iterable, Iterable):
|
| 325 |
+
it = iter(iterable)
|
| 326 |
+
if initial is initial_missing:
|
| 327 |
+
try:
|
| 328 |
+
value = cast(T, next(it))
|
| 329 |
+
except StopIteration:
|
| 330 |
+
raise TypeError(
|
| 331 |
+
"reduce() of empty sequence with no initial value"
|
| 332 |
+
) from None
|
| 333 |
+
else:
|
| 334 |
+
value = cast(T, initial)
|
| 335 |
+
|
| 336 |
+
for element in it:
|
| 337 |
+
value = await function(value, element)
|
| 338 |
+
function_called = True
|
| 339 |
+
else:
|
| 340 |
+
raise TypeError("reduce() argument 2 must be an iterable or async iterable")
|
| 341 |
+
|
| 342 |
+
# Make sure there is at least one checkpoint, even if an empty iterable and an
|
| 343 |
+
# initial value were given
|
| 344 |
+
if not function_called:
|
| 345 |
+
await checkpoint()
|
| 346 |
+
|
| 347 |
+
return value
|
env/lib/python3.13/site-packages/anyio/lowlevel.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
__all__ = (
|
| 4 |
+
"EventLoopToken",
|
| 5 |
+
"RunvarToken",
|
| 6 |
+
"RunVar",
|
| 7 |
+
"checkpoint",
|
| 8 |
+
"checkpoint_if_cancelled",
|
| 9 |
+
"cancel_shielded_checkpoint",
|
| 10 |
+
"current_token",
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
import enum
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from types import TracebackType
|
| 16 |
+
from typing import Any, Generic, Literal, TypeVar, final, overload
|
| 17 |
+
from weakref import WeakKeyDictionary
|
| 18 |
+
|
| 19 |
+
from ._core._eventloop import get_async_backend
|
| 20 |
+
from .abc import AsyncBackend
|
| 21 |
+
|
| 22 |
+
T = TypeVar("T")
|
| 23 |
+
D = TypeVar("D")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
async def checkpoint() -> None:
|
| 27 |
+
"""
|
| 28 |
+
Check for cancellation and allow the scheduler to switch to another task.
|
| 29 |
+
|
| 30 |
+
Equivalent to (but more efficient than)::
|
| 31 |
+
|
| 32 |
+
await checkpoint_if_cancelled()
|
| 33 |
+
await cancel_shielded_checkpoint()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
.. versionadded:: 3.0
|
| 37 |
+
|
| 38 |
+
"""
|
| 39 |
+
await get_async_backend().checkpoint()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
async def checkpoint_if_cancelled() -> None:
|
| 43 |
+
"""
|
| 44 |
+
Enter a checkpoint if the enclosing cancel scope has been cancelled.
|
| 45 |
+
|
| 46 |
+
This does not allow the scheduler to switch to a different task.
|
| 47 |
+
|
| 48 |
+
.. versionadded:: 3.0
|
| 49 |
+
|
| 50 |
+
"""
|
| 51 |
+
await get_async_backend().checkpoint_if_cancelled()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
async def cancel_shielded_checkpoint() -> None:
|
| 55 |
+
"""
|
| 56 |
+
Allow the scheduler to switch to another task but without checking for cancellation.
|
| 57 |
+
|
| 58 |
+
Equivalent to (but potentially more efficient than)::
|
| 59 |
+
|
| 60 |
+
with CancelScope(shield=True):
|
| 61 |
+
await checkpoint()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
.. versionadded:: 3.0
|
| 65 |
+
|
| 66 |
+
"""
|
| 67 |
+
await get_async_backend().cancel_shielded_checkpoint()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@final
|
| 71 |
+
@dataclass(frozen=True, repr=False)
|
| 72 |
+
class EventLoopToken:
|
| 73 |
+
"""
|
| 74 |
+
An opaque object that holds a reference to an event loop.
|
| 75 |
+
|
| 76 |
+
.. versionadded:: 4.11.0
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
backend_class: type[AsyncBackend]
|
| 80 |
+
native_token: object
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def current_token() -> EventLoopToken:
|
| 84 |
+
"""
|
| 85 |
+
Return a token object that can be used to call code in the current event loop from
|
| 86 |
+
another thread.
|
| 87 |
+
|
| 88 |
+
.. versionadded:: 4.11.0
|
| 89 |
+
|
| 90 |
+
"""
|
| 91 |
+
backend_class = get_async_backend()
|
| 92 |
+
raw_token = backend_class.current_token()
|
| 93 |
+
return EventLoopToken(backend_class, raw_token)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
_run_vars: WeakKeyDictionary[object, dict[RunVar[Any], Any]] = WeakKeyDictionary()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class _NoValueSet(enum.Enum):
|
| 100 |
+
NO_VALUE_SET = enum.auto()
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class RunvarToken(Generic[T]):
|
| 104 |
+
__slots__ = "_var", "_value", "_redeemed"
|
| 105 |
+
|
| 106 |
+
def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]):
|
| 107 |
+
self._var = var
|
| 108 |
+
self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value
|
| 109 |
+
self._redeemed = False
|
| 110 |
+
|
| 111 |
+
def __enter__(self) -> RunvarToken[T]:
|
| 112 |
+
return self
|
| 113 |
+
|
| 114 |
+
def __exit__(
|
| 115 |
+
self,
|
| 116 |
+
exc_type: type[BaseException] | None,
|
| 117 |
+
exc_val: BaseException | None,
|
| 118 |
+
exc_tb: TracebackType | None,
|
| 119 |
+
) -> None:
|
| 120 |
+
self._var.reset(self)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class RunVar(Generic[T]):
|
| 124 |
+
"""
|
| 125 |
+
Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop.
|
| 126 |
+
|
| 127 |
+
Can be used as a context manager, Just like :class:`~contextvars.ContextVar`, that
|
| 128 |
+
will reset the variable to its previous value when the context block is exited.
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
__slots__ = "_name", "_default"
|
| 132 |
+
|
| 133 |
+
NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET
|
| 134 |
+
|
| 135 |
+
def __init__(
|
| 136 |
+
self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET
|
| 137 |
+
):
|
| 138 |
+
self._name = name
|
| 139 |
+
self._default = default
|
| 140 |
+
|
| 141 |
+
@property
|
| 142 |
+
def _current_vars(self) -> dict[RunVar[T], T]:
|
| 143 |
+
native_token = current_token().native_token
|
| 144 |
+
try:
|
| 145 |
+
return _run_vars[native_token]
|
| 146 |
+
except KeyError:
|
| 147 |
+
run_vars = _run_vars[native_token] = {}
|
| 148 |
+
return run_vars
|
| 149 |
+
|
| 150 |
+
@overload
|
| 151 |
+
def get(self, default: D) -> T | D: ...
|
| 152 |
+
|
| 153 |
+
@overload
|
| 154 |
+
def get(self) -> T: ...
|
| 155 |
+
|
| 156 |
+
def get(
|
| 157 |
+
self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET
|
| 158 |
+
) -> T | D:
|
| 159 |
+
try:
|
| 160 |
+
return self._current_vars[self]
|
| 161 |
+
except KeyError:
|
| 162 |
+
if default is not RunVar.NO_VALUE_SET:
|
| 163 |
+
return default
|
| 164 |
+
elif self._default is not RunVar.NO_VALUE_SET:
|
| 165 |
+
return self._default
|
| 166 |
+
|
| 167 |
+
raise LookupError(
|
| 168 |
+
f'Run variable "{self._name}" has no value and no default set'
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
def set(self, value: T) -> RunvarToken[T]:
|
| 172 |
+
current_vars = self._current_vars
|
| 173 |
+
token = RunvarToken(self, current_vars.get(self, RunVar.NO_VALUE_SET))
|
| 174 |
+
current_vars[self] = value
|
| 175 |
+
return token
|
| 176 |
+
|
| 177 |
+
def reset(self, token: RunvarToken[T]) -> None:
|
| 178 |
+
if token._var is not self:
|
| 179 |
+
raise ValueError("This token does not belong to this RunVar")
|
| 180 |
+
|
| 181 |
+
if token._redeemed:
|
| 182 |
+
raise ValueError("This token has already been used")
|
| 183 |
+
|
| 184 |
+
if token._value is _NoValueSet.NO_VALUE_SET:
|
| 185 |
+
try:
|
| 186 |
+
del self._current_vars[self]
|
| 187 |
+
except KeyError:
|
| 188 |
+
pass
|
| 189 |
+
else:
|
| 190 |
+
self._current_vars[self] = token._value
|
| 191 |
+
|
| 192 |
+
token._redeemed = True
|
| 193 |
+
|
| 194 |
+
def __repr__(self) -> str:
|
| 195 |
+
return f"<RunVar name={self._name!r}>"
|
env/lib/python3.13/site-packages/anyio/py.typed
ADDED
|
File without changes
|
env/lib/python3.13/site-packages/anyio/pytest_plugin.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import socket
|
| 4 |
+
import sys
|
| 5 |
+
from collections.abc import Callable, Generator, Iterator
|
| 6 |
+
from contextlib import ExitStack, contextmanager
|
| 7 |
+
from inspect import isasyncgenfunction, iscoroutinefunction, ismethod
|
| 8 |
+
from typing import Any, cast
|
| 9 |
+
|
| 10 |
+
import pytest
|
| 11 |
+
from _pytest.fixtures import SubRequest
|
| 12 |
+
from _pytest.outcomes import Exit
|
| 13 |
+
|
| 14 |
+
from . import get_available_backends
|
| 15 |
+
from ._core._eventloop import (
|
| 16 |
+
current_async_library,
|
| 17 |
+
get_async_backend,
|
| 18 |
+
reset_current_async_library,
|
| 19 |
+
set_current_async_library,
|
| 20 |
+
)
|
| 21 |
+
from ._core._exceptions import iterate_exceptions
|
| 22 |
+
from .abc import TestRunner
|
| 23 |
+
|
| 24 |
+
if sys.version_info < (3, 11):
|
| 25 |
+
from exceptiongroup import ExceptionGroup
|
| 26 |
+
|
| 27 |
+
_current_runner: TestRunner | None = None
|
| 28 |
+
_runner_stack: ExitStack | None = None
|
| 29 |
+
_runner_leases = 0
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]:
|
| 33 |
+
if isinstance(backend, str):
|
| 34 |
+
return backend, {}
|
| 35 |
+
elif isinstance(backend, tuple) and len(backend) == 2:
|
| 36 |
+
if isinstance(backend[0], str) and isinstance(backend[1], dict):
|
| 37 |
+
return cast(tuple[str, dict[str, Any]], backend)
|
| 38 |
+
|
| 39 |
+
raise TypeError("anyio_backend must be either a string or tuple of (string, dict)")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@contextmanager
|
| 43 |
+
def get_runner(
|
| 44 |
+
backend_name: str, backend_options: dict[str, Any]
|
| 45 |
+
) -> Iterator[TestRunner]:
|
| 46 |
+
global _current_runner, _runner_leases, _runner_stack
|
| 47 |
+
if _current_runner is None:
|
| 48 |
+
asynclib = get_async_backend(backend_name)
|
| 49 |
+
_runner_stack = ExitStack()
|
| 50 |
+
if current_async_library() is None:
|
| 51 |
+
# Since we're in control of the event loop, we can cache the name of the
|
| 52 |
+
# async library
|
| 53 |
+
token = set_current_async_library(backend_name)
|
| 54 |
+
_runner_stack.callback(reset_current_async_library, token)
|
| 55 |
+
|
| 56 |
+
backend_options = backend_options or {}
|
| 57 |
+
_current_runner = _runner_stack.enter_context(
|
| 58 |
+
asynclib.create_test_runner(backend_options)
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
_runner_leases += 1
|
| 62 |
+
try:
|
| 63 |
+
yield _current_runner
|
| 64 |
+
finally:
|
| 65 |
+
_runner_leases -= 1
|
| 66 |
+
if not _runner_leases:
|
| 67 |
+
assert _runner_stack is not None
|
| 68 |
+
_runner_stack.close()
|
| 69 |
+
_runner_stack = _current_runner = None
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
| 73 |
+
parser.addini(
|
| 74 |
+
"anyio_mode",
|
| 75 |
+
default="strict",
|
| 76 |
+
help='AnyIO plugin mode (either "strict" or "auto")',
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def pytest_configure(config: pytest.Config) -> None:
|
| 81 |
+
config.addinivalue_line(
|
| 82 |
+
"markers",
|
| 83 |
+
"anyio: mark the (coroutine function) test to be run asynchronously via anyio.",
|
| 84 |
+
)
|
| 85 |
+
if (
|
| 86 |
+
config.getini("anyio_mode") == "auto"
|
| 87 |
+
and config.pluginmanager.has_plugin("asyncio")
|
| 88 |
+
and config.getini("asyncio_mode") == "auto"
|
| 89 |
+
):
|
| 90 |
+
config.issue_config_time_warning(
|
| 91 |
+
pytest.PytestConfigWarning(
|
| 92 |
+
"AnyIO auto mode has been enabled together with pytest-asyncio auto "
|
| 93 |
+
"mode. This may cause unexpected behavior."
|
| 94 |
+
),
|
| 95 |
+
1,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@pytest.hookimpl(hookwrapper=True)
|
| 100 |
+
def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]:
|
| 101 |
+
def wrapper(anyio_backend: Any, request: SubRequest, **kwargs: Any) -> Any:
|
| 102 |
+
# Rebind any fixture methods to the request instance
|
| 103 |
+
if (
|
| 104 |
+
request.instance
|
| 105 |
+
and ismethod(func)
|
| 106 |
+
and type(func.__self__) is type(request.instance)
|
| 107 |
+
):
|
| 108 |
+
local_func = func.__func__.__get__(request.instance)
|
| 109 |
+
else:
|
| 110 |
+
local_func = func
|
| 111 |
+
|
| 112 |
+
backend_name, backend_options = extract_backend_and_options(anyio_backend)
|
| 113 |
+
if has_backend_arg:
|
| 114 |
+
kwargs["anyio_backend"] = anyio_backend
|
| 115 |
+
|
| 116 |
+
if has_request_arg:
|
| 117 |
+
kwargs["request"] = request
|
| 118 |
+
|
| 119 |
+
with get_runner(backend_name, backend_options) as runner:
|
| 120 |
+
if isasyncgenfunction(local_func):
|
| 121 |
+
yield from runner.run_asyncgen_fixture(local_func, kwargs)
|
| 122 |
+
else:
|
| 123 |
+
yield runner.run_fixture(local_func, kwargs)
|
| 124 |
+
|
| 125 |
+
# Only apply this to coroutine functions and async generator functions in requests
|
| 126 |
+
# that involve the anyio_backend fixture
|
| 127 |
+
func = fixturedef.func
|
| 128 |
+
if isasyncgenfunction(func) or iscoroutinefunction(func):
|
| 129 |
+
if "anyio_backend" in request.fixturenames:
|
| 130 |
+
fixturedef.func = wrapper
|
| 131 |
+
original_argname = fixturedef.argnames
|
| 132 |
+
|
| 133 |
+
if not (has_backend_arg := "anyio_backend" in fixturedef.argnames):
|
| 134 |
+
fixturedef.argnames += ("anyio_backend",)
|
| 135 |
+
|
| 136 |
+
if not (has_request_arg := "request" in fixturedef.argnames):
|
| 137 |
+
fixturedef.argnames += ("request",)
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
return (yield)
|
| 141 |
+
finally:
|
| 142 |
+
fixturedef.func = func
|
| 143 |
+
fixturedef.argnames = original_argname
|
| 144 |
+
|
| 145 |
+
return (yield)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@pytest.hookimpl(tryfirst=True)
|
| 149 |
+
def pytest_pycollect_makeitem(
|
| 150 |
+
collector: pytest.Module | pytest.Class, name: str, obj: object
|
| 151 |
+
) -> None:
|
| 152 |
+
if collector.istestfunction(obj, name):
|
| 153 |
+
inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj
|
| 154 |
+
if iscoroutinefunction(inner_func):
|
| 155 |
+
anyio_auto_mode = collector.config.getini("anyio_mode") == "auto"
|
| 156 |
+
marker = collector.get_closest_marker("anyio")
|
| 157 |
+
own_markers = getattr(obj, "pytestmark", ())
|
| 158 |
+
if (
|
| 159 |
+
anyio_auto_mode
|
| 160 |
+
or marker
|
| 161 |
+
or any(marker.name == "anyio" for marker in own_markers)
|
| 162 |
+
):
|
| 163 |
+
pytest.mark.usefixtures("anyio_backend")(obj)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@pytest.hookimpl(tryfirst=True)
|
| 167 |
+
def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None:
|
| 168 |
+
def run_with_hypothesis(**kwargs: Any) -> None:
|
| 169 |
+
with get_runner(backend_name, backend_options) as runner:
|
| 170 |
+
runner.run_test(original_func, kwargs)
|
| 171 |
+
|
| 172 |
+
backend = pyfuncitem.funcargs.get("anyio_backend")
|
| 173 |
+
if backend:
|
| 174 |
+
backend_name, backend_options = extract_backend_and_options(backend)
|
| 175 |
+
|
| 176 |
+
if hasattr(pyfuncitem.obj, "hypothesis"):
|
| 177 |
+
# Wrap the inner test function unless it's already wrapped
|
| 178 |
+
original_func = pyfuncitem.obj.hypothesis.inner_test
|
| 179 |
+
if original_func.__qualname__ != run_with_hypothesis.__qualname__:
|
| 180 |
+
if iscoroutinefunction(original_func):
|
| 181 |
+
pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis
|
| 182 |
+
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
if iscoroutinefunction(pyfuncitem.obj):
|
| 186 |
+
funcargs = pyfuncitem.funcargs
|
| 187 |
+
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
|
| 188 |
+
with get_runner(backend_name, backend_options) as runner:
|
| 189 |
+
try:
|
| 190 |
+
runner.run_test(pyfuncitem.obj, testargs)
|
| 191 |
+
except ExceptionGroup as excgrp:
|
| 192 |
+
for exc in iterate_exceptions(excgrp):
|
| 193 |
+
if isinstance(exc, (Exit, KeyboardInterrupt, SystemExit)):
|
| 194 |
+
raise exc from excgrp
|
| 195 |
+
|
| 196 |
+
raise
|
| 197 |
+
|
| 198 |
+
return True
|
| 199 |
+
|
| 200 |
+
return None
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@pytest.fixture(scope="module", params=get_available_backends())
|
| 204 |
+
def anyio_backend(request: Any) -> Any:
|
| 205 |
+
return request.param
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@pytest.fixture
|
| 209 |
+
def anyio_backend_name(anyio_backend: Any) -> str:
|
| 210 |
+
if isinstance(anyio_backend, str):
|
| 211 |
+
return anyio_backend
|
| 212 |
+
else:
|
| 213 |
+
return anyio_backend[0]
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@pytest.fixture
|
| 217 |
+
def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]:
|
| 218 |
+
if isinstance(anyio_backend, str):
|
| 219 |
+
return {}
|
| 220 |
+
else:
|
| 221 |
+
return anyio_backend[1]
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
class FreePortFactory:
|
| 225 |
+
"""
|
| 226 |
+
Manages port generation based on specified socket kind, ensuring no duplicate
|
| 227 |
+
ports are generated.
|
| 228 |
+
|
| 229 |
+
This class provides functionality for generating available free ports on the
|
| 230 |
+
system. It is initialized with a specific socket kind and can generate ports
|
| 231 |
+
for given address families while avoiding reuse of previously generated ports.
|
| 232 |
+
|
| 233 |
+
Users should not instantiate this class directly, but use the
|
| 234 |
+
``free_tcp_port_factory`` and ``free_udp_port_factory`` fixtures instead. For simple
|
| 235 |
+
uses cases, ``free_tcp_port`` and ``free_udp_port`` can be used instead.
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
def __init__(self, kind: socket.SocketKind) -> None:
|
| 239 |
+
self._kind = kind
|
| 240 |
+
self._generated = set[int]()
|
| 241 |
+
|
| 242 |
+
@property
|
| 243 |
+
def kind(self) -> socket.SocketKind:
|
| 244 |
+
"""
|
| 245 |
+
The type of socket connection (e.g., :data:`~socket.SOCK_STREAM` or
|
| 246 |
+
:data:`~socket.SOCK_DGRAM`) used to bind for checking port availability
|
| 247 |
+
|
| 248 |
+
"""
|
| 249 |
+
return self._kind
|
| 250 |
+
|
| 251 |
+
def __call__(self, family: socket.AddressFamily | None = None) -> int:
|
| 252 |
+
"""
|
| 253 |
+
Return an unbound port for the given address family.
|
| 254 |
+
|
| 255 |
+
:param family: if omitted, both IPv4 and IPv6 addresses will be tried
|
| 256 |
+
:return: a port number
|
| 257 |
+
|
| 258 |
+
"""
|
| 259 |
+
if family is not None:
|
| 260 |
+
families = [family]
|
| 261 |
+
else:
|
| 262 |
+
families = [socket.AF_INET]
|
| 263 |
+
if socket.has_ipv6:
|
| 264 |
+
families.append(socket.AF_INET6)
|
| 265 |
+
|
| 266 |
+
while True:
|
| 267 |
+
port = 0
|
| 268 |
+
with ExitStack() as stack:
|
| 269 |
+
for family in families:
|
| 270 |
+
sock = stack.enter_context(socket.socket(family, self._kind))
|
| 271 |
+
addr = "::1" if family == socket.AF_INET6 else "127.0.0.1"
|
| 272 |
+
try:
|
| 273 |
+
sock.bind((addr, port))
|
| 274 |
+
except OSError:
|
| 275 |
+
break
|
| 276 |
+
|
| 277 |
+
if not port:
|
| 278 |
+
port = sock.getsockname()[1]
|
| 279 |
+
else:
|
| 280 |
+
if port not in self._generated:
|
| 281 |
+
self._generated.add(port)
|
| 282 |
+
return port
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
@pytest.fixture(scope="session")
|
| 286 |
+
def free_tcp_port_factory() -> FreePortFactory:
|
| 287 |
+
return FreePortFactory(socket.SOCK_STREAM)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
@pytest.fixture(scope="session")
|
| 291 |
+
def free_udp_port_factory() -> FreePortFactory:
|
| 292 |
+
return FreePortFactory(socket.SOCK_DGRAM)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
@pytest.fixture
|
| 296 |
+
def free_tcp_port(free_tcp_port_factory: Callable[[], int]) -> int:
|
| 297 |
+
return free_tcp_port_factory()
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
@pytest.fixture
|
| 301 |
+
def free_udp_port(free_udp_port_factory: Callable[[], int]) -> int:
|
| 302 |
+
return free_udp_port_factory()
|
env/lib/python3.13/site-packages/anyio/to_interpreter.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
__all__ = (
|
| 4 |
+
"run_sync",
|
| 5 |
+
"current_default_interpreter_limiter",
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
import atexit
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
from collections import deque
|
| 12 |
+
from collections.abc import Callable
|
| 13 |
+
from typing import Any, Final, TypeVar
|
| 14 |
+
|
| 15 |
+
from . import current_time, to_thread
|
| 16 |
+
from ._core._exceptions import BrokenWorkerInterpreter
|
| 17 |
+
from ._core._synchronization import CapacityLimiter
|
| 18 |
+
from .lowlevel import RunVar
|
| 19 |
+
|
| 20 |
+
if sys.version_info >= (3, 11):
|
| 21 |
+
from typing import TypeVarTuple, Unpack
|
| 22 |
+
else:
|
| 23 |
+
from typing_extensions import TypeVarTuple, Unpack
|
| 24 |
+
|
| 25 |
+
if sys.version_info >= (3, 14):
|
| 26 |
+
from concurrent.interpreters import ExecutionFailed, create
|
| 27 |
+
|
| 28 |
+
def _interp_call(
|
| 29 |
+
func: Callable[..., Any], args: tuple[Any, ...]
|
| 30 |
+
) -> tuple[Any, bool]:
|
| 31 |
+
try:
|
| 32 |
+
retval = func(*args)
|
| 33 |
+
except BaseException as exc:
|
| 34 |
+
return exc, True
|
| 35 |
+
else:
|
| 36 |
+
return retval, False
|
| 37 |
+
|
| 38 |
+
class _Worker:
|
| 39 |
+
last_used: float = 0
|
| 40 |
+
|
| 41 |
+
def __init__(self) -> None:
|
| 42 |
+
self._interpreter = create()
|
| 43 |
+
|
| 44 |
+
def destroy(self) -> None:
|
| 45 |
+
self._interpreter.close()
|
| 46 |
+
|
| 47 |
+
def call(
|
| 48 |
+
self,
|
| 49 |
+
func: Callable[..., T_Retval],
|
| 50 |
+
args: tuple[Any, ...],
|
| 51 |
+
) -> T_Retval:
|
| 52 |
+
try:
|
| 53 |
+
res, is_exception = self._interpreter.call(_interp_call, func, args)
|
| 54 |
+
except ExecutionFailed as exc:
|
| 55 |
+
raise BrokenWorkerInterpreter(exc.excinfo) from exc
|
| 56 |
+
|
| 57 |
+
if is_exception:
|
| 58 |
+
raise res
|
| 59 |
+
|
| 60 |
+
return res
|
| 61 |
+
elif sys.version_info >= (3, 13):
|
| 62 |
+
import _interpqueues
|
| 63 |
+
import _interpreters
|
| 64 |
+
|
| 65 |
+
UNBOUND: Final = 2 # I have no clue how this works, but it was used in the stdlib
|
| 66 |
+
FMT_UNPICKLED: Final = 0
|
| 67 |
+
FMT_PICKLED: Final = 1
|
| 68 |
+
QUEUE_PICKLE_ARGS: Final = (FMT_PICKLED, UNBOUND)
|
| 69 |
+
QUEUE_UNPICKLE_ARGS: Final = (FMT_UNPICKLED, UNBOUND)
|
| 70 |
+
|
| 71 |
+
_run_func = compile(
|
| 72 |
+
"""
|
| 73 |
+
import _interpqueues
|
| 74 |
+
from _interpreters import NotShareableError
|
| 75 |
+
from pickle import loads, dumps, HIGHEST_PROTOCOL
|
| 76 |
+
|
| 77 |
+
QUEUE_PICKLE_ARGS = (1, 2)
|
| 78 |
+
QUEUE_UNPICKLE_ARGS = (0, 2)
|
| 79 |
+
|
| 80 |
+
item = _interpqueues.get(queue_id)[0]
|
| 81 |
+
try:
|
| 82 |
+
func, args = loads(item)
|
| 83 |
+
retval = func(*args)
|
| 84 |
+
except BaseException as exc:
|
| 85 |
+
is_exception = True
|
| 86 |
+
retval = exc
|
| 87 |
+
else:
|
| 88 |
+
is_exception = False
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
_interpqueues.put(queue_id, (retval, is_exception), *QUEUE_UNPICKLE_ARGS)
|
| 92 |
+
except NotShareableError:
|
| 93 |
+
retval = dumps(retval, HIGHEST_PROTOCOL)
|
| 94 |
+
_interpqueues.put(queue_id, (retval, is_exception), *QUEUE_PICKLE_ARGS)
|
| 95 |
+
""",
|
| 96 |
+
"<string>",
|
| 97 |
+
"exec",
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
class _Worker:
|
| 101 |
+
last_used: float = 0
|
| 102 |
+
|
| 103 |
+
def __init__(self) -> None:
|
| 104 |
+
self._interpreter_id = _interpreters.create()
|
| 105 |
+
self._queue_id = _interpqueues.create(1, *QUEUE_UNPICKLE_ARGS)
|
| 106 |
+
_interpreters.set___main___attrs(
|
| 107 |
+
self._interpreter_id, {"queue_id": self._queue_id}
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
def destroy(self) -> None:
|
| 111 |
+
_interpqueues.destroy(self._queue_id)
|
| 112 |
+
_interpreters.destroy(self._interpreter_id)
|
| 113 |
+
|
| 114 |
+
def call(
|
| 115 |
+
self,
|
| 116 |
+
func: Callable[..., T_Retval],
|
| 117 |
+
args: tuple[Any, ...],
|
| 118 |
+
) -> T_Retval:
|
| 119 |
+
import pickle
|
| 120 |
+
|
| 121 |
+
item = pickle.dumps((func, args), pickle.HIGHEST_PROTOCOL)
|
| 122 |
+
_interpqueues.put(self._queue_id, item, *QUEUE_PICKLE_ARGS)
|
| 123 |
+
exc_info = _interpreters.exec(self._interpreter_id, _run_func)
|
| 124 |
+
if exc_info:
|
| 125 |
+
raise BrokenWorkerInterpreter(exc_info)
|
| 126 |
+
|
| 127 |
+
res = _interpqueues.get(self._queue_id)
|
| 128 |
+
(res, is_exception), fmt = res[:2]
|
| 129 |
+
if fmt == FMT_PICKLED:
|
| 130 |
+
res = pickle.loads(res)
|
| 131 |
+
|
| 132 |
+
if is_exception:
|
| 133 |
+
raise res
|
| 134 |
+
|
| 135 |
+
return res
|
| 136 |
+
else:
|
| 137 |
+
|
| 138 |
+
class _Worker:
|
| 139 |
+
last_used: float = 0
|
| 140 |
+
|
| 141 |
+
def __init__(self) -> None:
|
| 142 |
+
raise RuntimeError("subinterpreters require at least Python 3.13")
|
| 143 |
+
|
| 144 |
+
def call(
|
| 145 |
+
self,
|
| 146 |
+
func: Callable[..., T_Retval],
|
| 147 |
+
args: tuple[Any, ...],
|
| 148 |
+
) -> T_Retval:
|
| 149 |
+
raise NotImplementedError
|
| 150 |
+
|
| 151 |
+
def destroy(self) -> None:
|
| 152 |
+
pass
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
DEFAULT_CPU_COUNT: Final = 8 # this is just an arbitrarily selected value
|
| 156 |
+
MAX_WORKER_IDLE_TIME = (
|
| 157 |
+
30 # seconds a subinterpreter can be idle before becoming eligible for pruning
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
T_Retval = TypeVar("T_Retval")
|
| 161 |
+
PosArgsT = TypeVarTuple("PosArgsT")
|
| 162 |
+
|
| 163 |
+
_idle_workers = RunVar[deque[_Worker]]("_available_workers")
|
| 164 |
+
_default_interpreter_limiter = RunVar[CapacityLimiter]("_default_interpreter_limiter")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _stop_workers(workers: deque[_Worker]) -> None:
|
| 168 |
+
for worker in workers:
|
| 169 |
+
worker.destroy()
|
| 170 |
+
|
| 171 |
+
workers.clear()
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
async def run_sync(
|
| 175 |
+
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
| 176 |
+
*args: Unpack[PosArgsT],
|
| 177 |
+
limiter: CapacityLimiter | None = None,
|
| 178 |
+
) -> T_Retval:
|
| 179 |
+
"""
|
| 180 |
+
Call the given function with the given arguments in a subinterpreter.
|
| 181 |
+
|
| 182 |
+
.. warning:: On Python 3.13, the :mod:`concurrent.interpreters` module was not yet
|
| 183 |
+
available, so the code path for that Python version relies on an undocumented,
|
| 184 |
+
private API. As such, it is recommended to not rely on this function for anything
|
| 185 |
+
mission-critical on Python 3.13.
|
| 186 |
+
|
| 187 |
+
:param func: a callable
|
| 188 |
+
:param args: the positional arguments for the callable
|
| 189 |
+
:param limiter: capacity limiter to use to limit the total number of subinterpreters
|
| 190 |
+
running (if omitted, the default limiter is used)
|
| 191 |
+
:return: the result of the call
|
| 192 |
+
:raises BrokenWorkerInterpreter: if there's an internal error in a subinterpreter
|
| 193 |
+
|
| 194 |
+
"""
|
| 195 |
+
if limiter is None:
|
| 196 |
+
limiter = current_default_interpreter_limiter()
|
| 197 |
+
|
| 198 |
+
try:
|
| 199 |
+
idle_workers = _idle_workers.get()
|
| 200 |
+
except LookupError:
|
| 201 |
+
idle_workers = deque()
|
| 202 |
+
_idle_workers.set(idle_workers)
|
| 203 |
+
atexit.register(_stop_workers, idle_workers)
|
| 204 |
+
|
| 205 |
+
async with limiter:
|
| 206 |
+
try:
|
| 207 |
+
worker = idle_workers.pop()
|
| 208 |
+
except IndexError:
|
| 209 |
+
worker = _Worker()
|
| 210 |
+
|
| 211 |
+
try:
|
| 212 |
+
return await to_thread.run_sync(
|
| 213 |
+
worker.call,
|
| 214 |
+
func,
|
| 215 |
+
args,
|
| 216 |
+
limiter=limiter,
|
| 217 |
+
)
|
| 218 |
+
finally:
|
| 219 |
+
# Prune workers that have been idle for too long
|
| 220 |
+
now = current_time()
|
| 221 |
+
while idle_workers:
|
| 222 |
+
if now - idle_workers[0].last_used <= MAX_WORKER_IDLE_TIME:
|
| 223 |
+
break
|
| 224 |
+
|
| 225 |
+
await to_thread.run_sync(idle_workers.popleft().destroy, limiter=limiter)
|
| 226 |
+
|
| 227 |
+
worker.last_used = current_time()
|
| 228 |
+
idle_workers.append(worker)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def current_default_interpreter_limiter() -> CapacityLimiter:
|
| 232 |
+
"""
|
| 233 |
+
Return the capacity limiter used by default to limit the number of concurrently
|
| 234 |
+
running subinterpreters.
|
| 235 |
+
|
| 236 |
+
Defaults to the number of CPU cores.
|
| 237 |
+
|
| 238 |
+
:return: a capacity limiter object
|
| 239 |
+
|
| 240 |
+
"""
|
| 241 |
+
try:
|
| 242 |
+
return _default_interpreter_limiter.get()
|
| 243 |
+
except LookupError:
|
| 244 |
+
limiter = CapacityLimiter(os.cpu_count() or DEFAULT_CPU_COUNT)
|
| 245 |
+
_default_interpreter_limiter.set(limiter)
|
| 246 |
+
return limiter
|
env/lib/python3.13/site-packages/anyio/to_process.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
__all__ = (
|
| 4 |
+
"current_default_process_limiter",
|
| 5 |
+
"process_worker",
|
| 6 |
+
"run_sync",
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import pickle
|
| 11 |
+
import subprocess
|
| 12 |
+
import sys
|
| 13 |
+
from collections import deque
|
| 14 |
+
from collections.abc import Callable
|
| 15 |
+
from importlib.util import module_from_spec, spec_from_file_location
|
| 16 |
+
from typing import TypeVar, cast
|
| 17 |
+
|
| 18 |
+
from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class
|
| 19 |
+
from ._core._exceptions import BrokenWorkerProcess
|
| 20 |
+
from ._core._subprocesses import open_process
|
| 21 |
+
from ._core._synchronization import CapacityLimiter
|
| 22 |
+
from ._core._tasks import CancelScope, fail_after
|
| 23 |
+
from .abc import ByteReceiveStream, ByteSendStream, Process
|
| 24 |
+
from .lowlevel import RunVar, checkpoint_if_cancelled
|
| 25 |
+
from .streams.buffered import BufferedByteReceiveStream
|
| 26 |
+
|
| 27 |
+
if sys.version_info >= (3, 11):
|
| 28 |
+
from typing import TypeVarTuple, Unpack
|
| 29 |
+
else:
|
| 30 |
+
from typing_extensions import TypeVarTuple, Unpack
|
| 31 |
+
|
| 32 |
+
WORKER_MAX_IDLE_TIME = 300 # 5 minutes
|
| 33 |
+
|
| 34 |
+
T_Retval = TypeVar("T_Retval")
|
| 35 |
+
PosArgsT = TypeVarTuple("PosArgsT")
|
| 36 |
+
|
| 37 |
+
_process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers")
|
| 38 |
+
_process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar(
|
| 39 |
+
"_process_pool_idle_workers"
|
| 40 |
+
)
|
| 41 |
+
_default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
async def run_sync( # type: ignore[return]
|
| 45 |
+
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
| 46 |
+
*args: Unpack[PosArgsT],
|
| 47 |
+
cancellable: bool = False,
|
| 48 |
+
limiter: CapacityLimiter | None = None,
|
| 49 |
+
) -> T_Retval:
|
| 50 |
+
"""
|
| 51 |
+
Call the given function with the given arguments in a worker process.
|
| 52 |
+
|
| 53 |
+
If the ``cancellable`` option is enabled and the task waiting for its completion is
|
| 54 |
+
cancelled, the worker process running it will be abruptly terminated using SIGKILL
|
| 55 |
+
(or ``terminateProcess()`` on Windows).
|
| 56 |
+
|
| 57 |
+
:param func: a callable
|
| 58 |
+
:param args: positional arguments for the callable
|
| 59 |
+
:param cancellable: ``True`` to allow cancellation of the operation while it's
|
| 60 |
+
running
|
| 61 |
+
:param limiter: capacity limiter to use to limit the total amount of processes
|
| 62 |
+
running (if omitted, the default limiter is used)
|
| 63 |
+
:return: an awaitable that yields the return value of the function.
|
| 64 |
+
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
async def send_raw_command(pickled_cmd: bytes) -> object:
|
| 68 |
+
try:
|
| 69 |
+
await stdin.send(pickled_cmd)
|
| 70 |
+
response = await buffered.receive_until(b"\n", 50)
|
| 71 |
+
status, length = response.split(b" ")
|
| 72 |
+
if status not in (b"RETURN", b"EXCEPTION"):
|
| 73 |
+
raise RuntimeError(
|
| 74 |
+
f"Worker process returned unexpected response: {response!r}"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
pickled_response = await buffered.receive_exactly(int(length))
|
| 78 |
+
except BaseException as exc:
|
| 79 |
+
workers.discard(process)
|
| 80 |
+
try:
|
| 81 |
+
process.kill()
|
| 82 |
+
with CancelScope(shield=True):
|
| 83 |
+
await process.aclose()
|
| 84 |
+
except ProcessLookupError:
|
| 85 |
+
pass
|
| 86 |
+
|
| 87 |
+
if isinstance(exc, get_cancelled_exc_class()):
|
| 88 |
+
raise
|
| 89 |
+
else:
|
| 90 |
+
raise BrokenWorkerProcess from exc
|
| 91 |
+
|
| 92 |
+
retval = pickle.loads(pickled_response)
|
| 93 |
+
if status == b"EXCEPTION":
|
| 94 |
+
assert isinstance(retval, BaseException)
|
| 95 |
+
raise retval
|
| 96 |
+
else:
|
| 97 |
+
return retval
|
| 98 |
+
|
| 99 |
+
# First pickle the request before trying to reserve a worker process
|
| 100 |
+
await checkpoint_if_cancelled()
|
| 101 |
+
request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL)
|
| 102 |
+
|
| 103 |
+
# If this is the first run in this event loop thread, set up the necessary variables
|
| 104 |
+
try:
|
| 105 |
+
workers = _process_pool_workers.get()
|
| 106 |
+
idle_workers = _process_pool_idle_workers.get()
|
| 107 |
+
except LookupError:
|
| 108 |
+
workers = set()
|
| 109 |
+
idle_workers = deque()
|
| 110 |
+
_process_pool_workers.set(workers)
|
| 111 |
+
_process_pool_idle_workers.set(idle_workers)
|
| 112 |
+
get_async_backend().setup_process_pool_exit_at_shutdown(workers)
|
| 113 |
+
|
| 114 |
+
async with limiter or current_default_process_limiter():
|
| 115 |
+
# Pop processes from the pool (starting from the most recently used) until we
|
| 116 |
+
# find one that hasn't exited yet
|
| 117 |
+
process: Process
|
| 118 |
+
while idle_workers:
|
| 119 |
+
process, idle_since = idle_workers.pop()
|
| 120 |
+
if process.returncode is None:
|
| 121 |
+
stdin = cast(ByteSendStream, process.stdin)
|
| 122 |
+
buffered = BufferedByteReceiveStream(
|
| 123 |
+
cast(ByteReceiveStream, process.stdout)
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME
|
| 127 |
+
# seconds or longer
|
| 128 |
+
now = current_time()
|
| 129 |
+
killed_processes: list[Process] = []
|
| 130 |
+
while idle_workers:
|
| 131 |
+
if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME:
|
| 132 |
+
break
|
| 133 |
+
|
| 134 |
+
process_to_kill, idle_since = idle_workers.popleft()
|
| 135 |
+
process_to_kill.kill()
|
| 136 |
+
workers.remove(process_to_kill)
|
| 137 |
+
killed_processes.append(process_to_kill)
|
| 138 |
+
|
| 139 |
+
with CancelScope(shield=True):
|
| 140 |
+
for killed_process in killed_processes:
|
| 141 |
+
await killed_process.aclose()
|
| 142 |
+
|
| 143 |
+
break
|
| 144 |
+
|
| 145 |
+
workers.remove(process)
|
| 146 |
+
else:
|
| 147 |
+
command = [sys.executable, "-u", "-m", __name__]
|
| 148 |
+
process = await open_process(
|
| 149 |
+
command, stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
| 150 |
+
)
|
| 151 |
+
try:
|
| 152 |
+
stdin = cast(ByteSendStream, process.stdin)
|
| 153 |
+
buffered = BufferedByteReceiveStream(
|
| 154 |
+
cast(ByteReceiveStream, process.stdout)
|
| 155 |
+
)
|
| 156 |
+
with fail_after(20):
|
| 157 |
+
message = await buffered.receive(6)
|
| 158 |
+
|
| 159 |
+
if message != b"READY\n":
|
| 160 |
+
raise BrokenWorkerProcess(
|
| 161 |
+
f"Worker process returned unexpected response: {message!r}"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
main_module_path = getattr(sys.modules["__main__"], "__file__", None)
|
| 165 |
+
pickled = pickle.dumps(
|
| 166 |
+
("init", sys.path, main_module_path),
|
| 167 |
+
protocol=pickle.HIGHEST_PROTOCOL,
|
| 168 |
+
)
|
| 169 |
+
await send_raw_command(pickled)
|
| 170 |
+
except (BrokenWorkerProcess, get_cancelled_exc_class()):
|
| 171 |
+
raise
|
| 172 |
+
except BaseException as exc:
|
| 173 |
+
process.kill()
|
| 174 |
+
raise BrokenWorkerProcess(
|
| 175 |
+
"Error during worker process initialization"
|
| 176 |
+
) from exc
|
| 177 |
+
|
| 178 |
+
workers.add(process)
|
| 179 |
+
|
| 180 |
+
with CancelScope(shield=not cancellable):
|
| 181 |
+
try:
|
| 182 |
+
return cast(T_Retval, await send_raw_command(request))
|
| 183 |
+
finally:
|
| 184 |
+
if process in workers:
|
| 185 |
+
idle_workers.append((process, current_time()))
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def current_default_process_limiter() -> CapacityLimiter:
|
| 189 |
+
"""
|
| 190 |
+
Return the capacity limiter that is used by default to limit the number of worker
|
| 191 |
+
processes.
|
| 192 |
+
|
| 193 |
+
:return: a capacity limiter object
|
| 194 |
+
|
| 195 |
+
"""
|
| 196 |
+
try:
|
| 197 |
+
return _default_process_limiter.get()
|
| 198 |
+
except LookupError:
|
| 199 |
+
limiter = CapacityLimiter(os.cpu_count() or 2)
|
| 200 |
+
_default_process_limiter.set(limiter)
|
| 201 |
+
return limiter
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def process_worker() -> None:
|
| 205 |
+
# Redirect standard streams to os.devnull so that user code won't interfere with the
|
| 206 |
+
# parent-worker communication
|
| 207 |
+
stdin = sys.stdin
|
| 208 |
+
stdout = sys.stdout
|
| 209 |
+
sys.stdin = open(os.devnull)
|
| 210 |
+
sys.stdout = open(os.devnull, "w")
|
| 211 |
+
|
| 212 |
+
stdout.buffer.write(b"READY\n")
|
| 213 |
+
while True:
|
| 214 |
+
retval = exception = None
|
| 215 |
+
try:
|
| 216 |
+
command, *args = pickle.load(stdin.buffer)
|
| 217 |
+
except EOFError:
|
| 218 |
+
return
|
| 219 |
+
except BaseException as exc:
|
| 220 |
+
exception = exc
|
| 221 |
+
else:
|
| 222 |
+
if command == "run":
|
| 223 |
+
func, args = args
|
| 224 |
+
try:
|
| 225 |
+
retval = func(*args)
|
| 226 |
+
except BaseException as exc:
|
| 227 |
+
exception = exc
|
| 228 |
+
elif command == "init":
|
| 229 |
+
main_module_path: str | None
|
| 230 |
+
sys.path, main_module_path = args
|
| 231 |
+
del sys.modules["__main__"]
|
| 232 |
+
if main_module_path and os.path.isfile(main_module_path):
|
| 233 |
+
# Load the parent's main module but as __mp_main__ instead of
|
| 234 |
+
# __main__ (like multiprocessing does) to avoid infinite recursion
|
| 235 |
+
try:
|
| 236 |
+
spec = spec_from_file_location("__mp_main__", main_module_path)
|
| 237 |
+
if spec and spec.loader:
|
| 238 |
+
main = module_from_spec(spec)
|
| 239 |
+
spec.loader.exec_module(main)
|
| 240 |
+
sys.modules["__main__"] = main
|
| 241 |
+
except BaseException as exc:
|
| 242 |
+
exception = exc
|
| 243 |
+
try:
|
| 244 |
+
if exception is not None:
|
| 245 |
+
status = b"EXCEPTION"
|
| 246 |
+
pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL)
|
| 247 |
+
else:
|
| 248 |
+
status = b"RETURN"
|
| 249 |
+
pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL)
|
| 250 |
+
except BaseException as exc:
|
| 251 |
+
exception = exc
|
| 252 |
+
status = b"EXCEPTION"
|
| 253 |
+
pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL)
|
| 254 |
+
|
| 255 |
+
stdout.buffer.write(b"%s %d\n" % (status, len(pickled)))
|
| 256 |
+
stdout.buffer.write(pickled)
|
| 257 |
+
|
| 258 |
+
# Respect SIGTERM
|
| 259 |
+
if isinstance(exception, SystemExit):
|
| 260 |
+
raise exception
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
process_worker()
|
env/lib/python3.13/site-packages/anyio/to_thread.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
__all__ = (
|
| 4 |
+
"run_sync",
|
| 5 |
+
"current_default_thread_limiter",
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
import sys
|
| 9 |
+
from collections.abc import Callable
|
| 10 |
+
from typing import TypeVar
|
| 11 |
+
from warnings import warn
|
| 12 |
+
|
| 13 |
+
from ._core._eventloop import get_async_backend
|
| 14 |
+
from .abc import CapacityLimiter
|
| 15 |
+
|
| 16 |
+
if sys.version_info >= (3, 11):
|
| 17 |
+
from typing import TypeVarTuple, Unpack
|
| 18 |
+
else:
|
| 19 |
+
from typing_extensions import TypeVarTuple, Unpack
|
| 20 |
+
|
| 21 |
+
T_Retval = TypeVar("T_Retval")
|
| 22 |
+
PosArgsT = TypeVarTuple("PosArgsT")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
async def run_sync(
|
| 26 |
+
func: Callable[[Unpack[PosArgsT]], T_Retval],
|
| 27 |
+
*args: Unpack[PosArgsT],
|
| 28 |
+
abandon_on_cancel: bool = False,
|
| 29 |
+
cancellable: bool | None = None,
|
| 30 |
+
limiter: CapacityLimiter | None = None,
|
| 31 |
+
) -> T_Retval:
|
| 32 |
+
"""
|
| 33 |
+
Call the given function with the given arguments in a worker thread.
|
| 34 |
+
|
| 35 |
+
If the ``cancellable`` option is enabled and the task waiting for its completion is
|
| 36 |
+
cancelled, the thread will still run its course but its return value (or any raised
|
| 37 |
+
exception) will be ignored.
|
| 38 |
+
|
| 39 |
+
:param func: a callable
|
| 40 |
+
:param args: positional arguments for the callable
|
| 41 |
+
:param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run
|
| 42 |
+
unchecked on own) if the host task is cancelled, ``False`` to ignore
|
| 43 |
+
cancellations in the host task until the operation has completed in the worker
|
| 44 |
+
thread
|
| 45 |
+
:param cancellable: deprecated alias of ``abandon_on_cancel``; will override
|
| 46 |
+
``abandon_on_cancel`` if both parameters are passed
|
| 47 |
+
:param limiter: capacity limiter to use to limit the total amount of threads running
|
| 48 |
+
(if omitted, the default limiter is used)
|
| 49 |
+
:return: an awaitable that yields the return value of the function.
|
| 50 |
+
|
| 51 |
+
"""
|
| 52 |
+
if cancellable is not None:
|
| 53 |
+
abandon_on_cancel = cancellable
|
| 54 |
+
warn(
|
| 55 |
+
"The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is "
|
| 56 |
+
"deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead",
|
| 57 |
+
DeprecationWarning,
|
| 58 |
+
stacklevel=2,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
return await get_async_backend().run_sync_in_worker_thread(
|
| 62 |
+
func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def current_default_thread_limiter() -> CapacityLimiter:
|
| 67 |
+
"""
|
| 68 |
+
Return the capacity limiter that is used by default to limit the number of
|
| 69 |
+
concurrent threads.
|
| 70 |
+
|
| 71 |
+
:return: a capacity limiter object
|
| 72 |
+
|
| 73 |
+
"""
|
| 74 |
+
return get_async_backend().current_default_thread_limiter()
|
env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/METADATA
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: certifi
|
| 3 |
+
Version: 2025.11.12
|
| 4 |
+
Summary: Python package for providing Mozilla's CA Bundle.
|
| 5 |
+
Home-page: https://github.com/certifi/python-certifi
|
| 6 |
+
Author: Kenneth Reitz
|
| 7 |
+
Author-email: me@kennethreitz.com
|
| 8 |
+
License: MPL-2.0
|
| 9 |
+
Project-URL: Source, https://github.com/certifi/python-certifi
|
| 10 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 11 |
+
Classifier: Intended Audience :: Developers
|
| 12 |
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
| 13 |
+
Classifier: Natural Language :: English
|
| 14 |
+
Classifier: Programming Language :: Python
|
| 15 |
+
Classifier: Programming Language :: Python :: 3
|
| 16 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 17 |
+
Classifier: Programming Language :: Python :: 3.7
|
| 18 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.14
|
| 25 |
+
Requires-Python: >=3.7
|
| 26 |
+
License-File: LICENSE
|
| 27 |
+
Dynamic: author
|
| 28 |
+
Dynamic: author-email
|
| 29 |
+
Dynamic: classifier
|
| 30 |
+
Dynamic: description
|
| 31 |
+
Dynamic: home-page
|
| 32 |
+
Dynamic: license
|
| 33 |
+
Dynamic: license-file
|
| 34 |
+
Dynamic: project-url
|
| 35 |
+
Dynamic: requires-python
|
| 36 |
+
Dynamic: summary
|
| 37 |
+
|
| 38 |
+
Certifi: Python SSL Certificates
|
| 39 |
+
================================
|
| 40 |
+
|
| 41 |
+
Certifi provides Mozilla's carefully curated collection of Root Certificates for
|
| 42 |
+
validating the trustworthiness of SSL certificates while verifying the identity
|
| 43 |
+
of TLS hosts. It has been extracted from the `Requests`_ project.
|
| 44 |
+
|
| 45 |
+
Installation
|
| 46 |
+
------------
|
| 47 |
+
|
| 48 |
+
``certifi`` is available on PyPI. Simply install it with ``pip``::
|
| 49 |
+
|
| 50 |
+
$ pip install certifi
|
| 51 |
+
|
| 52 |
+
Usage
|
| 53 |
+
-----
|
| 54 |
+
|
| 55 |
+
To reference the installed certificate authority (CA) bundle, you can use the
|
| 56 |
+
built-in function::
|
| 57 |
+
|
| 58 |
+
>>> import certifi
|
| 59 |
+
|
| 60 |
+
>>> certifi.where()
|
| 61 |
+
'/usr/local/lib/python3.7/site-packages/certifi/cacert.pem'
|
| 62 |
+
|
| 63 |
+
Or from the command line::
|
| 64 |
+
|
| 65 |
+
$ python -m certifi
|
| 66 |
+
/usr/local/lib/python3.7/site-packages/certifi/cacert.pem
|
| 67 |
+
|
| 68 |
+
Enjoy!
|
| 69 |
+
|
| 70 |
+
.. _`Requests`: https://requests.readthedocs.io/en/master/
|
| 71 |
+
|
| 72 |
+
Addition/Removal of Certificates
|
| 73 |
+
--------------------------------
|
| 74 |
+
|
| 75 |
+
Certifi does not support any addition/removal or other modification of the
|
| 76 |
+
CA trust store content. This project is intended to provide a reliable and
|
| 77 |
+
highly portable root of trust to python deployments. Look to upstream projects
|
| 78 |
+
for methods to use alternate trust.
|
env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/RECORD
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
certifi-2025.11.12.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
certifi-2025.11.12.dist-info/METADATA,sha256=_JprGu_1lWSdHlruRBKcorXnrfvBDhvX_6KRr8HQbLc,2475
|
| 3 |
+
certifi-2025.11.12.dist-info/RECORD,,
|
| 4 |
+
certifi-2025.11.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
| 5 |
+
certifi-2025.11.12.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989
|
| 6 |
+
certifi-2025.11.12.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8
|
| 7 |
+
certifi/__init__.py,sha256=1BRSxNMnZW7CZ2oJtYWLoJgfHfcB9i273exwiPwfjJM,94
|
| 8 |
+
certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243
|
| 9 |
+
certifi/__pycache__/__init__.cpython-313.pyc,,
|
| 10 |
+
certifi/__pycache__/__main__.cpython-313.pyc,,
|
| 11 |
+
certifi/__pycache__/core.cpython-313.pyc,,
|
| 12 |
+
certifi/cacert.pem,sha256=oa1dZD4hxDtb7XTH4IkdzbWPavUcis4eTwINZUqlKhY,283932
|
| 13 |
+
certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394
|
| 14 |
+
certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (80.9.0)
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
| 5 |
+
|
env/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
certifi
|
env/lib/python3.13/site-packages/click/__init__.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Click is a simple Python module inspired by the stdlib optparse to make
|
| 3 |
+
writing command line scripts fun. Unlike other modules, it's based
|
| 4 |
+
around a simple API that does not come with too much magic and is
|
| 5 |
+
composable.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from .core import Argument as Argument
|
| 11 |
+
from .core import Command as Command
|
| 12 |
+
from .core import CommandCollection as CommandCollection
|
| 13 |
+
from .core import Context as Context
|
| 14 |
+
from .core import Group as Group
|
| 15 |
+
from .core import Option as Option
|
| 16 |
+
from .core import Parameter as Parameter
|
| 17 |
+
from .decorators import argument as argument
|
| 18 |
+
from .decorators import command as command
|
| 19 |
+
from .decorators import confirmation_option as confirmation_option
|
| 20 |
+
from .decorators import group as group
|
| 21 |
+
from .decorators import help_option as help_option
|
| 22 |
+
from .decorators import make_pass_decorator as make_pass_decorator
|
| 23 |
+
from .decorators import option as option
|
| 24 |
+
from .decorators import pass_context as pass_context
|
| 25 |
+
from .decorators import pass_obj as pass_obj
|
| 26 |
+
from .decorators import password_option as password_option
|
| 27 |
+
from .decorators import version_option as version_option
|
| 28 |
+
from .exceptions import Abort as Abort
|
| 29 |
+
from .exceptions import BadArgumentUsage as BadArgumentUsage
|
| 30 |
+
from .exceptions import BadOptionUsage as BadOptionUsage
|
| 31 |
+
from .exceptions import BadParameter as BadParameter
|
| 32 |
+
from .exceptions import ClickException as ClickException
|
| 33 |
+
from .exceptions import FileError as FileError
|
| 34 |
+
from .exceptions import MissingParameter as MissingParameter
|
| 35 |
+
from .exceptions import NoSuchOption as NoSuchOption
|
| 36 |
+
from .exceptions import UsageError as UsageError
|
| 37 |
+
from .formatting import HelpFormatter as HelpFormatter
|
| 38 |
+
from .formatting import wrap_text as wrap_text
|
| 39 |
+
from .globals import get_current_context as get_current_context
|
| 40 |
+
from .termui import clear as clear
|
| 41 |
+
from .termui import confirm as confirm
|
| 42 |
+
from .termui import echo_via_pager as echo_via_pager
|
| 43 |
+
from .termui import edit as edit
|
| 44 |
+
from .termui import getchar as getchar
|
| 45 |
+
from .termui import launch as launch
|
| 46 |
+
from .termui import pause as pause
|
| 47 |
+
from .termui import progressbar as progressbar
|
| 48 |
+
from .termui import prompt as prompt
|
| 49 |
+
from .termui import secho as secho
|
| 50 |
+
from .termui import style as style
|
| 51 |
+
from .termui import unstyle as unstyle
|
| 52 |
+
from .types import BOOL as BOOL
|
| 53 |
+
from .types import Choice as Choice
|
| 54 |
+
from .types import DateTime as DateTime
|
| 55 |
+
from .types import File as File
|
| 56 |
+
from .types import FLOAT as FLOAT
|
| 57 |
+
from .types import FloatRange as FloatRange
|
| 58 |
+
from .types import INT as INT
|
| 59 |
+
from .types import IntRange as IntRange
|
| 60 |
+
from .types import ParamType as ParamType
|
| 61 |
+
from .types import Path as Path
|
| 62 |
+
from .types import STRING as STRING
|
| 63 |
+
from .types import Tuple as Tuple
|
| 64 |
+
from .types import UNPROCESSED as UNPROCESSED
|
| 65 |
+
from .types import UUID as UUID
|
| 66 |
+
from .utils import echo as echo
|
| 67 |
+
from .utils import format_filename as format_filename
|
| 68 |
+
from .utils import get_app_dir as get_app_dir
|
| 69 |
+
from .utils import get_binary_stream as get_binary_stream
|
| 70 |
+
from .utils import get_text_stream as get_text_stream
|
| 71 |
+
from .utils import open_file as open_file
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def __getattr__(name: str) -> object:
|
| 75 |
+
import warnings
|
| 76 |
+
|
| 77 |
+
if name == "BaseCommand":
|
| 78 |
+
from .core import _BaseCommand
|
| 79 |
+
|
| 80 |
+
warnings.warn(
|
| 81 |
+
"'BaseCommand' is deprecated and will be removed in Click 9.0. Use"
|
| 82 |
+
" 'Command' instead.",
|
| 83 |
+
DeprecationWarning,
|
| 84 |
+
stacklevel=2,
|
| 85 |
+
)
|
| 86 |
+
return _BaseCommand
|
| 87 |
+
|
| 88 |
+
if name == "MultiCommand":
|
| 89 |
+
from .core import _MultiCommand
|
| 90 |
+
|
| 91 |
+
warnings.warn(
|
| 92 |
+
"'MultiCommand' is deprecated and will be removed in Click 9.0. Use"
|
| 93 |
+
" 'Group' instead.",
|
| 94 |
+
DeprecationWarning,
|
| 95 |
+
stacklevel=2,
|
| 96 |
+
)
|
| 97 |
+
return _MultiCommand
|
| 98 |
+
|
| 99 |
+
if name == "OptionParser":
|
| 100 |
+
from .parser import _OptionParser
|
| 101 |
+
|
| 102 |
+
warnings.warn(
|
| 103 |
+
"'OptionParser' is deprecated and will be removed in Click 9.0. The"
|
| 104 |
+
" old parser is available in 'optparse'.",
|
| 105 |
+
DeprecationWarning,
|
| 106 |
+
stacklevel=2,
|
| 107 |
+
)
|
| 108 |
+
return _OptionParser
|
| 109 |
+
|
| 110 |
+
if name == "__version__":
|
| 111 |
+
import importlib.metadata
|
| 112 |
+
import warnings
|
| 113 |
+
|
| 114 |
+
warnings.warn(
|
| 115 |
+
"The '__version__' attribute is deprecated and will be removed in"
|
| 116 |
+
" Click 9.1. Use feature detection or"
|
| 117 |
+
" 'importlib.metadata.version(\"click\")' instead.",
|
| 118 |
+
DeprecationWarning,
|
| 119 |
+
stacklevel=2,
|
| 120 |
+
)
|
| 121 |
+
return importlib.metadata.version("click")
|
| 122 |
+
|
| 123 |
+
raise AttributeError(name)
|
env/lib/python3.13/site-packages/click/_compat.py
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import codecs
|
| 4 |
+
import collections.abc as cabc
|
| 5 |
+
import io
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import sys
|
| 9 |
+
import typing as t
|
| 10 |
+
from types import TracebackType
|
| 11 |
+
from weakref import WeakKeyDictionary
|
| 12 |
+
|
| 13 |
+
CYGWIN = sys.platform.startswith("cygwin")
|
| 14 |
+
WIN = sys.platform.startswith("win")
|
| 15 |
+
auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None
|
| 16 |
+
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _make_text_stream(
|
| 20 |
+
stream: t.BinaryIO,
|
| 21 |
+
encoding: str | None,
|
| 22 |
+
errors: str | None,
|
| 23 |
+
force_readable: bool = False,
|
| 24 |
+
force_writable: bool = False,
|
| 25 |
+
) -> t.TextIO:
|
| 26 |
+
if encoding is None:
|
| 27 |
+
encoding = get_best_encoding(stream)
|
| 28 |
+
if errors is None:
|
| 29 |
+
errors = "replace"
|
| 30 |
+
return _NonClosingTextIOWrapper(
|
| 31 |
+
stream,
|
| 32 |
+
encoding,
|
| 33 |
+
errors,
|
| 34 |
+
line_buffering=True,
|
| 35 |
+
force_readable=force_readable,
|
| 36 |
+
force_writable=force_writable,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def is_ascii_encoding(encoding: str) -> bool:
|
| 41 |
+
"""Checks if a given encoding is ascii."""
|
| 42 |
+
try:
|
| 43 |
+
return codecs.lookup(encoding).name == "ascii"
|
| 44 |
+
except LookupError:
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def get_best_encoding(stream: t.IO[t.Any]) -> str:
|
| 49 |
+
"""Returns the default stream encoding if not found."""
|
| 50 |
+
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
| 51 |
+
if is_ascii_encoding(rv):
|
| 52 |
+
return "utf-8"
|
| 53 |
+
return rv
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
| 57 |
+
def __init__(
|
| 58 |
+
self,
|
| 59 |
+
stream: t.BinaryIO,
|
| 60 |
+
encoding: str | None,
|
| 61 |
+
errors: str | None,
|
| 62 |
+
force_readable: bool = False,
|
| 63 |
+
force_writable: bool = False,
|
| 64 |
+
**extra: t.Any,
|
| 65 |
+
) -> None:
|
| 66 |
+
self._stream = stream = t.cast(
|
| 67 |
+
t.BinaryIO, _FixupStream(stream, force_readable, force_writable)
|
| 68 |
+
)
|
| 69 |
+
super().__init__(stream, encoding, errors, **extra)
|
| 70 |
+
|
| 71 |
+
def __del__(self) -> None:
|
| 72 |
+
try:
|
| 73 |
+
self.detach()
|
| 74 |
+
except Exception:
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
def isatty(self) -> bool:
|
| 78 |
+
# https://bitbucket.org/pypy/pypy/issue/1803
|
| 79 |
+
return self._stream.isatty()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class _FixupStream:
|
| 83 |
+
"""The new io interface needs more from streams than streams
|
| 84 |
+
traditionally implement. As such, this fix-up code is necessary in
|
| 85 |
+
some circumstances.
|
| 86 |
+
|
| 87 |
+
The forcing of readable and writable flags are there because some tools
|
| 88 |
+
put badly patched objects on sys (one such offender are certain version
|
| 89 |
+
of jupyter notebook).
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
stream: t.BinaryIO,
|
| 95 |
+
force_readable: bool = False,
|
| 96 |
+
force_writable: bool = False,
|
| 97 |
+
):
|
| 98 |
+
self._stream = stream
|
| 99 |
+
self._force_readable = force_readable
|
| 100 |
+
self._force_writable = force_writable
|
| 101 |
+
|
| 102 |
+
def __getattr__(self, name: str) -> t.Any:
|
| 103 |
+
return getattr(self._stream, name)
|
| 104 |
+
|
| 105 |
+
def read1(self, size: int) -> bytes:
|
| 106 |
+
f = getattr(self._stream, "read1", None)
|
| 107 |
+
|
| 108 |
+
if f is not None:
|
| 109 |
+
return t.cast(bytes, f(size))
|
| 110 |
+
|
| 111 |
+
return self._stream.read(size)
|
| 112 |
+
|
| 113 |
+
def readable(self) -> bool:
|
| 114 |
+
if self._force_readable:
|
| 115 |
+
return True
|
| 116 |
+
x = getattr(self._stream, "readable", None)
|
| 117 |
+
if x is not None:
|
| 118 |
+
return t.cast(bool, x())
|
| 119 |
+
try:
|
| 120 |
+
self._stream.read(0)
|
| 121 |
+
except Exception:
|
| 122 |
+
return False
|
| 123 |
+
return True
|
| 124 |
+
|
| 125 |
+
def writable(self) -> bool:
|
| 126 |
+
if self._force_writable:
|
| 127 |
+
return True
|
| 128 |
+
x = getattr(self._stream, "writable", None)
|
| 129 |
+
if x is not None:
|
| 130 |
+
return t.cast(bool, x())
|
| 131 |
+
try:
|
| 132 |
+
self._stream.write(b"")
|
| 133 |
+
except Exception:
|
| 134 |
+
try:
|
| 135 |
+
self._stream.write(b"")
|
| 136 |
+
except Exception:
|
| 137 |
+
return False
|
| 138 |
+
return True
|
| 139 |
+
|
| 140 |
+
def seekable(self) -> bool:
|
| 141 |
+
x = getattr(self._stream, "seekable", None)
|
| 142 |
+
if x is not None:
|
| 143 |
+
return t.cast(bool, x())
|
| 144 |
+
try:
|
| 145 |
+
self._stream.seek(self._stream.tell())
|
| 146 |
+
except Exception:
|
| 147 |
+
return False
|
| 148 |
+
return True
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool:
|
| 152 |
+
try:
|
| 153 |
+
return isinstance(stream.read(0), bytes)
|
| 154 |
+
except Exception:
|
| 155 |
+
return default
|
| 156 |
+
# This happens in some cases where the stream was already
|
| 157 |
+
# closed. In this case, we assume the default.
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool:
|
| 161 |
+
try:
|
| 162 |
+
stream.write(b"")
|
| 163 |
+
except Exception:
|
| 164 |
+
try:
|
| 165 |
+
stream.write("")
|
| 166 |
+
return False
|
| 167 |
+
except Exception:
|
| 168 |
+
pass
|
| 169 |
+
return default
|
| 170 |
+
return True
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _find_binary_reader(stream: t.IO[t.Any]) -> t.BinaryIO | None:
|
| 174 |
+
# We need to figure out if the given stream is already binary.
|
| 175 |
+
# This can happen because the official docs recommend detaching
|
| 176 |
+
# the streams to get binary streams. Some code might do this, so
|
| 177 |
+
# we need to deal with this case explicitly.
|
| 178 |
+
if _is_binary_reader(stream, False):
|
| 179 |
+
return t.cast(t.BinaryIO, stream)
|
| 180 |
+
|
| 181 |
+
buf = getattr(stream, "buffer", None)
|
| 182 |
+
|
| 183 |
+
# Same situation here; this time we assume that the buffer is
|
| 184 |
+
# actually binary in case it's closed.
|
| 185 |
+
if buf is not None and _is_binary_reader(buf, True):
|
| 186 |
+
return t.cast(t.BinaryIO, buf)
|
| 187 |
+
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _find_binary_writer(stream: t.IO[t.Any]) -> t.BinaryIO | None:
|
| 192 |
+
# We need to figure out if the given stream is already binary.
|
| 193 |
+
# This can happen because the official docs recommend detaching
|
| 194 |
+
# the streams to get binary streams. Some code might do this, so
|
| 195 |
+
# we need to deal with this case explicitly.
|
| 196 |
+
if _is_binary_writer(stream, False):
|
| 197 |
+
return t.cast(t.BinaryIO, stream)
|
| 198 |
+
|
| 199 |
+
buf = getattr(stream, "buffer", None)
|
| 200 |
+
|
| 201 |
+
# Same situation here; this time we assume that the buffer is
|
| 202 |
+
# actually binary in case it's closed.
|
| 203 |
+
if buf is not None and _is_binary_writer(buf, True):
|
| 204 |
+
return t.cast(t.BinaryIO, buf)
|
| 205 |
+
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _stream_is_misconfigured(stream: t.TextIO) -> bool:
|
| 210 |
+
"""A stream is misconfigured if its encoding is ASCII."""
|
| 211 |
+
# If the stream does not have an encoding set, we assume it's set
|
| 212 |
+
# to ASCII. This appears to happen in certain unittest
|
| 213 |
+
# environments. It's not quite clear what the correct behavior is
|
| 214 |
+
# but this at least will force Click to recover somehow.
|
| 215 |
+
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: str | None) -> bool:
|
| 219 |
+
"""A stream attribute is compatible if it is equal to the
|
| 220 |
+
desired value or the desired value is unset and the attribute
|
| 221 |
+
has a value.
|
| 222 |
+
"""
|
| 223 |
+
stream_value = getattr(stream, attr, None)
|
| 224 |
+
return stream_value == value or (value is None and stream_value is not None)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _is_compatible_text_stream(
|
| 228 |
+
stream: t.TextIO, encoding: str | None, errors: str | None
|
| 229 |
+
) -> bool:
|
| 230 |
+
"""Check if a stream's encoding and errors attributes are
|
| 231 |
+
compatible with the desired values.
|
| 232 |
+
"""
|
| 233 |
+
return _is_compat_stream_attr(
|
| 234 |
+
stream, "encoding", encoding
|
| 235 |
+
) and _is_compat_stream_attr(stream, "errors", errors)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _force_correct_text_stream(
|
| 239 |
+
text_stream: t.IO[t.Any],
|
| 240 |
+
encoding: str | None,
|
| 241 |
+
errors: str | None,
|
| 242 |
+
is_binary: t.Callable[[t.IO[t.Any], bool], bool],
|
| 243 |
+
find_binary: t.Callable[[t.IO[t.Any]], t.BinaryIO | None],
|
| 244 |
+
force_readable: bool = False,
|
| 245 |
+
force_writable: bool = False,
|
| 246 |
+
) -> t.TextIO:
|
| 247 |
+
if is_binary(text_stream, False):
|
| 248 |
+
binary_reader = t.cast(t.BinaryIO, text_stream)
|
| 249 |
+
else:
|
| 250 |
+
text_stream = t.cast(t.TextIO, text_stream)
|
| 251 |
+
# If the stream looks compatible, and won't default to a
|
| 252 |
+
# misconfigured ascii encoding, return it as-is.
|
| 253 |
+
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
|
| 254 |
+
encoding is None and _stream_is_misconfigured(text_stream)
|
| 255 |
+
):
|
| 256 |
+
return text_stream
|
| 257 |
+
|
| 258 |
+
# Otherwise, get the underlying binary reader.
|
| 259 |
+
possible_binary_reader = find_binary(text_stream)
|
| 260 |
+
|
| 261 |
+
# If that's not possible, silently use the original reader
|
| 262 |
+
# and get mojibake instead of exceptions.
|
| 263 |
+
if possible_binary_reader is None:
|
| 264 |
+
return text_stream
|
| 265 |
+
|
| 266 |
+
binary_reader = possible_binary_reader
|
| 267 |
+
|
| 268 |
+
# Default errors to replace instead of strict in order to get
|
| 269 |
+
# something that works.
|
| 270 |
+
if errors is None:
|
| 271 |
+
errors = "replace"
|
| 272 |
+
|
| 273 |
+
# Wrap the binary stream in a text stream with the correct
|
| 274 |
+
# encoding parameters.
|
| 275 |
+
return _make_text_stream(
|
| 276 |
+
binary_reader,
|
| 277 |
+
encoding,
|
| 278 |
+
errors,
|
| 279 |
+
force_readable=force_readable,
|
| 280 |
+
force_writable=force_writable,
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def _force_correct_text_reader(
|
| 285 |
+
text_reader: t.IO[t.Any],
|
| 286 |
+
encoding: str | None,
|
| 287 |
+
errors: str | None,
|
| 288 |
+
force_readable: bool = False,
|
| 289 |
+
) -> t.TextIO:
|
| 290 |
+
return _force_correct_text_stream(
|
| 291 |
+
text_reader,
|
| 292 |
+
encoding,
|
| 293 |
+
errors,
|
| 294 |
+
_is_binary_reader,
|
| 295 |
+
_find_binary_reader,
|
| 296 |
+
force_readable=force_readable,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _force_correct_text_writer(
|
| 301 |
+
text_writer: t.IO[t.Any],
|
| 302 |
+
encoding: str | None,
|
| 303 |
+
errors: str | None,
|
| 304 |
+
force_writable: bool = False,
|
| 305 |
+
) -> t.TextIO:
|
| 306 |
+
return _force_correct_text_stream(
|
| 307 |
+
text_writer,
|
| 308 |
+
encoding,
|
| 309 |
+
errors,
|
| 310 |
+
_is_binary_writer,
|
| 311 |
+
_find_binary_writer,
|
| 312 |
+
force_writable=force_writable,
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def get_binary_stdin() -> t.BinaryIO:
|
| 317 |
+
reader = _find_binary_reader(sys.stdin)
|
| 318 |
+
if reader is None:
|
| 319 |
+
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
|
| 320 |
+
return reader
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def get_binary_stdout() -> t.BinaryIO:
|
| 324 |
+
writer = _find_binary_writer(sys.stdout)
|
| 325 |
+
if writer is None:
|
| 326 |
+
raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
|
| 327 |
+
return writer
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def get_binary_stderr() -> t.BinaryIO:
|
| 331 |
+
writer = _find_binary_writer(sys.stderr)
|
| 332 |
+
if writer is None:
|
| 333 |
+
raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
|
| 334 |
+
return writer
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
|
| 338 |
+
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
| 339 |
+
if rv is not None:
|
| 340 |
+
return rv
|
| 341 |
+
return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
|
| 345 |
+
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
| 346 |
+
if rv is not None:
|
| 347 |
+
return rv
|
| 348 |
+
return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True)
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
|
| 352 |
+
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
| 353 |
+
if rv is not None:
|
| 354 |
+
return rv
|
| 355 |
+
return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def _wrap_io_open(
|
| 359 |
+
file: str | os.PathLike[str] | int,
|
| 360 |
+
mode: str,
|
| 361 |
+
encoding: str | None,
|
| 362 |
+
errors: str | None,
|
| 363 |
+
) -> t.IO[t.Any]:
|
| 364 |
+
"""Handles not passing ``encoding`` and ``errors`` in binary mode."""
|
| 365 |
+
if "b" in mode:
|
| 366 |
+
return open(file, mode)
|
| 367 |
+
|
| 368 |
+
return open(file, mode, encoding=encoding, errors=errors)
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def open_stream(
|
| 372 |
+
filename: str | os.PathLike[str],
|
| 373 |
+
mode: str = "r",
|
| 374 |
+
encoding: str | None = None,
|
| 375 |
+
errors: str | None = "strict",
|
| 376 |
+
atomic: bool = False,
|
| 377 |
+
) -> tuple[t.IO[t.Any], bool]:
|
| 378 |
+
binary = "b" in mode
|
| 379 |
+
filename = os.fspath(filename)
|
| 380 |
+
|
| 381 |
+
# Standard streams first. These are simple because they ignore the
|
| 382 |
+
# atomic flag. Use fsdecode to handle Path("-").
|
| 383 |
+
if os.fsdecode(filename) == "-":
|
| 384 |
+
if any(m in mode for m in ["w", "a", "x"]):
|
| 385 |
+
if binary:
|
| 386 |
+
return get_binary_stdout(), False
|
| 387 |
+
return get_text_stdout(encoding=encoding, errors=errors), False
|
| 388 |
+
if binary:
|
| 389 |
+
return get_binary_stdin(), False
|
| 390 |
+
return get_text_stdin(encoding=encoding, errors=errors), False
|
| 391 |
+
|
| 392 |
+
# Non-atomic writes directly go out through the regular open functions.
|
| 393 |
+
if not atomic:
|
| 394 |
+
return _wrap_io_open(filename, mode, encoding, errors), True
|
| 395 |
+
|
| 396 |
+
# Some usability stuff for atomic writes
|
| 397 |
+
if "a" in mode:
|
| 398 |
+
raise ValueError(
|
| 399 |
+
"Appending to an existing file is not supported, because that"
|
| 400 |
+
" would involve an expensive `copy`-operation to a temporary"
|
| 401 |
+
" file. Open the file in normal `w`-mode and copy explicitly"
|
| 402 |
+
" if that's what you're after."
|
| 403 |
+
)
|
| 404 |
+
if "x" in mode:
|
| 405 |
+
raise ValueError("Use the `overwrite`-parameter instead.")
|
| 406 |
+
if "w" not in mode:
|
| 407 |
+
raise ValueError("Atomic writes only make sense with `w`-mode.")
|
| 408 |
+
|
| 409 |
+
# Atomic writes are more complicated. They work by opening a file
|
| 410 |
+
# as a proxy in the same folder and then using the fdopen
|
| 411 |
+
# functionality to wrap it in a Python file. Then we wrap it in an
|
| 412 |
+
# atomic file that moves the file over on close.
|
| 413 |
+
import errno
|
| 414 |
+
import random
|
| 415 |
+
|
| 416 |
+
try:
|
| 417 |
+
perm: int | None = os.stat(filename).st_mode
|
| 418 |
+
except OSError:
|
| 419 |
+
perm = None
|
| 420 |
+
|
| 421 |
+
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
| 422 |
+
|
| 423 |
+
if binary:
|
| 424 |
+
flags |= getattr(os, "O_BINARY", 0)
|
| 425 |
+
|
| 426 |
+
while True:
|
| 427 |
+
tmp_filename = os.path.join(
|
| 428 |
+
os.path.dirname(filename),
|
| 429 |
+
f".__atomic-write{random.randrange(1 << 32):08x}",
|
| 430 |
+
)
|
| 431 |
+
try:
|
| 432 |
+
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
|
| 433 |
+
break
|
| 434 |
+
except OSError as e:
|
| 435 |
+
if e.errno == errno.EEXIST or (
|
| 436 |
+
os.name == "nt"
|
| 437 |
+
and e.errno == errno.EACCES
|
| 438 |
+
and os.path.isdir(e.filename)
|
| 439 |
+
and os.access(e.filename, os.W_OK)
|
| 440 |
+
):
|
| 441 |
+
continue
|
| 442 |
+
raise
|
| 443 |
+
|
| 444 |
+
if perm is not None:
|
| 445 |
+
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
|
| 446 |
+
|
| 447 |
+
f = _wrap_io_open(fd, mode, encoding, errors)
|
| 448 |
+
af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
|
| 449 |
+
return t.cast(t.IO[t.Any], af), True
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
class _AtomicFile:
|
| 453 |
+
def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None:
|
| 454 |
+
self._f = f
|
| 455 |
+
self._tmp_filename = tmp_filename
|
| 456 |
+
self._real_filename = real_filename
|
| 457 |
+
self.closed = False
|
| 458 |
+
|
| 459 |
+
@property
|
| 460 |
+
def name(self) -> str:
|
| 461 |
+
return self._real_filename
|
| 462 |
+
|
| 463 |
+
def close(self, delete: bool = False) -> None:
|
| 464 |
+
if self.closed:
|
| 465 |
+
return
|
| 466 |
+
self._f.close()
|
| 467 |
+
os.replace(self._tmp_filename, self._real_filename)
|
| 468 |
+
self.closed = True
|
| 469 |
+
|
| 470 |
+
def __getattr__(self, name: str) -> t.Any:
|
| 471 |
+
return getattr(self._f, name)
|
| 472 |
+
|
| 473 |
+
def __enter__(self) -> _AtomicFile:
|
| 474 |
+
return self
|
| 475 |
+
|
| 476 |
+
def __exit__(
|
| 477 |
+
self,
|
| 478 |
+
exc_type: type[BaseException] | None,
|
| 479 |
+
exc_value: BaseException | None,
|
| 480 |
+
tb: TracebackType | None,
|
| 481 |
+
) -> None:
|
| 482 |
+
self.close(delete=exc_type is not None)
|
| 483 |
+
|
| 484 |
+
def __repr__(self) -> str:
|
| 485 |
+
return repr(self._f)
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
def strip_ansi(value: str) -> str:
|
| 489 |
+
return _ansi_re.sub("", value)
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool:
|
| 493 |
+
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
| 494 |
+
stream = stream._stream
|
| 495 |
+
|
| 496 |
+
return stream.__class__.__module__.startswith("ipykernel.")
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
def should_strip_ansi(
|
| 500 |
+
stream: t.IO[t.Any] | None = None, color: bool | None = None
|
| 501 |
+
) -> bool:
|
| 502 |
+
if color is None:
|
| 503 |
+
if stream is None:
|
| 504 |
+
stream = sys.stdin
|
| 505 |
+
return not isatty(stream) and not _is_jupyter_kernel_output(stream)
|
| 506 |
+
return not color
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
# On Windows, wrap the output streams with colorama to support ANSI
|
| 510 |
+
# color codes.
|
| 511 |
+
# NOTE: double check is needed so mypy does not analyze this on Linux
|
| 512 |
+
if sys.platform.startswith("win") and WIN:
|
| 513 |
+
from ._winconsole import _get_windows_console_stream
|
| 514 |
+
|
| 515 |
+
def _get_argv_encoding() -> str:
|
| 516 |
+
import locale
|
| 517 |
+
|
| 518 |
+
return locale.getpreferredencoding()
|
| 519 |
+
|
| 520 |
+
_ansi_stream_wrappers: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
| 521 |
+
|
| 522 |
+
def auto_wrap_for_ansi(stream: t.TextIO, color: bool | None = None) -> t.TextIO:
|
| 523 |
+
"""Support ANSI color and style codes on Windows by wrapping a
|
| 524 |
+
stream with colorama.
|
| 525 |
+
"""
|
| 526 |
+
try:
|
| 527 |
+
cached = _ansi_stream_wrappers.get(stream)
|
| 528 |
+
except Exception:
|
| 529 |
+
cached = None
|
| 530 |
+
|
| 531 |
+
if cached is not None:
|
| 532 |
+
return cached
|
| 533 |
+
|
| 534 |
+
import colorama
|
| 535 |
+
|
| 536 |
+
strip = should_strip_ansi(stream, color)
|
| 537 |
+
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
| 538 |
+
rv = t.cast(t.TextIO, ansi_wrapper.stream)
|
| 539 |
+
_write = rv.write
|
| 540 |
+
|
| 541 |
+
def _safe_write(s: str) -> int:
|
| 542 |
+
try:
|
| 543 |
+
return _write(s)
|
| 544 |
+
except BaseException:
|
| 545 |
+
ansi_wrapper.reset_all()
|
| 546 |
+
raise
|
| 547 |
+
|
| 548 |
+
rv.write = _safe_write # type: ignore[method-assign]
|
| 549 |
+
|
| 550 |
+
try:
|
| 551 |
+
_ansi_stream_wrappers[stream] = rv
|
| 552 |
+
except Exception:
|
| 553 |
+
pass
|
| 554 |
+
|
| 555 |
+
return rv
|
| 556 |
+
|
| 557 |
+
else:
|
| 558 |
+
|
| 559 |
+
def _get_argv_encoding() -> str:
|
| 560 |
+
return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding()
|
| 561 |
+
|
| 562 |
+
def _get_windows_console_stream(
|
| 563 |
+
f: t.TextIO, encoding: str | None, errors: str | None
|
| 564 |
+
) -> t.TextIO | None:
|
| 565 |
+
return None
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def term_len(x: str) -> int:
|
| 569 |
+
return len(strip_ansi(x))
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def isatty(stream: t.IO[t.Any]) -> bool:
|
| 573 |
+
try:
|
| 574 |
+
return stream.isatty()
|
| 575 |
+
except Exception:
|
| 576 |
+
return False
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def _make_cached_stream_func(
|
| 580 |
+
src_func: t.Callable[[], t.TextIO | None],
|
| 581 |
+
wrapper_func: t.Callable[[], t.TextIO],
|
| 582 |
+
) -> t.Callable[[], t.TextIO | None]:
|
| 583 |
+
cache: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
|
| 584 |
+
|
| 585 |
+
def func() -> t.TextIO | None:
|
| 586 |
+
stream = src_func()
|
| 587 |
+
|
| 588 |
+
if stream is None:
|
| 589 |
+
return None
|
| 590 |
+
|
| 591 |
+
try:
|
| 592 |
+
rv = cache.get(stream)
|
| 593 |
+
except Exception:
|
| 594 |
+
rv = None
|
| 595 |
+
if rv is not None:
|
| 596 |
+
return rv
|
| 597 |
+
rv = wrapper_func()
|
| 598 |
+
try:
|
| 599 |
+
cache[stream] = rv
|
| 600 |
+
except Exception:
|
| 601 |
+
pass
|
| 602 |
+
return rv
|
| 603 |
+
|
| 604 |
+
return func
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
|
| 608 |
+
_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
|
| 609 |
+
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
binary_streams: cabc.Mapping[str, t.Callable[[], t.BinaryIO]] = {
|
| 613 |
+
"stdin": get_binary_stdin,
|
| 614 |
+
"stdout": get_binary_stdout,
|
| 615 |
+
"stderr": get_binary_stderr,
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
text_streams: cabc.Mapping[str, t.Callable[[str | None, str | None], t.TextIO]] = {
|
| 619 |
+
"stdin": get_text_stdin,
|
| 620 |
+
"stdout": get_text_stdout,
|
| 621 |
+
"stderr": get_text_stderr,
|
| 622 |
+
}
|
env/lib/python3.13/site-packages/click/_termui_impl.py
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This module contains implementations for the termui module. To keep the
|
| 3 |
+
import time of Click down, some infrequently used functionality is
|
| 4 |
+
placed in this module and only imported as needed.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import collections.abc as cabc
|
| 10 |
+
import contextlib
|
| 11 |
+
import math
|
| 12 |
+
import os
|
| 13 |
+
import shlex
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
import typing as t
|
| 17 |
+
from gettext import gettext as _
|
| 18 |
+
from io import StringIO
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from types import TracebackType
|
| 21 |
+
|
| 22 |
+
from ._compat import _default_text_stdout
|
| 23 |
+
from ._compat import CYGWIN
|
| 24 |
+
from ._compat import get_best_encoding
|
| 25 |
+
from ._compat import isatty
|
| 26 |
+
from ._compat import open_stream
|
| 27 |
+
from ._compat import strip_ansi
|
| 28 |
+
from ._compat import term_len
|
| 29 |
+
from ._compat import WIN
|
| 30 |
+
from .exceptions import ClickException
|
| 31 |
+
from .utils import echo
|
| 32 |
+
|
| 33 |
+
V = t.TypeVar("V")
|
| 34 |
+
|
| 35 |
+
if os.name == "nt":
|
| 36 |
+
BEFORE_BAR = "\r"
|
| 37 |
+
AFTER_BAR = "\n"
|
| 38 |
+
else:
|
| 39 |
+
BEFORE_BAR = "\r\033[?25l"
|
| 40 |
+
AFTER_BAR = "\033[?25h\n"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class ProgressBar(t.Generic[V]):
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
iterable: cabc.Iterable[V] | None,
|
| 47 |
+
length: int | None = None,
|
| 48 |
+
fill_char: str = "#",
|
| 49 |
+
empty_char: str = " ",
|
| 50 |
+
bar_template: str = "%(bar)s",
|
| 51 |
+
info_sep: str = " ",
|
| 52 |
+
hidden: bool = False,
|
| 53 |
+
show_eta: bool = True,
|
| 54 |
+
show_percent: bool | None = None,
|
| 55 |
+
show_pos: bool = False,
|
| 56 |
+
item_show_func: t.Callable[[V | None], str | None] | None = None,
|
| 57 |
+
label: str | None = None,
|
| 58 |
+
file: t.TextIO | None = None,
|
| 59 |
+
color: bool | None = None,
|
| 60 |
+
update_min_steps: int = 1,
|
| 61 |
+
width: int = 30,
|
| 62 |
+
) -> None:
|
| 63 |
+
self.fill_char = fill_char
|
| 64 |
+
self.empty_char = empty_char
|
| 65 |
+
self.bar_template = bar_template
|
| 66 |
+
self.info_sep = info_sep
|
| 67 |
+
self.hidden = hidden
|
| 68 |
+
self.show_eta = show_eta
|
| 69 |
+
self.show_percent = show_percent
|
| 70 |
+
self.show_pos = show_pos
|
| 71 |
+
self.item_show_func = item_show_func
|
| 72 |
+
self.label: str = label or ""
|
| 73 |
+
|
| 74 |
+
if file is None:
|
| 75 |
+
file = _default_text_stdout()
|
| 76 |
+
|
| 77 |
+
# There are no standard streams attached to write to. For example,
|
| 78 |
+
# pythonw on Windows.
|
| 79 |
+
if file is None:
|
| 80 |
+
file = StringIO()
|
| 81 |
+
|
| 82 |
+
self.file = file
|
| 83 |
+
self.color = color
|
| 84 |
+
self.update_min_steps = update_min_steps
|
| 85 |
+
self._completed_intervals = 0
|
| 86 |
+
self.width: int = width
|
| 87 |
+
self.autowidth: bool = width == 0
|
| 88 |
+
|
| 89 |
+
if length is None:
|
| 90 |
+
from operator import length_hint
|
| 91 |
+
|
| 92 |
+
length = length_hint(iterable, -1)
|
| 93 |
+
|
| 94 |
+
if length == -1:
|
| 95 |
+
length = None
|
| 96 |
+
if iterable is None:
|
| 97 |
+
if length is None:
|
| 98 |
+
raise TypeError("iterable or length is required")
|
| 99 |
+
iterable = t.cast("cabc.Iterable[V]", range(length))
|
| 100 |
+
self.iter: cabc.Iterable[V] = iter(iterable)
|
| 101 |
+
self.length = length
|
| 102 |
+
self.pos: int = 0
|
| 103 |
+
self.avg: list[float] = []
|
| 104 |
+
self.last_eta: float
|
| 105 |
+
self.start: float
|
| 106 |
+
self.start = self.last_eta = time.time()
|
| 107 |
+
self.eta_known: bool = False
|
| 108 |
+
self.finished: bool = False
|
| 109 |
+
self.max_width: int | None = None
|
| 110 |
+
self.entered: bool = False
|
| 111 |
+
self.current_item: V | None = None
|
| 112 |
+
self._is_atty = isatty(self.file)
|
| 113 |
+
self._last_line: str | None = None
|
| 114 |
+
|
| 115 |
+
def __enter__(self) -> ProgressBar[V]:
|
| 116 |
+
self.entered = True
|
| 117 |
+
self.render_progress()
|
| 118 |
+
return self
|
| 119 |
+
|
| 120 |
+
def __exit__(
|
| 121 |
+
self,
|
| 122 |
+
exc_type: type[BaseException] | None,
|
| 123 |
+
exc_value: BaseException | None,
|
| 124 |
+
tb: TracebackType | None,
|
| 125 |
+
) -> None:
|
| 126 |
+
self.render_finish()
|
| 127 |
+
|
| 128 |
+
def __iter__(self) -> cabc.Iterator[V]:
|
| 129 |
+
if not self.entered:
|
| 130 |
+
raise RuntimeError("You need to use progress bars in a with block.")
|
| 131 |
+
self.render_progress()
|
| 132 |
+
return self.generator()
|
| 133 |
+
|
| 134 |
+
def __next__(self) -> V:
|
| 135 |
+
# Iteration is defined in terms of a generator function,
|
| 136 |
+
# returned by iter(self); use that to define next(). This works
|
| 137 |
+
# because `self.iter` is an iterable consumed by that generator,
|
| 138 |
+
# so it is re-entry safe. Calling `next(self.generator())`
|
| 139 |
+
# twice works and does "what you want".
|
| 140 |
+
return next(iter(self))
|
| 141 |
+
|
| 142 |
+
def render_finish(self) -> None:
|
| 143 |
+
if self.hidden or not self._is_atty:
|
| 144 |
+
return
|
| 145 |
+
self.file.write(AFTER_BAR)
|
| 146 |
+
self.file.flush()
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def pct(self) -> float:
|
| 150 |
+
if self.finished:
|
| 151 |
+
return 1.0
|
| 152 |
+
return min(self.pos / (float(self.length or 1) or 1), 1.0)
|
| 153 |
+
|
| 154 |
+
@property
|
| 155 |
+
def time_per_iteration(self) -> float:
|
| 156 |
+
if not self.avg:
|
| 157 |
+
return 0.0
|
| 158 |
+
return sum(self.avg) / float(len(self.avg))
|
| 159 |
+
|
| 160 |
+
@property
|
| 161 |
+
def eta(self) -> float:
|
| 162 |
+
if self.length is not None and not self.finished:
|
| 163 |
+
return self.time_per_iteration * (self.length - self.pos)
|
| 164 |
+
return 0.0
|
| 165 |
+
|
| 166 |
+
def format_eta(self) -> str:
|
| 167 |
+
if self.eta_known:
|
| 168 |
+
t = int(self.eta)
|
| 169 |
+
seconds = t % 60
|
| 170 |
+
t //= 60
|
| 171 |
+
minutes = t % 60
|
| 172 |
+
t //= 60
|
| 173 |
+
hours = t % 24
|
| 174 |
+
t //= 24
|
| 175 |
+
if t > 0:
|
| 176 |
+
return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
|
| 177 |
+
else:
|
| 178 |
+
return f"{hours:02}:{minutes:02}:{seconds:02}"
|
| 179 |
+
return ""
|
| 180 |
+
|
| 181 |
+
def format_pos(self) -> str:
|
| 182 |
+
pos = str(self.pos)
|
| 183 |
+
if self.length is not None:
|
| 184 |
+
pos += f"/{self.length}"
|
| 185 |
+
return pos
|
| 186 |
+
|
| 187 |
+
def format_pct(self) -> str:
|
| 188 |
+
return f"{int(self.pct * 100): 4}%"[1:]
|
| 189 |
+
|
| 190 |
+
def format_bar(self) -> str:
|
| 191 |
+
if self.length is not None:
|
| 192 |
+
bar_length = int(self.pct * self.width)
|
| 193 |
+
bar = self.fill_char * bar_length
|
| 194 |
+
bar += self.empty_char * (self.width - bar_length)
|
| 195 |
+
elif self.finished:
|
| 196 |
+
bar = self.fill_char * self.width
|
| 197 |
+
else:
|
| 198 |
+
chars = list(self.empty_char * (self.width or 1))
|
| 199 |
+
if self.time_per_iteration != 0:
|
| 200 |
+
chars[
|
| 201 |
+
int(
|
| 202 |
+
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
| 203 |
+
* self.width
|
| 204 |
+
)
|
| 205 |
+
] = self.fill_char
|
| 206 |
+
bar = "".join(chars)
|
| 207 |
+
return bar
|
| 208 |
+
|
| 209 |
+
def format_progress_line(self) -> str:
|
| 210 |
+
show_percent = self.show_percent
|
| 211 |
+
|
| 212 |
+
info_bits = []
|
| 213 |
+
if self.length is not None and show_percent is None:
|
| 214 |
+
show_percent = not self.show_pos
|
| 215 |
+
|
| 216 |
+
if self.show_pos:
|
| 217 |
+
info_bits.append(self.format_pos())
|
| 218 |
+
if show_percent:
|
| 219 |
+
info_bits.append(self.format_pct())
|
| 220 |
+
if self.show_eta and self.eta_known and not self.finished:
|
| 221 |
+
info_bits.append(self.format_eta())
|
| 222 |
+
if self.item_show_func is not None:
|
| 223 |
+
item_info = self.item_show_func(self.current_item)
|
| 224 |
+
if item_info is not None:
|
| 225 |
+
info_bits.append(item_info)
|
| 226 |
+
|
| 227 |
+
return (
|
| 228 |
+
self.bar_template
|
| 229 |
+
% {
|
| 230 |
+
"label": self.label,
|
| 231 |
+
"bar": self.format_bar(),
|
| 232 |
+
"info": self.info_sep.join(info_bits),
|
| 233 |
+
}
|
| 234 |
+
).rstrip()
|
| 235 |
+
|
| 236 |
+
def render_progress(self) -> None:
|
| 237 |
+
if self.hidden:
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
if not self._is_atty:
|
| 241 |
+
# Only output the label once if the output is not a TTY.
|
| 242 |
+
if self._last_line != self.label:
|
| 243 |
+
self._last_line = self.label
|
| 244 |
+
echo(self.label, file=self.file, color=self.color)
|
| 245 |
+
return
|
| 246 |
+
|
| 247 |
+
buf = []
|
| 248 |
+
# Update width in case the terminal has been resized
|
| 249 |
+
if self.autowidth:
|
| 250 |
+
import shutil
|
| 251 |
+
|
| 252 |
+
old_width = self.width
|
| 253 |
+
self.width = 0
|
| 254 |
+
clutter_length = term_len(self.format_progress_line())
|
| 255 |
+
new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
|
| 256 |
+
if new_width < old_width and self.max_width is not None:
|
| 257 |
+
buf.append(BEFORE_BAR)
|
| 258 |
+
buf.append(" " * self.max_width)
|
| 259 |
+
self.max_width = new_width
|
| 260 |
+
self.width = new_width
|
| 261 |
+
|
| 262 |
+
clear_width = self.width
|
| 263 |
+
if self.max_width is not None:
|
| 264 |
+
clear_width = self.max_width
|
| 265 |
+
|
| 266 |
+
buf.append(BEFORE_BAR)
|
| 267 |
+
line = self.format_progress_line()
|
| 268 |
+
line_len = term_len(line)
|
| 269 |
+
if self.max_width is None or self.max_width < line_len:
|
| 270 |
+
self.max_width = line_len
|
| 271 |
+
|
| 272 |
+
buf.append(line)
|
| 273 |
+
buf.append(" " * (clear_width - line_len))
|
| 274 |
+
line = "".join(buf)
|
| 275 |
+
# Render the line only if it changed.
|
| 276 |
+
|
| 277 |
+
if line != self._last_line:
|
| 278 |
+
self._last_line = line
|
| 279 |
+
echo(line, file=self.file, color=self.color, nl=False)
|
| 280 |
+
self.file.flush()
|
| 281 |
+
|
| 282 |
+
def make_step(self, n_steps: int) -> None:
|
| 283 |
+
self.pos += n_steps
|
| 284 |
+
if self.length is not None and self.pos >= self.length:
|
| 285 |
+
self.finished = True
|
| 286 |
+
|
| 287 |
+
if (time.time() - self.last_eta) < 1.0:
|
| 288 |
+
return
|
| 289 |
+
|
| 290 |
+
self.last_eta = time.time()
|
| 291 |
+
|
| 292 |
+
# self.avg is a rolling list of length <= 7 of steps where steps are
|
| 293 |
+
# defined as time elapsed divided by the total progress through
|
| 294 |
+
# self.length.
|
| 295 |
+
if self.pos:
|
| 296 |
+
step = (time.time() - self.start) / self.pos
|
| 297 |
+
else:
|
| 298 |
+
step = time.time() - self.start
|
| 299 |
+
|
| 300 |
+
self.avg = self.avg[-6:] + [step]
|
| 301 |
+
|
| 302 |
+
self.eta_known = self.length is not None
|
| 303 |
+
|
| 304 |
+
def update(self, n_steps: int, current_item: V | None = None) -> None:
|
| 305 |
+
"""Update the progress bar by advancing a specified number of
|
| 306 |
+
steps, and optionally set the ``current_item`` for this new
|
| 307 |
+
position.
|
| 308 |
+
|
| 309 |
+
:param n_steps: Number of steps to advance.
|
| 310 |
+
:param current_item: Optional item to set as ``current_item``
|
| 311 |
+
for the updated position.
|
| 312 |
+
|
| 313 |
+
.. versionchanged:: 8.0
|
| 314 |
+
Added the ``current_item`` optional parameter.
|
| 315 |
+
|
| 316 |
+
.. versionchanged:: 8.0
|
| 317 |
+
Only render when the number of steps meets the
|
| 318 |
+
``update_min_steps`` threshold.
|
| 319 |
+
"""
|
| 320 |
+
if current_item is not None:
|
| 321 |
+
self.current_item = current_item
|
| 322 |
+
|
| 323 |
+
self._completed_intervals += n_steps
|
| 324 |
+
|
| 325 |
+
if self._completed_intervals >= self.update_min_steps:
|
| 326 |
+
self.make_step(self._completed_intervals)
|
| 327 |
+
self.render_progress()
|
| 328 |
+
self._completed_intervals = 0
|
| 329 |
+
|
| 330 |
+
def finish(self) -> None:
|
| 331 |
+
self.eta_known = False
|
| 332 |
+
self.current_item = None
|
| 333 |
+
self.finished = True
|
| 334 |
+
|
| 335 |
+
def generator(self) -> cabc.Iterator[V]:
|
| 336 |
+
"""Return a generator which yields the items added to the bar
|
| 337 |
+
during construction, and updates the progress bar *after* the
|
| 338 |
+
yielded block returns.
|
| 339 |
+
"""
|
| 340 |
+
# WARNING: the iterator interface for `ProgressBar` relies on
|
| 341 |
+
# this and only works because this is a simple generator which
|
| 342 |
+
# doesn't create or manage additional state. If this function
|
| 343 |
+
# changes, the impact should be evaluated both against
|
| 344 |
+
# `iter(bar)` and `next(bar)`. `next()` in particular may call
|
| 345 |
+
# `self.generator()` repeatedly, and this must remain safe in
|
| 346 |
+
# order for that interface to work.
|
| 347 |
+
if not self.entered:
|
| 348 |
+
raise RuntimeError("You need to use progress bars in a with block.")
|
| 349 |
+
|
| 350 |
+
if not self._is_atty:
|
| 351 |
+
yield from self.iter
|
| 352 |
+
else:
|
| 353 |
+
for rv in self.iter:
|
| 354 |
+
self.current_item = rv
|
| 355 |
+
|
| 356 |
+
# This allows show_item_func to be updated before the
|
| 357 |
+
# item is processed. Only trigger at the beginning of
|
| 358 |
+
# the update interval.
|
| 359 |
+
if self._completed_intervals == 0:
|
| 360 |
+
self.render_progress()
|
| 361 |
+
|
| 362 |
+
yield rv
|
| 363 |
+
self.update(1)
|
| 364 |
+
|
| 365 |
+
self.finish()
|
| 366 |
+
self.render_progress()
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None:
|
| 370 |
+
"""Decide what method to use for paging through text."""
|
| 371 |
+
stdout = _default_text_stdout()
|
| 372 |
+
|
| 373 |
+
# There are no standard streams attached to write to. For example,
|
| 374 |
+
# pythonw on Windows.
|
| 375 |
+
if stdout is None:
|
| 376 |
+
stdout = StringIO()
|
| 377 |
+
|
| 378 |
+
if not isatty(sys.stdin) or not isatty(stdout):
|
| 379 |
+
return _nullpager(stdout, generator, color)
|
| 380 |
+
|
| 381 |
+
# Split and normalize the pager command into parts.
|
| 382 |
+
pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False)
|
| 383 |
+
if pager_cmd_parts:
|
| 384 |
+
if WIN:
|
| 385 |
+
if _tempfilepager(generator, pager_cmd_parts, color):
|
| 386 |
+
return
|
| 387 |
+
elif _pipepager(generator, pager_cmd_parts, color):
|
| 388 |
+
return
|
| 389 |
+
|
| 390 |
+
if os.environ.get("TERM") in ("dumb", "emacs"):
|
| 391 |
+
return _nullpager(stdout, generator, color)
|
| 392 |
+
if (WIN or sys.platform.startswith("os2")) and _tempfilepager(
|
| 393 |
+
generator, ["more"], color
|
| 394 |
+
):
|
| 395 |
+
return
|
| 396 |
+
if _pipepager(generator, ["less"], color):
|
| 397 |
+
return
|
| 398 |
+
|
| 399 |
+
import tempfile
|
| 400 |
+
|
| 401 |
+
fd, filename = tempfile.mkstemp()
|
| 402 |
+
os.close(fd)
|
| 403 |
+
try:
|
| 404 |
+
if _pipepager(generator, ["more"], color):
|
| 405 |
+
return
|
| 406 |
+
return _nullpager(stdout, generator, color)
|
| 407 |
+
finally:
|
| 408 |
+
os.unlink(filename)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def _pipepager(
|
| 412 |
+
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
|
| 413 |
+
) -> bool:
|
| 414 |
+
"""Page through text by feeding it to another program. Invoking a
|
| 415 |
+
pager through this might support colors.
|
| 416 |
+
|
| 417 |
+
Returns `True` if the command was found, `False` otherwise and thus another
|
| 418 |
+
pager should be attempted.
|
| 419 |
+
"""
|
| 420 |
+
# Split the command into the invoked CLI and its parameters.
|
| 421 |
+
if not cmd_parts:
|
| 422 |
+
return False
|
| 423 |
+
|
| 424 |
+
import shutil
|
| 425 |
+
|
| 426 |
+
cmd = cmd_parts[0]
|
| 427 |
+
cmd_params = cmd_parts[1:]
|
| 428 |
+
|
| 429 |
+
cmd_filepath = shutil.which(cmd)
|
| 430 |
+
if not cmd_filepath:
|
| 431 |
+
return False
|
| 432 |
+
|
| 433 |
+
# Produces a normalized absolute path string.
|
| 434 |
+
# multi-call binaries such as busybox derive their identity from the symlink
|
| 435 |
+
# less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
|
| 436 |
+
cmd_path = Path(cmd_filepath).absolute()
|
| 437 |
+
cmd_name = cmd_path.name
|
| 438 |
+
|
| 439 |
+
import subprocess
|
| 440 |
+
|
| 441 |
+
# Make a local copy of the environment to not affect the global one.
|
| 442 |
+
env = dict(os.environ)
|
| 443 |
+
|
| 444 |
+
# If we're piping to less and the user hasn't decided on colors, we enable
|
| 445 |
+
# them by default we find the -R flag in the command line arguments.
|
| 446 |
+
if color is None and cmd_name == "less":
|
| 447 |
+
less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_params)}"
|
| 448 |
+
if not less_flags:
|
| 449 |
+
env["LESS"] = "-R"
|
| 450 |
+
color = True
|
| 451 |
+
elif "r" in less_flags or "R" in less_flags:
|
| 452 |
+
color = True
|
| 453 |
+
|
| 454 |
+
c = subprocess.Popen(
|
| 455 |
+
[str(cmd_path)] + cmd_params,
|
| 456 |
+
shell=False,
|
| 457 |
+
stdin=subprocess.PIPE,
|
| 458 |
+
env=env,
|
| 459 |
+
errors="replace",
|
| 460 |
+
text=True,
|
| 461 |
+
)
|
| 462 |
+
assert c.stdin is not None
|
| 463 |
+
try:
|
| 464 |
+
for text in generator:
|
| 465 |
+
if not color:
|
| 466 |
+
text = strip_ansi(text)
|
| 467 |
+
|
| 468 |
+
c.stdin.write(text)
|
| 469 |
+
except BrokenPipeError:
|
| 470 |
+
# In case the pager exited unexpectedly, ignore the broken pipe error.
|
| 471 |
+
pass
|
| 472 |
+
except Exception as e:
|
| 473 |
+
# In case there is an exception we want to close the pager immediately
|
| 474 |
+
# and let the caller handle it.
|
| 475 |
+
# Otherwise the pager will keep running, and the user may not notice
|
| 476 |
+
# the error message, or worse yet it may leave the terminal in a broken state.
|
| 477 |
+
c.terminate()
|
| 478 |
+
raise e
|
| 479 |
+
finally:
|
| 480 |
+
# We must close stdin and wait for the pager to exit before we continue
|
| 481 |
+
try:
|
| 482 |
+
c.stdin.close()
|
| 483 |
+
# Close implies flush, so it might throw a BrokenPipeError if the pager
|
| 484 |
+
# process exited already.
|
| 485 |
+
except BrokenPipeError:
|
| 486 |
+
pass
|
| 487 |
+
|
| 488 |
+
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
| 489 |
+
# search or other commands inside less).
|
| 490 |
+
#
|
| 491 |
+
# That means when the user hits ^C, the parent process (click) terminates,
|
| 492 |
+
# but less is still alive, paging the output and messing up the terminal.
|
| 493 |
+
#
|
| 494 |
+
# If the user wants to make the pager exit on ^C, they should set
|
| 495 |
+
# `LESS='-K'`. It's not our decision to make.
|
| 496 |
+
while True:
|
| 497 |
+
try:
|
| 498 |
+
c.wait()
|
| 499 |
+
except KeyboardInterrupt:
|
| 500 |
+
pass
|
| 501 |
+
else:
|
| 502 |
+
break
|
| 503 |
+
|
| 504 |
+
return True
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def _tempfilepager(
|
| 508 |
+
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
|
| 509 |
+
) -> bool:
|
| 510 |
+
"""Page through text by invoking a program on a temporary file.
|
| 511 |
+
|
| 512 |
+
Returns `True` if the command was found, `False` otherwise and thus another
|
| 513 |
+
pager should be attempted.
|
| 514 |
+
"""
|
| 515 |
+
# Split the command into the invoked CLI and its parameters.
|
| 516 |
+
if not cmd_parts:
|
| 517 |
+
return False
|
| 518 |
+
|
| 519 |
+
import shutil
|
| 520 |
+
|
| 521 |
+
cmd = cmd_parts[0]
|
| 522 |
+
|
| 523 |
+
cmd_filepath = shutil.which(cmd)
|
| 524 |
+
if not cmd_filepath:
|
| 525 |
+
return False
|
| 526 |
+
# Produces a normalized absolute path string.
|
| 527 |
+
# multi-call binaries such as busybox derive their identity from the symlink
|
| 528 |
+
# less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
|
| 529 |
+
cmd_path = Path(cmd_filepath).absolute()
|
| 530 |
+
|
| 531 |
+
import subprocess
|
| 532 |
+
import tempfile
|
| 533 |
+
|
| 534 |
+
fd, filename = tempfile.mkstemp()
|
| 535 |
+
# TODO: This never terminates if the passed generator never terminates.
|
| 536 |
+
text = "".join(generator)
|
| 537 |
+
if not color:
|
| 538 |
+
text = strip_ansi(text)
|
| 539 |
+
encoding = get_best_encoding(sys.stdout)
|
| 540 |
+
with open_stream(filename, "wb")[0] as f:
|
| 541 |
+
f.write(text.encode(encoding))
|
| 542 |
+
try:
|
| 543 |
+
subprocess.call([str(cmd_path), filename])
|
| 544 |
+
except OSError:
|
| 545 |
+
# Command not found
|
| 546 |
+
pass
|
| 547 |
+
finally:
|
| 548 |
+
os.close(fd)
|
| 549 |
+
os.unlink(filename)
|
| 550 |
+
|
| 551 |
+
return True
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
def _nullpager(
|
| 555 |
+
stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None
|
| 556 |
+
) -> None:
|
| 557 |
+
"""Simply print unformatted text. This is the ultimate fallback."""
|
| 558 |
+
for text in generator:
|
| 559 |
+
if not color:
|
| 560 |
+
text = strip_ansi(text)
|
| 561 |
+
stream.write(text)
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
class Editor:
|
| 565 |
+
def __init__(
|
| 566 |
+
self,
|
| 567 |
+
editor: str | None = None,
|
| 568 |
+
env: cabc.Mapping[str, str] | None = None,
|
| 569 |
+
require_save: bool = True,
|
| 570 |
+
extension: str = ".txt",
|
| 571 |
+
) -> None:
|
| 572 |
+
self.editor = editor
|
| 573 |
+
self.env = env
|
| 574 |
+
self.require_save = require_save
|
| 575 |
+
self.extension = extension
|
| 576 |
+
|
| 577 |
+
def get_editor(self) -> str:
|
| 578 |
+
if self.editor is not None:
|
| 579 |
+
return self.editor
|
| 580 |
+
for key in "VISUAL", "EDITOR":
|
| 581 |
+
rv = os.environ.get(key)
|
| 582 |
+
if rv:
|
| 583 |
+
return rv
|
| 584 |
+
if WIN:
|
| 585 |
+
return "notepad"
|
| 586 |
+
|
| 587 |
+
from shutil import which
|
| 588 |
+
|
| 589 |
+
for editor in "sensible-editor", "vim", "nano":
|
| 590 |
+
if which(editor) is not None:
|
| 591 |
+
return editor
|
| 592 |
+
return "vi"
|
| 593 |
+
|
| 594 |
+
def edit_files(self, filenames: cabc.Iterable[str]) -> None:
|
| 595 |
+
import subprocess
|
| 596 |
+
|
| 597 |
+
editor = self.get_editor()
|
| 598 |
+
environ: dict[str, str] | None = None
|
| 599 |
+
|
| 600 |
+
if self.env:
|
| 601 |
+
environ = os.environ.copy()
|
| 602 |
+
environ.update(self.env)
|
| 603 |
+
|
| 604 |
+
exc_filename = " ".join(f'"{filename}"' for filename in filenames)
|
| 605 |
+
|
| 606 |
+
try:
|
| 607 |
+
c = subprocess.Popen(
|
| 608 |
+
args=f"{editor} {exc_filename}", env=environ, shell=True
|
| 609 |
+
)
|
| 610 |
+
exit_code = c.wait()
|
| 611 |
+
if exit_code != 0:
|
| 612 |
+
raise ClickException(
|
| 613 |
+
_("{editor}: Editing failed").format(editor=editor)
|
| 614 |
+
)
|
| 615 |
+
except OSError as e:
|
| 616 |
+
raise ClickException(
|
| 617 |
+
_("{editor}: Editing failed: {e}").format(editor=editor, e=e)
|
| 618 |
+
) from e
|
| 619 |
+
|
| 620 |
+
@t.overload
|
| 621 |
+
def edit(self, text: bytes | bytearray) -> bytes | None: ...
|
| 622 |
+
|
| 623 |
+
# We cannot know whether or not the type expected is str or bytes when None
|
| 624 |
+
# is passed, so str is returned as that was what was done before.
|
| 625 |
+
@t.overload
|
| 626 |
+
def edit(self, text: str | None) -> str | None: ...
|
| 627 |
+
|
| 628 |
+
def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None:
|
| 629 |
+
import tempfile
|
| 630 |
+
|
| 631 |
+
if text is None:
|
| 632 |
+
data: bytes | bytearray = b""
|
| 633 |
+
elif isinstance(text, (bytes, bytearray)):
|
| 634 |
+
data = text
|
| 635 |
+
else:
|
| 636 |
+
if text and not text.endswith("\n"):
|
| 637 |
+
text += "\n"
|
| 638 |
+
|
| 639 |
+
if WIN:
|
| 640 |
+
data = text.replace("\n", "\r\n").encode("utf-8-sig")
|
| 641 |
+
else:
|
| 642 |
+
data = text.encode("utf-8")
|
| 643 |
+
|
| 644 |
+
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
|
| 645 |
+
f: t.BinaryIO
|
| 646 |
+
|
| 647 |
+
try:
|
| 648 |
+
with os.fdopen(fd, "wb") as f:
|
| 649 |
+
f.write(data)
|
| 650 |
+
|
| 651 |
+
# If the filesystem resolution is 1 second, like Mac OS
|
| 652 |
+
# 10.12 Extended, or 2 seconds, like FAT32, and the editor
|
| 653 |
+
# closes very fast, require_save can fail. Set the modified
|
| 654 |
+
# time to be 2 seconds in the past to work around this.
|
| 655 |
+
os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2))
|
| 656 |
+
# Depending on the resolution, the exact value might not be
|
| 657 |
+
# recorded, so get the new recorded value.
|
| 658 |
+
timestamp = os.path.getmtime(name)
|
| 659 |
+
|
| 660 |
+
self.edit_files((name,))
|
| 661 |
+
|
| 662 |
+
if self.require_save and os.path.getmtime(name) == timestamp:
|
| 663 |
+
return None
|
| 664 |
+
|
| 665 |
+
with open(name, "rb") as f:
|
| 666 |
+
rv = f.read()
|
| 667 |
+
|
| 668 |
+
if isinstance(text, (bytes, bytearray)):
|
| 669 |
+
return rv
|
| 670 |
+
|
| 671 |
+
return rv.decode("utf-8-sig").replace("\r\n", "\n")
|
| 672 |
+
finally:
|
| 673 |
+
os.unlink(name)
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
|
| 677 |
+
import subprocess
|
| 678 |
+
|
| 679 |
+
def _unquote_file(url: str) -> str:
|
| 680 |
+
from urllib.parse import unquote
|
| 681 |
+
|
| 682 |
+
if url.startswith("file://"):
|
| 683 |
+
url = unquote(url[7:])
|
| 684 |
+
|
| 685 |
+
return url
|
| 686 |
+
|
| 687 |
+
if sys.platform == "darwin":
|
| 688 |
+
args = ["open"]
|
| 689 |
+
if wait:
|
| 690 |
+
args.append("-W")
|
| 691 |
+
if locate:
|
| 692 |
+
args.append("-R")
|
| 693 |
+
args.append(_unquote_file(url))
|
| 694 |
+
null = open("/dev/null", "w")
|
| 695 |
+
try:
|
| 696 |
+
return subprocess.Popen(args, stderr=null).wait()
|
| 697 |
+
finally:
|
| 698 |
+
null.close()
|
| 699 |
+
elif WIN:
|
| 700 |
+
if locate:
|
| 701 |
+
url = _unquote_file(url)
|
| 702 |
+
args = ["explorer", f"/select,{url}"]
|
| 703 |
+
else:
|
| 704 |
+
args = ["start"]
|
| 705 |
+
if wait:
|
| 706 |
+
args.append("/WAIT")
|
| 707 |
+
args.append("")
|
| 708 |
+
args.append(url)
|
| 709 |
+
try:
|
| 710 |
+
return subprocess.call(args)
|
| 711 |
+
except OSError:
|
| 712 |
+
# Command not found
|
| 713 |
+
return 127
|
| 714 |
+
elif CYGWIN:
|
| 715 |
+
if locate:
|
| 716 |
+
url = _unquote_file(url)
|
| 717 |
+
args = ["cygstart", os.path.dirname(url)]
|
| 718 |
+
else:
|
| 719 |
+
args = ["cygstart"]
|
| 720 |
+
if wait:
|
| 721 |
+
args.append("-w")
|
| 722 |
+
args.append(url)
|
| 723 |
+
try:
|
| 724 |
+
return subprocess.call(args)
|
| 725 |
+
except OSError:
|
| 726 |
+
# Command not found
|
| 727 |
+
return 127
|
| 728 |
+
|
| 729 |
+
try:
|
| 730 |
+
if locate:
|
| 731 |
+
url = os.path.dirname(_unquote_file(url)) or "."
|
| 732 |
+
else:
|
| 733 |
+
url = _unquote_file(url)
|
| 734 |
+
c = subprocess.Popen(["xdg-open", url])
|
| 735 |
+
if wait:
|
| 736 |
+
return c.wait()
|
| 737 |
+
return 0
|
| 738 |
+
except OSError:
|
| 739 |
+
if url.startswith(("http://", "https://")) and not locate and not wait:
|
| 740 |
+
import webbrowser
|
| 741 |
+
|
| 742 |
+
webbrowser.open(url)
|
| 743 |
+
return 0
|
| 744 |
+
return 1
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
def _translate_ch_to_exc(ch: str) -> None:
|
| 748 |
+
if ch == "\x03":
|
| 749 |
+
raise KeyboardInterrupt()
|
| 750 |
+
|
| 751 |
+
if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
|
| 752 |
+
raise EOFError()
|
| 753 |
+
|
| 754 |
+
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
|
| 755 |
+
raise EOFError()
|
| 756 |
+
|
| 757 |
+
return None
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
if sys.platform == "win32":
|
| 761 |
+
import msvcrt
|
| 762 |
+
|
| 763 |
+
@contextlib.contextmanager
|
| 764 |
+
def raw_terminal() -> cabc.Iterator[int]:
|
| 765 |
+
yield -1
|
| 766 |
+
|
| 767 |
+
def getchar(echo: bool) -> str:
|
| 768 |
+
# The function `getch` will return a bytes object corresponding to
|
| 769 |
+
# the pressed character. Since Windows 10 build 1803, it will also
|
| 770 |
+
# return \x00 when called a second time after pressing a regular key.
|
| 771 |
+
#
|
| 772 |
+
# `getwch` does not share this probably-bugged behavior. Moreover, it
|
| 773 |
+
# returns a Unicode object by default, which is what we want.
|
| 774 |
+
#
|
| 775 |
+
# Either of these functions will return \x00 or \xe0 to indicate
|
| 776 |
+
# a special key, and you need to call the same function again to get
|
| 777 |
+
# the "rest" of the code. The fun part is that \u00e0 is
|
| 778 |
+
# "latin small letter a with grave", so if you type that on a French
|
| 779 |
+
# keyboard, you _also_ get a \xe0.
|
| 780 |
+
# E.g., consider the Up arrow. This returns \xe0 and then \x48. The
|
| 781 |
+
# resulting Unicode string reads as "a with grave" + "capital H".
|
| 782 |
+
# This is indistinguishable from when the user actually types
|
| 783 |
+
# "a with grave" and then "capital H".
|
| 784 |
+
#
|
| 785 |
+
# When \xe0 is returned, we assume it's part of a special-key sequence
|
| 786 |
+
# and call `getwch` again, but that means that when the user types
|
| 787 |
+
# the \u00e0 character, `getchar` doesn't return until a second
|
| 788 |
+
# character is typed.
|
| 789 |
+
# The alternative is returning immediately, but that would mess up
|
| 790 |
+
# cross-platform handling of arrow keys and others that start with
|
| 791 |
+
# \xe0. Another option is using `getch`, but then we can't reliably
|
| 792 |
+
# read non-ASCII characters, because return values of `getch` are
|
| 793 |
+
# limited to the current 8-bit codepage.
|
| 794 |
+
#
|
| 795 |
+
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
|
| 796 |
+
# is doing the right thing in more situations than with `getch`.
|
| 797 |
+
|
| 798 |
+
if echo:
|
| 799 |
+
func = t.cast(t.Callable[[], str], msvcrt.getwche)
|
| 800 |
+
else:
|
| 801 |
+
func = t.cast(t.Callable[[], str], msvcrt.getwch)
|
| 802 |
+
|
| 803 |
+
rv = func()
|
| 804 |
+
|
| 805 |
+
if rv in ("\x00", "\xe0"):
|
| 806 |
+
# \x00 and \xe0 are control characters that indicate special key,
|
| 807 |
+
# see above.
|
| 808 |
+
rv += func()
|
| 809 |
+
|
| 810 |
+
_translate_ch_to_exc(rv)
|
| 811 |
+
return rv
|
| 812 |
+
|
| 813 |
+
else:
|
| 814 |
+
import termios
|
| 815 |
+
import tty
|
| 816 |
+
|
| 817 |
+
@contextlib.contextmanager
|
| 818 |
+
def raw_terminal() -> cabc.Iterator[int]:
|
| 819 |
+
f: t.TextIO | None
|
| 820 |
+
fd: int
|
| 821 |
+
|
| 822 |
+
if not isatty(sys.stdin):
|
| 823 |
+
f = open("/dev/tty")
|
| 824 |
+
fd = f.fileno()
|
| 825 |
+
else:
|
| 826 |
+
fd = sys.stdin.fileno()
|
| 827 |
+
f = None
|
| 828 |
+
|
| 829 |
+
try:
|
| 830 |
+
old_settings = termios.tcgetattr(fd)
|
| 831 |
+
|
| 832 |
+
try:
|
| 833 |
+
tty.setraw(fd)
|
| 834 |
+
yield fd
|
| 835 |
+
finally:
|
| 836 |
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
| 837 |
+
sys.stdout.flush()
|
| 838 |
+
|
| 839 |
+
if f is not None:
|
| 840 |
+
f.close()
|
| 841 |
+
except termios.error:
|
| 842 |
+
pass
|
| 843 |
+
|
| 844 |
+
def getchar(echo: bool) -> str:
|
| 845 |
+
with raw_terminal() as fd:
|
| 846 |
+
ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
|
| 847 |
+
|
| 848 |
+
if echo and isatty(sys.stdout):
|
| 849 |
+
sys.stdout.write(ch)
|
| 850 |
+
|
| 851 |
+
_translate_ch_to_exc(ch)
|
| 852 |
+
return ch
|
env/lib/python3.13/site-packages/click/_textwrap.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
import textwrap
|
| 5 |
+
from contextlib import contextmanager
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class TextWrapper(textwrap.TextWrapper):
|
| 9 |
+
def _handle_long_word(
|
| 10 |
+
self,
|
| 11 |
+
reversed_chunks: list[str],
|
| 12 |
+
cur_line: list[str],
|
| 13 |
+
cur_len: int,
|
| 14 |
+
width: int,
|
| 15 |
+
) -> None:
|
| 16 |
+
space_left = max(width - cur_len, 1)
|
| 17 |
+
|
| 18 |
+
if self.break_long_words:
|
| 19 |
+
last = reversed_chunks[-1]
|
| 20 |
+
cut = last[:space_left]
|
| 21 |
+
res = last[space_left:]
|
| 22 |
+
cur_line.append(cut)
|
| 23 |
+
reversed_chunks[-1] = res
|
| 24 |
+
elif not cur_line:
|
| 25 |
+
cur_line.append(reversed_chunks.pop())
|
| 26 |
+
|
| 27 |
+
@contextmanager
|
| 28 |
+
def extra_indent(self, indent: str) -> cabc.Iterator[None]:
|
| 29 |
+
old_initial_indent = self.initial_indent
|
| 30 |
+
old_subsequent_indent = self.subsequent_indent
|
| 31 |
+
self.initial_indent += indent
|
| 32 |
+
self.subsequent_indent += indent
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
yield
|
| 36 |
+
finally:
|
| 37 |
+
self.initial_indent = old_initial_indent
|
| 38 |
+
self.subsequent_indent = old_subsequent_indent
|
| 39 |
+
|
| 40 |
+
def indent_only(self, text: str) -> str:
|
| 41 |
+
rv = []
|
| 42 |
+
|
| 43 |
+
for idx, line in enumerate(text.splitlines()):
|
| 44 |
+
indent = self.initial_indent
|
| 45 |
+
|
| 46 |
+
if idx > 0:
|
| 47 |
+
indent = self.subsequent_indent
|
| 48 |
+
|
| 49 |
+
rv.append(f"{indent}{line}")
|
| 50 |
+
|
| 51 |
+
return "\n".join(rv)
|
env/lib/python3.13/site-packages/click/_utils.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import enum
|
| 4 |
+
import typing as t
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Sentinel(enum.Enum):
|
| 8 |
+
"""Enum used to define sentinel values.
|
| 9 |
+
|
| 10 |
+
.. seealso::
|
| 11 |
+
|
| 12 |
+
`PEP 661 - Sentinel Values <https://peps.python.org/pep-0661/>`_.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
UNSET = object()
|
| 16 |
+
FLAG_NEEDS_VALUE = object()
|
| 17 |
+
|
| 18 |
+
def __repr__(self) -> str:
|
| 19 |
+
return f"{self.__class__.__name__}.{self.name}"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
UNSET = Sentinel.UNSET
|
| 23 |
+
"""Sentinel used to indicate that a value is not set."""
|
| 24 |
+
|
| 25 |
+
FLAG_NEEDS_VALUE = Sentinel.FLAG_NEEDS_VALUE
|
| 26 |
+
"""Sentinel used to indicate an option was passed as a flag without a
|
| 27 |
+
value but is not a flag option.
|
| 28 |
+
|
| 29 |
+
``Option.consume_value`` uses this to prompt or use the ``flag_value``.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
T_UNSET = t.Literal[UNSET] # type: ignore[valid-type]
|
| 33 |
+
"""Type hint for the :data:`UNSET` sentinel value."""
|
| 34 |
+
|
| 35 |
+
T_FLAG_NEEDS_VALUE = t.Literal[FLAG_NEEDS_VALUE] # type: ignore[valid-type]
|
| 36 |
+
"""Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value."""
|
env/lib/python3.13/site-packages/click/_winconsole.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This module is based on the excellent work by Adam Bartoš who
|
| 2 |
+
# provided a lot of what went into the implementation here in
|
| 3 |
+
# the discussion to issue1602 in the Python bug tracker.
|
| 4 |
+
#
|
| 5 |
+
# There are some general differences in regards to how this works
|
| 6 |
+
# compared to the original patches as we do not need to patch
|
| 7 |
+
# the entire interpreter but just work in our little world of
|
| 8 |
+
# echo and prompt.
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import collections.abc as cabc
|
| 12 |
+
import io
|
| 13 |
+
import sys
|
| 14 |
+
import time
|
| 15 |
+
import typing as t
|
| 16 |
+
from ctypes import Array
|
| 17 |
+
from ctypes import byref
|
| 18 |
+
from ctypes import c_char
|
| 19 |
+
from ctypes import c_char_p
|
| 20 |
+
from ctypes import c_int
|
| 21 |
+
from ctypes import c_ssize_t
|
| 22 |
+
from ctypes import c_ulong
|
| 23 |
+
from ctypes import c_void_p
|
| 24 |
+
from ctypes import POINTER
|
| 25 |
+
from ctypes import py_object
|
| 26 |
+
from ctypes import Structure
|
| 27 |
+
from ctypes.wintypes import DWORD
|
| 28 |
+
from ctypes.wintypes import HANDLE
|
| 29 |
+
from ctypes.wintypes import LPCWSTR
|
| 30 |
+
from ctypes.wintypes import LPWSTR
|
| 31 |
+
|
| 32 |
+
from ._compat import _NonClosingTextIOWrapper
|
| 33 |
+
|
| 34 |
+
assert sys.platform == "win32"
|
| 35 |
+
import msvcrt # noqa: E402
|
| 36 |
+
from ctypes import windll # noqa: E402
|
| 37 |
+
from ctypes import WINFUNCTYPE # noqa: E402
|
| 38 |
+
|
| 39 |
+
c_ssize_p = POINTER(c_ssize_t)
|
| 40 |
+
|
| 41 |
+
kernel32 = windll.kernel32
|
| 42 |
+
GetStdHandle = kernel32.GetStdHandle
|
| 43 |
+
ReadConsoleW = kernel32.ReadConsoleW
|
| 44 |
+
WriteConsoleW = kernel32.WriteConsoleW
|
| 45 |
+
GetConsoleMode = kernel32.GetConsoleMode
|
| 46 |
+
GetLastError = kernel32.GetLastError
|
| 47 |
+
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
|
| 48 |
+
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
| 49 |
+
("CommandLineToArgvW", windll.shell32)
|
| 50 |
+
)
|
| 51 |
+
LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32))
|
| 52 |
+
|
| 53 |
+
STDIN_HANDLE = GetStdHandle(-10)
|
| 54 |
+
STDOUT_HANDLE = GetStdHandle(-11)
|
| 55 |
+
STDERR_HANDLE = GetStdHandle(-12)
|
| 56 |
+
|
| 57 |
+
PyBUF_SIMPLE = 0
|
| 58 |
+
PyBUF_WRITABLE = 1
|
| 59 |
+
|
| 60 |
+
ERROR_SUCCESS = 0
|
| 61 |
+
ERROR_NOT_ENOUGH_MEMORY = 8
|
| 62 |
+
ERROR_OPERATION_ABORTED = 995
|
| 63 |
+
|
| 64 |
+
STDIN_FILENO = 0
|
| 65 |
+
STDOUT_FILENO = 1
|
| 66 |
+
STDERR_FILENO = 2
|
| 67 |
+
|
| 68 |
+
EOF = b"\x1a"
|
| 69 |
+
MAX_BYTES_WRITTEN = 32767
|
| 70 |
+
|
| 71 |
+
if t.TYPE_CHECKING:
|
| 72 |
+
try:
|
| 73 |
+
# Using `typing_extensions.Buffer` instead of `collections.abc`
|
| 74 |
+
# on Windows for some reason does not have `Sized` implemented.
|
| 75 |
+
from collections.abc import Buffer # type: ignore
|
| 76 |
+
except ImportError:
|
| 77 |
+
from typing_extensions import Buffer
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
from ctypes import pythonapi
|
| 81 |
+
except ImportError:
|
| 82 |
+
# On PyPy we cannot get buffers so our ability to operate here is
|
| 83 |
+
# severely limited.
|
| 84 |
+
get_buffer = None
|
| 85 |
+
else:
|
| 86 |
+
|
| 87 |
+
class Py_buffer(Structure):
|
| 88 |
+
_fields_ = [ # noqa: RUF012
|
| 89 |
+
("buf", c_void_p),
|
| 90 |
+
("obj", py_object),
|
| 91 |
+
("len", c_ssize_t),
|
| 92 |
+
("itemsize", c_ssize_t),
|
| 93 |
+
("readonly", c_int),
|
| 94 |
+
("ndim", c_int),
|
| 95 |
+
("format", c_char_p),
|
| 96 |
+
("shape", c_ssize_p),
|
| 97 |
+
("strides", c_ssize_p),
|
| 98 |
+
("suboffsets", c_ssize_p),
|
| 99 |
+
("internal", c_void_p),
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
|
| 103 |
+
PyBuffer_Release = pythonapi.PyBuffer_Release
|
| 104 |
+
|
| 105 |
+
def get_buffer(obj: Buffer, writable: bool = False) -> Array[c_char]:
|
| 106 |
+
buf = Py_buffer()
|
| 107 |
+
flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
| 108 |
+
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
buffer_type = c_char * buf.len
|
| 112 |
+
out: Array[c_char] = buffer_type.from_address(buf.buf)
|
| 113 |
+
return out
|
| 114 |
+
finally:
|
| 115 |
+
PyBuffer_Release(byref(buf))
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class _WindowsConsoleRawIOBase(io.RawIOBase):
|
| 119 |
+
def __init__(self, handle: int | None) -> None:
|
| 120 |
+
self.handle = handle
|
| 121 |
+
|
| 122 |
+
def isatty(self) -> t.Literal[True]:
|
| 123 |
+
super().isatty()
|
| 124 |
+
return True
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
|
| 128 |
+
def readable(self) -> t.Literal[True]:
|
| 129 |
+
return True
|
| 130 |
+
|
| 131 |
+
def readinto(self, b: Buffer) -> int:
|
| 132 |
+
bytes_to_be_read = len(b)
|
| 133 |
+
if not bytes_to_be_read:
|
| 134 |
+
return 0
|
| 135 |
+
elif bytes_to_be_read % 2:
|
| 136 |
+
raise ValueError(
|
| 137 |
+
"cannot read odd number of bytes from UTF-16-LE encoded console"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
buffer = get_buffer(b, writable=True)
|
| 141 |
+
code_units_to_be_read = bytes_to_be_read // 2
|
| 142 |
+
code_units_read = c_ulong()
|
| 143 |
+
|
| 144 |
+
rv = ReadConsoleW(
|
| 145 |
+
HANDLE(self.handle),
|
| 146 |
+
buffer,
|
| 147 |
+
code_units_to_be_read,
|
| 148 |
+
byref(code_units_read),
|
| 149 |
+
None,
|
| 150 |
+
)
|
| 151 |
+
if GetLastError() == ERROR_OPERATION_ABORTED:
|
| 152 |
+
# wait for KeyboardInterrupt
|
| 153 |
+
time.sleep(0.1)
|
| 154 |
+
if not rv:
|
| 155 |
+
raise OSError(f"Windows error: {GetLastError()}")
|
| 156 |
+
|
| 157 |
+
if buffer[0] == EOF:
|
| 158 |
+
return 0
|
| 159 |
+
return 2 * code_units_read.value
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
| 163 |
+
def writable(self) -> t.Literal[True]:
|
| 164 |
+
return True
|
| 165 |
+
|
| 166 |
+
@staticmethod
|
| 167 |
+
def _get_error_message(errno: int) -> str:
|
| 168 |
+
if errno == ERROR_SUCCESS:
|
| 169 |
+
return "ERROR_SUCCESS"
|
| 170 |
+
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
| 171 |
+
return "ERROR_NOT_ENOUGH_MEMORY"
|
| 172 |
+
return f"Windows error {errno}"
|
| 173 |
+
|
| 174 |
+
def write(self, b: Buffer) -> int:
|
| 175 |
+
bytes_to_be_written = len(b)
|
| 176 |
+
buf = get_buffer(b)
|
| 177 |
+
code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
|
| 178 |
+
code_units_written = c_ulong()
|
| 179 |
+
|
| 180 |
+
WriteConsoleW(
|
| 181 |
+
HANDLE(self.handle),
|
| 182 |
+
buf,
|
| 183 |
+
code_units_to_be_written,
|
| 184 |
+
byref(code_units_written),
|
| 185 |
+
None,
|
| 186 |
+
)
|
| 187 |
+
bytes_written = 2 * code_units_written.value
|
| 188 |
+
|
| 189 |
+
if bytes_written == 0 and bytes_to_be_written > 0:
|
| 190 |
+
raise OSError(self._get_error_message(GetLastError()))
|
| 191 |
+
return bytes_written
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
class ConsoleStream:
|
| 195 |
+
def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None:
|
| 196 |
+
self._text_stream = text_stream
|
| 197 |
+
self.buffer = byte_stream
|
| 198 |
+
|
| 199 |
+
@property
|
| 200 |
+
def name(self) -> str:
|
| 201 |
+
return self.buffer.name
|
| 202 |
+
|
| 203 |
+
def write(self, x: t.AnyStr) -> int:
|
| 204 |
+
if isinstance(x, str):
|
| 205 |
+
return self._text_stream.write(x)
|
| 206 |
+
try:
|
| 207 |
+
self.flush()
|
| 208 |
+
except Exception:
|
| 209 |
+
pass
|
| 210 |
+
return self.buffer.write(x)
|
| 211 |
+
|
| 212 |
+
def writelines(self, lines: cabc.Iterable[t.AnyStr]) -> None:
|
| 213 |
+
for line in lines:
|
| 214 |
+
self.write(line)
|
| 215 |
+
|
| 216 |
+
def __getattr__(self, name: str) -> t.Any:
|
| 217 |
+
return getattr(self._text_stream, name)
|
| 218 |
+
|
| 219 |
+
def isatty(self) -> bool:
|
| 220 |
+
return self.buffer.isatty()
|
| 221 |
+
|
| 222 |
+
def __repr__(self) -> str:
|
| 223 |
+
return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>"
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO:
|
| 227 |
+
text_stream = _NonClosingTextIOWrapper(
|
| 228 |
+
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
| 229 |
+
"utf-16-le",
|
| 230 |
+
"strict",
|
| 231 |
+
line_buffering=True,
|
| 232 |
+
)
|
| 233 |
+
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO:
|
| 237 |
+
text_stream = _NonClosingTextIOWrapper(
|
| 238 |
+
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
| 239 |
+
"utf-16-le",
|
| 240 |
+
"strict",
|
| 241 |
+
line_buffering=True,
|
| 242 |
+
)
|
| 243 |
+
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO:
|
| 247 |
+
text_stream = _NonClosingTextIOWrapper(
|
| 248 |
+
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
| 249 |
+
"utf-16-le",
|
| 250 |
+
"strict",
|
| 251 |
+
line_buffering=True,
|
| 252 |
+
)
|
| 253 |
+
return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream))
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
_stream_factories: cabc.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = {
|
| 257 |
+
0: _get_text_stdin,
|
| 258 |
+
1: _get_text_stdout,
|
| 259 |
+
2: _get_text_stderr,
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def _is_console(f: t.TextIO) -> bool:
|
| 264 |
+
if not hasattr(f, "fileno"):
|
| 265 |
+
return False
|
| 266 |
+
|
| 267 |
+
try:
|
| 268 |
+
fileno = f.fileno()
|
| 269 |
+
except (OSError, io.UnsupportedOperation):
|
| 270 |
+
return False
|
| 271 |
+
|
| 272 |
+
handle = msvcrt.get_osfhandle(fileno)
|
| 273 |
+
return bool(GetConsoleMode(handle, byref(DWORD())))
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _get_windows_console_stream(
|
| 277 |
+
f: t.TextIO, encoding: str | None, errors: str | None
|
| 278 |
+
) -> t.TextIO | None:
|
| 279 |
+
if (
|
| 280 |
+
get_buffer is None
|
| 281 |
+
or encoding not in {"utf-16-le", None}
|
| 282 |
+
or errors not in {"strict", None}
|
| 283 |
+
or not _is_console(f)
|
| 284 |
+
):
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
func = _stream_factories.get(f.fileno())
|
| 288 |
+
if func is None:
|
| 289 |
+
return None
|
| 290 |
+
|
| 291 |
+
b = getattr(f, "buffer", None)
|
| 292 |
+
|
| 293 |
+
if b is None:
|
| 294 |
+
return None
|
| 295 |
+
|
| 296 |
+
return func(b)
|
env/lib/python3.13/site-packages/click/core.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
env/lib/python3.13/site-packages/click/decorators.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
import typing as t
|
| 5 |
+
from functools import update_wrapper
|
| 6 |
+
from gettext import gettext as _
|
| 7 |
+
|
| 8 |
+
from .core import Argument
|
| 9 |
+
from .core import Command
|
| 10 |
+
from .core import Context
|
| 11 |
+
from .core import Group
|
| 12 |
+
from .core import Option
|
| 13 |
+
from .core import Parameter
|
| 14 |
+
from .globals import get_current_context
|
| 15 |
+
from .utils import echo
|
| 16 |
+
|
| 17 |
+
if t.TYPE_CHECKING:
|
| 18 |
+
import typing_extensions as te
|
| 19 |
+
|
| 20 |
+
P = te.ParamSpec("P")
|
| 21 |
+
|
| 22 |
+
R = t.TypeVar("R")
|
| 23 |
+
T = t.TypeVar("T")
|
| 24 |
+
_AnyCallable = t.Callable[..., t.Any]
|
| 25 |
+
FC = t.TypeVar("FC", bound="_AnyCallable | Command")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def pass_context(f: t.Callable[te.Concatenate[Context, P], R]) -> t.Callable[P, R]:
|
| 29 |
+
"""Marks a callback as wanting to receive the current context
|
| 30 |
+
object as first argument.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
|
| 34 |
+
return f(get_current_context(), *args, **kwargs)
|
| 35 |
+
|
| 36 |
+
return update_wrapper(new_func, f)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def pass_obj(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]:
|
| 40 |
+
"""Similar to :func:`pass_context`, but only pass the object on the
|
| 41 |
+
context onwards (:attr:`Context.obj`). This is useful if that object
|
| 42 |
+
represents the state of a nested system.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
|
| 46 |
+
return f(get_current_context().obj, *args, **kwargs)
|
| 47 |
+
|
| 48 |
+
return update_wrapper(new_func, f)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def make_pass_decorator(
|
| 52 |
+
object_type: type[T], ensure: bool = False
|
| 53 |
+
) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]:
|
| 54 |
+
"""Given an object type this creates a decorator that will work
|
| 55 |
+
similar to :func:`pass_obj` but instead of passing the object of the
|
| 56 |
+
current context, it will find the innermost context of type
|
| 57 |
+
:func:`object_type`.
|
| 58 |
+
|
| 59 |
+
This generates a decorator that works roughly like this::
|
| 60 |
+
|
| 61 |
+
from functools import update_wrapper
|
| 62 |
+
|
| 63 |
+
def decorator(f):
|
| 64 |
+
@pass_context
|
| 65 |
+
def new_func(ctx, *args, **kwargs):
|
| 66 |
+
obj = ctx.find_object(object_type)
|
| 67 |
+
return ctx.invoke(f, obj, *args, **kwargs)
|
| 68 |
+
return update_wrapper(new_func, f)
|
| 69 |
+
return decorator
|
| 70 |
+
|
| 71 |
+
:param object_type: the type of the object to pass.
|
| 72 |
+
:param ensure: if set to `True`, a new object will be created and
|
| 73 |
+
remembered on the context if it's not there yet.
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]:
|
| 77 |
+
def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
|
| 78 |
+
ctx = get_current_context()
|
| 79 |
+
|
| 80 |
+
obj: T | None
|
| 81 |
+
if ensure:
|
| 82 |
+
obj = ctx.ensure_object(object_type)
|
| 83 |
+
else:
|
| 84 |
+
obj = ctx.find_object(object_type)
|
| 85 |
+
|
| 86 |
+
if obj is None:
|
| 87 |
+
raise RuntimeError(
|
| 88 |
+
"Managed to invoke callback without a context"
|
| 89 |
+
f" object of type {object_type.__name__!r}"
|
| 90 |
+
" existing."
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
return ctx.invoke(f, obj, *args, **kwargs)
|
| 94 |
+
|
| 95 |
+
return update_wrapper(new_func, f)
|
| 96 |
+
|
| 97 |
+
return decorator
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def pass_meta_key(
|
| 101 |
+
key: str, *, doc_description: str | None = None
|
| 102 |
+
) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]:
|
| 103 |
+
"""Create a decorator that passes a key from
|
| 104 |
+
:attr:`click.Context.meta` as the first argument to the decorated
|
| 105 |
+
function.
|
| 106 |
+
|
| 107 |
+
:param key: Key in ``Context.meta`` to pass.
|
| 108 |
+
:param doc_description: Description of the object being passed,
|
| 109 |
+
inserted into the decorator's docstring. Defaults to "the 'key'
|
| 110 |
+
key from Context.meta".
|
| 111 |
+
|
| 112 |
+
.. versionadded:: 8.0
|
| 113 |
+
"""
|
| 114 |
+
|
| 115 |
+
def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]:
|
| 116 |
+
def new_func(*args: P.args, **kwargs: P.kwargs) -> R:
|
| 117 |
+
ctx = get_current_context()
|
| 118 |
+
obj = ctx.meta[key]
|
| 119 |
+
return ctx.invoke(f, obj, *args, **kwargs)
|
| 120 |
+
|
| 121 |
+
return update_wrapper(new_func, f)
|
| 122 |
+
|
| 123 |
+
if doc_description is None:
|
| 124 |
+
doc_description = f"the {key!r} key from :attr:`click.Context.meta`"
|
| 125 |
+
|
| 126 |
+
decorator.__doc__ = (
|
| 127 |
+
f"Decorator that passes {doc_description} as the first argument"
|
| 128 |
+
" to the decorated function."
|
| 129 |
+
)
|
| 130 |
+
return decorator
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
CmdType = t.TypeVar("CmdType", bound=Command)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# variant: no call, directly as decorator for a function.
|
| 137 |
+
@t.overload
|
| 138 |
+
def command(name: _AnyCallable) -> Command: ...
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# variant: with positional name and with positional or keyword cls argument:
|
| 142 |
+
# @command(namearg, CommandCls, ...) or @command(namearg, cls=CommandCls, ...)
|
| 143 |
+
@t.overload
|
| 144 |
+
def command(
|
| 145 |
+
name: str | None,
|
| 146 |
+
cls: type[CmdType],
|
| 147 |
+
**attrs: t.Any,
|
| 148 |
+
) -> t.Callable[[_AnyCallable], CmdType]: ...
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...)
|
| 152 |
+
@t.overload
|
| 153 |
+
def command(
|
| 154 |
+
name: None = None,
|
| 155 |
+
*,
|
| 156 |
+
cls: type[CmdType],
|
| 157 |
+
**attrs: t.Any,
|
| 158 |
+
) -> t.Callable[[_AnyCallable], CmdType]: ...
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# variant: with optional string name, no cls argument provided.
|
| 162 |
+
@t.overload
|
| 163 |
+
def command(
|
| 164 |
+
name: str | None = ..., cls: None = None, **attrs: t.Any
|
| 165 |
+
) -> t.Callable[[_AnyCallable], Command]: ...
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def command(
|
| 169 |
+
name: str | _AnyCallable | None = None,
|
| 170 |
+
cls: type[CmdType] | None = None,
|
| 171 |
+
**attrs: t.Any,
|
| 172 |
+
) -> Command | t.Callable[[_AnyCallable], Command | CmdType]:
|
| 173 |
+
r"""Creates a new :class:`Command` and uses the decorated function as
|
| 174 |
+
callback. This will also automatically attach all decorated
|
| 175 |
+
:func:`option`\s and :func:`argument`\s as parameters to the command.
|
| 176 |
+
|
| 177 |
+
The name of the command defaults to the name of the function, converted to
|
| 178 |
+
lowercase, with underscores ``_`` replaced by dashes ``-``, and the suffixes
|
| 179 |
+
``_command``, ``_cmd``, ``_group``, and ``_grp`` are removed. For example,
|
| 180 |
+
``init_data_command`` becomes ``init-data``.
|
| 181 |
+
|
| 182 |
+
All keyword arguments are forwarded to the underlying command class.
|
| 183 |
+
For the ``params`` argument, any decorated params are appended to
|
| 184 |
+
the end of the list.
|
| 185 |
+
|
| 186 |
+
Once decorated the function turns into a :class:`Command` instance
|
| 187 |
+
that can be invoked as a command line utility or be attached to a
|
| 188 |
+
command :class:`Group`.
|
| 189 |
+
|
| 190 |
+
:param name: The name of the command. Defaults to modifying the function's
|
| 191 |
+
name as described above.
|
| 192 |
+
:param cls: The command class to create. Defaults to :class:`Command`.
|
| 193 |
+
|
| 194 |
+
.. versionchanged:: 8.2
|
| 195 |
+
The suffixes ``_command``, ``_cmd``, ``_group``, and ``_grp`` are
|
| 196 |
+
removed when generating the name.
|
| 197 |
+
|
| 198 |
+
.. versionchanged:: 8.1
|
| 199 |
+
This decorator can be applied without parentheses.
|
| 200 |
+
|
| 201 |
+
.. versionchanged:: 8.1
|
| 202 |
+
The ``params`` argument can be used. Decorated params are
|
| 203 |
+
appended to the end of the list.
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
func: t.Callable[[_AnyCallable], t.Any] | None = None
|
| 207 |
+
|
| 208 |
+
if callable(name):
|
| 209 |
+
func = name
|
| 210 |
+
name = None
|
| 211 |
+
assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class."
|
| 212 |
+
assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments."
|
| 213 |
+
|
| 214 |
+
if cls is None:
|
| 215 |
+
cls = t.cast("type[CmdType]", Command)
|
| 216 |
+
|
| 217 |
+
def decorator(f: _AnyCallable) -> CmdType:
|
| 218 |
+
if isinstance(f, Command):
|
| 219 |
+
raise TypeError("Attempted to convert a callback into a command twice.")
|
| 220 |
+
|
| 221 |
+
attr_params = attrs.pop("params", None)
|
| 222 |
+
params = attr_params if attr_params is not None else []
|
| 223 |
+
|
| 224 |
+
try:
|
| 225 |
+
decorator_params = f.__click_params__ # type: ignore
|
| 226 |
+
except AttributeError:
|
| 227 |
+
pass
|
| 228 |
+
else:
|
| 229 |
+
del f.__click_params__ # type: ignore
|
| 230 |
+
params.extend(reversed(decorator_params))
|
| 231 |
+
|
| 232 |
+
if attrs.get("help") is None:
|
| 233 |
+
attrs["help"] = f.__doc__
|
| 234 |
+
|
| 235 |
+
if t.TYPE_CHECKING:
|
| 236 |
+
assert cls is not None
|
| 237 |
+
assert not callable(name)
|
| 238 |
+
|
| 239 |
+
if name is not None:
|
| 240 |
+
cmd_name = name
|
| 241 |
+
else:
|
| 242 |
+
cmd_name = f.__name__.lower().replace("_", "-")
|
| 243 |
+
cmd_left, sep, suffix = cmd_name.rpartition("-")
|
| 244 |
+
|
| 245 |
+
if sep and suffix in {"command", "cmd", "group", "grp"}:
|
| 246 |
+
cmd_name = cmd_left
|
| 247 |
+
|
| 248 |
+
cmd = cls(name=cmd_name, callback=f, params=params, **attrs)
|
| 249 |
+
cmd.__doc__ = f.__doc__
|
| 250 |
+
return cmd
|
| 251 |
+
|
| 252 |
+
if func is not None:
|
| 253 |
+
return decorator(func)
|
| 254 |
+
|
| 255 |
+
return decorator
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
GrpType = t.TypeVar("GrpType", bound=Group)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# variant: no call, directly as decorator for a function.
|
| 262 |
+
@t.overload
|
| 263 |
+
def group(name: _AnyCallable) -> Group: ...
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# variant: with positional name and with positional or keyword cls argument:
|
| 267 |
+
# @group(namearg, GroupCls, ...) or @group(namearg, cls=GroupCls, ...)
|
| 268 |
+
@t.overload
|
| 269 |
+
def group(
|
| 270 |
+
name: str | None,
|
| 271 |
+
cls: type[GrpType],
|
| 272 |
+
**attrs: t.Any,
|
| 273 |
+
) -> t.Callable[[_AnyCallable], GrpType]: ...
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...)
|
| 277 |
+
@t.overload
|
| 278 |
+
def group(
|
| 279 |
+
name: None = None,
|
| 280 |
+
*,
|
| 281 |
+
cls: type[GrpType],
|
| 282 |
+
**attrs: t.Any,
|
| 283 |
+
) -> t.Callable[[_AnyCallable], GrpType]: ...
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# variant: with optional string name, no cls argument provided.
|
| 287 |
+
@t.overload
|
| 288 |
+
def group(
|
| 289 |
+
name: str | None = ..., cls: None = None, **attrs: t.Any
|
| 290 |
+
) -> t.Callable[[_AnyCallable], Group]: ...
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def group(
|
| 294 |
+
name: str | _AnyCallable | None = None,
|
| 295 |
+
cls: type[GrpType] | None = None,
|
| 296 |
+
**attrs: t.Any,
|
| 297 |
+
) -> Group | t.Callable[[_AnyCallable], Group | GrpType]:
|
| 298 |
+
"""Creates a new :class:`Group` with a function as callback. This
|
| 299 |
+
works otherwise the same as :func:`command` just that the `cls`
|
| 300 |
+
parameter is set to :class:`Group`.
|
| 301 |
+
|
| 302 |
+
.. versionchanged:: 8.1
|
| 303 |
+
This decorator can be applied without parentheses.
|
| 304 |
+
"""
|
| 305 |
+
if cls is None:
|
| 306 |
+
cls = t.cast("type[GrpType]", Group)
|
| 307 |
+
|
| 308 |
+
if callable(name):
|
| 309 |
+
return command(cls=cls, **attrs)(name)
|
| 310 |
+
|
| 311 |
+
return command(name, cls, **attrs)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None:
|
| 315 |
+
if isinstance(f, Command):
|
| 316 |
+
f.params.append(param)
|
| 317 |
+
else:
|
| 318 |
+
if not hasattr(f, "__click_params__"):
|
| 319 |
+
f.__click_params__ = [] # type: ignore
|
| 320 |
+
|
| 321 |
+
f.__click_params__.append(param) # type: ignore
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def argument(
|
| 325 |
+
*param_decls: str, cls: type[Argument] | None = None, **attrs: t.Any
|
| 326 |
+
) -> t.Callable[[FC], FC]:
|
| 327 |
+
"""Attaches an argument to the command. All positional arguments are
|
| 328 |
+
passed as parameter declarations to :class:`Argument`; all keyword
|
| 329 |
+
arguments are forwarded unchanged (except ``cls``).
|
| 330 |
+
This is equivalent to creating an :class:`Argument` instance manually
|
| 331 |
+
and attaching it to the :attr:`Command.params` list.
|
| 332 |
+
|
| 333 |
+
For the default argument class, refer to :class:`Argument` and
|
| 334 |
+
:class:`Parameter` for descriptions of parameters.
|
| 335 |
+
|
| 336 |
+
:param cls: the argument class to instantiate. This defaults to
|
| 337 |
+
:class:`Argument`.
|
| 338 |
+
:param param_decls: Passed as positional arguments to the constructor of
|
| 339 |
+
``cls``.
|
| 340 |
+
:param attrs: Passed as keyword arguments to the constructor of ``cls``.
|
| 341 |
+
"""
|
| 342 |
+
if cls is None:
|
| 343 |
+
cls = Argument
|
| 344 |
+
|
| 345 |
+
def decorator(f: FC) -> FC:
|
| 346 |
+
_param_memo(f, cls(param_decls, **attrs))
|
| 347 |
+
return f
|
| 348 |
+
|
| 349 |
+
return decorator
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def option(
|
| 353 |
+
*param_decls: str, cls: type[Option] | None = None, **attrs: t.Any
|
| 354 |
+
) -> t.Callable[[FC], FC]:
|
| 355 |
+
"""Attaches an option to the command. All positional arguments are
|
| 356 |
+
passed as parameter declarations to :class:`Option`; all keyword
|
| 357 |
+
arguments are forwarded unchanged (except ``cls``).
|
| 358 |
+
This is equivalent to creating an :class:`Option` instance manually
|
| 359 |
+
and attaching it to the :attr:`Command.params` list.
|
| 360 |
+
|
| 361 |
+
For the default option class, refer to :class:`Option` and
|
| 362 |
+
:class:`Parameter` for descriptions of parameters.
|
| 363 |
+
|
| 364 |
+
:param cls: the option class to instantiate. This defaults to
|
| 365 |
+
:class:`Option`.
|
| 366 |
+
:param param_decls: Passed as positional arguments to the constructor of
|
| 367 |
+
``cls``.
|
| 368 |
+
:param attrs: Passed as keyword arguments to the constructor of ``cls``.
|
| 369 |
+
"""
|
| 370 |
+
if cls is None:
|
| 371 |
+
cls = Option
|
| 372 |
+
|
| 373 |
+
def decorator(f: FC) -> FC:
|
| 374 |
+
_param_memo(f, cls(param_decls, **attrs))
|
| 375 |
+
return f
|
| 376 |
+
|
| 377 |
+
return decorator
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
| 381 |
+
"""Add a ``--yes`` option which shows a prompt before continuing if
|
| 382 |
+
not passed. If the prompt is declined, the program will exit.
|
| 383 |
+
|
| 384 |
+
:param param_decls: One or more option names. Defaults to the single
|
| 385 |
+
value ``"--yes"``.
|
| 386 |
+
:param kwargs: Extra arguments are passed to :func:`option`.
|
| 387 |
+
"""
|
| 388 |
+
|
| 389 |
+
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
| 390 |
+
if not value:
|
| 391 |
+
ctx.abort()
|
| 392 |
+
|
| 393 |
+
if not param_decls:
|
| 394 |
+
param_decls = ("--yes",)
|
| 395 |
+
|
| 396 |
+
kwargs.setdefault("is_flag", True)
|
| 397 |
+
kwargs.setdefault("callback", callback)
|
| 398 |
+
kwargs.setdefault("expose_value", False)
|
| 399 |
+
kwargs.setdefault("prompt", "Do you want to continue?")
|
| 400 |
+
kwargs.setdefault("help", "Confirm the action without prompting.")
|
| 401 |
+
return option(*param_decls, **kwargs)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
| 405 |
+
"""Add a ``--password`` option which prompts for a password, hiding
|
| 406 |
+
input and asking to enter the value again for confirmation.
|
| 407 |
+
|
| 408 |
+
:param param_decls: One or more option names. Defaults to the single
|
| 409 |
+
value ``"--password"``.
|
| 410 |
+
:param kwargs: Extra arguments are passed to :func:`option`.
|
| 411 |
+
"""
|
| 412 |
+
if not param_decls:
|
| 413 |
+
param_decls = ("--password",)
|
| 414 |
+
|
| 415 |
+
kwargs.setdefault("prompt", True)
|
| 416 |
+
kwargs.setdefault("confirmation_prompt", True)
|
| 417 |
+
kwargs.setdefault("hide_input", True)
|
| 418 |
+
return option(*param_decls, **kwargs)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def version_option(
|
| 422 |
+
version: str | None = None,
|
| 423 |
+
*param_decls: str,
|
| 424 |
+
package_name: str | None = None,
|
| 425 |
+
prog_name: str | None = None,
|
| 426 |
+
message: str | None = None,
|
| 427 |
+
**kwargs: t.Any,
|
| 428 |
+
) -> t.Callable[[FC], FC]:
|
| 429 |
+
"""Add a ``--version`` option which immediately prints the version
|
| 430 |
+
number and exits the program.
|
| 431 |
+
|
| 432 |
+
If ``version`` is not provided, Click will try to detect it using
|
| 433 |
+
:func:`importlib.metadata.version` to get the version for the
|
| 434 |
+
``package_name``.
|
| 435 |
+
|
| 436 |
+
If ``package_name`` is not provided, Click will try to detect it by
|
| 437 |
+
inspecting the stack frames. This will be used to detect the
|
| 438 |
+
version, so it must match the name of the installed package.
|
| 439 |
+
|
| 440 |
+
:param version: The version number to show. If not provided, Click
|
| 441 |
+
will try to detect it.
|
| 442 |
+
:param param_decls: One or more option names. Defaults to the single
|
| 443 |
+
value ``"--version"``.
|
| 444 |
+
:param package_name: The package name to detect the version from. If
|
| 445 |
+
not provided, Click will try to detect it.
|
| 446 |
+
:param prog_name: The name of the CLI to show in the message. If not
|
| 447 |
+
provided, it will be detected from the command.
|
| 448 |
+
:param message: The message to show. The values ``%(prog)s``,
|
| 449 |
+
``%(package)s``, and ``%(version)s`` are available. Defaults to
|
| 450 |
+
``"%(prog)s, version %(version)s"``.
|
| 451 |
+
:param kwargs: Extra arguments are passed to :func:`option`.
|
| 452 |
+
:raise RuntimeError: ``version`` could not be detected.
|
| 453 |
+
|
| 454 |
+
.. versionchanged:: 8.0
|
| 455 |
+
Add the ``package_name`` parameter, and the ``%(package)s``
|
| 456 |
+
value for messages.
|
| 457 |
+
|
| 458 |
+
.. versionchanged:: 8.0
|
| 459 |
+
Use :mod:`importlib.metadata` instead of ``pkg_resources``. The
|
| 460 |
+
version is detected based on the package name, not the entry
|
| 461 |
+
point name. The Python package name must match the installed
|
| 462 |
+
package name, or be passed with ``package_name=``.
|
| 463 |
+
"""
|
| 464 |
+
if message is None:
|
| 465 |
+
message = _("%(prog)s, version %(version)s")
|
| 466 |
+
|
| 467 |
+
if version is None and package_name is None:
|
| 468 |
+
frame = inspect.currentframe()
|
| 469 |
+
f_back = frame.f_back if frame is not None else None
|
| 470 |
+
f_globals = f_back.f_globals if f_back is not None else None
|
| 471 |
+
# break reference cycle
|
| 472 |
+
# https://docs.python.org/3/library/inspect.html#the-interpreter-stack
|
| 473 |
+
del frame
|
| 474 |
+
|
| 475 |
+
if f_globals is not None:
|
| 476 |
+
package_name = f_globals.get("__name__")
|
| 477 |
+
|
| 478 |
+
if package_name == "__main__":
|
| 479 |
+
package_name = f_globals.get("__package__")
|
| 480 |
+
|
| 481 |
+
if package_name:
|
| 482 |
+
package_name = package_name.partition(".")[0]
|
| 483 |
+
|
| 484 |
+
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
| 485 |
+
if not value or ctx.resilient_parsing:
|
| 486 |
+
return
|
| 487 |
+
|
| 488 |
+
nonlocal prog_name
|
| 489 |
+
nonlocal version
|
| 490 |
+
|
| 491 |
+
if prog_name is None:
|
| 492 |
+
prog_name = ctx.find_root().info_name
|
| 493 |
+
|
| 494 |
+
if version is None and package_name is not None:
|
| 495 |
+
import importlib.metadata
|
| 496 |
+
|
| 497 |
+
try:
|
| 498 |
+
version = importlib.metadata.version(package_name)
|
| 499 |
+
except importlib.metadata.PackageNotFoundError:
|
| 500 |
+
raise RuntimeError(
|
| 501 |
+
f"{package_name!r} is not installed. Try passing"
|
| 502 |
+
" 'package_name' instead."
|
| 503 |
+
) from None
|
| 504 |
+
|
| 505 |
+
if version is None:
|
| 506 |
+
raise RuntimeError(
|
| 507 |
+
f"Could not determine the version for {package_name!r} automatically."
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
echo(
|
| 511 |
+
message % {"prog": prog_name, "package": package_name, "version": version},
|
| 512 |
+
color=ctx.color,
|
| 513 |
+
)
|
| 514 |
+
ctx.exit()
|
| 515 |
+
|
| 516 |
+
if not param_decls:
|
| 517 |
+
param_decls = ("--version",)
|
| 518 |
+
|
| 519 |
+
kwargs.setdefault("is_flag", True)
|
| 520 |
+
kwargs.setdefault("expose_value", False)
|
| 521 |
+
kwargs.setdefault("is_eager", True)
|
| 522 |
+
kwargs.setdefault("help", _("Show the version and exit."))
|
| 523 |
+
kwargs["callback"] = callback
|
| 524 |
+
return option(*param_decls, **kwargs)
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
|
| 528 |
+
"""Pre-configured ``--help`` option which immediately prints the help page
|
| 529 |
+
and exits the program.
|
| 530 |
+
|
| 531 |
+
:param param_decls: One or more option names. Defaults to the single
|
| 532 |
+
value ``"--help"``.
|
| 533 |
+
:param kwargs: Extra arguments are passed to :func:`option`.
|
| 534 |
+
"""
|
| 535 |
+
|
| 536 |
+
def show_help(ctx: Context, param: Parameter, value: bool) -> None:
|
| 537 |
+
"""Callback that print the help page on ``<stdout>`` and exits."""
|
| 538 |
+
if value and not ctx.resilient_parsing:
|
| 539 |
+
echo(ctx.get_help(), color=ctx.color)
|
| 540 |
+
ctx.exit()
|
| 541 |
+
|
| 542 |
+
if not param_decls:
|
| 543 |
+
param_decls = ("--help",)
|
| 544 |
+
|
| 545 |
+
kwargs.setdefault("is_flag", True)
|
| 546 |
+
kwargs.setdefault("expose_value", False)
|
| 547 |
+
kwargs.setdefault("is_eager", True)
|
| 548 |
+
kwargs.setdefault("help", _("Show this message and exit."))
|
| 549 |
+
kwargs.setdefault("callback", show_help)
|
| 550 |
+
|
| 551 |
+
return option(*param_decls, **kwargs)
|
env/lib/python3.13/site-packages/click/exceptions.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
import typing as t
|
| 5 |
+
from gettext import gettext as _
|
| 6 |
+
from gettext import ngettext
|
| 7 |
+
|
| 8 |
+
from ._compat import get_text_stderr
|
| 9 |
+
from .globals import resolve_color_default
|
| 10 |
+
from .utils import echo
|
| 11 |
+
from .utils import format_filename
|
| 12 |
+
|
| 13 |
+
if t.TYPE_CHECKING:
|
| 14 |
+
from .core import Command
|
| 15 |
+
from .core import Context
|
| 16 |
+
from .core import Parameter
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _join_param_hints(param_hint: cabc.Sequence[str] | str | None) -> str | None:
|
| 20 |
+
if param_hint is not None and not isinstance(param_hint, str):
|
| 21 |
+
return " / ".join(repr(x) for x in param_hint)
|
| 22 |
+
|
| 23 |
+
return param_hint
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ClickException(Exception):
|
| 27 |
+
"""An exception that Click can handle and show to the user."""
|
| 28 |
+
|
| 29 |
+
#: The exit code for this exception.
|
| 30 |
+
exit_code = 1
|
| 31 |
+
|
| 32 |
+
def __init__(self, message: str) -> None:
|
| 33 |
+
super().__init__(message)
|
| 34 |
+
# The context will be removed by the time we print the message, so cache
|
| 35 |
+
# the color settings here to be used later on (in `show`)
|
| 36 |
+
self.show_color: bool | None = resolve_color_default()
|
| 37 |
+
self.message = message
|
| 38 |
+
|
| 39 |
+
def format_message(self) -> str:
|
| 40 |
+
return self.message
|
| 41 |
+
|
| 42 |
+
def __str__(self) -> str:
|
| 43 |
+
return self.message
|
| 44 |
+
|
| 45 |
+
def show(self, file: t.IO[t.Any] | None = None) -> None:
|
| 46 |
+
if file is None:
|
| 47 |
+
file = get_text_stderr()
|
| 48 |
+
|
| 49 |
+
echo(
|
| 50 |
+
_("Error: {message}").format(message=self.format_message()),
|
| 51 |
+
file=file,
|
| 52 |
+
color=self.show_color,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class UsageError(ClickException):
|
| 57 |
+
"""An internal exception that signals a usage error. This typically
|
| 58 |
+
aborts any further handling.
|
| 59 |
+
|
| 60 |
+
:param message: the error message to display.
|
| 61 |
+
:param ctx: optionally the context that caused this error. Click will
|
| 62 |
+
fill in the context automatically in some situations.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
exit_code = 2
|
| 66 |
+
|
| 67 |
+
def __init__(self, message: str, ctx: Context | None = None) -> None:
|
| 68 |
+
super().__init__(message)
|
| 69 |
+
self.ctx = ctx
|
| 70 |
+
self.cmd: Command | None = self.ctx.command if self.ctx else None
|
| 71 |
+
|
| 72 |
+
def show(self, file: t.IO[t.Any] | None = None) -> None:
|
| 73 |
+
if file is None:
|
| 74 |
+
file = get_text_stderr()
|
| 75 |
+
color = None
|
| 76 |
+
hint = ""
|
| 77 |
+
if (
|
| 78 |
+
self.ctx is not None
|
| 79 |
+
and self.ctx.command.get_help_option(self.ctx) is not None
|
| 80 |
+
):
|
| 81 |
+
hint = _("Try '{command} {option}' for help.").format(
|
| 82 |
+
command=self.ctx.command_path, option=self.ctx.help_option_names[0]
|
| 83 |
+
)
|
| 84 |
+
hint = f"{hint}\n"
|
| 85 |
+
if self.ctx is not None:
|
| 86 |
+
color = self.ctx.color
|
| 87 |
+
echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color)
|
| 88 |
+
echo(
|
| 89 |
+
_("Error: {message}").format(message=self.format_message()),
|
| 90 |
+
file=file,
|
| 91 |
+
color=color,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class BadParameter(UsageError):
|
| 96 |
+
"""An exception that formats out a standardized error message for a
|
| 97 |
+
bad parameter. This is useful when thrown from a callback or type as
|
| 98 |
+
Click will attach contextual information to it (for instance, which
|
| 99 |
+
parameter it is).
|
| 100 |
+
|
| 101 |
+
.. versionadded:: 2.0
|
| 102 |
+
|
| 103 |
+
:param param: the parameter object that caused this error. This can
|
| 104 |
+
be left out, and Click will attach this info itself
|
| 105 |
+
if possible.
|
| 106 |
+
:param param_hint: a string that shows up as parameter name. This
|
| 107 |
+
can be used as alternative to `param` in cases
|
| 108 |
+
where custom validation should happen. If it is
|
| 109 |
+
a string it's used as such, if it's a list then
|
| 110 |
+
each item is quoted and separated.
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
def __init__(
|
| 114 |
+
self,
|
| 115 |
+
message: str,
|
| 116 |
+
ctx: Context | None = None,
|
| 117 |
+
param: Parameter | None = None,
|
| 118 |
+
param_hint: cabc.Sequence[str] | str | None = None,
|
| 119 |
+
) -> None:
|
| 120 |
+
super().__init__(message, ctx)
|
| 121 |
+
self.param = param
|
| 122 |
+
self.param_hint = param_hint
|
| 123 |
+
|
| 124 |
+
def format_message(self) -> str:
|
| 125 |
+
if self.param_hint is not None:
|
| 126 |
+
param_hint = self.param_hint
|
| 127 |
+
elif self.param is not None:
|
| 128 |
+
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
|
| 129 |
+
else:
|
| 130 |
+
return _("Invalid value: {message}").format(message=self.message)
|
| 131 |
+
|
| 132 |
+
return _("Invalid value for {param_hint}: {message}").format(
|
| 133 |
+
param_hint=_join_param_hints(param_hint), message=self.message
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
class MissingParameter(BadParameter):
|
| 138 |
+
"""Raised if click required an option or argument but it was not
|
| 139 |
+
provided when invoking the script.
|
| 140 |
+
|
| 141 |
+
.. versionadded:: 4.0
|
| 142 |
+
|
| 143 |
+
:param param_type: a string that indicates the type of the parameter.
|
| 144 |
+
The default is to inherit the parameter type from
|
| 145 |
+
the given `param`. Valid values are ``'parameter'``,
|
| 146 |
+
``'option'`` or ``'argument'``.
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
def __init__(
|
| 150 |
+
self,
|
| 151 |
+
message: str | None = None,
|
| 152 |
+
ctx: Context | None = None,
|
| 153 |
+
param: Parameter | None = None,
|
| 154 |
+
param_hint: cabc.Sequence[str] | str | None = None,
|
| 155 |
+
param_type: str | None = None,
|
| 156 |
+
) -> None:
|
| 157 |
+
super().__init__(message or "", ctx, param, param_hint)
|
| 158 |
+
self.param_type = param_type
|
| 159 |
+
|
| 160 |
+
def format_message(self) -> str:
|
| 161 |
+
if self.param_hint is not None:
|
| 162 |
+
param_hint: cabc.Sequence[str] | str | None = self.param_hint
|
| 163 |
+
elif self.param is not None:
|
| 164 |
+
param_hint = self.param.get_error_hint(self.ctx) # type: ignore
|
| 165 |
+
else:
|
| 166 |
+
param_hint = None
|
| 167 |
+
|
| 168 |
+
param_hint = _join_param_hints(param_hint)
|
| 169 |
+
param_hint = f" {param_hint}" if param_hint else ""
|
| 170 |
+
|
| 171 |
+
param_type = self.param_type
|
| 172 |
+
if param_type is None and self.param is not None:
|
| 173 |
+
param_type = self.param.param_type_name
|
| 174 |
+
|
| 175 |
+
msg = self.message
|
| 176 |
+
if self.param is not None:
|
| 177 |
+
msg_extra = self.param.type.get_missing_message(
|
| 178 |
+
param=self.param, ctx=self.ctx
|
| 179 |
+
)
|
| 180 |
+
if msg_extra:
|
| 181 |
+
if msg:
|
| 182 |
+
msg += f". {msg_extra}"
|
| 183 |
+
else:
|
| 184 |
+
msg = msg_extra
|
| 185 |
+
|
| 186 |
+
msg = f" {msg}" if msg else ""
|
| 187 |
+
|
| 188 |
+
# Translate param_type for known types.
|
| 189 |
+
if param_type == "argument":
|
| 190 |
+
missing = _("Missing argument")
|
| 191 |
+
elif param_type == "option":
|
| 192 |
+
missing = _("Missing option")
|
| 193 |
+
elif param_type == "parameter":
|
| 194 |
+
missing = _("Missing parameter")
|
| 195 |
+
else:
|
| 196 |
+
missing = _("Missing {param_type}").format(param_type=param_type)
|
| 197 |
+
|
| 198 |
+
return f"{missing}{param_hint}.{msg}"
|
| 199 |
+
|
| 200 |
+
def __str__(self) -> str:
|
| 201 |
+
if not self.message:
|
| 202 |
+
param_name = self.param.name if self.param else None
|
| 203 |
+
return _("Missing parameter: {param_name}").format(param_name=param_name)
|
| 204 |
+
else:
|
| 205 |
+
return self.message
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class NoSuchOption(UsageError):
|
| 209 |
+
"""Raised if click attempted to handle an option that does not
|
| 210 |
+
exist.
|
| 211 |
+
|
| 212 |
+
.. versionadded:: 4.0
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
option_name: str,
|
| 218 |
+
message: str | None = None,
|
| 219 |
+
possibilities: cabc.Sequence[str] | None = None,
|
| 220 |
+
ctx: Context | None = None,
|
| 221 |
+
) -> None:
|
| 222 |
+
if message is None:
|
| 223 |
+
message = _("No such option: {name}").format(name=option_name)
|
| 224 |
+
|
| 225 |
+
super().__init__(message, ctx)
|
| 226 |
+
self.option_name = option_name
|
| 227 |
+
self.possibilities = possibilities
|
| 228 |
+
|
| 229 |
+
def format_message(self) -> str:
|
| 230 |
+
if not self.possibilities:
|
| 231 |
+
return self.message
|
| 232 |
+
|
| 233 |
+
possibility_str = ", ".join(sorted(self.possibilities))
|
| 234 |
+
suggest = ngettext(
|
| 235 |
+
"Did you mean {possibility}?",
|
| 236 |
+
"(Possible options: {possibilities})",
|
| 237 |
+
len(self.possibilities),
|
| 238 |
+
).format(possibility=possibility_str, possibilities=possibility_str)
|
| 239 |
+
return f"{self.message} {suggest}"
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class BadOptionUsage(UsageError):
|
| 243 |
+
"""Raised if an option is generally supplied but the use of the option
|
| 244 |
+
was incorrect. This is for instance raised if the number of arguments
|
| 245 |
+
for an option is not correct.
|
| 246 |
+
|
| 247 |
+
.. versionadded:: 4.0
|
| 248 |
+
|
| 249 |
+
:param option_name: the name of the option being used incorrectly.
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
def __init__(
|
| 253 |
+
self, option_name: str, message: str, ctx: Context | None = None
|
| 254 |
+
) -> None:
|
| 255 |
+
super().__init__(message, ctx)
|
| 256 |
+
self.option_name = option_name
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class BadArgumentUsage(UsageError):
|
| 260 |
+
"""Raised if an argument is generally supplied but the use of the argument
|
| 261 |
+
was incorrect. This is for instance raised if the number of values
|
| 262 |
+
for an argument is not correct.
|
| 263 |
+
|
| 264 |
+
.. versionadded:: 6.0
|
| 265 |
+
"""
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class NoArgsIsHelpError(UsageError):
|
| 269 |
+
def __init__(self, ctx: Context) -> None:
|
| 270 |
+
self.ctx: Context
|
| 271 |
+
super().__init__(ctx.get_help(), ctx=ctx)
|
| 272 |
+
|
| 273 |
+
def show(self, file: t.IO[t.Any] | None = None) -> None:
|
| 274 |
+
echo(self.format_message(), file=file, err=True, color=self.ctx.color)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class FileError(ClickException):
|
| 278 |
+
"""Raised if a file cannot be opened."""
|
| 279 |
+
|
| 280 |
+
def __init__(self, filename: str, hint: str | None = None) -> None:
|
| 281 |
+
if hint is None:
|
| 282 |
+
hint = _("unknown error")
|
| 283 |
+
|
| 284 |
+
super().__init__(hint)
|
| 285 |
+
self.ui_filename: str = format_filename(filename)
|
| 286 |
+
self.filename = filename
|
| 287 |
+
|
| 288 |
+
def format_message(self) -> str:
|
| 289 |
+
return _("Could not open file {filename!r}: {message}").format(
|
| 290 |
+
filename=self.ui_filename, message=self.message
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class Abort(RuntimeError):
|
| 295 |
+
"""An internal signalling exception that signals Click to abort."""
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
class Exit(RuntimeError):
|
| 299 |
+
"""An exception that indicates that the application should exit with some
|
| 300 |
+
status code.
|
| 301 |
+
|
| 302 |
+
:param code: the status code to exit with.
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
__slots__ = ("exit_code",)
|
| 306 |
+
|
| 307 |
+
def __init__(self, code: int = 0) -> None:
|
| 308 |
+
self.exit_code: int = code
|
env/lib/python3.13/site-packages/click/formatting.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
from contextlib import contextmanager
|
| 5 |
+
from gettext import gettext as _
|
| 6 |
+
|
| 7 |
+
from ._compat import term_len
|
| 8 |
+
from .parser import _split_opt
|
| 9 |
+
|
| 10 |
+
# Can force a width. This is used by the test system
|
| 11 |
+
FORCED_WIDTH: int | None = None
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def measure_table(rows: cabc.Iterable[tuple[str, str]]) -> tuple[int, ...]:
|
| 15 |
+
widths: dict[int, int] = {}
|
| 16 |
+
|
| 17 |
+
for row in rows:
|
| 18 |
+
for idx, col in enumerate(row):
|
| 19 |
+
widths[idx] = max(widths.get(idx, 0), term_len(col))
|
| 20 |
+
|
| 21 |
+
return tuple(y for x, y in sorted(widths.items()))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def iter_rows(
|
| 25 |
+
rows: cabc.Iterable[tuple[str, str]], col_count: int
|
| 26 |
+
) -> cabc.Iterator[tuple[str, ...]]:
|
| 27 |
+
for row in rows:
|
| 28 |
+
yield row + ("",) * (col_count - len(row))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def wrap_text(
|
| 32 |
+
text: str,
|
| 33 |
+
width: int = 78,
|
| 34 |
+
initial_indent: str = "",
|
| 35 |
+
subsequent_indent: str = "",
|
| 36 |
+
preserve_paragraphs: bool = False,
|
| 37 |
+
) -> str:
|
| 38 |
+
"""A helper function that intelligently wraps text. By default, it
|
| 39 |
+
assumes that it operates on a single paragraph of text but if the
|
| 40 |
+
`preserve_paragraphs` parameter is provided it will intelligently
|
| 41 |
+
handle paragraphs (defined by two empty lines).
|
| 42 |
+
|
| 43 |
+
If paragraphs are handled, a paragraph can be prefixed with an empty
|
| 44 |
+
line containing the ``\\b`` character (``\\x08``) to indicate that
|
| 45 |
+
no rewrapping should happen in that block.
|
| 46 |
+
|
| 47 |
+
:param text: the text that should be rewrapped.
|
| 48 |
+
:param width: the maximum width for the text.
|
| 49 |
+
:param initial_indent: the initial indent that should be placed on the
|
| 50 |
+
first line as a string.
|
| 51 |
+
:param subsequent_indent: the indent string that should be placed on
|
| 52 |
+
each consecutive line.
|
| 53 |
+
:param preserve_paragraphs: if this flag is set then the wrapping will
|
| 54 |
+
intelligently handle paragraphs.
|
| 55 |
+
"""
|
| 56 |
+
from ._textwrap import TextWrapper
|
| 57 |
+
|
| 58 |
+
text = text.expandtabs()
|
| 59 |
+
wrapper = TextWrapper(
|
| 60 |
+
width,
|
| 61 |
+
initial_indent=initial_indent,
|
| 62 |
+
subsequent_indent=subsequent_indent,
|
| 63 |
+
replace_whitespace=False,
|
| 64 |
+
)
|
| 65 |
+
if not preserve_paragraphs:
|
| 66 |
+
return wrapper.fill(text)
|
| 67 |
+
|
| 68 |
+
p: list[tuple[int, bool, str]] = []
|
| 69 |
+
buf: list[str] = []
|
| 70 |
+
indent = None
|
| 71 |
+
|
| 72 |
+
def _flush_par() -> None:
|
| 73 |
+
if not buf:
|
| 74 |
+
return
|
| 75 |
+
if buf[0].strip() == "\b":
|
| 76 |
+
p.append((indent or 0, True, "\n".join(buf[1:])))
|
| 77 |
+
else:
|
| 78 |
+
p.append((indent or 0, False, " ".join(buf)))
|
| 79 |
+
del buf[:]
|
| 80 |
+
|
| 81 |
+
for line in text.splitlines():
|
| 82 |
+
if not line:
|
| 83 |
+
_flush_par()
|
| 84 |
+
indent = None
|
| 85 |
+
else:
|
| 86 |
+
if indent is None:
|
| 87 |
+
orig_len = term_len(line)
|
| 88 |
+
line = line.lstrip()
|
| 89 |
+
indent = orig_len - term_len(line)
|
| 90 |
+
buf.append(line)
|
| 91 |
+
_flush_par()
|
| 92 |
+
|
| 93 |
+
rv = []
|
| 94 |
+
for indent, raw, text in p:
|
| 95 |
+
with wrapper.extra_indent(" " * indent):
|
| 96 |
+
if raw:
|
| 97 |
+
rv.append(wrapper.indent_only(text))
|
| 98 |
+
else:
|
| 99 |
+
rv.append(wrapper.fill(text))
|
| 100 |
+
|
| 101 |
+
return "\n\n".join(rv)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class HelpFormatter:
|
| 105 |
+
"""This class helps with formatting text-based help pages. It's
|
| 106 |
+
usually just needed for very special internal cases, but it's also
|
| 107 |
+
exposed so that developers can write their own fancy outputs.
|
| 108 |
+
|
| 109 |
+
At present, it always writes into memory.
|
| 110 |
+
|
| 111 |
+
:param indent_increment: the additional increment for each level.
|
| 112 |
+
:param width: the width for the text. This defaults to the terminal
|
| 113 |
+
width clamped to a maximum of 78.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
indent_increment: int = 2,
|
| 119 |
+
width: int | None = None,
|
| 120 |
+
max_width: int | None = None,
|
| 121 |
+
) -> None:
|
| 122 |
+
self.indent_increment = indent_increment
|
| 123 |
+
if max_width is None:
|
| 124 |
+
max_width = 80
|
| 125 |
+
if width is None:
|
| 126 |
+
import shutil
|
| 127 |
+
|
| 128 |
+
width = FORCED_WIDTH
|
| 129 |
+
if width is None:
|
| 130 |
+
width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50)
|
| 131 |
+
self.width = width
|
| 132 |
+
self.current_indent: int = 0
|
| 133 |
+
self.buffer: list[str] = []
|
| 134 |
+
|
| 135 |
+
def write(self, string: str) -> None:
|
| 136 |
+
"""Writes a unicode string into the internal buffer."""
|
| 137 |
+
self.buffer.append(string)
|
| 138 |
+
|
| 139 |
+
def indent(self) -> None:
|
| 140 |
+
"""Increases the indentation."""
|
| 141 |
+
self.current_indent += self.indent_increment
|
| 142 |
+
|
| 143 |
+
def dedent(self) -> None:
|
| 144 |
+
"""Decreases the indentation."""
|
| 145 |
+
self.current_indent -= self.indent_increment
|
| 146 |
+
|
| 147 |
+
def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None:
|
| 148 |
+
"""Writes a usage line into the buffer.
|
| 149 |
+
|
| 150 |
+
:param prog: the program name.
|
| 151 |
+
:param args: whitespace separated list of arguments.
|
| 152 |
+
:param prefix: The prefix for the first line. Defaults to
|
| 153 |
+
``"Usage: "``.
|
| 154 |
+
"""
|
| 155 |
+
if prefix is None:
|
| 156 |
+
prefix = f"{_('Usage:')} "
|
| 157 |
+
|
| 158 |
+
usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
|
| 159 |
+
text_width = self.width - self.current_indent
|
| 160 |
+
|
| 161 |
+
if text_width >= (term_len(usage_prefix) + 20):
|
| 162 |
+
# The arguments will fit to the right of the prefix.
|
| 163 |
+
indent = " " * term_len(usage_prefix)
|
| 164 |
+
self.write(
|
| 165 |
+
wrap_text(
|
| 166 |
+
args,
|
| 167 |
+
text_width,
|
| 168 |
+
initial_indent=usage_prefix,
|
| 169 |
+
subsequent_indent=indent,
|
| 170 |
+
)
|
| 171 |
+
)
|
| 172 |
+
else:
|
| 173 |
+
# The prefix is too long, put the arguments on the next line.
|
| 174 |
+
self.write(usage_prefix)
|
| 175 |
+
self.write("\n")
|
| 176 |
+
indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
|
| 177 |
+
self.write(
|
| 178 |
+
wrap_text(
|
| 179 |
+
args, text_width, initial_indent=indent, subsequent_indent=indent
|
| 180 |
+
)
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
self.write("\n")
|
| 184 |
+
|
| 185 |
+
def write_heading(self, heading: str) -> None:
|
| 186 |
+
"""Writes a heading into the buffer."""
|
| 187 |
+
self.write(f"{'':>{self.current_indent}}{heading}:\n")
|
| 188 |
+
|
| 189 |
+
def write_paragraph(self) -> None:
|
| 190 |
+
"""Writes a paragraph into the buffer."""
|
| 191 |
+
if self.buffer:
|
| 192 |
+
self.write("\n")
|
| 193 |
+
|
| 194 |
+
def write_text(self, text: str) -> None:
|
| 195 |
+
"""Writes re-indented text into the buffer. This rewraps and
|
| 196 |
+
preserves paragraphs.
|
| 197 |
+
"""
|
| 198 |
+
indent = " " * self.current_indent
|
| 199 |
+
self.write(
|
| 200 |
+
wrap_text(
|
| 201 |
+
text,
|
| 202 |
+
self.width,
|
| 203 |
+
initial_indent=indent,
|
| 204 |
+
subsequent_indent=indent,
|
| 205 |
+
preserve_paragraphs=True,
|
| 206 |
+
)
|
| 207 |
+
)
|
| 208 |
+
self.write("\n")
|
| 209 |
+
|
| 210 |
+
def write_dl(
|
| 211 |
+
self,
|
| 212 |
+
rows: cabc.Sequence[tuple[str, str]],
|
| 213 |
+
col_max: int = 30,
|
| 214 |
+
col_spacing: int = 2,
|
| 215 |
+
) -> None:
|
| 216 |
+
"""Writes a definition list into the buffer. This is how options
|
| 217 |
+
and commands are usually formatted.
|
| 218 |
+
|
| 219 |
+
:param rows: a list of two item tuples for the terms and values.
|
| 220 |
+
:param col_max: the maximum width of the first column.
|
| 221 |
+
:param col_spacing: the number of spaces between the first and
|
| 222 |
+
second column.
|
| 223 |
+
"""
|
| 224 |
+
rows = list(rows)
|
| 225 |
+
widths = measure_table(rows)
|
| 226 |
+
if len(widths) != 2:
|
| 227 |
+
raise TypeError("Expected two columns for definition list")
|
| 228 |
+
|
| 229 |
+
first_col = min(widths[0], col_max) + col_spacing
|
| 230 |
+
|
| 231 |
+
for first, second in iter_rows(rows, len(widths)):
|
| 232 |
+
self.write(f"{'':>{self.current_indent}}{first}")
|
| 233 |
+
if not second:
|
| 234 |
+
self.write("\n")
|
| 235 |
+
continue
|
| 236 |
+
if term_len(first) <= first_col - col_spacing:
|
| 237 |
+
self.write(" " * (first_col - term_len(first)))
|
| 238 |
+
else:
|
| 239 |
+
self.write("\n")
|
| 240 |
+
self.write(" " * (first_col + self.current_indent))
|
| 241 |
+
|
| 242 |
+
text_width = max(self.width - first_col - 2, 10)
|
| 243 |
+
wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
|
| 244 |
+
lines = wrapped_text.splitlines()
|
| 245 |
+
|
| 246 |
+
if lines:
|
| 247 |
+
self.write(f"{lines[0]}\n")
|
| 248 |
+
|
| 249 |
+
for line in lines[1:]:
|
| 250 |
+
self.write(f"{'':>{first_col + self.current_indent}}{line}\n")
|
| 251 |
+
else:
|
| 252 |
+
self.write("\n")
|
| 253 |
+
|
| 254 |
+
@contextmanager
|
| 255 |
+
def section(self, name: str) -> cabc.Iterator[None]:
|
| 256 |
+
"""Helpful context manager that writes a paragraph, a heading,
|
| 257 |
+
and the indents.
|
| 258 |
+
|
| 259 |
+
:param name: the section name that is written as heading.
|
| 260 |
+
"""
|
| 261 |
+
self.write_paragraph()
|
| 262 |
+
self.write_heading(name)
|
| 263 |
+
self.indent()
|
| 264 |
+
try:
|
| 265 |
+
yield
|
| 266 |
+
finally:
|
| 267 |
+
self.dedent()
|
| 268 |
+
|
| 269 |
+
@contextmanager
|
| 270 |
+
def indentation(self) -> cabc.Iterator[None]:
|
| 271 |
+
"""A context manager that increases the indentation."""
|
| 272 |
+
self.indent()
|
| 273 |
+
try:
|
| 274 |
+
yield
|
| 275 |
+
finally:
|
| 276 |
+
self.dedent()
|
| 277 |
+
|
| 278 |
+
def getvalue(self) -> str:
|
| 279 |
+
"""Returns the buffer contents."""
|
| 280 |
+
return "".join(self.buffer)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def join_options(options: cabc.Sequence[str]) -> tuple[str, bool]:
|
| 284 |
+
"""Given a list of option strings this joins them in the most appropriate
|
| 285 |
+
way and returns them in the form ``(formatted_string,
|
| 286 |
+
any_prefix_is_slash)`` where the second item in the tuple is a flag that
|
| 287 |
+
indicates if any of the option prefixes was a slash.
|
| 288 |
+
"""
|
| 289 |
+
rv = []
|
| 290 |
+
any_prefix_is_slash = False
|
| 291 |
+
|
| 292 |
+
for opt in options:
|
| 293 |
+
prefix = _split_opt(opt)[0]
|
| 294 |
+
|
| 295 |
+
if prefix == "/":
|
| 296 |
+
any_prefix_is_slash = True
|
| 297 |
+
|
| 298 |
+
rv.append((len(prefix), opt))
|
| 299 |
+
|
| 300 |
+
rv.sort(key=lambda x: x[0])
|
| 301 |
+
return ", ".join(x[1] for x in rv), any_prefix_is_slash
|
env/lib/python3.13/site-packages/click/globals.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing as t
|
| 4 |
+
from threading import local
|
| 5 |
+
|
| 6 |
+
if t.TYPE_CHECKING:
|
| 7 |
+
from .core import Context
|
| 8 |
+
|
| 9 |
+
_local = local()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@t.overload
|
| 13 |
+
def get_current_context(silent: t.Literal[False] = False) -> Context: ...
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@t.overload
|
| 17 |
+
def get_current_context(silent: bool = ...) -> Context | None: ...
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_current_context(silent: bool = False) -> Context | None:
|
| 21 |
+
"""Returns the current click context. This can be used as a way to
|
| 22 |
+
access the current context object from anywhere. This is a more implicit
|
| 23 |
+
alternative to the :func:`pass_context` decorator. This function is
|
| 24 |
+
primarily useful for helpers such as :func:`echo` which might be
|
| 25 |
+
interested in changing its behavior based on the current context.
|
| 26 |
+
|
| 27 |
+
To push the current context, :meth:`Context.scope` can be used.
|
| 28 |
+
|
| 29 |
+
.. versionadded:: 5.0
|
| 30 |
+
|
| 31 |
+
:param silent: if set to `True` the return value is `None` if no context
|
| 32 |
+
is available. The default behavior is to raise a
|
| 33 |
+
:exc:`RuntimeError`.
|
| 34 |
+
"""
|
| 35 |
+
try:
|
| 36 |
+
return t.cast("Context", _local.stack[-1])
|
| 37 |
+
except (AttributeError, IndexError) as e:
|
| 38 |
+
if not silent:
|
| 39 |
+
raise RuntimeError("There is no active click context.") from e
|
| 40 |
+
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def push_context(ctx: Context) -> None:
|
| 45 |
+
"""Pushes a new context to the current stack."""
|
| 46 |
+
_local.__dict__.setdefault("stack", []).append(ctx)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def pop_context() -> None:
|
| 50 |
+
"""Removes the top level from the stack."""
|
| 51 |
+
_local.stack.pop()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def resolve_color_default(color: bool | None = None) -> bool | None:
|
| 55 |
+
"""Internal helper to get the default value of the color flag. If a
|
| 56 |
+
value is passed it's returned unchanged, otherwise it's looked up from
|
| 57 |
+
the current context.
|
| 58 |
+
"""
|
| 59 |
+
if color is not None:
|
| 60 |
+
return color
|
| 61 |
+
|
| 62 |
+
ctx = get_current_context(silent=True)
|
| 63 |
+
|
| 64 |
+
if ctx is not None:
|
| 65 |
+
return ctx.color
|
| 66 |
+
|
| 67 |
+
return None
|
env/lib/python3.13/site-packages/click/parser.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This module started out as largely a copy paste from the stdlib's
|
| 3 |
+
optparse module with the features removed that we do not need from
|
| 4 |
+
optparse because we implement them in Click on a higher level (for
|
| 5 |
+
instance type handling, help formatting and a lot more).
|
| 6 |
+
|
| 7 |
+
The plan is to remove more and more from here over time.
|
| 8 |
+
|
| 9 |
+
The reason this is a different module and not optparse from the stdlib
|
| 10 |
+
is that there are differences in 2.x and 3.x about the error messages
|
| 11 |
+
generated and optparse in the stdlib uses gettext for no good reason
|
| 12 |
+
and might cause us issues.
|
| 13 |
+
|
| 14 |
+
Click uses parts of optparse written by Gregory P. Ward and maintained
|
| 15 |
+
by the Python Software Foundation. This is limited to code in parser.py.
|
| 16 |
+
|
| 17 |
+
Copyright 2001-2006 Gregory P. Ward. All rights reserved.
|
| 18 |
+
Copyright 2002-2006 Python Software Foundation. All rights reserved.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# This code uses parts of optparse written by Gregory P. Ward and
|
| 22 |
+
# maintained by the Python Software Foundation.
|
| 23 |
+
# Copyright 2001-2006 Gregory P. Ward
|
| 24 |
+
# Copyright 2002-2006 Python Software Foundation
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import collections.abc as cabc
|
| 28 |
+
import typing as t
|
| 29 |
+
from collections import deque
|
| 30 |
+
from gettext import gettext as _
|
| 31 |
+
from gettext import ngettext
|
| 32 |
+
|
| 33 |
+
from ._utils import FLAG_NEEDS_VALUE
|
| 34 |
+
from ._utils import UNSET
|
| 35 |
+
from .exceptions import BadArgumentUsage
|
| 36 |
+
from .exceptions import BadOptionUsage
|
| 37 |
+
from .exceptions import NoSuchOption
|
| 38 |
+
from .exceptions import UsageError
|
| 39 |
+
|
| 40 |
+
if t.TYPE_CHECKING:
|
| 41 |
+
from ._utils import T_FLAG_NEEDS_VALUE
|
| 42 |
+
from ._utils import T_UNSET
|
| 43 |
+
from .core import Argument as CoreArgument
|
| 44 |
+
from .core import Context
|
| 45 |
+
from .core import Option as CoreOption
|
| 46 |
+
from .core import Parameter as CoreParameter
|
| 47 |
+
|
| 48 |
+
V = t.TypeVar("V")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _unpack_args(
|
| 52 |
+
args: cabc.Sequence[str], nargs_spec: cabc.Sequence[int]
|
| 53 |
+
) -> tuple[cabc.Sequence[str | cabc.Sequence[str | None] | None], list[str]]:
|
| 54 |
+
"""Given an iterable of arguments and an iterable of nargs specifications,
|
| 55 |
+
it returns a tuple with all the unpacked arguments at the first index
|
| 56 |
+
and all remaining arguments as the second.
|
| 57 |
+
|
| 58 |
+
The nargs specification is the number of arguments that should be consumed
|
| 59 |
+
or `-1` to indicate that this position should eat up all the remainders.
|
| 60 |
+
|
| 61 |
+
Missing items are filled with ``UNSET``.
|
| 62 |
+
"""
|
| 63 |
+
args = deque(args)
|
| 64 |
+
nargs_spec = deque(nargs_spec)
|
| 65 |
+
rv: list[str | tuple[str | T_UNSET, ...] | T_UNSET] = []
|
| 66 |
+
spos: int | None = None
|
| 67 |
+
|
| 68 |
+
def _fetch(c: deque[V]) -> V | T_UNSET:
|
| 69 |
+
try:
|
| 70 |
+
if spos is None:
|
| 71 |
+
return c.popleft()
|
| 72 |
+
else:
|
| 73 |
+
return c.pop()
|
| 74 |
+
except IndexError:
|
| 75 |
+
return UNSET
|
| 76 |
+
|
| 77 |
+
while nargs_spec:
|
| 78 |
+
nargs = _fetch(nargs_spec)
|
| 79 |
+
|
| 80 |
+
if nargs is None:
|
| 81 |
+
continue
|
| 82 |
+
|
| 83 |
+
if nargs == 1:
|
| 84 |
+
rv.append(_fetch(args)) # type: ignore[arg-type]
|
| 85 |
+
elif nargs > 1:
|
| 86 |
+
x = [_fetch(args) for _ in range(nargs)]
|
| 87 |
+
|
| 88 |
+
# If we're reversed, we're pulling in the arguments in reverse,
|
| 89 |
+
# so we need to turn them around.
|
| 90 |
+
if spos is not None:
|
| 91 |
+
x.reverse()
|
| 92 |
+
|
| 93 |
+
rv.append(tuple(x))
|
| 94 |
+
elif nargs < 0:
|
| 95 |
+
if spos is not None:
|
| 96 |
+
raise TypeError("Cannot have two nargs < 0")
|
| 97 |
+
|
| 98 |
+
spos = len(rv)
|
| 99 |
+
rv.append(UNSET)
|
| 100 |
+
|
| 101 |
+
# spos is the position of the wildcard (star). If it's not `None`,
|
| 102 |
+
# we fill it with the remainder.
|
| 103 |
+
if spos is not None:
|
| 104 |
+
rv[spos] = tuple(args)
|
| 105 |
+
args = []
|
| 106 |
+
rv[spos + 1 :] = reversed(rv[spos + 1 :])
|
| 107 |
+
|
| 108 |
+
return tuple(rv), list(args)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _split_opt(opt: str) -> tuple[str, str]:
|
| 112 |
+
first = opt[:1]
|
| 113 |
+
if first.isalnum():
|
| 114 |
+
return "", opt
|
| 115 |
+
if opt[1:2] == first:
|
| 116 |
+
return opt[:2], opt[2:]
|
| 117 |
+
return first, opt[1:]
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _normalize_opt(opt: str, ctx: Context | None) -> str:
|
| 121 |
+
if ctx is None or ctx.token_normalize_func is None:
|
| 122 |
+
return opt
|
| 123 |
+
prefix, opt = _split_opt(opt)
|
| 124 |
+
return f"{prefix}{ctx.token_normalize_func(opt)}"
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class _Option:
|
| 128 |
+
def __init__(
|
| 129 |
+
self,
|
| 130 |
+
obj: CoreOption,
|
| 131 |
+
opts: cabc.Sequence[str],
|
| 132 |
+
dest: str | None,
|
| 133 |
+
action: str | None = None,
|
| 134 |
+
nargs: int = 1,
|
| 135 |
+
const: t.Any | None = None,
|
| 136 |
+
):
|
| 137 |
+
self._short_opts = []
|
| 138 |
+
self._long_opts = []
|
| 139 |
+
self.prefixes: set[str] = set()
|
| 140 |
+
|
| 141 |
+
for opt in opts:
|
| 142 |
+
prefix, value = _split_opt(opt)
|
| 143 |
+
if not prefix:
|
| 144 |
+
raise ValueError(f"Invalid start character for option ({opt})")
|
| 145 |
+
self.prefixes.add(prefix[0])
|
| 146 |
+
if len(prefix) == 1 and len(value) == 1:
|
| 147 |
+
self._short_opts.append(opt)
|
| 148 |
+
else:
|
| 149 |
+
self._long_opts.append(opt)
|
| 150 |
+
self.prefixes.add(prefix)
|
| 151 |
+
|
| 152 |
+
if action is None:
|
| 153 |
+
action = "store"
|
| 154 |
+
|
| 155 |
+
self.dest = dest
|
| 156 |
+
self.action = action
|
| 157 |
+
self.nargs = nargs
|
| 158 |
+
self.const = const
|
| 159 |
+
self.obj = obj
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def takes_value(self) -> bool:
|
| 163 |
+
return self.action in ("store", "append")
|
| 164 |
+
|
| 165 |
+
def process(self, value: t.Any, state: _ParsingState) -> None:
|
| 166 |
+
if self.action == "store":
|
| 167 |
+
state.opts[self.dest] = value # type: ignore
|
| 168 |
+
elif self.action == "store_const":
|
| 169 |
+
state.opts[self.dest] = self.const # type: ignore
|
| 170 |
+
elif self.action == "append":
|
| 171 |
+
state.opts.setdefault(self.dest, []).append(value) # type: ignore
|
| 172 |
+
elif self.action == "append_const":
|
| 173 |
+
state.opts.setdefault(self.dest, []).append(self.const) # type: ignore
|
| 174 |
+
elif self.action == "count":
|
| 175 |
+
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore
|
| 176 |
+
else:
|
| 177 |
+
raise ValueError(f"unknown action '{self.action}'")
|
| 178 |
+
state.order.append(self.obj)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class _Argument:
|
| 182 |
+
def __init__(self, obj: CoreArgument, dest: str | None, nargs: int = 1):
|
| 183 |
+
self.dest = dest
|
| 184 |
+
self.nargs = nargs
|
| 185 |
+
self.obj = obj
|
| 186 |
+
|
| 187 |
+
def process(
|
| 188 |
+
self,
|
| 189 |
+
value: str | cabc.Sequence[str | None] | None | T_UNSET,
|
| 190 |
+
state: _ParsingState,
|
| 191 |
+
) -> None:
|
| 192 |
+
if self.nargs > 1:
|
| 193 |
+
assert isinstance(value, cabc.Sequence)
|
| 194 |
+
holes = sum(1 for x in value if x is UNSET)
|
| 195 |
+
if holes == len(value):
|
| 196 |
+
value = UNSET
|
| 197 |
+
elif holes != 0:
|
| 198 |
+
raise BadArgumentUsage(
|
| 199 |
+
_("Argument {name!r} takes {nargs} values.").format(
|
| 200 |
+
name=self.dest, nargs=self.nargs
|
| 201 |
+
)
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
# We failed to collect any argument value so we consider the argument as unset.
|
| 205 |
+
if value == ():
|
| 206 |
+
value = UNSET
|
| 207 |
+
|
| 208 |
+
state.opts[self.dest] = value # type: ignore
|
| 209 |
+
state.order.append(self.obj)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class _ParsingState:
|
| 213 |
+
def __init__(self, rargs: list[str]) -> None:
|
| 214 |
+
self.opts: dict[str, t.Any] = {}
|
| 215 |
+
self.largs: list[str] = []
|
| 216 |
+
self.rargs = rargs
|
| 217 |
+
self.order: list[CoreParameter] = []
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class _OptionParser:
|
| 221 |
+
"""The option parser is an internal class that is ultimately used to
|
| 222 |
+
parse options and arguments. It's modelled after optparse and brings
|
| 223 |
+
a similar but vastly simplified API. It should generally not be used
|
| 224 |
+
directly as the high level Click classes wrap it for you.
|
| 225 |
+
|
| 226 |
+
It's not nearly as extensible as optparse or argparse as it does not
|
| 227 |
+
implement features that are implemented on a higher level (such as
|
| 228 |
+
types or defaults).
|
| 229 |
+
|
| 230 |
+
:param ctx: optionally the :class:`~click.Context` where this parser
|
| 231 |
+
should go with.
|
| 232 |
+
|
| 233 |
+
.. deprecated:: 8.2
|
| 234 |
+
Will be removed in Click 9.0.
|
| 235 |
+
"""
|
| 236 |
+
|
| 237 |
+
def __init__(self, ctx: Context | None = None) -> None:
|
| 238 |
+
#: The :class:`~click.Context` for this parser. This might be
|
| 239 |
+
#: `None` for some advanced use cases.
|
| 240 |
+
self.ctx = ctx
|
| 241 |
+
#: This controls how the parser deals with interspersed arguments.
|
| 242 |
+
#: If this is set to `False`, the parser will stop on the first
|
| 243 |
+
#: non-option. Click uses this to implement nested subcommands
|
| 244 |
+
#: safely.
|
| 245 |
+
self.allow_interspersed_args: bool = True
|
| 246 |
+
#: This tells the parser how to deal with unknown options. By
|
| 247 |
+
#: default it will error out (which is sensible), but there is a
|
| 248 |
+
#: second mode where it will ignore it and continue processing
|
| 249 |
+
#: after shifting all the unknown options into the resulting args.
|
| 250 |
+
self.ignore_unknown_options: bool = False
|
| 251 |
+
|
| 252 |
+
if ctx is not None:
|
| 253 |
+
self.allow_interspersed_args = ctx.allow_interspersed_args
|
| 254 |
+
self.ignore_unknown_options = ctx.ignore_unknown_options
|
| 255 |
+
|
| 256 |
+
self._short_opt: dict[str, _Option] = {}
|
| 257 |
+
self._long_opt: dict[str, _Option] = {}
|
| 258 |
+
self._opt_prefixes = {"-", "--"}
|
| 259 |
+
self._args: list[_Argument] = []
|
| 260 |
+
|
| 261 |
+
def add_option(
|
| 262 |
+
self,
|
| 263 |
+
obj: CoreOption,
|
| 264 |
+
opts: cabc.Sequence[str],
|
| 265 |
+
dest: str | None,
|
| 266 |
+
action: str | None = None,
|
| 267 |
+
nargs: int = 1,
|
| 268 |
+
const: t.Any | None = None,
|
| 269 |
+
) -> None:
|
| 270 |
+
"""Adds a new option named `dest` to the parser. The destination
|
| 271 |
+
is not inferred (unlike with optparse) and needs to be explicitly
|
| 272 |
+
provided. Action can be any of ``store``, ``store_const``,
|
| 273 |
+
``append``, ``append_const`` or ``count``.
|
| 274 |
+
|
| 275 |
+
The `obj` can be used to identify the option in the order list
|
| 276 |
+
that is returned from the parser.
|
| 277 |
+
"""
|
| 278 |
+
opts = [_normalize_opt(opt, self.ctx) for opt in opts]
|
| 279 |
+
option = _Option(obj, opts, dest, action=action, nargs=nargs, const=const)
|
| 280 |
+
self._opt_prefixes.update(option.prefixes)
|
| 281 |
+
for opt in option._short_opts:
|
| 282 |
+
self._short_opt[opt] = option
|
| 283 |
+
for opt in option._long_opts:
|
| 284 |
+
self._long_opt[opt] = option
|
| 285 |
+
|
| 286 |
+
def add_argument(self, obj: CoreArgument, dest: str | None, nargs: int = 1) -> None:
|
| 287 |
+
"""Adds a positional argument named `dest` to the parser.
|
| 288 |
+
|
| 289 |
+
The `obj` can be used to identify the option in the order list
|
| 290 |
+
that is returned from the parser.
|
| 291 |
+
"""
|
| 292 |
+
self._args.append(_Argument(obj, dest=dest, nargs=nargs))
|
| 293 |
+
|
| 294 |
+
def parse_args(
|
| 295 |
+
self, args: list[str]
|
| 296 |
+
) -> tuple[dict[str, t.Any], list[str], list[CoreParameter]]:
|
| 297 |
+
"""Parses positional arguments and returns ``(values, args, order)``
|
| 298 |
+
for the parsed options and arguments as well as the leftover
|
| 299 |
+
arguments if there are any. The order is a list of objects as they
|
| 300 |
+
appear on the command line. If arguments appear multiple times they
|
| 301 |
+
will be memorized multiple times as well.
|
| 302 |
+
"""
|
| 303 |
+
state = _ParsingState(args)
|
| 304 |
+
try:
|
| 305 |
+
self._process_args_for_options(state)
|
| 306 |
+
self._process_args_for_args(state)
|
| 307 |
+
except UsageError:
|
| 308 |
+
if self.ctx is None or not self.ctx.resilient_parsing:
|
| 309 |
+
raise
|
| 310 |
+
return state.opts, state.largs, state.order
|
| 311 |
+
|
| 312 |
+
def _process_args_for_args(self, state: _ParsingState) -> None:
|
| 313 |
+
pargs, args = _unpack_args(
|
| 314 |
+
state.largs + state.rargs, [x.nargs for x in self._args]
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
for idx, arg in enumerate(self._args):
|
| 318 |
+
arg.process(pargs[idx], state)
|
| 319 |
+
|
| 320 |
+
state.largs = args
|
| 321 |
+
state.rargs = []
|
| 322 |
+
|
| 323 |
+
def _process_args_for_options(self, state: _ParsingState) -> None:
|
| 324 |
+
while state.rargs:
|
| 325 |
+
arg = state.rargs.pop(0)
|
| 326 |
+
arglen = len(arg)
|
| 327 |
+
# Double dashes always handled explicitly regardless of what
|
| 328 |
+
# prefixes are valid.
|
| 329 |
+
if arg == "--":
|
| 330 |
+
return
|
| 331 |
+
elif arg[:1] in self._opt_prefixes and arglen > 1:
|
| 332 |
+
self._process_opts(arg, state)
|
| 333 |
+
elif self.allow_interspersed_args:
|
| 334 |
+
state.largs.append(arg)
|
| 335 |
+
else:
|
| 336 |
+
state.rargs.insert(0, arg)
|
| 337 |
+
return
|
| 338 |
+
|
| 339 |
+
# Say this is the original argument list:
|
| 340 |
+
# [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
|
| 341 |
+
# ^
|
| 342 |
+
# (we are about to process arg(i)).
|
| 343 |
+
#
|
| 344 |
+
# Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
|
| 345 |
+
# [arg0, ..., arg(i-1)] (any options and their arguments will have
|
| 346 |
+
# been removed from largs).
|
| 347 |
+
#
|
| 348 |
+
# The while loop will usually consume 1 or more arguments per pass.
|
| 349 |
+
# If it consumes 1 (eg. arg is an option that takes no arguments),
|
| 350 |
+
# then after _process_arg() is done the situation is:
|
| 351 |
+
#
|
| 352 |
+
# largs = subset of [arg0, ..., arg(i)]
|
| 353 |
+
# rargs = [arg(i+1), ..., arg(N-1)]
|
| 354 |
+
#
|
| 355 |
+
# If allow_interspersed_args is false, largs will always be
|
| 356 |
+
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
| 357 |
+
# not a very interesting subset!
|
| 358 |
+
|
| 359 |
+
def _match_long_opt(
|
| 360 |
+
self, opt: str, explicit_value: str | None, state: _ParsingState
|
| 361 |
+
) -> None:
|
| 362 |
+
if opt not in self._long_opt:
|
| 363 |
+
from difflib import get_close_matches
|
| 364 |
+
|
| 365 |
+
possibilities = get_close_matches(opt, self._long_opt)
|
| 366 |
+
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
|
| 367 |
+
|
| 368 |
+
option = self._long_opt[opt]
|
| 369 |
+
if option.takes_value:
|
| 370 |
+
# At this point it's safe to modify rargs by injecting the
|
| 371 |
+
# explicit value, because no exception is raised in this
|
| 372 |
+
# branch. This means that the inserted value will be fully
|
| 373 |
+
# consumed.
|
| 374 |
+
if explicit_value is not None:
|
| 375 |
+
state.rargs.insert(0, explicit_value)
|
| 376 |
+
|
| 377 |
+
value = self._get_value_from_state(opt, option, state)
|
| 378 |
+
|
| 379 |
+
elif explicit_value is not None:
|
| 380 |
+
raise BadOptionUsage(
|
| 381 |
+
opt, _("Option {name!r} does not take a value.").format(name=opt)
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
else:
|
| 385 |
+
value = UNSET
|
| 386 |
+
|
| 387 |
+
option.process(value, state)
|
| 388 |
+
|
| 389 |
+
def _match_short_opt(self, arg: str, state: _ParsingState) -> None:
|
| 390 |
+
stop = False
|
| 391 |
+
i = 1
|
| 392 |
+
prefix = arg[0]
|
| 393 |
+
unknown_options = []
|
| 394 |
+
|
| 395 |
+
for ch in arg[1:]:
|
| 396 |
+
opt = _normalize_opt(f"{prefix}{ch}", self.ctx)
|
| 397 |
+
option = self._short_opt.get(opt)
|
| 398 |
+
i += 1
|
| 399 |
+
|
| 400 |
+
if not option:
|
| 401 |
+
if self.ignore_unknown_options:
|
| 402 |
+
unknown_options.append(ch)
|
| 403 |
+
continue
|
| 404 |
+
raise NoSuchOption(opt, ctx=self.ctx)
|
| 405 |
+
if option.takes_value:
|
| 406 |
+
# Any characters left in arg? Pretend they're the
|
| 407 |
+
# next arg, and stop consuming characters of arg.
|
| 408 |
+
if i < len(arg):
|
| 409 |
+
state.rargs.insert(0, arg[i:])
|
| 410 |
+
stop = True
|
| 411 |
+
|
| 412 |
+
value = self._get_value_from_state(opt, option, state)
|
| 413 |
+
|
| 414 |
+
else:
|
| 415 |
+
value = UNSET
|
| 416 |
+
|
| 417 |
+
option.process(value, state)
|
| 418 |
+
|
| 419 |
+
if stop:
|
| 420 |
+
break
|
| 421 |
+
|
| 422 |
+
# If we got any unknown options we recombine the string of the
|
| 423 |
+
# remaining options and re-attach the prefix, then report that
|
| 424 |
+
# to the state as new larg. This way there is basic combinatorics
|
| 425 |
+
# that can be achieved while still ignoring unknown arguments.
|
| 426 |
+
if self.ignore_unknown_options and unknown_options:
|
| 427 |
+
state.largs.append(f"{prefix}{''.join(unknown_options)}")
|
| 428 |
+
|
| 429 |
+
def _get_value_from_state(
|
| 430 |
+
self, option_name: str, option: _Option, state: _ParsingState
|
| 431 |
+
) -> str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE:
|
| 432 |
+
nargs = option.nargs
|
| 433 |
+
|
| 434 |
+
value: str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE
|
| 435 |
+
|
| 436 |
+
if len(state.rargs) < nargs:
|
| 437 |
+
if option.obj._flag_needs_value:
|
| 438 |
+
# Option allows omitting the value.
|
| 439 |
+
value = FLAG_NEEDS_VALUE
|
| 440 |
+
else:
|
| 441 |
+
raise BadOptionUsage(
|
| 442 |
+
option_name,
|
| 443 |
+
ngettext(
|
| 444 |
+
"Option {name!r} requires an argument.",
|
| 445 |
+
"Option {name!r} requires {nargs} arguments.",
|
| 446 |
+
nargs,
|
| 447 |
+
).format(name=option_name, nargs=nargs),
|
| 448 |
+
)
|
| 449 |
+
elif nargs == 1:
|
| 450 |
+
next_rarg = state.rargs[0]
|
| 451 |
+
|
| 452 |
+
if (
|
| 453 |
+
option.obj._flag_needs_value
|
| 454 |
+
and isinstance(next_rarg, str)
|
| 455 |
+
and next_rarg[:1] in self._opt_prefixes
|
| 456 |
+
and len(next_rarg) > 1
|
| 457 |
+
):
|
| 458 |
+
# The next arg looks like the start of an option, don't
|
| 459 |
+
# use it as the value if omitting the value is allowed.
|
| 460 |
+
value = FLAG_NEEDS_VALUE
|
| 461 |
+
else:
|
| 462 |
+
value = state.rargs.pop(0)
|
| 463 |
+
else:
|
| 464 |
+
value = tuple(state.rargs[:nargs])
|
| 465 |
+
del state.rargs[:nargs]
|
| 466 |
+
|
| 467 |
+
return value
|
| 468 |
+
|
| 469 |
+
def _process_opts(self, arg: str, state: _ParsingState) -> None:
|
| 470 |
+
explicit_value = None
|
| 471 |
+
# Long option handling happens in two parts. The first part is
|
| 472 |
+
# supporting explicitly attached values. In any case, we will try
|
| 473 |
+
# to long match the option first.
|
| 474 |
+
if "=" in arg:
|
| 475 |
+
long_opt, explicit_value = arg.split("=", 1)
|
| 476 |
+
else:
|
| 477 |
+
long_opt = arg
|
| 478 |
+
norm_long_opt = _normalize_opt(long_opt, self.ctx)
|
| 479 |
+
|
| 480 |
+
# At this point we will match the (assumed) long option through
|
| 481 |
+
# the long option matching code. Note that this allows options
|
| 482 |
+
# like "-foo" to be matched as long options.
|
| 483 |
+
try:
|
| 484 |
+
self._match_long_opt(norm_long_opt, explicit_value, state)
|
| 485 |
+
except NoSuchOption:
|
| 486 |
+
# At this point the long option matching failed, and we need
|
| 487 |
+
# to try with short options. However there is a special rule
|
| 488 |
+
# which says, that if we have a two character options prefix
|
| 489 |
+
# (applies to "--foo" for instance), we do not dispatch to the
|
| 490 |
+
# short option code and will instead raise the no option
|
| 491 |
+
# error.
|
| 492 |
+
if arg[:2] not in self._opt_prefixes:
|
| 493 |
+
self._match_short_opt(arg, state)
|
| 494 |
+
return
|
| 495 |
+
|
| 496 |
+
if not self.ignore_unknown_options:
|
| 497 |
+
raise
|
| 498 |
+
|
| 499 |
+
state.largs.append(arg)
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def __getattr__(name: str) -> object:
|
| 503 |
+
import warnings
|
| 504 |
+
|
| 505 |
+
if name in {
|
| 506 |
+
"OptionParser",
|
| 507 |
+
"Argument",
|
| 508 |
+
"Option",
|
| 509 |
+
"split_opt",
|
| 510 |
+
"normalize_opt",
|
| 511 |
+
"ParsingState",
|
| 512 |
+
}:
|
| 513 |
+
warnings.warn(
|
| 514 |
+
f"'parser.{name}' is deprecated and will be removed in Click 9.0."
|
| 515 |
+
" The old parser is available in 'optparse'.",
|
| 516 |
+
DeprecationWarning,
|
| 517 |
+
stacklevel=2,
|
| 518 |
+
)
|
| 519 |
+
return globals()[f"_{name}"]
|
| 520 |
+
|
| 521 |
+
if name == "split_arg_string":
|
| 522 |
+
from .shell_completion import split_arg_string
|
| 523 |
+
|
| 524 |
+
warnings.warn(
|
| 525 |
+
"Importing 'parser.split_arg_string' is deprecated, it will only be"
|
| 526 |
+
" available in 'shell_completion' in Click 9.0.",
|
| 527 |
+
DeprecationWarning,
|
| 528 |
+
stacklevel=2,
|
| 529 |
+
)
|
| 530 |
+
return split_arg_string
|
| 531 |
+
|
| 532 |
+
raise AttributeError(name)
|
env/lib/python3.13/site-packages/click/py.typed
ADDED
|
File without changes
|
env/lib/python3.13/site-packages/click/shell_completion.py
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import typing as t
|
| 7 |
+
from gettext import gettext as _
|
| 8 |
+
|
| 9 |
+
from .core import Argument
|
| 10 |
+
from .core import Command
|
| 11 |
+
from .core import Context
|
| 12 |
+
from .core import Group
|
| 13 |
+
from .core import Option
|
| 14 |
+
from .core import Parameter
|
| 15 |
+
from .core import ParameterSource
|
| 16 |
+
from .utils import echo
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def shell_complete(
|
| 20 |
+
cli: Command,
|
| 21 |
+
ctx_args: cabc.MutableMapping[str, t.Any],
|
| 22 |
+
prog_name: str,
|
| 23 |
+
complete_var: str,
|
| 24 |
+
instruction: str,
|
| 25 |
+
) -> int:
|
| 26 |
+
"""Perform shell completion for the given CLI program.
|
| 27 |
+
|
| 28 |
+
:param cli: Command being called.
|
| 29 |
+
:param ctx_args: Extra arguments to pass to
|
| 30 |
+
``cli.make_context``.
|
| 31 |
+
:param prog_name: Name of the executable in the shell.
|
| 32 |
+
:param complete_var: Name of the environment variable that holds
|
| 33 |
+
the completion instruction.
|
| 34 |
+
:param instruction: Value of ``complete_var`` with the completion
|
| 35 |
+
instruction and shell, in the form ``instruction_shell``.
|
| 36 |
+
:return: Status code to exit with.
|
| 37 |
+
"""
|
| 38 |
+
shell, _, instruction = instruction.partition("_")
|
| 39 |
+
comp_cls = get_completion_class(shell)
|
| 40 |
+
|
| 41 |
+
if comp_cls is None:
|
| 42 |
+
return 1
|
| 43 |
+
|
| 44 |
+
comp = comp_cls(cli, ctx_args, prog_name, complete_var)
|
| 45 |
+
|
| 46 |
+
if instruction == "source":
|
| 47 |
+
echo(comp.source())
|
| 48 |
+
return 0
|
| 49 |
+
|
| 50 |
+
if instruction == "complete":
|
| 51 |
+
echo(comp.complete())
|
| 52 |
+
return 0
|
| 53 |
+
|
| 54 |
+
return 1
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class CompletionItem:
|
| 58 |
+
"""Represents a completion value and metadata about the value. The
|
| 59 |
+
default metadata is ``type`` to indicate special shell handling,
|
| 60 |
+
and ``help`` if a shell supports showing a help string next to the
|
| 61 |
+
value.
|
| 62 |
+
|
| 63 |
+
Arbitrary parameters can be passed when creating the object, and
|
| 64 |
+
accessed using ``item.attr``. If an attribute wasn't passed,
|
| 65 |
+
accessing it returns ``None``.
|
| 66 |
+
|
| 67 |
+
:param value: The completion suggestion.
|
| 68 |
+
:param type: Tells the shell script to provide special completion
|
| 69 |
+
support for the type. Click uses ``"dir"`` and ``"file"``.
|
| 70 |
+
:param help: String shown next to the value if supported.
|
| 71 |
+
:param kwargs: Arbitrary metadata. The built-in implementations
|
| 72 |
+
don't use this, but custom type completions paired with custom
|
| 73 |
+
shell support could use it.
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
__slots__ = ("value", "type", "help", "_info")
|
| 77 |
+
|
| 78 |
+
def __init__(
|
| 79 |
+
self,
|
| 80 |
+
value: t.Any,
|
| 81 |
+
type: str = "plain",
|
| 82 |
+
help: str | None = None,
|
| 83 |
+
**kwargs: t.Any,
|
| 84 |
+
) -> None:
|
| 85 |
+
self.value: t.Any = value
|
| 86 |
+
self.type: str = type
|
| 87 |
+
self.help: str | None = help
|
| 88 |
+
self._info = kwargs
|
| 89 |
+
|
| 90 |
+
def __getattr__(self, name: str) -> t.Any:
|
| 91 |
+
return self._info.get(name)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# Only Bash >= 4.4 has the nosort option.
|
| 95 |
+
_SOURCE_BASH = """\
|
| 96 |
+
%(complete_func)s() {
|
| 97 |
+
local IFS=$'\\n'
|
| 98 |
+
local response
|
| 99 |
+
|
| 100 |
+
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \
|
| 101 |
+
%(complete_var)s=bash_complete $1)
|
| 102 |
+
|
| 103 |
+
for completion in $response; do
|
| 104 |
+
IFS=',' read type value <<< "$completion"
|
| 105 |
+
|
| 106 |
+
if [[ $type == 'dir' ]]; then
|
| 107 |
+
COMPREPLY=()
|
| 108 |
+
compopt -o dirnames
|
| 109 |
+
elif [[ $type == 'file' ]]; then
|
| 110 |
+
COMPREPLY=()
|
| 111 |
+
compopt -o default
|
| 112 |
+
elif [[ $type == 'plain' ]]; then
|
| 113 |
+
COMPREPLY+=($value)
|
| 114 |
+
fi
|
| 115 |
+
done
|
| 116 |
+
|
| 117 |
+
return 0
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
%(complete_func)s_setup() {
|
| 121 |
+
complete -o nosort -F %(complete_func)s %(prog_name)s
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
%(complete_func)s_setup;
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
# See ZshComplete.format_completion below, and issue #2703, before
|
| 128 |
+
# changing this script.
|
| 129 |
+
#
|
| 130 |
+
# (TL;DR: _describe is picky about the format, but this Zsh script snippet
|
| 131 |
+
# is already widely deployed. So freeze this script, and use clever-ish
|
| 132 |
+
# handling of colons in ZshComplet.format_completion.)
|
| 133 |
+
_SOURCE_ZSH = """\
|
| 134 |
+
#compdef %(prog_name)s
|
| 135 |
+
|
| 136 |
+
%(complete_func)s() {
|
| 137 |
+
local -a completions
|
| 138 |
+
local -a completions_with_descriptions
|
| 139 |
+
local -a response
|
| 140 |
+
(( ! $+commands[%(prog_name)s] )) && return 1
|
| 141 |
+
|
| 142 |
+
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \
|
| 143 |
+
%(complete_var)s=zsh_complete %(prog_name)s)}")
|
| 144 |
+
|
| 145 |
+
for type key descr in ${response}; do
|
| 146 |
+
if [[ "$type" == "plain" ]]; then
|
| 147 |
+
if [[ "$descr" == "_" ]]; then
|
| 148 |
+
completions+=("$key")
|
| 149 |
+
else
|
| 150 |
+
completions_with_descriptions+=("$key":"$descr")
|
| 151 |
+
fi
|
| 152 |
+
elif [[ "$type" == "dir" ]]; then
|
| 153 |
+
_path_files -/
|
| 154 |
+
elif [[ "$type" == "file" ]]; then
|
| 155 |
+
_path_files -f
|
| 156 |
+
fi
|
| 157 |
+
done
|
| 158 |
+
|
| 159 |
+
if [ -n "$completions_with_descriptions" ]; then
|
| 160 |
+
_describe -V unsorted completions_with_descriptions -U
|
| 161 |
+
fi
|
| 162 |
+
|
| 163 |
+
if [ -n "$completions" ]; then
|
| 164 |
+
compadd -U -V unsorted -a completions
|
| 165 |
+
fi
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
|
| 169 |
+
# autoload from fpath, call function directly
|
| 170 |
+
%(complete_func)s "$@"
|
| 171 |
+
else
|
| 172 |
+
# eval/source/. command, register function for later
|
| 173 |
+
compdef %(complete_func)s %(prog_name)s
|
| 174 |
+
fi
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
_SOURCE_FISH = """\
|
| 178 |
+
function %(complete_func)s;
|
| 179 |
+
set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \
|
| 180 |
+
COMP_CWORD=(commandline -t) %(prog_name)s);
|
| 181 |
+
|
| 182 |
+
for completion in $response;
|
| 183 |
+
set -l metadata (string split "," $completion);
|
| 184 |
+
|
| 185 |
+
if test $metadata[1] = "dir";
|
| 186 |
+
__fish_complete_directories $metadata[2];
|
| 187 |
+
else if test $metadata[1] = "file";
|
| 188 |
+
__fish_complete_path $metadata[2];
|
| 189 |
+
else if test $metadata[1] = "plain";
|
| 190 |
+
echo $metadata[2];
|
| 191 |
+
end;
|
| 192 |
+
end;
|
| 193 |
+
end;
|
| 194 |
+
|
| 195 |
+
complete --no-files --command %(prog_name)s --arguments \
|
| 196 |
+
"(%(complete_func)s)";
|
| 197 |
+
"""
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class ShellComplete:
|
| 201 |
+
"""Base class for providing shell completion support. A subclass for
|
| 202 |
+
a given shell will override attributes and methods to implement the
|
| 203 |
+
completion instructions (``source`` and ``complete``).
|
| 204 |
+
|
| 205 |
+
:param cli: Command being called.
|
| 206 |
+
:param prog_name: Name of the executable in the shell.
|
| 207 |
+
:param complete_var: Name of the environment variable that holds
|
| 208 |
+
the completion instruction.
|
| 209 |
+
|
| 210 |
+
.. versionadded:: 8.0
|
| 211 |
+
"""
|
| 212 |
+
|
| 213 |
+
name: t.ClassVar[str]
|
| 214 |
+
"""Name to register the shell as with :func:`add_completion_class`.
|
| 215 |
+
This is used in completion instructions (``{name}_source`` and
|
| 216 |
+
``{name}_complete``).
|
| 217 |
+
"""
|
| 218 |
+
|
| 219 |
+
source_template: t.ClassVar[str]
|
| 220 |
+
"""Completion script template formatted by :meth:`source`. This must
|
| 221 |
+
be provided by subclasses.
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
def __init__(
|
| 225 |
+
self,
|
| 226 |
+
cli: Command,
|
| 227 |
+
ctx_args: cabc.MutableMapping[str, t.Any],
|
| 228 |
+
prog_name: str,
|
| 229 |
+
complete_var: str,
|
| 230 |
+
) -> None:
|
| 231 |
+
self.cli = cli
|
| 232 |
+
self.ctx_args = ctx_args
|
| 233 |
+
self.prog_name = prog_name
|
| 234 |
+
self.complete_var = complete_var
|
| 235 |
+
|
| 236 |
+
@property
|
| 237 |
+
def func_name(self) -> str:
|
| 238 |
+
"""The name of the shell function defined by the completion
|
| 239 |
+
script.
|
| 240 |
+
"""
|
| 241 |
+
safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII)
|
| 242 |
+
return f"_{safe_name}_completion"
|
| 243 |
+
|
| 244 |
+
def source_vars(self) -> dict[str, t.Any]:
|
| 245 |
+
"""Vars for formatting :attr:`source_template`.
|
| 246 |
+
|
| 247 |
+
By default this provides ``complete_func``, ``complete_var``,
|
| 248 |
+
and ``prog_name``.
|
| 249 |
+
"""
|
| 250 |
+
return {
|
| 251 |
+
"complete_func": self.func_name,
|
| 252 |
+
"complete_var": self.complete_var,
|
| 253 |
+
"prog_name": self.prog_name,
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
def source(self) -> str:
|
| 257 |
+
"""Produce the shell script that defines the completion
|
| 258 |
+
function. By default this ``%``-style formats
|
| 259 |
+
:attr:`source_template` with the dict returned by
|
| 260 |
+
:meth:`source_vars`.
|
| 261 |
+
"""
|
| 262 |
+
return self.source_template % self.source_vars()
|
| 263 |
+
|
| 264 |
+
def get_completion_args(self) -> tuple[list[str], str]:
|
| 265 |
+
"""Use the env vars defined by the shell script to return a
|
| 266 |
+
tuple of ``args, incomplete``. This must be implemented by
|
| 267 |
+
subclasses.
|
| 268 |
+
"""
|
| 269 |
+
raise NotImplementedError
|
| 270 |
+
|
| 271 |
+
def get_completions(self, args: list[str], incomplete: str) -> list[CompletionItem]:
|
| 272 |
+
"""Determine the context and last complete command or parameter
|
| 273 |
+
from the complete args. Call that object's ``shell_complete``
|
| 274 |
+
method to get the completions for the incomplete value.
|
| 275 |
+
|
| 276 |
+
:param args: List of complete args before the incomplete value.
|
| 277 |
+
:param incomplete: Value being completed. May be empty.
|
| 278 |
+
"""
|
| 279 |
+
ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)
|
| 280 |
+
obj, incomplete = _resolve_incomplete(ctx, args, incomplete)
|
| 281 |
+
return obj.shell_complete(ctx, incomplete)
|
| 282 |
+
|
| 283 |
+
def format_completion(self, item: CompletionItem) -> str:
|
| 284 |
+
"""Format a completion item into the form recognized by the
|
| 285 |
+
shell script. This must be implemented by subclasses.
|
| 286 |
+
|
| 287 |
+
:param item: Completion item to format.
|
| 288 |
+
"""
|
| 289 |
+
raise NotImplementedError
|
| 290 |
+
|
| 291 |
+
def complete(self) -> str:
|
| 292 |
+
"""Produce the completion data to send back to the shell.
|
| 293 |
+
|
| 294 |
+
By default this calls :meth:`get_completion_args`, gets the
|
| 295 |
+
completions, then calls :meth:`format_completion` for each
|
| 296 |
+
completion.
|
| 297 |
+
"""
|
| 298 |
+
args, incomplete = self.get_completion_args()
|
| 299 |
+
completions = self.get_completions(args, incomplete)
|
| 300 |
+
out = [self.format_completion(item) for item in completions]
|
| 301 |
+
return "\n".join(out)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class BashComplete(ShellComplete):
|
| 305 |
+
"""Shell completion for Bash."""
|
| 306 |
+
|
| 307 |
+
name = "bash"
|
| 308 |
+
source_template = _SOURCE_BASH
|
| 309 |
+
|
| 310 |
+
@staticmethod
|
| 311 |
+
def _check_version() -> None:
|
| 312 |
+
import shutil
|
| 313 |
+
import subprocess
|
| 314 |
+
|
| 315 |
+
bash_exe = shutil.which("bash")
|
| 316 |
+
|
| 317 |
+
if bash_exe is None:
|
| 318 |
+
match = None
|
| 319 |
+
else:
|
| 320 |
+
output = subprocess.run(
|
| 321 |
+
[bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'],
|
| 322 |
+
stdout=subprocess.PIPE,
|
| 323 |
+
)
|
| 324 |
+
match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())
|
| 325 |
+
|
| 326 |
+
if match is not None:
|
| 327 |
+
major, minor = match.groups()
|
| 328 |
+
|
| 329 |
+
if major < "4" or major == "4" and minor < "4":
|
| 330 |
+
echo(
|
| 331 |
+
_(
|
| 332 |
+
"Shell completion is not supported for Bash"
|
| 333 |
+
" versions older than 4.4."
|
| 334 |
+
),
|
| 335 |
+
err=True,
|
| 336 |
+
)
|
| 337 |
+
else:
|
| 338 |
+
echo(
|
| 339 |
+
_("Couldn't detect Bash version, shell completion is not supported."),
|
| 340 |
+
err=True,
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
def source(self) -> str:
|
| 344 |
+
self._check_version()
|
| 345 |
+
return super().source()
|
| 346 |
+
|
| 347 |
+
def get_completion_args(self) -> tuple[list[str], str]:
|
| 348 |
+
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
| 349 |
+
cword = int(os.environ["COMP_CWORD"])
|
| 350 |
+
args = cwords[1:cword]
|
| 351 |
+
|
| 352 |
+
try:
|
| 353 |
+
incomplete = cwords[cword]
|
| 354 |
+
except IndexError:
|
| 355 |
+
incomplete = ""
|
| 356 |
+
|
| 357 |
+
return args, incomplete
|
| 358 |
+
|
| 359 |
+
def format_completion(self, item: CompletionItem) -> str:
|
| 360 |
+
return f"{item.type},{item.value}"
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
class ZshComplete(ShellComplete):
|
| 364 |
+
"""Shell completion for Zsh."""
|
| 365 |
+
|
| 366 |
+
name = "zsh"
|
| 367 |
+
source_template = _SOURCE_ZSH
|
| 368 |
+
|
| 369 |
+
def get_completion_args(self) -> tuple[list[str], str]:
|
| 370 |
+
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
| 371 |
+
cword = int(os.environ["COMP_CWORD"])
|
| 372 |
+
args = cwords[1:cword]
|
| 373 |
+
|
| 374 |
+
try:
|
| 375 |
+
incomplete = cwords[cword]
|
| 376 |
+
except IndexError:
|
| 377 |
+
incomplete = ""
|
| 378 |
+
|
| 379 |
+
return args, incomplete
|
| 380 |
+
|
| 381 |
+
def format_completion(self, item: CompletionItem) -> str:
|
| 382 |
+
help_ = item.help or "_"
|
| 383 |
+
# The zsh completion script uses `_describe` on items with help
|
| 384 |
+
# texts (which splits the item help from the item value at the
|
| 385 |
+
# first unescaped colon) and `compadd` on items without help
|
| 386 |
+
# text (which uses the item value as-is and does not support
|
| 387 |
+
# colon escaping). So escape colons in the item value if and
|
| 388 |
+
# only if the item help is not the sentinel "_" value, as used
|
| 389 |
+
# by the completion script.
|
| 390 |
+
#
|
| 391 |
+
# (The zsh completion script is potentially widely deployed, and
|
| 392 |
+
# thus harder to fix than this method.)
|
| 393 |
+
#
|
| 394 |
+
# See issue #1812 and issue #2703 for further context.
|
| 395 |
+
value = item.value.replace(":", r"\:") if help_ != "_" else item.value
|
| 396 |
+
return f"{item.type}\n{value}\n{help_}"
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
class FishComplete(ShellComplete):
|
| 400 |
+
"""Shell completion for Fish."""
|
| 401 |
+
|
| 402 |
+
name = "fish"
|
| 403 |
+
source_template = _SOURCE_FISH
|
| 404 |
+
|
| 405 |
+
def get_completion_args(self) -> tuple[list[str], str]:
|
| 406 |
+
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
| 407 |
+
incomplete = os.environ["COMP_CWORD"]
|
| 408 |
+
if incomplete:
|
| 409 |
+
incomplete = split_arg_string(incomplete)[0]
|
| 410 |
+
args = cwords[1:]
|
| 411 |
+
|
| 412 |
+
# Fish stores the partial word in both COMP_WORDS and
|
| 413 |
+
# COMP_CWORD, remove it from complete args.
|
| 414 |
+
if incomplete and args and args[-1] == incomplete:
|
| 415 |
+
args.pop()
|
| 416 |
+
|
| 417 |
+
return args, incomplete
|
| 418 |
+
|
| 419 |
+
def format_completion(self, item: CompletionItem) -> str:
|
| 420 |
+
if item.help:
|
| 421 |
+
return f"{item.type},{item.value}\t{item.help}"
|
| 422 |
+
|
| 423 |
+
return f"{item.type},{item.value}"
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]")
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
_available_shells: dict[str, type[ShellComplete]] = {
|
| 430 |
+
"bash": BashComplete,
|
| 431 |
+
"fish": FishComplete,
|
| 432 |
+
"zsh": ZshComplete,
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def add_completion_class(
|
| 437 |
+
cls: ShellCompleteType, name: str | None = None
|
| 438 |
+
) -> ShellCompleteType:
|
| 439 |
+
"""Register a :class:`ShellComplete` subclass under the given name.
|
| 440 |
+
The name will be provided by the completion instruction environment
|
| 441 |
+
variable during completion.
|
| 442 |
+
|
| 443 |
+
:param cls: The completion class that will handle completion for the
|
| 444 |
+
shell.
|
| 445 |
+
:param name: Name to register the class under. Defaults to the
|
| 446 |
+
class's ``name`` attribute.
|
| 447 |
+
"""
|
| 448 |
+
if name is None:
|
| 449 |
+
name = cls.name
|
| 450 |
+
|
| 451 |
+
_available_shells[name] = cls
|
| 452 |
+
|
| 453 |
+
return cls
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def get_completion_class(shell: str) -> type[ShellComplete] | None:
|
| 457 |
+
"""Look up a registered :class:`ShellComplete` subclass by the name
|
| 458 |
+
provided by the completion instruction environment variable. If the
|
| 459 |
+
name isn't registered, returns ``None``.
|
| 460 |
+
|
| 461 |
+
:param shell: Name the class is registered under.
|
| 462 |
+
"""
|
| 463 |
+
return _available_shells.get(shell)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def split_arg_string(string: str) -> list[str]:
|
| 467 |
+
"""Split an argument string as with :func:`shlex.split`, but don't
|
| 468 |
+
fail if the string is incomplete. Ignores a missing closing quote or
|
| 469 |
+
incomplete escape sequence and uses the partial token as-is.
|
| 470 |
+
|
| 471 |
+
.. code-block:: python
|
| 472 |
+
|
| 473 |
+
split_arg_string("example 'my file")
|
| 474 |
+
["example", "my file"]
|
| 475 |
+
|
| 476 |
+
split_arg_string("example my\\")
|
| 477 |
+
["example", "my"]
|
| 478 |
+
|
| 479 |
+
:param string: String to split.
|
| 480 |
+
|
| 481 |
+
.. versionchanged:: 8.2
|
| 482 |
+
Moved to ``shell_completion`` from ``parser``.
|
| 483 |
+
"""
|
| 484 |
+
import shlex
|
| 485 |
+
|
| 486 |
+
lex = shlex.shlex(string, posix=True)
|
| 487 |
+
lex.whitespace_split = True
|
| 488 |
+
lex.commenters = ""
|
| 489 |
+
out = []
|
| 490 |
+
|
| 491 |
+
try:
|
| 492 |
+
for token in lex:
|
| 493 |
+
out.append(token)
|
| 494 |
+
except ValueError:
|
| 495 |
+
# Raised when end-of-string is reached in an invalid state. Use
|
| 496 |
+
# the partial token as-is. The quote or escape character is in
|
| 497 |
+
# lex.state, not lex.token.
|
| 498 |
+
out.append(lex.token)
|
| 499 |
+
|
| 500 |
+
return out
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
|
| 504 |
+
"""Determine if the given parameter is an argument that can still
|
| 505 |
+
accept values.
|
| 506 |
+
|
| 507 |
+
:param ctx: Invocation context for the command represented by the
|
| 508 |
+
parsed complete args.
|
| 509 |
+
:param param: Argument object being checked.
|
| 510 |
+
"""
|
| 511 |
+
if not isinstance(param, Argument):
|
| 512 |
+
return False
|
| 513 |
+
|
| 514 |
+
assert param.name is not None
|
| 515 |
+
# Will be None if expose_value is False.
|
| 516 |
+
value = ctx.params.get(param.name)
|
| 517 |
+
return (
|
| 518 |
+
param.nargs == -1
|
| 519 |
+
or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE
|
| 520 |
+
or (
|
| 521 |
+
param.nargs > 1
|
| 522 |
+
and isinstance(value, (tuple, list))
|
| 523 |
+
and len(value) < param.nargs
|
| 524 |
+
)
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def _start_of_option(ctx: Context, value: str) -> bool:
|
| 529 |
+
"""Check if the value looks like the start of an option."""
|
| 530 |
+
if not value:
|
| 531 |
+
return False
|
| 532 |
+
|
| 533 |
+
c = value[0]
|
| 534 |
+
return c in ctx._opt_prefixes
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
def _is_incomplete_option(ctx: Context, args: list[str], param: Parameter) -> bool:
|
| 538 |
+
"""Determine if the given parameter is an option that needs a value.
|
| 539 |
+
|
| 540 |
+
:param args: List of complete args before the incomplete value.
|
| 541 |
+
:param param: Option object being checked.
|
| 542 |
+
"""
|
| 543 |
+
if not isinstance(param, Option):
|
| 544 |
+
return False
|
| 545 |
+
|
| 546 |
+
if param.is_flag or param.count:
|
| 547 |
+
return False
|
| 548 |
+
|
| 549 |
+
last_option = None
|
| 550 |
+
|
| 551 |
+
for index, arg in enumerate(reversed(args)):
|
| 552 |
+
if index + 1 > param.nargs:
|
| 553 |
+
break
|
| 554 |
+
|
| 555 |
+
if _start_of_option(ctx, arg):
|
| 556 |
+
last_option = arg
|
| 557 |
+
break
|
| 558 |
+
|
| 559 |
+
return last_option is not None and last_option in param.opts
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
def _resolve_context(
|
| 563 |
+
cli: Command,
|
| 564 |
+
ctx_args: cabc.MutableMapping[str, t.Any],
|
| 565 |
+
prog_name: str,
|
| 566 |
+
args: list[str],
|
| 567 |
+
) -> Context:
|
| 568 |
+
"""Produce the context hierarchy starting with the command and
|
| 569 |
+
traversing the complete arguments. This only follows the commands,
|
| 570 |
+
it doesn't trigger input prompts or callbacks.
|
| 571 |
+
|
| 572 |
+
:param cli: Command being called.
|
| 573 |
+
:param prog_name: Name of the executable in the shell.
|
| 574 |
+
:param args: List of complete args before the incomplete value.
|
| 575 |
+
"""
|
| 576 |
+
ctx_args["resilient_parsing"] = True
|
| 577 |
+
with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx:
|
| 578 |
+
args = ctx._protected_args + ctx.args
|
| 579 |
+
|
| 580 |
+
while args:
|
| 581 |
+
command = ctx.command
|
| 582 |
+
|
| 583 |
+
if isinstance(command, Group):
|
| 584 |
+
if not command.chain:
|
| 585 |
+
name, cmd, args = command.resolve_command(ctx, args)
|
| 586 |
+
|
| 587 |
+
if cmd is None:
|
| 588 |
+
return ctx
|
| 589 |
+
|
| 590 |
+
with cmd.make_context(
|
| 591 |
+
name, args, parent=ctx, resilient_parsing=True
|
| 592 |
+
) as sub_ctx:
|
| 593 |
+
ctx = sub_ctx
|
| 594 |
+
args = ctx._protected_args + ctx.args
|
| 595 |
+
else:
|
| 596 |
+
sub_ctx = ctx
|
| 597 |
+
|
| 598 |
+
while args:
|
| 599 |
+
name, cmd, args = command.resolve_command(ctx, args)
|
| 600 |
+
|
| 601 |
+
if cmd is None:
|
| 602 |
+
return ctx
|
| 603 |
+
|
| 604 |
+
with cmd.make_context(
|
| 605 |
+
name,
|
| 606 |
+
args,
|
| 607 |
+
parent=ctx,
|
| 608 |
+
allow_extra_args=True,
|
| 609 |
+
allow_interspersed_args=False,
|
| 610 |
+
resilient_parsing=True,
|
| 611 |
+
) as sub_sub_ctx:
|
| 612 |
+
sub_ctx = sub_sub_ctx
|
| 613 |
+
args = sub_ctx.args
|
| 614 |
+
|
| 615 |
+
ctx = sub_ctx
|
| 616 |
+
args = [*sub_ctx._protected_args, *sub_ctx.args]
|
| 617 |
+
else:
|
| 618 |
+
break
|
| 619 |
+
|
| 620 |
+
return ctx
|
| 621 |
+
|
| 622 |
+
|
| 623 |
+
def _resolve_incomplete(
|
| 624 |
+
ctx: Context, args: list[str], incomplete: str
|
| 625 |
+
) -> tuple[Command | Parameter, str]:
|
| 626 |
+
"""Find the Click object that will handle the completion of the
|
| 627 |
+
incomplete value. Return the object and the incomplete value.
|
| 628 |
+
|
| 629 |
+
:param ctx: Invocation context for the command represented by
|
| 630 |
+
the parsed complete args.
|
| 631 |
+
:param args: List of complete args before the incomplete value.
|
| 632 |
+
:param incomplete: Value being completed. May be empty.
|
| 633 |
+
"""
|
| 634 |
+
# Different shells treat an "=" between a long option name and
|
| 635 |
+
# value differently. Might keep the value joined, return the "="
|
| 636 |
+
# as a separate item, or return the split name and value. Always
|
| 637 |
+
# split and discard the "=" to make completion easier.
|
| 638 |
+
if incomplete == "=":
|
| 639 |
+
incomplete = ""
|
| 640 |
+
elif "=" in incomplete and _start_of_option(ctx, incomplete):
|
| 641 |
+
name, _, incomplete = incomplete.partition("=")
|
| 642 |
+
args.append(name)
|
| 643 |
+
|
| 644 |
+
# The "--" marker tells Click to stop treating values as options
|
| 645 |
+
# even if they start with the option character. If it hasn't been
|
| 646 |
+
# given and the incomplete arg looks like an option, the current
|
| 647 |
+
# command will provide option name completions.
|
| 648 |
+
if "--" not in args and _start_of_option(ctx, incomplete):
|
| 649 |
+
return ctx.command, incomplete
|
| 650 |
+
|
| 651 |
+
params = ctx.command.get_params(ctx)
|
| 652 |
+
|
| 653 |
+
# If the last complete arg is an option name with an incomplete
|
| 654 |
+
# value, the option will provide value completions.
|
| 655 |
+
for param in params:
|
| 656 |
+
if _is_incomplete_option(ctx, args, param):
|
| 657 |
+
return param, incomplete
|
| 658 |
+
|
| 659 |
+
# It's not an option name or value. The first argument without a
|
| 660 |
+
# parsed value will provide value completions.
|
| 661 |
+
for param in params:
|
| 662 |
+
if _is_incomplete_argument(ctx, param):
|
| 663 |
+
return param, incomplete
|
| 664 |
+
|
| 665 |
+
# There were no unparsed arguments, the command may be a group that
|
| 666 |
+
# will provide command name completions.
|
| 667 |
+
return ctx.command, incomplete
|
env/lib/python3.13/site-packages/click/termui.py
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
import inspect
|
| 5 |
+
import io
|
| 6 |
+
import itertools
|
| 7 |
+
import sys
|
| 8 |
+
import typing as t
|
| 9 |
+
from contextlib import AbstractContextManager
|
| 10 |
+
from gettext import gettext as _
|
| 11 |
+
|
| 12 |
+
from ._compat import isatty
|
| 13 |
+
from ._compat import strip_ansi
|
| 14 |
+
from .exceptions import Abort
|
| 15 |
+
from .exceptions import UsageError
|
| 16 |
+
from .globals import resolve_color_default
|
| 17 |
+
from .types import Choice
|
| 18 |
+
from .types import convert_type
|
| 19 |
+
from .types import ParamType
|
| 20 |
+
from .utils import echo
|
| 21 |
+
from .utils import LazyFile
|
| 22 |
+
|
| 23 |
+
if t.TYPE_CHECKING:
|
| 24 |
+
from ._termui_impl import ProgressBar
|
| 25 |
+
|
| 26 |
+
V = t.TypeVar("V")
|
| 27 |
+
|
| 28 |
+
# The prompt functions to use. The doc tools currently override these
|
| 29 |
+
# functions to customize how they work.
|
| 30 |
+
visible_prompt_func: t.Callable[[str], str] = input
|
| 31 |
+
|
| 32 |
+
_ansi_colors = {
|
| 33 |
+
"black": 30,
|
| 34 |
+
"red": 31,
|
| 35 |
+
"green": 32,
|
| 36 |
+
"yellow": 33,
|
| 37 |
+
"blue": 34,
|
| 38 |
+
"magenta": 35,
|
| 39 |
+
"cyan": 36,
|
| 40 |
+
"white": 37,
|
| 41 |
+
"reset": 39,
|
| 42 |
+
"bright_black": 90,
|
| 43 |
+
"bright_red": 91,
|
| 44 |
+
"bright_green": 92,
|
| 45 |
+
"bright_yellow": 93,
|
| 46 |
+
"bright_blue": 94,
|
| 47 |
+
"bright_magenta": 95,
|
| 48 |
+
"bright_cyan": 96,
|
| 49 |
+
"bright_white": 97,
|
| 50 |
+
}
|
| 51 |
+
_ansi_reset_all = "\033[0m"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def hidden_prompt_func(prompt: str) -> str:
|
| 55 |
+
import getpass
|
| 56 |
+
|
| 57 |
+
return getpass.getpass(prompt)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _build_prompt(
|
| 61 |
+
text: str,
|
| 62 |
+
suffix: str,
|
| 63 |
+
show_default: bool = False,
|
| 64 |
+
default: t.Any | None = None,
|
| 65 |
+
show_choices: bool = True,
|
| 66 |
+
type: ParamType | None = None,
|
| 67 |
+
) -> str:
|
| 68 |
+
prompt = text
|
| 69 |
+
if type is not None and show_choices and isinstance(type, Choice):
|
| 70 |
+
prompt += f" ({', '.join(map(str, type.choices))})"
|
| 71 |
+
if default is not None and show_default:
|
| 72 |
+
prompt = f"{prompt} [{_format_default(default)}]"
|
| 73 |
+
return f"{prompt}{suffix}"
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _format_default(default: t.Any) -> t.Any:
|
| 77 |
+
if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
|
| 78 |
+
return default.name
|
| 79 |
+
|
| 80 |
+
return default
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def prompt(
|
| 84 |
+
text: str,
|
| 85 |
+
default: t.Any | None = None,
|
| 86 |
+
hide_input: bool = False,
|
| 87 |
+
confirmation_prompt: bool | str = False,
|
| 88 |
+
type: ParamType | t.Any | None = None,
|
| 89 |
+
value_proc: t.Callable[[str], t.Any] | None = None,
|
| 90 |
+
prompt_suffix: str = ": ",
|
| 91 |
+
show_default: bool = True,
|
| 92 |
+
err: bool = False,
|
| 93 |
+
show_choices: bool = True,
|
| 94 |
+
) -> t.Any:
|
| 95 |
+
"""Prompts a user for input. This is a convenience function that can
|
| 96 |
+
be used to prompt a user for input later.
|
| 97 |
+
|
| 98 |
+
If the user aborts the input by sending an interrupt signal, this
|
| 99 |
+
function will catch it and raise a :exc:`Abort` exception.
|
| 100 |
+
|
| 101 |
+
:param text: the text to show for the prompt.
|
| 102 |
+
:param default: the default value to use if no input happens. If this
|
| 103 |
+
is not given it will prompt until it's aborted.
|
| 104 |
+
:param hide_input: if this is set to true then the input value will
|
| 105 |
+
be hidden.
|
| 106 |
+
:param confirmation_prompt: Prompt a second time to confirm the
|
| 107 |
+
value. Can be set to a string instead of ``True`` to customize
|
| 108 |
+
the message.
|
| 109 |
+
:param type: the type to use to check the value against.
|
| 110 |
+
:param value_proc: if this parameter is provided it's a function that
|
| 111 |
+
is invoked instead of the type conversion to
|
| 112 |
+
convert a value.
|
| 113 |
+
:param prompt_suffix: a suffix that should be added to the prompt.
|
| 114 |
+
:param show_default: shows or hides the default value in the prompt.
|
| 115 |
+
:param err: if set to true the file defaults to ``stderr`` instead of
|
| 116 |
+
``stdout``, the same as with echo.
|
| 117 |
+
:param show_choices: Show or hide choices if the passed type is a Choice.
|
| 118 |
+
For example if type is a Choice of either day or week,
|
| 119 |
+
show_choices is true and text is "Group by" then the
|
| 120 |
+
prompt will be "Group by (day, week): ".
|
| 121 |
+
|
| 122 |
+
.. versionchanged:: 8.3.1
|
| 123 |
+
A space is no longer appended to the prompt.
|
| 124 |
+
|
| 125 |
+
.. versionadded:: 8.0
|
| 126 |
+
``confirmation_prompt`` can be a custom string.
|
| 127 |
+
|
| 128 |
+
.. versionadded:: 7.0
|
| 129 |
+
Added the ``show_choices`` parameter.
|
| 130 |
+
|
| 131 |
+
.. versionadded:: 6.0
|
| 132 |
+
Added unicode support for cmd.exe on Windows.
|
| 133 |
+
|
| 134 |
+
.. versionadded:: 4.0
|
| 135 |
+
Added the `err` parameter.
|
| 136 |
+
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
def prompt_func(text: str) -> str:
|
| 140 |
+
f = hidden_prompt_func if hide_input else visible_prompt_func
|
| 141 |
+
try:
|
| 142 |
+
# Write the prompt separately so that we get nice
|
| 143 |
+
# coloring through colorama on Windows
|
| 144 |
+
echo(text[:-1], nl=False, err=err)
|
| 145 |
+
# Echo the last character to stdout to work around an issue where
|
| 146 |
+
# readline causes backspace to clear the whole line.
|
| 147 |
+
return f(text[-1:])
|
| 148 |
+
except (KeyboardInterrupt, EOFError):
|
| 149 |
+
# getpass doesn't print a newline if the user aborts input with ^C.
|
| 150 |
+
# Allegedly this behavior is inherited from getpass(3).
|
| 151 |
+
# A doc bug has been filed at https://bugs.python.org/issue24711
|
| 152 |
+
if hide_input:
|
| 153 |
+
echo(None, err=err)
|
| 154 |
+
raise Abort() from None
|
| 155 |
+
|
| 156 |
+
if value_proc is None:
|
| 157 |
+
value_proc = convert_type(type, default)
|
| 158 |
+
|
| 159 |
+
prompt = _build_prompt(
|
| 160 |
+
text, prompt_suffix, show_default, default, show_choices, type
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
if confirmation_prompt:
|
| 164 |
+
if confirmation_prompt is True:
|
| 165 |
+
confirmation_prompt = _("Repeat for confirmation")
|
| 166 |
+
|
| 167 |
+
confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
|
| 168 |
+
|
| 169 |
+
while True:
|
| 170 |
+
while True:
|
| 171 |
+
value = prompt_func(prompt)
|
| 172 |
+
if value:
|
| 173 |
+
break
|
| 174 |
+
elif default is not None:
|
| 175 |
+
value = default
|
| 176 |
+
break
|
| 177 |
+
try:
|
| 178 |
+
result = value_proc(value)
|
| 179 |
+
except UsageError as e:
|
| 180 |
+
if hide_input:
|
| 181 |
+
echo(_("Error: The value you entered was invalid."), err=err)
|
| 182 |
+
else:
|
| 183 |
+
echo(_("Error: {e.message}").format(e=e), err=err)
|
| 184 |
+
continue
|
| 185 |
+
if not confirmation_prompt:
|
| 186 |
+
return result
|
| 187 |
+
while True:
|
| 188 |
+
value2 = prompt_func(confirmation_prompt)
|
| 189 |
+
is_empty = not value and not value2
|
| 190 |
+
if value2 or is_empty:
|
| 191 |
+
break
|
| 192 |
+
if value == value2:
|
| 193 |
+
return result
|
| 194 |
+
echo(_("Error: The two entered values do not match."), err=err)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def confirm(
|
| 198 |
+
text: str,
|
| 199 |
+
default: bool | None = False,
|
| 200 |
+
abort: bool = False,
|
| 201 |
+
prompt_suffix: str = ": ",
|
| 202 |
+
show_default: bool = True,
|
| 203 |
+
err: bool = False,
|
| 204 |
+
) -> bool:
|
| 205 |
+
"""Prompts for confirmation (yes/no question).
|
| 206 |
+
|
| 207 |
+
If the user aborts the input by sending a interrupt signal this
|
| 208 |
+
function will catch it and raise a :exc:`Abort` exception.
|
| 209 |
+
|
| 210 |
+
:param text: the question to ask.
|
| 211 |
+
:param default: The default value to use when no input is given. If
|
| 212 |
+
``None``, repeat until input is given.
|
| 213 |
+
:param abort: if this is set to `True` a negative answer aborts the
|
| 214 |
+
exception by raising :exc:`Abort`.
|
| 215 |
+
:param prompt_suffix: a suffix that should be added to the prompt.
|
| 216 |
+
:param show_default: shows or hides the default value in the prompt.
|
| 217 |
+
:param err: if set to true the file defaults to ``stderr`` instead of
|
| 218 |
+
``stdout``, the same as with echo.
|
| 219 |
+
|
| 220 |
+
.. versionchanged:: 8.3.1
|
| 221 |
+
A space is no longer appended to the prompt.
|
| 222 |
+
|
| 223 |
+
.. versionchanged:: 8.0
|
| 224 |
+
Repeat until input is given if ``default`` is ``None``.
|
| 225 |
+
|
| 226 |
+
.. versionadded:: 4.0
|
| 227 |
+
Added the ``err`` parameter.
|
| 228 |
+
"""
|
| 229 |
+
prompt = _build_prompt(
|
| 230 |
+
text,
|
| 231 |
+
prompt_suffix,
|
| 232 |
+
show_default,
|
| 233 |
+
"y/n" if default is None else ("Y/n" if default else "y/N"),
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
while True:
|
| 237 |
+
try:
|
| 238 |
+
# Write the prompt separately so that we get nice
|
| 239 |
+
# coloring through colorama on Windows
|
| 240 |
+
echo(prompt[:-1], nl=False, err=err)
|
| 241 |
+
# Echo the last character to stdout to work around an issue where
|
| 242 |
+
# readline causes backspace to clear the whole line.
|
| 243 |
+
value = visible_prompt_func(prompt[-1:]).lower().strip()
|
| 244 |
+
except (KeyboardInterrupt, EOFError):
|
| 245 |
+
raise Abort() from None
|
| 246 |
+
if value in ("y", "yes"):
|
| 247 |
+
rv = True
|
| 248 |
+
elif value in ("n", "no"):
|
| 249 |
+
rv = False
|
| 250 |
+
elif default is not None and value == "":
|
| 251 |
+
rv = default
|
| 252 |
+
else:
|
| 253 |
+
echo(_("Error: invalid input"), err=err)
|
| 254 |
+
continue
|
| 255 |
+
break
|
| 256 |
+
if abort and not rv:
|
| 257 |
+
raise Abort()
|
| 258 |
+
return rv
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def echo_via_pager(
|
| 262 |
+
text_or_generator: cabc.Iterable[str] | t.Callable[[], cabc.Iterable[str]] | str,
|
| 263 |
+
color: bool | None = None,
|
| 264 |
+
) -> None:
|
| 265 |
+
"""This function takes a text and shows it via an environment specific
|
| 266 |
+
pager on stdout.
|
| 267 |
+
|
| 268 |
+
.. versionchanged:: 3.0
|
| 269 |
+
Added the `color` flag.
|
| 270 |
+
|
| 271 |
+
:param text_or_generator: the text to page, or alternatively, a
|
| 272 |
+
generator emitting the text to page.
|
| 273 |
+
:param color: controls if the pager supports ANSI colors or not. The
|
| 274 |
+
default is autodetection.
|
| 275 |
+
"""
|
| 276 |
+
color = resolve_color_default(color)
|
| 277 |
+
|
| 278 |
+
if inspect.isgeneratorfunction(text_or_generator):
|
| 279 |
+
i = t.cast("t.Callable[[], cabc.Iterable[str]]", text_or_generator)()
|
| 280 |
+
elif isinstance(text_or_generator, str):
|
| 281 |
+
i = [text_or_generator]
|
| 282 |
+
else:
|
| 283 |
+
i = iter(t.cast("cabc.Iterable[str]", text_or_generator))
|
| 284 |
+
|
| 285 |
+
# convert every element of i to a text type if necessary
|
| 286 |
+
text_generator = (el if isinstance(el, str) else str(el) for el in i)
|
| 287 |
+
|
| 288 |
+
from ._termui_impl import pager
|
| 289 |
+
|
| 290 |
+
return pager(itertools.chain(text_generator, "\n"), color)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@t.overload
|
| 294 |
+
def progressbar(
|
| 295 |
+
*,
|
| 296 |
+
length: int,
|
| 297 |
+
label: str | None = None,
|
| 298 |
+
hidden: bool = False,
|
| 299 |
+
show_eta: bool = True,
|
| 300 |
+
show_percent: bool | None = None,
|
| 301 |
+
show_pos: bool = False,
|
| 302 |
+
fill_char: str = "#",
|
| 303 |
+
empty_char: str = "-",
|
| 304 |
+
bar_template: str = "%(label)s [%(bar)s] %(info)s",
|
| 305 |
+
info_sep: str = " ",
|
| 306 |
+
width: int = 36,
|
| 307 |
+
file: t.TextIO | None = None,
|
| 308 |
+
color: bool | None = None,
|
| 309 |
+
update_min_steps: int = 1,
|
| 310 |
+
) -> ProgressBar[int]: ...
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@t.overload
|
| 314 |
+
def progressbar(
|
| 315 |
+
iterable: cabc.Iterable[V] | None = None,
|
| 316 |
+
length: int | None = None,
|
| 317 |
+
label: str | None = None,
|
| 318 |
+
hidden: bool = False,
|
| 319 |
+
show_eta: bool = True,
|
| 320 |
+
show_percent: bool | None = None,
|
| 321 |
+
show_pos: bool = False,
|
| 322 |
+
item_show_func: t.Callable[[V | None], str | None] | None = None,
|
| 323 |
+
fill_char: str = "#",
|
| 324 |
+
empty_char: str = "-",
|
| 325 |
+
bar_template: str = "%(label)s [%(bar)s] %(info)s",
|
| 326 |
+
info_sep: str = " ",
|
| 327 |
+
width: int = 36,
|
| 328 |
+
file: t.TextIO | None = None,
|
| 329 |
+
color: bool | None = None,
|
| 330 |
+
update_min_steps: int = 1,
|
| 331 |
+
) -> ProgressBar[V]: ...
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def progressbar(
|
| 335 |
+
iterable: cabc.Iterable[V] | None = None,
|
| 336 |
+
length: int | None = None,
|
| 337 |
+
label: str | None = None,
|
| 338 |
+
hidden: bool = False,
|
| 339 |
+
show_eta: bool = True,
|
| 340 |
+
show_percent: bool | None = None,
|
| 341 |
+
show_pos: bool = False,
|
| 342 |
+
item_show_func: t.Callable[[V | None], str | None] | None = None,
|
| 343 |
+
fill_char: str = "#",
|
| 344 |
+
empty_char: str = "-",
|
| 345 |
+
bar_template: str = "%(label)s [%(bar)s] %(info)s",
|
| 346 |
+
info_sep: str = " ",
|
| 347 |
+
width: int = 36,
|
| 348 |
+
file: t.TextIO | None = None,
|
| 349 |
+
color: bool | None = None,
|
| 350 |
+
update_min_steps: int = 1,
|
| 351 |
+
) -> ProgressBar[V]:
|
| 352 |
+
"""This function creates an iterable context manager that can be used
|
| 353 |
+
to iterate over something while showing a progress bar. It will
|
| 354 |
+
either iterate over the `iterable` or `length` items (that are counted
|
| 355 |
+
up). While iteration happens, this function will print a rendered
|
| 356 |
+
progress bar to the given `file` (defaults to stdout) and will attempt
|
| 357 |
+
to calculate remaining time and more. By default, this progress bar
|
| 358 |
+
will not be rendered if the file is not a terminal.
|
| 359 |
+
|
| 360 |
+
The context manager creates the progress bar. When the context
|
| 361 |
+
manager is entered the progress bar is already created. With every
|
| 362 |
+
iteration over the progress bar, the iterable passed to the bar is
|
| 363 |
+
advanced and the bar is updated. When the context manager exits,
|
| 364 |
+
a newline is printed and the progress bar is finalized on screen.
|
| 365 |
+
|
| 366 |
+
Note: The progress bar is currently designed for use cases where the
|
| 367 |
+
total progress can be expected to take at least several seconds.
|
| 368 |
+
Because of this, the ProgressBar class object won't display
|
| 369 |
+
progress that is considered too fast, and progress where the time
|
| 370 |
+
between steps is less than a second.
|
| 371 |
+
|
| 372 |
+
No printing must happen or the progress bar will be unintentionally
|
| 373 |
+
destroyed.
|
| 374 |
+
|
| 375 |
+
Example usage::
|
| 376 |
+
|
| 377 |
+
with progressbar(items) as bar:
|
| 378 |
+
for item in bar:
|
| 379 |
+
do_something_with(item)
|
| 380 |
+
|
| 381 |
+
Alternatively, if no iterable is specified, one can manually update the
|
| 382 |
+
progress bar through the `update()` method instead of directly
|
| 383 |
+
iterating over the progress bar. The update method accepts the number
|
| 384 |
+
of steps to increment the bar with::
|
| 385 |
+
|
| 386 |
+
with progressbar(length=chunks.total_bytes) as bar:
|
| 387 |
+
for chunk in chunks:
|
| 388 |
+
process_chunk(chunk)
|
| 389 |
+
bar.update(chunks.bytes)
|
| 390 |
+
|
| 391 |
+
The ``update()`` method also takes an optional value specifying the
|
| 392 |
+
``current_item`` at the new position. This is useful when used
|
| 393 |
+
together with ``item_show_func`` to customize the output for each
|
| 394 |
+
manual step::
|
| 395 |
+
|
| 396 |
+
with click.progressbar(
|
| 397 |
+
length=total_size,
|
| 398 |
+
label='Unzipping archive',
|
| 399 |
+
item_show_func=lambda a: a.filename
|
| 400 |
+
) as bar:
|
| 401 |
+
for archive in zip_file:
|
| 402 |
+
archive.extract()
|
| 403 |
+
bar.update(archive.size, archive)
|
| 404 |
+
|
| 405 |
+
:param iterable: an iterable to iterate over. If not provided the length
|
| 406 |
+
is required.
|
| 407 |
+
:param length: the number of items to iterate over. By default the
|
| 408 |
+
progressbar will attempt to ask the iterator about its
|
| 409 |
+
length, which might or might not work. If an iterable is
|
| 410 |
+
also provided this parameter can be used to override the
|
| 411 |
+
length. If an iterable is not provided the progress bar
|
| 412 |
+
will iterate over a range of that length.
|
| 413 |
+
:param label: the label to show next to the progress bar.
|
| 414 |
+
:param hidden: hide the progressbar. Defaults to ``False``. When no tty is
|
| 415 |
+
detected, it will only print the progressbar label. Setting this to
|
| 416 |
+
``False`` also disables that.
|
| 417 |
+
:param show_eta: enables or disables the estimated time display. This is
|
| 418 |
+
automatically disabled if the length cannot be
|
| 419 |
+
determined.
|
| 420 |
+
:param show_percent: enables or disables the percentage display. The
|
| 421 |
+
default is `True` if the iterable has a length or
|
| 422 |
+
`False` if not.
|
| 423 |
+
:param show_pos: enables or disables the absolute position display. The
|
| 424 |
+
default is `False`.
|
| 425 |
+
:param item_show_func: A function called with the current item which
|
| 426 |
+
can return a string to show next to the progress bar. If the
|
| 427 |
+
function returns ``None`` nothing is shown. The current item can
|
| 428 |
+
be ``None``, such as when entering and exiting the bar.
|
| 429 |
+
:param fill_char: the character to use to show the filled part of the
|
| 430 |
+
progress bar.
|
| 431 |
+
:param empty_char: the character to use to show the non-filled part of
|
| 432 |
+
the progress bar.
|
| 433 |
+
:param bar_template: the format string to use as template for the bar.
|
| 434 |
+
The parameters in it are ``label`` for the label,
|
| 435 |
+
``bar`` for the progress bar and ``info`` for the
|
| 436 |
+
info section.
|
| 437 |
+
:param info_sep: the separator between multiple info items (eta etc.)
|
| 438 |
+
:param width: the width of the progress bar in characters, 0 means full
|
| 439 |
+
terminal width
|
| 440 |
+
:param file: The file to write to. If this is not a terminal then
|
| 441 |
+
only the label is printed.
|
| 442 |
+
:param color: controls if the terminal supports ANSI colors or not. The
|
| 443 |
+
default is autodetection. This is only needed if ANSI
|
| 444 |
+
codes are included anywhere in the progress bar output
|
| 445 |
+
which is not the case by default.
|
| 446 |
+
:param update_min_steps: Render only when this many updates have
|
| 447 |
+
completed. This allows tuning for very fast iterators.
|
| 448 |
+
|
| 449 |
+
.. versionadded:: 8.2
|
| 450 |
+
The ``hidden`` argument.
|
| 451 |
+
|
| 452 |
+
.. versionchanged:: 8.0
|
| 453 |
+
Output is shown even if execution time is less than 0.5 seconds.
|
| 454 |
+
|
| 455 |
+
.. versionchanged:: 8.0
|
| 456 |
+
``item_show_func`` shows the current item, not the previous one.
|
| 457 |
+
|
| 458 |
+
.. versionchanged:: 8.0
|
| 459 |
+
Labels are echoed if the output is not a TTY. Reverts a change
|
| 460 |
+
in 7.0 that removed all output.
|
| 461 |
+
|
| 462 |
+
.. versionadded:: 8.0
|
| 463 |
+
The ``update_min_steps`` parameter.
|
| 464 |
+
|
| 465 |
+
.. versionadded:: 4.0
|
| 466 |
+
The ``color`` parameter and ``update`` method.
|
| 467 |
+
|
| 468 |
+
.. versionadded:: 2.0
|
| 469 |
+
"""
|
| 470 |
+
from ._termui_impl import ProgressBar
|
| 471 |
+
|
| 472 |
+
color = resolve_color_default(color)
|
| 473 |
+
return ProgressBar(
|
| 474 |
+
iterable=iterable,
|
| 475 |
+
length=length,
|
| 476 |
+
hidden=hidden,
|
| 477 |
+
show_eta=show_eta,
|
| 478 |
+
show_percent=show_percent,
|
| 479 |
+
show_pos=show_pos,
|
| 480 |
+
item_show_func=item_show_func,
|
| 481 |
+
fill_char=fill_char,
|
| 482 |
+
empty_char=empty_char,
|
| 483 |
+
bar_template=bar_template,
|
| 484 |
+
info_sep=info_sep,
|
| 485 |
+
file=file,
|
| 486 |
+
label=label,
|
| 487 |
+
width=width,
|
| 488 |
+
color=color,
|
| 489 |
+
update_min_steps=update_min_steps,
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def clear() -> None:
|
| 494 |
+
"""Clears the terminal screen. This will have the effect of clearing
|
| 495 |
+
the whole visible space of the terminal and moving the cursor to the
|
| 496 |
+
top left. This does not do anything if not connected to a terminal.
|
| 497 |
+
|
| 498 |
+
.. versionadded:: 2.0
|
| 499 |
+
"""
|
| 500 |
+
if not isatty(sys.stdout):
|
| 501 |
+
return
|
| 502 |
+
|
| 503 |
+
# ANSI escape \033[2J clears the screen, \033[1;1H moves the cursor
|
| 504 |
+
echo("\033[2J\033[1;1H", nl=False)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def _interpret_color(color: int | tuple[int, int, int] | str, offset: int = 0) -> str:
|
| 508 |
+
if isinstance(color, int):
|
| 509 |
+
return f"{38 + offset};5;{color:d}"
|
| 510 |
+
|
| 511 |
+
if isinstance(color, (tuple, list)):
|
| 512 |
+
r, g, b = color
|
| 513 |
+
return f"{38 + offset};2;{r:d};{g:d};{b:d}"
|
| 514 |
+
|
| 515 |
+
return str(_ansi_colors[color] + offset)
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
def style(
|
| 519 |
+
text: t.Any,
|
| 520 |
+
fg: int | tuple[int, int, int] | str | None = None,
|
| 521 |
+
bg: int | tuple[int, int, int] | str | None = None,
|
| 522 |
+
bold: bool | None = None,
|
| 523 |
+
dim: bool | None = None,
|
| 524 |
+
underline: bool | None = None,
|
| 525 |
+
overline: bool | None = None,
|
| 526 |
+
italic: bool | None = None,
|
| 527 |
+
blink: bool | None = None,
|
| 528 |
+
reverse: bool | None = None,
|
| 529 |
+
strikethrough: bool | None = None,
|
| 530 |
+
reset: bool = True,
|
| 531 |
+
) -> str:
|
| 532 |
+
"""Styles a text with ANSI styles and returns the new string. By
|
| 533 |
+
default the styling is self contained which means that at the end
|
| 534 |
+
of the string a reset code is issued. This can be prevented by
|
| 535 |
+
passing ``reset=False``.
|
| 536 |
+
|
| 537 |
+
Examples::
|
| 538 |
+
|
| 539 |
+
click.echo(click.style('Hello World!', fg='green'))
|
| 540 |
+
click.echo(click.style('ATTENTION!', blink=True))
|
| 541 |
+
click.echo(click.style('Some things', reverse=True, fg='cyan'))
|
| 542 |
+
click.echo(click.style('More colors', fg=(255, 12, 128), bg=117))
|
| 543 |
+
|
| 544 |
+
Supported color names:
|
| 545 |
+
|
| 546 |
+
* ``black`` (might be a gray)
|
| 547 |
+
* ``red``
|
| 548 |
+
* ``green``
|
| 549 |
+
* ``yellow`` (might be an orange)
|
| 550 |
+
* ``blue``
|
| 551 |
+
* ``magenta``
|
| 552 |
+
* ``cyan``
|
| 553 |
+
* ``white`` (might be light gray)
|
| 554 |
+
* ``bright_black``
|
| 555 |
+
* ``bright_red``
|
| 556 |
+
* ``bright_green``
|
| 557 |
+
* ``bright_yellow``
|
| 558 |
+
* ``bright_blue``
|
| 559 |
+
* ``bright_magenta``
|
| 560 |
+
* ``bright_cyan``
|
| 561 |
+
* ``bright_white``
|
| 562 |
+
* ``reset`` (reset the color code only)
|
| 563 |
+
|
| 564 |
+
If the terminal supports it, color may also be specified as:
|
| 565 |
+
|
| 566 |
+
- An integer in the interval [0, 255]. The terminal must support
|
| 567 |
+
8-bit/256-color mode.
|
| 568 |
+
- An RGB tuple of three integers in [0, 255]. The terminal must
|
| 569 |
+
support 24-bit/true-color mode.
|
| 570 |
+
|
| 571 |
+
See https://en.wikipedia.org/wiki/ANSI_color and
|
| 572 |
+
https://gist.github.com/XVilka/8346728 for more information.
|
| 573 |
+
|
| 574 |
+
:param text: the string to style with ansi codes.
|
| 575 |
+
:param fg: if provided this will become the foreground color.
|
| 576 |
+
:param bg: if provided this will become the background color.
|
| 577 |
+
:param bold: if provided this will enable or disable bold mode.
|
| 578 |
+
:param dim: if provided this will enable or disable dim mode. This is
|
| 579 |
+
badly supported.
|
| 580 |
+
:param underline: if provided this will enable or disable underline.
|
| 581 |
+
:param overline: if provided this will enable or disable overline.
|
| 582 |
+
:param italic: if provided this will enable or disable italic.
|
| 583 |
+
:param blink: if provided this will enable or disable blinking.
|
| 584 |
+
:param reverse: if provided this will enable or disable inverse
|
| 585 |
+
rendering (foreground becomes background and the
|
| 586 |
+
other way round).
|
| 587 |
+
:param strikethrough: if provided this will enable or disable
|
| 588 |
+
striking through text.
|
| 589 |
+
:param reset: by default a reset-all code is added at the end of the
|
| 590 |
+
string which means that styles do not carry over. This
|
| 591 |
+
can be disabled to compose styles.
|
| 592 |
+
|
| 593 |
+
.. versionchanged:: 8.0
|
| 594 |
+
A non-string ``message`` is converted to a string.
|
| 595 |
+
|
| 596 |
+
.. versionchanged:: 8.0
|
| 597 |
+
Added support for 256 and RGB color codes.
|
| 598 |
+
|
| 599 |
+
.. versionchanged:: 8.0
|
| 600 |
+
Added the ``strikethrough``, ``italic``, and ``overline``
|
| 601 |
+
parameters.
|
| 602 |
+
|
| 603 |
+
.. versionchanged:: 7.0
|
| 604 |
+
Added support for bright colors.
|
| 605 |
+
|
| 606 |
+
.. versionadded:: 2.0
|
| 607 |
+
"""
|
| 608 |
+
if not isinstance(text, str):
|
| 609 |
+
text = str(text)
|
| 610 |
+
|
| 611 |
+
bits = []
|
| 612 |
+
|
| 613 |
+
if fg:
|
| 614 |
+
try:
|
| 615 |
+
bits.append(f"\033[{_interpret_color(fg)}m")
|
| 616 |
+
except KeyError:
|
| 617 |
+
raise TypeError(f"Unknown color {fg!r}") from None
|
| 618 |
+
|
| 619 |
+
if bg:
|
| 620 |
+
try:
|
| 621 |
+
bits.append(f"\033[{_interpret_color(bg, 10)}m")
|
| 622 |
+
except KeyError:
|
| 623 |
+
raise TypeError(f"Unknown color {bg!r}") from None
|
| 624 |
+
|
| 625 |
+
if bold is not None:
|
| 626 |
+
bits.append(f"\033[{1 if bold else 22}m")
|
| 627 |
+
if dim is not None:
|
| 628 |
+
bits.append(f"\033[{2 if dim else 22}m")
|
| 629 |
+
if underline is not None:
|
| 630 |
+
bits.append(f"\033[{4 if underline else 24}m")
|
| 631 |
+
if overline is not None:
|
| 632 |
+
bits.append(f"\033[{53 if overline else 55}m")
|
| 633 |
+
if italic is not None:
|
| 634 |
+
bits.append(f"\033[{3 if italic else 23}m")
|
| 635 |
+
if blink is not None:
|
| 636 |
+
bits.append(f"\033[{5 if blink else 25}m")
|
| 637 |
+
if reverse is not None:
|
| 638 |
+
bits.append(f"\033[{7 if reverse else 27}m")
|
| 639 |
+
if strikethrough is not None:
|
| 640 |
+
bits.append(f"\033[{9 if strikethrough else 29}m")
|
| 641 |
+
bits.append(text)
|
| 642 |
+
if reset:
|
| 643 |
+
bits.append(_ansi_reset_all)
|
| 644 |
+
return "".join(bits)
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
def unstyle(text: str) -> str:
|
| 648 |
+
"""Removes ANSI styling information from a string. Usually it's not
|
| 649 |
+
necessary to use this function as Click's echo function will
|
| 650 |
+
automatically remove styling if necessary.
|
| 651 |
+
|
| 652 |
+
.. versionadded:: 2.0
|
| 653 |
+
|
| 654 |
+
:param text: the text to remove style information from.
|
| 655 |
+
"""
|
| 656 |
+
return strip_ansi(text)
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def secho(
|
| 660 |
+
message: t.Any | None = None,
|
| 661 |
+
file: t.IO[t.AnyStr] | None = None,
|
| 662 |
+
nl: bool = True,
|
| 663 |
+
err: bool = False,
|
| 664 |
+
color: bool | None = None,
|
| 665 |
+
**styles: t.Any,
|
| 666 |
+
) -> None:
|
| 667 |
+
"""This function combines :func:`echo` and :func:`style` into one
|
| 668 |
+
call. As such the following two calls are the same::
|
| 669 |
+
|
| 670 |
+
click.secho('Hello World!', fg='green')
|
| 671 |
+
click.echo(click.style('Hello World!', fg='green'))
|
| 672 |
+
|
| 673 |
+
All keyword arguments are forwarded to the underlying functions
|
| 674 |
+
depending on which one they go with.
|
| 675 |
+
|
| 676 |
+
Non-string types will be converted to :class:`str`. However,
|
| 677 |
+
:class:`bytes` are passed directly to :meth:`echo` without applying
|
| 678 |
+
style. If you want to style bytes that represent text, call
|
| 679 |
+
:meth:`bytes.decode` first.
|
| 680 |
+
|
| 681 |
+
.. versionchanged:: 8.0
|
| 682 |
+
A non-string ``message`` is converted to a string. Bytes are
|
| 683 |
+
passed through without style applied.
|
| 684 |
+
|
| 685 |
+
.. versionadded:: 2.0
|
| 686 |
+
"""
|
| 687 |
+
if message is not None and not isinstance(message, (bytes, bytearray)):
|
| 688 |
+
message = style(message, **styles)
|
| 689 |
+
|
| 690 |
+
return echo(message, file=file, nl=nl, err=err, color=color)
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
@t.overload
|
| 694 |
+
def edit(
|
| 695 |
+
text: bytes | bytearray,
|
| 696 |
+
editor: str | None = None,
|
| 697 |
+
env: cabc.Mapping[str, str] | None = None,
|
| 698 |
+
require_save: bool = False,
|
| 699 |
+
extension: str = ".txt",
|
| 700 |
+
) -> bytes | None: ...
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
@t.overload
|
| 704 |
+
def edit(
|
| 705 |
+
text: str,
|
| 706 |
+
editor: str | None = None,
|
| 707 |
+
env: cabc.Mapping[str, str] | None = None,
|
| 708 |
+
require_save: bool = True,
|
| 709 |
+
extension: str = ".txt",
|
| 710 |
+
) -> str | None: ...
|
| 711 |
+
|
| 712 |
+
|
| 713 |
+
@t.overload
|
| 714 |
+
def edit(
|
| 715 |
+
text: None = None,
|
| 716 |
+
editor: str | None = None,
|
| 717 |
+
env: cabc.Mapping[str, str] | None = None,
|
| 718 |
+
require_save: bool = True,
|
| 719 |
+
extension: str = ".txt",
|
| 720 |
+
filename: str | cabc.Iterable[str] | None = None,
|
| 721 |
+
) -> None: ...
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
def edit(
|
| 725 |
+
text: str | bytes | bytearray | None = None,
|
| 726 |
+
editor: str | None = None,
|
| 727 |
+
env: cabc.Mapping[str, str] | None = None,
|
| 728 |
+
require_save: bool = True,
|
| 729 |
+
extension: str = ".txt",
|
| 730 |
+
filename: str | cabc.Iterable[str] | None = None,
|
| 731 |
+
) -> str | bytes | bytearray | None:
|
| 732 |
+
r"""Edits the given text in the defined editor. If an editor is given
|
| 733 |
+
(should be the full path to the executable but the regular operating
|
| 734 |
+
system search path is used for finding the executable) it overrides
|
| 735 |
+
the detected editor. Optionally, some environment variables can be
|
| 736 |
+
used. If the editor is closed without changes, `None` is returned. In
|
| 737 |
+
case a file is edited directly the return value is always `None` and
|
| 738 |
+
`require_save` and `extension` are ignored.
|
| 739 |
+
|
| 740 |
+
If the editor cannot be opened a :exc:`UsageError` is raised.
|
| 741 |
+
|
| 742 |
+
Note for Windows: to simplify cross-platform usage, the newlines are
|
| 743 |
+
automatically converted from POSIX to Windows and vice versa. As such,
|
| 744 |
+
the message here will have ``\n`` as newline markers.
|
| 745 |
+
|
| 746 |
+
:param text: the text to edit.
|
| 747 |
+
:param editor: optionally the editor to use. Defaults to automatic
|
| 748 |
+
detection.
|
| 749 |
+
:param env: environment variables to forward to the editor.
|
| 750 |
+
:param require_save: if this is true, then not saving in the editor
|
| 751 |
+
will make the return value become `None`.
|
| 752 |
+
:param extension: the extension to tell the editor about. This defaults
|
| 753 |
+
to `.txt` but changing this might change syntax
|
| 754 |
+
highlighting.
|
| 755 |
+
:param filename: if provided it will edit this file instead of the
|
| 756 |
+
provided text contents. It will not use a temporary
|
| 757 |
+
file as an indirection in that case. If the editor supports
|
| 758 |
+
editing multiple files at once, a sequence of files may be
|
| 759 |
+
passed as well. Invoke `click.file` once per file instead
|
| 760 |
+
if multiple files cannot be managed at once or editing the
|
| 761 |
+
files serially is desired.
|
| 762 |
+
|
| 763 |
+
.. versionchanged:: 8.2.0
|
| 764 |
+
``filename`` now accepts any ``Iterable[str]`` in addition to a ``str``
|
| 765 |
+
if the ``editor`` supports editing multiple files at once.
|
| 766 |
+
|
| 767 |
+
"""
|
| 768 |
+
from ._termui_impl import Editor
|
| 769 |
+
|
| 770 |
+
ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension)
|
| 771 |
+
|
| 772 |
+
if filename is None:
|
| 773 |
+
return ed.edit(text)
|
| 774 |
+
|
| 775 |
+
if isinstance(filename, str):
|
| 776 |
+
filename = (filename,)
|
| 777 |
+
|
| 778 |
+
ed.edit_files(filenames=filename)
|
| 779 |
+
return None
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
def launch(url: str, wait: bool = False, locate: bool = False) -> int:
|
| 783 |
+
"""This function launches the given URL (or filename) in the default
|
| 784 |
+
viewer application for this file type. If this is an executable, it
|
| 785 |
+
might launch the executable in a new session. The return value is
|
| 786 |
+
the exit code of the launched application. Usually, ``0`` indicates
|
| 787 |
+
success.
|
| 788 |
+
|
| 789 |
+
Examples::
|
| 790 |
+
|
| 791 |
+
click.launch('https://click.palletsprojects.com/')
|
| 792 |
+
click.launch('/my/downloaded/file', locate=True)
|
| 793 |
+
|
| 794 |
+
.. versionadded:: 2.0
|
| 795 |
+
|
| 796 |
+
:param url: URL or filename of the thing to launch.
|
| 797 |
+
:param wait: Wait for the program to exit before returning. This
|
| 798 |
+
only works if the launched program blocks. In particular,
|
| 799 |
+
``xdg-open`` on Linux does not block.
|
| 800 |
+
:param locate: if this is set to `True` then instead of launching the
|
| 801 |
+
application associated with the URL it will attempt to
|
| 802 |
+
launch a file manager with the file located. This
|
| 803 |
+
might have weird effects if the URL does not point to
|
| 804 |
+
the filesystem.
|
| 805 |
+
"""
|
| 806 |
+
from ._termui_impl import open_url
|
| 807 |
+
|
| 808 |
+
return open_url(url, wait=wait, locate=locate)
|
| 809 |
+
|
| 810 |
+
|
| 811 |
+
# If this is provided, getchar() calls into this instead. This is used
|
| 812 |
+
# for unittesting purposes.
|
| 813 |
+
_getchar: t.Callable[[bool], str] | None = None
|
| 814 |
+
|
| 815 |
+
|
| 816 |
+
def getchar(echo: bool = False) -> str:
|
| 817 |
+
"""Fetches a single character from the terminal and returns it. This
|
| 818 |
+
will always return a unicode character and under certain rare
|
| 819 |
+
circumstances this might return more than one character. The
|
| 820 |
+
situations which more than one character is returned is when for
|
| 821 |
+
whatever reason multiple characters end up in the terminal buffer or
|
| 822 |
+
standard input was not actually a terminal.
|
| 823 |
+
|
| 824 |
+
Note that this will always read from the terminal, even if something
|
| 825 |
+
is piped into the standard input.
|
| 826 |
+
|
| 827 |
+
Note for Windows: in rare cases when typing non-ASCII characters, this
|
| 828 |
+
function might wait for a second character and then return both at once.
|
| 829 |
+
This is because certain Unicode characters look like special-key markers.
|
| 830 |
+
|
| 831 |
+
.. versionadded:: 2.0
|
| 832 |
+
|
| 833 |
+
:param echo: if set to `True`, the character read will also show up on
|
| 834 |
+
the terminal. The default is to not show it.
|
| 835 |
+
"""
|
| 836 |
+
global _getchar
|
| 837 |
+
|
| 838 |
+
if _getchar is None:
|
| 839 |
+
from ._termui_impl import getchar as f
|
| 840 |
+
|
| 841 |
+
_getchar = f
|
| 842 |
+
|
| 843 |
+
return _getchar(echo)
|
| 844 |
+
|
| 845 |
+
|
| 846 |
+
def raw_terminal() -> AbstractContextManager[int]:
|
| 847 |
+
from ._termui_impl import raw_terminal as f
|
| 848 |
+
|
| 849 |
+
return f()
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
def pause(info: str | None = None, err: bool = False) -> None:
|
| 853 |
+
"""This command stops execution and waits for the user to press any
|
| 854 |
+
key to continue. This is similar to the Windows batch "pause"
|
| 855 |
+
command. If the program is not run through a terminal, this command
|
| 856 |
+
will instead do nothing.
|
| 857 |
+
|
| 858 |
+
.. versionadded:: 2.0
|
| 859 |
+
|
| 860 |
+
.. versionadded:: 4.0
|
| 861 |
+
Added the `err` parameter.
|
| 862 |
+
|
| 863 |
+
:param info: The message to print before pausing. Defaults to
|
| 864 |
+
``"Press any key to continue..."``.
|
| 865 |
+
:param err: if set to message goes to ``stderr`` instead of
|
| 866 |
+
``stdout``, the same as with echo.
|
| 867 |
+
"""
|
| 868 |
+
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
| 869 |
+
return
|
| 870 |
+
|
| 871 |
+
if info is None:
|
| 872 |
+
info = _("Press any key to continue...")
|
| 873 |
+
|
| 874 |
+
try:
|
| 875 |
+
if info:
|
| 876 |
+
echo(info, nl=False, err=err)
|
| 877 |
+
try:
|
| 878 |
+
getchar()
|
| 879 |
+
except (KeyboardInterrupt, EOFError):
|
| 880 |
+
pass
|
| 881 |
+
finally:
|
| 882 |
+
if info:
|
| 883 |
+
echo(err=err)
|
env/lib/python3.13/site-packages/click/testing.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
import contextlib
|
| 5 |
+
import io
|
| 6 |
+
import os
|
| 7 |
+
import shlex
|
| 8 |
+
import sys
|
| 9 |
+
import tempfile
|
| 10 |
+
import typing as t
|
| 11 |
+
from types import TracebackType
|
| 12 |
+
|
| 13 |
+
from . import _compat
|
| 14 |
+
from . import formatting
|
| 15 |
+
from . import termui
|
| 16 |
+
from . import utils
|
| 17 |
+
from ._compat import _find_binary_reader
|
| 18 |
+
|
| 19 |
+
if t.TYPE_CHECKING:
|
| 20 |
+
from _typeshed import ReadableBuffer
|
| 21 |
+
|
| 22 |
+
from .core import Command
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class EchoingStdin:
|
| 26 |
+
def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
|
| 27 |
+
self._input = input
|
| 28 |
+
self._output = output
|
| 29 |
+
self._paused = False
|
| 30 |
+
|
| 31 |
+
def __getattr__(self, x: str) -> t.Any:
|
| 32 |
+
return getattr(self._input, x)
|
| 33 |
+
|
| 34 |
+
def _echo(self, rv: bytes) -> bytes:
|
| 35 |
+
if not self._paused:
|
| 36 |
+
self._output.write(rv)
|
| 37 |
+
|
| 38 |
+
return rv
|
| 39 |
+
|
| 40 |
+
def read(self, n: int = -1) -> bytes:
|
| 41 |
+
return self._echo(self._input.read(n))
|
| 42 |
+
|
| 43 |
+
def read1(self, n: int = -1) -> bytes:
|
| 44 |
+
return self._echo(self._input.read1(n)) # type: ignore
|
| 45 |
+
|
| 46 |
+
def readline(self, n: int = -1) -> bytes:
|
| 47 |
+
return self._echo(self._input.readline(n))
|
| 48 |
+
|
| 49 |
+
def readlines(self) -> list[bytes]:
|
| 50 |
+
return [self._echo(x) for x in self._input.readlines()]
|
| 51 |
+
|
| 52 |
+
def __iter__(self) -> cabc.Iterator[bytes]:
|
| 53 |
+
return iter(self._echo(x) for x in self._input)
|
| 54 |
+
|
| 55 |
+
def __repr__(self) -> str:
|
| 56 |
+
return repr(self._input)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@contextlib.contextmanager
|
| 60 |
+
def _pause_echo(stream: EchoingStdin | None) -> cabc.Iterator[None]:
|
| 61 |
+
if stream is None:
|
| 62 |
+
yield
|
| 63 |
+
else:
|
| 64 |
+
stream._paused = True
|
| 65 |
+
yield
|
| 66 |
+
stream._paused = False
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class BytesIOCopy(io.BytesIO):
|
| 70 |
+
"""Patch ``io.BytesIO`` to let the written stream be copied to another.
|
| 71 |
+
|
| 72 |
+
.. versionadded:: 8.2
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
def __init__(self, copy_to: io.BytesIO) -> None:
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.copy_to = copy_to
|
| 78 |
+
|
| 79 |
+
def flush(self) -> None:
|
| 80 |
+
super().flush()
|
| 81 |
+
self.copy_to.flush()
|
| 82 |
+
|
| 83 |
+
def write(self, b: ReadableBuffer) -> int:
|
| 84 |
+
self.copy_to.write(b)
|
| 85 |
+
return super().write(b)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class StreamMixer:
|
| 89 |
+
"""Mixes `<stdout>` and `<stderr>` streams.
|
| 90 |
+
|
| 91 |
+
The result is available in the ``output`` attribute.
|
| 92 |
+
|
| 93 |
+
.. versionadded:: 8.2
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
def __init__(self) -> None:
|
| 97 |
+
self.output: io.BytesIO = io.BytesIO()
|
| 98 |
+
self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output)
|
| 99 |
+
self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output)
|
| 100 |
+
|
| 101 |
+
def __del__(self) -> None:
|
| 102 |
+
"""
|
| 103 |
+
Guarantee that embedded file-like objects are closed in a
|
| 104 |
+
predictable order, protecting against races between
|
| 105 |
+
self.output being closed and other streams being flushed on close
|
| 106 |
+
|
| 107 |
+
.. versionadded:: 8.2.2
|
| 108 |
+
"""
|
| 109 |
+
self.stderr.close()
|
| 110 |
+
self.stdout.close()
|
| 111 |
+
self.output.close()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class _NamedTextIOWrapper(io.TextIOWrapper):
|
| 115 |
+
def __init__(
|
| 116 |
+
self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any
|
| 117 |
+
) -> None:
|
| 118 |
+
super().__init__(buffer, **kwargs)
|
| 119 |
+
self._name = name
|
| 120 |
+
self._mode = mode
|
| 121 |
+
|
| 122 |
+
@property
|
| 123 |
+
def name(self) -> str:
|
| 124 |
+
return self._name
|
| 125 |
+
|
| 126 |
+
@property
|
| 127 |
+
def mode(self) -> str:
|
| 128 |
+
return self._mode
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def make_input_stream(
|
| 132 |
+
input: str | bytes | t.IO[t.Any] | None, charset: str
|
| 133 |
+
) -> t.BinaryIO:
|
| 134 |
+
# Is already an input stream.
|
| 135 |
+
if hasattr(input, "read"):
|
| 136 |
+
rv = _find_binary_reader(t.cast("t.IO[t.Any]", input))
|
| 137 |
+
|
| 138 |
+
if rv is not None:
|
| 139 |
+
return rv
|
| 140 |
+
|
| 141 |
+
raise TypeError("Could not find binary reader for input stream.")
|
| 142 |
+
|
| 143 |
+
if input is None:
|
| 144 |
+
input = b""
|
| 145 |
+
elif isinstance(input, str):
|
| 146 |
+
input = input.encode(charset)
|
| 147 |
+
|
| 148 |
+
return io.BytesIO(input)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class Result:
|
| 152 |
+
"""Holds the captured result of an invoked CLI script.
|
| 153 |
+
|
| 154 |
+
:param runner: The runner that created the result
|
| 155 |
+
:param stdout_bytes: The standard output as bytes.
|
| 156 |
+
:param stderr_bytes: The standard error as bytes.
|
| 157 |
+
:param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the
|
| 158 |
+
user would see it in its terminal.
|
| 159 |
+
:param return_value: The value returned from the invoked command.
|
| 160 |
+
:param exit_code: The exit code as integer.
|
| 161 |
+
:param exception: The exception that happened if one did.
|
| 162 |
+
:param exc_info: Exception information (exception type, exception instance,
|
| 163 |
+
traceback type).
|
| 164 |
+
|
| 165 |
+
.. versionchanged:: 8.2
|
| 166 |
+
``stderr_bytes`` no longer optional, ``output_bytes`` introduced and
|
| 167 |
+
``mix_stderr`` has been removed.
|
| 168 |
+
|
| 169 |
+
.. versionadded:: 8.0
|
| 170 |
+
Added ``return_value``.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
def __init__(
|
| 174 |
+
self,
|
| 175 |
+
runner: CliRunner,
|
| 176 |
+
stdout_bytes: bytes,
|
| 177 |
+
stderr_bytes: bytes,
|
| 178 |
+
output_bytes: bytes,
|
| 179 |
+
return_value: t.Any,
|
| 180 |
+
exit_code: int,
|
| 181 |
+
exception: BaseException | None,
|
| 182 |
+
exc_info: tuple[type[BaseException], BaseException, TracebackType]
|
| 183 |
+
| None = None,
|
| 184 |
+
):
|
| 185 |
+
self.runner = runner
|
| 186 |
+
self.stdout_bytes = stdout_bytes
|
| 187 |
+
self.stderr_bytes = stderr_bytes
|
| 188 |
+
self.output_bytes = output_bytes
|
| 189 |
+
self.return_value = return_value
|
| 190 |
+
self.exit_code = exit_code
|
| 191 |
+
self.exception = exception
|
| 192 |
+
self.exc_info = exc_info
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def output(self) -> str:
|
| 196 |
+
"""The terminal output as unicode string, as the user would see it.
|
| 197 |
+
|
| 198 |
+
.. versionchanged:: 8.2
|
| 199 |
+
No longer a proxy for ``self.stdout``. Now has its own independent stream
|
| 200 |
+
that is mixing `<stdout>` and `<stderr>`, in the order they were written.
|
| 201 |
+
"""
|
| 202 |
+
return self.output_bytes.decode(self.runner.charset, "replace").replace(
|
| 203 |
+
"\r\n", "\n"
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def stdout(self) -> str:
|
| 208 |
+
"""The standard output as unicode string."""
|
| 209 |
+
return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
|
| 210 |
+
"\r\n", "\n"
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
@property
|
| 214 |
+
def stderr(self) -> str:
|
| 215 |
+
"""The standard error as unicode string.
|
| 216 |
+
|
| 217 |
+
.. versionchanged:: 8.2
|
| 218 |
+
No longer raise an exception, always returns the `<stderr>` string.
|
| 219 |
+
"""
|
| 220 |
+
return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
|
| 221 |
+
"\r\n", "\n"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
def __repr__(self) -> str:
|
| 225 |
+
exc_str = repr(self.exception) if self.exception else "okay"
|
| 226 |
+
return f"<{type(self).__name__} {exc_str}>"
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
class CliRunner:
|
| 230 |
+
"""The CLI runner provides functionality to invoke a Click command line
|
| 231 |
+
script for unittesting purposes in a isolated environment. This only
|
| 232 |
+
works in single-threaded systems without any concurrency as it changes the
|
| 233 |
+
global interpreter state.
|
| 234 |
+
|
| 235 |
+
:param charset: the character set for the input and output data.
|
| 236 |
+
:param env: a dictionary with environment variables for overriding.
|
| 237 |
+
:param echo_stdin: if this is set to `True`, then reading from `<stdin>` writes
|
| 238 |
+
to `<stdout>`. This is useful for showing examples in
|
| 239 |
+
some circumstances. Note that regular prompts
|
| 240 |
+
will automatically echo the input.
|
| 241 |
+
:param catch_exceptions: Whether to catch any exceptions other than
|
| 242 |
+
``SystemExit`` when running :meth:`~CliRunner.invoke`.
|
| 243 |
+
|
| 244 |
+
.. versionchanged:: 8.2
|
| 245 |
+
Added the ``catch_exceptions`` parameter.
|
| 246 |
+
|
| 247 |
+
.. versionchanged:: 8.2
|
| 248 |
+
``mix_stderr`` parameter has been removed.
|
| 249 |
+
"""
|
| 250 |
+
|
| 251 |
+
def __init__(
|
| 252 |
+
self,
|
| 253 |
+
charset: str = "utf-8",
|
| 254 |
+
env: cabc.Mapping[str, str | None] | None = None,
|
| 255 |
+
echo_stdin: bool = False,
|
| 256 |
+
catch_exceptions: bool = True,
|
| 257 |
+
) -> None:
|
| 258 |
+
self.charset = charset
|
| 259 |
+
self.env: cabc.Mapping[str, str | None] = env or {}
|
| 260 |
+
self.echo_stdin = echo_stdin
|
| 261 |
+
self.catch_exceptions = catch_exceptions
|
| 262 |
+
|
| 263 |
+
def get_default_prog_name(self, cli: Command) -> str:
|
| 264 |
+
"""Given a command object it will return the default program name
|
| 265 |
+
for it. The default is the `name` attribute or ``"root"`` if not
|
| 266 |
+
set.
|
| 267 |
+
"""
|
| 268 |
+
return cli.name or "root"
|
| 269 |
+
|
| 270 |
+
def make_env(
|
| 271 |
+
self, overrides: cabc.Mapping[str, str | None] | None = None
|
| 272 |
+
) -> cabc.Mapping[str, str | None]:
|
| 273 |
+
"""Returns the environment overrides for invoking a script."""
|
| 274 |
+
rv = dict(self.env)
|
| 275 |
+
if overrides:
|
| 276 |
+
rv.update(overrides)
|
| 277 |
+
return rv
|
| 278 |
+
|
| 279 |
+
@contextlib.contextmanager
|
| 280 |
+
def isolation(
|
| 281 |
+
self,
|
| 282 |
+
input: str | bytes | t.IO[t.Any] | None = None,
|
| 283 |
+
env: cabc.Mapping[str, str | None] | None = None,
|
| 284 |
+
color: bool = False,
|
| 285 |
+
) -> cabc.Iterator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]:
|
| 286 |
+
"""A context manager that sets up the isolation for invoking of a
|
| 287 |
+
command line tool. This sets up `<stdin>` with the given input data
|
| 288 |
+
and `os.environ` with the overrides from the given dictionary.
|
| 289 |
+
This also rebinds some internals in Click to be mocked (like the
|
| 290 |
+
prompt functionality).
|
| 291 |
+
|
| 292 |
+
This is automatically done in the :meth:`invoke` method.
|
| 293 |
+
|
| 294 |
+
:param input: the input stream to put into `sys.stdin`.
|
| 295 |
+
:param env: the environment overrides as dictionary.
|
| 296 |
+
:param color: whether the output should contain color codes. The
|
| 297 |
+
application can still override this explicitly.
|
| 298 |
+
|
| 299 |
+
.. versionadded:: 8.2
|
| 300 |
+
An additional output stream is returned, which is a mix of
|
| 301 |
+
`<stdout>` and `<stderr>` streams.
|
| 302 |
+
|
| 303 |
+
.. versionchanged:: 8.2
|
| 304 |
+
Always returns the `<stderr>` stream.
|
| 305 |
+
|
| 306 |
+
.. versionchanged:: 8.0
|
| 307 |
+
`<stderr>` is opened with ``errors="backslashreplace"``
|
| 308 |
+
instead of the default ``"strict"``.
|
| 309 |
+
|
| 310 |
+
.. versionchanged:: 4.0
|
| 311 |
+
Added the ``color`` parameter.
|
| 312 |
+
"""
|
| 313 |
+
bytes_input = make_input_stream(input, self.charset)
|
| 314 |
+
echo_input = None
|
| 315 |
+
|
| 316 |
+
old_stdin = sys.stdin
|
| 317 |
+
old_stdout = sys.stdout
|
| 318 |
+
old_stderr = sys.stderr
|
| 319 |
+
old_forced_width = formatting.FORCED_WIDTH
|
| 320 |
+
formatting.FORCED_WIDTH = 80
|
| 321 |
+
|
| 322 |
+
env = self.make_env(env)
|
| 323 |
+
|
| 324 |
+
stream_mixer = StreamMixer()
|
| 325 |
+
|
| 326 |
+
if self.echo_stdin:
|
| 327 |
+
bytes_input = echo_input = t.cast(
|
| 328 |
+
t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout)
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
sys.stdin = text_input = _NamedTextIOWrapper(
|
| 332 |
+
bytes_input, encoding=self.charset, name="<stdin>", mode="r"
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
if self.echo_stdin:
|
| 336 |
+
# Force unbuffered reads, otherwise TextIOWrapper reads a
|
| 337 |
+
# large chunk which is echoed early.
|
| 338 |
+
text_input._CHUNK_SIZE = 1 # type: ignore
|
| 339 |
+
|
| 340 |
+
sys.stdout = _NamedTextIOWrapper(
|
| 341 |
+
stream_mixer.stdout, encoding=self.charset, name="<stdout>", mode="w"
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
sys.stderr = _NamedTextIOWrapper(
|
| 345 |
+
stream_mixer.stderr,
|
| 346 |
+
encoding=self.charset,
|
| 347 |
+
name="<stderr>",
|
| 348 |
+
mode="w",
|
| 349 |
+
errors="backslashreplace",
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
@_pause_echo(echo_input) # type: ignore
|
| 353 |
+
def visible_input(prompt: str | None = None) -> str:
|
| 354 |
+
sys.stdout.write(prompt or "")
|
| 355 |
+
try:
|
| 356 |
+
val = next(text_input).rstrip("\r\n")
|
| 357 |
+
except StopIteration as e:
|
| 358 |
+
raise EOFError() from e
|
| 359 |
+
sys.stdout.write(f"{val}\n")
|
| 360 |
+
sys.stdout.flush()
|
| 361 |
+
return val
|
| 362 |
+
|
| 363 |
+
@_pause_echo(echo_input) # type: ignore
|
| 364 |
+
def hidden_input(prompt: str | None = None) -> str:
|
| 365 |
+
sys.stdout.write(f"{prompt or ''}\n")
|
| 366 |
+
sys.stdout.flush()
|
| 367 |
+
try:
|
| 368 |
+
return next(text_input).rstrip("\r\n")
|
| 369 |
+
except StopIteration as e:
|
| 370 |
+
raise EOFError() from e
|
| 371 |
+
|
| 372 |
+
@_pause_echo(echo_input) # type: ignore
|
| 373 |
+
def _getchar(echo: bool) -> str:
|
| 374 |
+
char = sys.stdin.read(1)
|
| 375 |
+
|
| 376 |
+
if echo:
|
| 377 |
+
sys.stdout.write(char)
|
| 378 |
+
|
| 379 |
+
sys.stdout.flush()
|
| 380 |
+
return char
|
| 381 |
+
|
| 382 |
+
default_color = color
|
| 383 |
+
|
| 384 |
+
def should_strip_ansi(
|
| 385 |
+
stream: t.IO[t.Any] | None = None, color: bool | None = None
|
| 386 |
+
) -> bool:
|
| 387 |
+
if color is None:
|
| 388 |
+
return not default_color
|
| 389 |
+
return not color
|
| 390 |
+
|
| 391 |
+
old_visible_prompt_func = termui.visible_prompt_func
|
| 392 |
+
old_hidden_prompt_func = termui.hidden_prompt_func
|
| 393 |
+
old__getchar_func = termui._getchar
|
| 394 |
+
old_should_strip_ansi = utils.should_strip_ansi # type: ignore
|
| 395 |
+
old__compat_should_strip_ansi = _compat.should_strip_ansi
|
| 396 |
+
termui.visible_prompt_func = visible_input
|
| 397 |
+
termui.hidden_prompt_func = hidden_input
|
| 398 |
+
termui._getchar = _getchar
|
| 399 |
+
utils.should_strip_ansi = should_strip_ansi # type: ignore
|
| 400 |
+
_compat.should_strip_ansi = should_strip_ansi
|
| 401 |
+
|
| 402 |
+
old_env = {}
|
| 403 |
+
try:
|
| 404 |
+
for key, value in env.items():
|
| 405 |
+
old_env[key] = os.environ.get(key)
|
| 406 |
+
if value is None:
|
| 407 |
+
try:
|
| 408 |
+
del os.environ[key]
|
| 409 |
+
except Exception:
|
| 410 |
+
pass
|
| 411 |
+
else:
|
| 412 |
+
os.environ[key] = value
|
| 413 |
+
yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output)
|
| 414 |
+
finally:
|
| 415 |
+
for key, value in old_env.items():
|
| 416 |
+
if value is None:
|
| 417 |
+
try:
|
| 418 |
+
del os.environ[key]
|
| 419 |
+
except Exception:
|
| 420 |
+
pass
|
| 421 |
+
else:
|
| 422 |
+
os.environ[key] = value
|
| 423 |
+
sys.stdout = old_stdout
|
| 424 |
+
sys.stderr = old_stderr
|
| 425 |
+
sys.stdin = old_stdin
|
| 426 |
+
termui.visible_prompt_func = old_visible_prompt_func
|
| 427 |
+
termui.hidden_prompt_func = old_hidden_prompt_func
|
| 428 |
+
termui._getchar = old__getchar_func
|
| 429 |
+
utils.should_strip_ansi = old_should_strip_ansi # type: ignore
|
| 430 |
+
_compat.should_strip_ansi = old__compat_should_strip_ansi
|
| 431 |
+
formatting.FORCED_WIDTH = old_forced_width
|
| 432 |
+
|
| 433 |
+
def invoke(
|
| 434 |
+
self,
|
| 435 |
+
cli: Command,
|
| 436 |
+
args: str | cabc.Sequence[str] | None = None,
|
| 437 |
+
input: str | bytes | t.IO[t.Any] | None = None,
|
| 438 |
+
env: cabc.Mapping[str, str | None] | None = None,
|
| 439 |
+
catch_exceptions: bool | None = None,
|
| 440 |
+
color: bool = False,
|
| 441 |
+
**extra: t.Any,
|
| 442 |
+
) -> Result:
|
| 443 |
+
"""Invokes a command in an isolated environment. The arguments are
|
| 444 |
+
forwarded directly to the command line script, the `extra` keyword
|
| 445 |
+
arguments are passed to the :meth:`~clickpkg.Command.main` function of
|
| 446 |
+
the command.
|
| 447 |
+
|
| 448 |
+
This returns a :class:`Result` object.
|
| 449 |
+
|
| 450 |
+
:param cli: the command to invoke
|
| 451 |
+
:param args: the arguments to invoke. It may be given as an iterable
|
| 452 |
+
or a string. When given as string it will be interpreted
|
| 453 |
+
as a Unix shell command. More details at
|
| 454 |
+
:func:`shlex.split`.
|
| 455 |
+
:param input: the input data for `sys.stdin`.
|
| 456 |
+
:param env: the environment overrides.
|
| 457 |
+
:param catch_exceptions: Whether to catch any other exceptions than
|
| 458 |
+
``SystemExit``. If :data:`None`, the value
|
| 459 |
+
from :class:`CliRunner` is used.
|
| 460 |
+
:param extra: the keyword arguments to pass to :meth:`main`.
|
| 461 |
+
:param color: whether the output should contain color codes. The
|
| 462 |
+
application can still override this explicitly.
|
| 463 |
+
|
| 464 |
+
.. versionadded:: 8.2
|
| 465 |
+
The result object has the ``output_bytes`` attribute with
|
| 466 |
+
the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would
|
| 467 |
+
see it in its terminal.
|
| 468 |
+
|
| 469 |
+
.. versionchanged:: 8.2
|
| 470 |
+
The result object always returns the ``stderr_bytes`` stream.
|
| 471 |
+
|
| 472 |
+
.. versionchanged:: 8.0
|
| 473 |
+
The result object has the ``return_value`` attribute with
|
| 474 |
+
the value returned from the invoked command.
|
| 475 |
+
|
| 476 |
+
.. versionchanged:: 4.0
|
| 477 |
+
Added the ``color`` parameter.
|
| 478 |
+
|
| 479 |
+
.. versionchanged:: 3.0
|
| 480 |
+
Added the ``catch_exceptions`` parameter.
|
| 481 |
+
|
| 482 |
+
.. versionchanged:: 3.0
|
| 483 |
+
The result object has the ``exc_info`` attribute with the
|
| 484 |
+
traceback if available.
|
| 485 |
+
"""
|
| 486 |
+
exc_info = None
|
| 487 |
+
if catch_exceptions is None:
|
| 488 |
+
catch_exceptions = self.catch_exceptions
|
| 489 |
+
|
| 490 |
+
with self.isolation(input=input, env=env, color=color) as outstreams:
|
| 491 |
+
return_value = None
|
| 492 |
+
exception: BaseException | None = None
|
| 493 |
+
exit_code = 0
|
| 494 |
+
|
| 495 |
+
if isinstance(args, str):
|
| 496 |
+
args = shlex.split(args)
|
| 497 |
+
|
| 498 |
+
try:
|
| 499 |
+
prog_name = extra.pop("prog_name")
|
| 500 |
+
except KeyError:
|
| 501 |
+
prog_name = self.get_default_prog_name(cli)
|
| 502 |
+
|
| 503 |
+
try:
|
| 504 |
+
return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
|
| 505 |
+
except SystemExit as e:
|
| 506 |
+
exc_info = sys.exc_info()
|
| 507 |
+
e_code = t.cast("int | t.Any | None", e.code)
|
| 508 |
+
|
| 509 |
+
if e_code is None:
|
| 510 |
+
e_code = 0
|
| 511 |
+
|
| 512 |
+
if e_code != 0:
|
| 513 |
+
exception = e
|
| 514 |
+
|
| 515 |
+
if not isinstance(e_code, int):
|
| 516 |
+
sys.stdout.write(str(e_code))
|
| 517 |
+
sys.stdout.write("\n")
|
| 518 |
+
e_code = 1
|
| 519 |
+
|
| 520 |
+
exit_code = e_code
|
| 521 |
+
|
| 522 |
+
except Exception as e:
|
| 523 |
+
if not catch_exceptions:
|
| 524 |
+
raise
|
| 525 |
+
exception = e
|
| 526 |
+
exit_code = 1
|
| 527 |
+
exc_info = sys.exc_info()
|
| 528 |
+
finally:
|
| 529 |
+
sys.stdout.flush()
|
| 530 |
+
sys.stderr.flush()
|
| 531 |
+
stdout = outstreams[0].getvalue()
|
| 532 |
+
stderr = outstreams[1].getvalue()
|
| 533 |
+
output = outstreams[2].getvalue()
|
| 534 |
+
|
| 535 |
+
return Result(
|
| 536 |
+
runner=self,
|
| 537 |
+
stdout_bytes=stdout,
|
| 538 |
+
stderr_bytes=stderr,
|
| 539 |
+
output_bytes=output,
|
| 540 |
+
return_value=return_value,
|
| 541 |
+
exit_code=exit_code,
|
| 542 |
+
exception=exception,
|
| 543 |
+
exc_info=exc_info, # type: ignore
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
@contextlib.contextmanager
|
| 547 |
+
def isolated_filesystem(
|
| 548 |
+
self, temp_dir: str | os.PathLike[str] | None = None
|
| 549 |
+
) -> cabc.Iterator[str]:
|
| 550 |
+
"""A context manager that creates a temporary directory and
|
| 551 |
+
changes the current working directory to it. This isolates tests
|
| 552 |
+
that affect the contents of the CWD to prevent them from
|
| 553 |
+
interfering with each other.
|
| 554 |
+
|
| 555 |
+
:param temp_dir: Create the temporary directory under this
|
| 556 |
+
directory. If given, the created directory is not removed
|
| 557 |
+
when exiting.
|
| 558 |
+
|
| 559 |
+
.. versionchanged:: 8.0
|
| 560 |
+
Added the ``temp_dir`` parameter.
|
| 561 |
+
"""
|
| 562 |
+
cwd = os.getcwd()
|
| 563 |
+
dt = tempfile.mkdtemp(dir=temp_dir)
|
| 564 |
+
os.chdir(dt)
|
| 565 |
+
|
| 566 |
+
try:
|
| 567 |
+
yield dt
|
| 568 |
+
finally:
|
| 569 |
+
os.chdir(cwd)
|
| 570 |
+
|
| 571 |
+
if temp_dir is None:
|
| 572 |
+
import shutil
|
| 573 |
+
|
| 574 |
+
try:
|
| 575 |
+
shutil.rmtree(dt)
|
| 576 |
+
except OSError:
|
| 577 |
+
pass
|
env/lib/python3.13/site-packages/click/types.py
ADDED
|
@@ -0,0 +1,1209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
import enum
|
| 5 |
+
import os
|
| 6 |
+
import stat
|
| 7 |
+
import sys
|
| 8 |
+
import typing as t
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from gettext import gettext as _
|
| 11 |
+
from gettext import ngettext
|
| 12 |
+
|
| 13 |
+
from ._compat import _get_argv_encoding
|
| 14 |
+
from ._compat import open_stream
|
| 15 |
+
from .exceptions import BadParameter
|
| 16 |
+
from .utils import format_filename
|
| 17 |
+
from .utils import LazyFile
|
| 18 |
+
from .utils import safecall
|
| 19 |
+
|
| 20 |
+
if t.TYPE_CHECKING:
|
| 21 |
+
import typing_extensions as te
|
| 22 |
+
|
| 23 |
+
from .core import Context
|
| 24 |
+
from .core import Parameter
|
| 25 |
+
from .shell_completion import CompletionItem
|
| 26 |
+
|
| 27 |
+
ParamTypeValue = t.TypeVar("ParamTypeValue")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ParamType:
|
| 31 |
+
"""Represents the type of a parameter. Validates and converts values
|
| 32 |
+
from the command line or Python into the correct type.
|
| 33 |
+
|
| 34 |
+
To implement a custom type, subclass and implement at least the
|
| 35 |
+
following:
|
| 36 |
+
|
| 37 |
+
- The :attr:`name` class attribute must be set.
|
| 38 |
+
- Calling an instance of the type with ``None`` must return
|
| 39 |
+
``None``. This is already implemented by default.
|
| 40 |
+
- :meth:`convert` must convert string values to the correct type.
|
| 41 |
+
- :meth:`convert` must accept values that are already the correct
|
| 42 |
+
type.
|
| 43 |
+
- It must be able to convert a value if the ``ctx`` and ``param``
|
| 44 |
+
arguments are ``None``. This can occur when converting prompt
|
| 45 |
+
input.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
is_composite: t.ClassVar[bool] = False
|
| 49 |
+
arity: t.ClassVar[int] = 1
|
| 50 |
+
|
| 51 |
+
#: the descriptive name of this type
|
| 52 |
+
name: str
|
| 53 |
+
|
| 54 |
+
#: if a list of this type is expected and the value is pulled from a
|
| 55 |
+
#: string environment variable, this is what splits it up. `None`
|
| 56 |
+
#: means any whitespace. For all parameters the general rule is that
|
| 57 |
+
#: whitespace splits them up. The exception are paths and files which
|
| 58 |
+
#: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
|
| 59 |
+
#: Windows).
|
| 60 |
+
envvar_list_splitter: t.ClassVar[str | None] = None
|
| 61 |
+
|
| 62 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 63 |
+
"""Gather information that could be useful for a tool generating
|
| 64 |
+
user-facing documentation.
|
| 65 |
+
|
| 66 |
+
Use :meth:`click.Context.to_info_dict` to traverse the entire
|
| 67 |
+
CLI structure.
|
| 68 |
+
|
| 69 |
+
.. versionadded:: 8.0
|
| 70 |
+
"""
|
| 71 |
+
# The class name without the "ParamType" suffix.
|
| 72 |
+
param_type = type(self).__name__.partition("ParamType")[0]
|
| 73 |
+
param_type = param_type.partition("ParameterType")[0]
|
| 74 |
+
|
| 75 |
+
# Custom subclasses might not remember to set a name.
|
| 76 |
+
if hasattr(self, "name"):
|
| 77 |
+
name = self.name
|
| 78 |
+
else:
|
| 79 |
+
name = param_type
|
| 80 |
+
|
| 81 |
+
return {"param_type": param_type, "name": name}
|
| 82 |
+
|
| 83 |
+
def __call__(
|
| 84 |
+
self,
|
| 85 |
+
value: t.Any,
|
| 86 |
+
param: Parameter | None = None,
|
| 87 |
+
ctx: Context | None = None,
|
| 88 |
+
) -> t.Any:
|
| 89 |
+
if value is not None:
|
| 90 |
+
return self.convert(value, param, ctx)
|
| 91 |
+
|
| 92 |
+
def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
|
| 93 |
+
"""Returns the metavar default for this param if it provides one."""
|
| 94 |
+
|
| 95 |
+
def get_missing_message(self, param: Parameter, ctx: Context | None) -> str | None:
|
| 96 |
+
"""Optionally might return extra information about a missing
|
| 97 |
+
parameter.
|
| 98 |
+
|
| 99 |
+
.. versionadded:: 2.0
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
def convert(
|
| 103 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 104 |
+
) -> t.Any:
|
| 105 |
+
"""Convert the value to the correct type. This is not called if
|
| 106 |
+
the value is ``None`` (the missing value).
|
| 107 |
+
|
| 108 |
+
This must accept string values from the command line, as well as
|
| 109 |
+
values that are already the correct type. It may also convert
|
| 110 |
+
other compatible types.
|
| 111 |
+
|
| 112 |
+
The ``param`` and ``ctx`` arguments may be ``None`` in certain
|
| 113 |
+
situations, such as when converting prompt input.
|
| 114 |
+
|
| 115 |
+
If the value cannot be converted, call :meth:`fail` with a
|
| 116 |
+
descriptive message.
|
| 117 |
+
|
| 118 |
+
:param value: The value to convert.
|
| 119 |
+
:param param: The parameter that is using this type to convert
|
| 120 |
+
its value. May be ``None``.
|
| 121 |
+
:param ctx: The current context that arrived at this value. May
|
| 122 |
+
be ``None``.
|
| 123 |
+
"""
|
| 124 |
+
return value
|
| 125 |
+
|
| 126 |
+
def split_envvar_value(self, rv: str) -> cabc.Sequence[str]:
|
| 127 |
+
"""Given a value from an environment variable this splits it up
|
| 128 |
+
into small chunks depending on the defined envvar list splitter.
|
| 129 |
+
|
| 130 |
+
If the splitter is set to `None`, which means that whitespace splits,
|
| 131 |
+
then leading and trailing whitespace is ignored. Otherwise, leading
|
| 132 |
+
and trailing splitters usually lead to empty items being included.
|
| 133 |
+
"""
|
| 134 |
+
return (rv or "").split(self.envvar_list_splitter)
|
| 135 |
+
|
| 136 |
+
def fail(
|
| 137 |
+
self,
|
| 138 |
+
message: str,
|
| 139 |
+
param: Parameter | None = None,
|
| 140 |
+
ctx: Context | None = None,
|
| 141 |
+
) -> t.NoReturn:
|
| 142 |
+
"""Helper method to fail with an invalid value message."""
|
| 143 |
+
raise BadParameter(message, ctx=ctx, param=param)
|
| 144 |
+
|
| 145 |
+
def shell_complete(
|
| 146 |
+
self, ctx: Context, param: Parameter, incomplete: str
|
| 147 |
+
) -> list[CompletionItem]:
|
| 148 |
+
"""Return a list of
|
| 149 |
+
:class:`~click.shell_completion.CompletionItem` objects for the
|
| 150 |
+
incomplete value. Most types do not provide completions, but
|
| 151 |
+
some do, and this allows custom types to provide custom
|
| 152 |
+
completions as well.
|
| 153 |
+
|
| 154 |
+
:param ctx: Invocation context for this command.
|
| 155 |
+
:param param: The parameter that is requesting completion.
|
| 156 |
+
:param incomplete: Value being completed. May be empty.
|
| 157 |
+
|
| 158 |
+
.. versionadded:: 8.0
|
| 159 |
+
"""
|
| 160 |
+
return []
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class CompositeParamType(ParamType):
|
| 164 |
+
is_composite = True
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def arity(self) -> int: # type: ignore
|
| 168 |
+
raise NotImplementedError()
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class FuncParamType(ParamType):
|
| 172 |
+
def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None:
|
| 173 |
+
self.name: str = func.__name__
|
| 174 |
+
self.func = func
|
| 175 |
+
|
| 176 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 177 |
+
info_dict = super().to_info_dict()
|
| 178 |
+
info_dict["func"] = self.func
|
| 179 |
+
return info_dict
|
| 180 |
+
|
| 181 |
+
def convert(
|
| 182 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 183 |
+
) -> t.Any:
|
| 184 |
+
try:
|
| 185 |
+
return self.func(value)
|
| 186 |
+
except ValueError:
|
| 187 |
+
try:
|
| 188 |
+
value = str(value)
|
| 189 |
+
except UnicodeError:
|
| 190 |
+
value = value.decode("utf-8", "replace")
|
| 191 |
+
|
| 192 |
+
self.fail(value, param, ctx)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class UnprocessedParamType(ParamType):
|
| 196 |
+
name = "text"
|
| 197 |
+
|
| 198 |
+
def convert(
|
| 199 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 200 |
+
) -> t.Any:
|
| 201 |
+
return value
|
| 202 |
+
|
| 203 |
+
def __repr__(self) -> str:
|
| 204 |
+
return "UNPROCESSED"
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
class StringParamType(ParamType):
|
| 208 |
+
name = "text"
|
| 209 |
+
|
| 210 |
+
def convert(
|
| 211 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 212 |
+
) -> t.Any:
|
| 213 |
+
if isinstance(value, bytes):
|
| 214 |
+
enc = _get_argv_encoding()
|
| 215 |
+
try:
|
| 216 |
+
value = value.decode(enc)
|
| 217 |
+
except UnicodeError:
|
| 218 |
+
fs_enc = sys.getfilesystemencoding()
|
| 219 |
+
if fs_enc != enc:
|
| 220 |
+
try:
|
| 221 |
+
value = value.decode(fs_enc)
|
| 222 |
+
except UnicodeError:
|
| 223 |
+
value = value.decode("utf-8", "replace")
|
| 224 |
+
else:
|
| 225 |
+
value = value.decode("utf-8", "replace")
|
| 226 |
+
return value
|
| 227 |
+
return str(value)
|
| 228 |
+
|
| 229 |
+
def __repr__(self) -> str:
|
| 230 |
+
return "STRING"
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class Choice(ParamType, t.Generic[ParamTypeValue]):
|
| 234 |
+
"""The choice type allows a value to be checked against a fixed set
|
| 235 |
+
of supported values.
|
| 236 |
+
|
| 237 |
+
You may pass any iterable value which will be converted to a tuple
|
| 238 |
+
and thus will only be iterated once.
|
| 239 |
+
|
| 240 |
+
The resulting value will always be one of the originally passed choices.
|
| 241 |
+
See :meth:`normalize_choice` for more info on the mapping of strings
|
| 242 |
+
to choices. See :ref:`choice-opts` for an example.
|
| 243 |
+
|
| 244 |
+
:param case_sensitive: Set to false to make choices case
|
| 245 |
+
insensitive. Defaults to true.
|
| 246 |
+
|
| 247 |
+
.. versionchanged:: 8.2.0
|
| 248 |
+
Non-``str`` ``choices`` are now supported. It can additionally be any
|
| 249 |
+
iterable. Before you were not recommended to pass anything but a list or
|
| 250 |
+
tuple.
|
| 251 |
+
|
| 252 |
+
.. versionadded:: 8.2.0
|
| 253 |
+
Choice normalization can be overridden via :meth:`normalize_choice`.
|
| 254 |
+
"""
|
| 255 |
+
|
| 256 |
+
name = "choice"
|
| 257 |
+
|
| 258 |
+
def __init__(
|
| 259 |
+
self, choices: cabc.Iterable[ParamTypeValue], case_sensitive: bool = True
|
| 260 |
+
) -> None:
|
| 261 |
+
self.choices: cabc.Sequence[ParamTypeValue] = tuple(choices)
|
| 262 |
+
self.case_sensitive = case_sensitive
|
| 263 |
+
|
| 264 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 265 |
+
info_dict = super().to_info_dict()
|
| 266 |
+
info_dict["choices"] = self.choices
|
| 267 |
+
info_dict["case_sensitive"] = self.case_sensitive
|
| 268 |
+
return info_dict
|
| 269 |
+
|
| 270 |
+
def _normalized_mapping(
|
| 271 |
+
self, ctx: Context | None = None
|
| 272 |
+
) -> cabc.Mapping[ParamTypeValue, str]:
|
| 273 |
+
"""
|
| 274 |
+
Returns mapping where keys are the original choices and the values are
|
| 275 |
+
the normalized values that are accepted via the command line.
|
| 276 |
+
|
| 277 |
+
This is a simple wrapper around :meth:`normalize_choice`, use that
|
| 278 |
+
instead which is supported.
|
| 279 |
+
"""
|
| 280 |
+
return {
|
| 281 |
+
choice: self.normalize_choice(
|
| 282 |
+
choice=choice,
|
| 283 |
+
ctx=ctx,
|
| 284 |
+
)
|
| 285 |
+
for choice in self.choices
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
def normalize_choice(self, choice: ParamTypeValue, ctx: Context | None) -> str:
|
| 289 |
+
"""
|
| 290 |
+
Normalize a choice value, used to map a passed string to a choice.
|
| 291 |
+
Each choice must have a unique normalized value.
|
| 292 |
+
|
| 293 |
+
By default uses :meth:`Context.token_normalize_func` and if not case
|
| 294 |
+
sensitive, convert it to a casefolded value.
|
| 295 |
+
|
| 296 |
+
.. versionadded:: 8.2.0
|
| 297 |
+
"""
|
| 298 |
+
normed_value = choice.name if isinstance(choice, enum.Enum) else str(choice)
|
| 299 |
+
|
| 300 |
+
if ctx is not None and ctx.token_normalize_func is not None:
|
| 301 |
+
normed_value = ctx.token_normalize_func(normed_value)
|
| 302 |
+
|
| 303 |
+
if not self.case_sensitive:
|
| 304 |
+
normed_value = normed_value.casefold()
|
| 305 |
+
|
| 306 |
+
return normed_value
|
| 307 |
+
|
| 308 |
+
def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
|
| 309 |
+
if param.param_type_name == "option" and not param.show_choices: # type: ignore
|
| 310 |
+
choice_metavars = [
|
| 311 |
+
convert_type(type(choice)).name.upper() for choice in self.choices
|
| 312 |
+
]
|
| 313 |
+
choices_str = "|".join([*dict.fromkeys(choice_metavars)])
|
| 314 |
+
else:
|
| 315 |
+
choices_str = "|".join(
|
| 316 |
+
[str(i) for i in self._normalized_mapping(ctx=ctx).values()]
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
# Use curly braces to indicate a required argument.
|
| 320 |
+
if param.required and param.param_type_name == "argument":
|
| 321 |
+
return f"{{{choices_str}}}"
|
| 322 |
+
|
| 323 |
+
# Use square braces to indicate an option or optional argument.
|
| 324 |
+
return f"[{choices_str}]"
|
| 325 |
+
|
| 326 |
+
def get_missing_message(self, param: Parameter, ctx: Context | None) -> str:
|
| 327 |
+
"""
|
| 328 |
+
Message shown when no choice is passed.
|
| 329 |
+
|
| 330 |
+
.. versionchanged:: 8.2.0 Added ``ctx`` argument.
|
| 331 |
+
"""
|
| 332 |
+
return _("Choose from:\n\t{choices}").format(
|
| 333 |
+
choices=",\n\t".join(self._normalized_mapping(ctx=ctx).values())
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
def convert(
|
| 337 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 338 |
+
) -> ParamTypeValue:
|
| 339 |
+
"""
|
| 340 |
+
For a given value from the parser, normalize it and find its
|
| 341 |
+
matching normalized value in the list of choices. Then return the
|
| 342 |
+
matched "original" choice.
|
| 343 |
+
"""
|
| 344 |
+
normed_value = self.normalize_choice(choice=value, ctx=ctx)
|
| 345 |
+
normalized_mapping = self._normalized_mapping(ctx=ctx)
|
| 346 |
+
|
| 347 |
+
try:
|
| 348 |
+
return next(
|
| 349 |
+
original
|
| 350 |
+
for original, normalized in normalized_mapping.items()
|
| 351 |
+
if normalized == normed_value
|
| 352 |
+
)
|
| 353 |
+
except StopIteration:
|
| 354 |
+
self.fail(
|
| 355 |
+
self.get_invalid_choice_message(value=value, ctx=ctx),
|
| 356 |
+
param=param,
|
| 357 |
+
ctx=ctx,
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
def get_invalid_choice_message(self, value: t.Any, ctx: Context | None) -> str:
|
| 361 |
+
"""Get the error message when the given choice is invalid.
|
| 362 |
+
|
| 363 |
+
:param value: The invalid value.
|
| 364 |
+
|
| 365 |
+
.. versionadded:: 8.2
|
| 366 |
+
"""
|
| 367 |
+
choices_str = ", ".join(map(repr, self._normalized_mapping(ctx=ctx).values()))
|
| 368 |
+
return ngettext(
|
| 369 |
+
"{value!r} is not {choice}.",
|
| 370 |
+
"{value!r} is not one of {choices}.",
|
| 371 |
+
len(self.choices),
|
| 372 |
+
).format(value=value, choice=choices_str, choices=choices_str)
|
| 373 |
+
|
| 374 |
+
def __repr__(self) -> str:
|
| 375 |
+
return f"Choice({list(self.choices)})"
|
| 376 |
+
|
| 377 |
+
def shell_complete(
|
| 378 |
+
self, ctx: Context, param: Parameter, incomplete: str
|
| 379 |
+
) -> list[CompletionItem]:
|
| 380 |
+
"""Complete choices that start with the incomplete value.
|
| 381 |
+
|
| 382 |
+
:param ctx: Invocation context for this command.
|
| 383 |
+
:param param: The parameter that is requesting completion.
|
| 384 |
+
:param incomplete: Value being completed. May be empty.
|
| 385 |
+
|
| 386 |
+
.. versionadded:: 8.0
|
| 387 |
+
"""
|
| 388 |
+
from click.shell_completion import CompletionItem
|
| 389 |
+
|
| 390 |
+
str_choices = map(str, self.choices)
|
| 391 |
+
|
| 392 |
+
if self.case_sensitive:
|
| 393 |
+
matched = (c for c in str_choices if c.startswith(incomplete))
|
| 394 |
+
else:
|
| 395 |
+
incomplete = incomplete.lower()
|
| 396 |
+
matched = (c for c in str_choices if c.lower().startswith(incomplete))
|
| 397 |
+
|
| 398 |
+
return [CompletionItem(c) for c in matched]
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
class DateTime(ParamType):
|
| 402 |
+
"""The DateTime type converts date strings into `datetime` objects.
|
| 403 |
+
|
| 404 |
+
The format strings which are checked are configurable, but default to some
|
| 405 |
+
common (non-timezone aware) ISO 8601 formats.
|
| 406 |
+
|
| 407 |
+
When specifying *DateTime* formats, you should only pass a list or a tuple.
|
| 408 |
+
Other iterables, like generators, may lead to surprising results.
|
| 409 |
+
|
| 410 |
+
The format strings are processed using ``datetime.strptime``, and this
|
| 411 |
+
consequently defines the format strings which are allowed.
|
| 412 |
+
|
| 413 |
+
Parsing is tried using each format, in order, and the first format which
|
| 414 |
+
parses successfully is used.
|
| 415 |
+
|
| 416 |
+
:param formats: A list or tuple of date format strings, in the order in
|
| 417 |
+
which they should be tried. Defaults to
|
| 418 |
+
``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
|
| 419 |
+
``'%Y-%m-%d %H:%M:%S'``.
|
| 420 |
+
"""
|
| 421 |
+
|
| 422 |
+
name = "datetime"
|
| 423 |
+
|
| 424 |
+
def __init__(self, formats: cabc.Sequence[str] | None = None):
|
| 425 |
+
self.formats: cabc.Sequence[str] = formats or [
|
| 426 |
+
"%Y-%m-%d",
|
| 427 |
+
"%Y-%m-%dT%H:%M:%S",
|
| 428 |
+
"%Y-%m-%d %H:%M:%S",
|
| 429 |
+
]
|
| 430 |
+
|
| 431 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 432 |
+
info_dict = super().to_info_dict()
|
| 433 |
+
info_dict["formats"] = self.formats
|
| 434 |
+
return info_dict
|
| 435 |
+
|
| 436 |
+
def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
|
| 437 |
+
return f"[{'|'.join(self.formats)}]"
|
| 438 |
+
|
| 439 |
+
def _try_to_convert_date(self, value: t.Any, format: str) -> datetime | None:
|
| 440 |
+
try:
|
| 441 |
+
return datetime.strptime(value, format)
|
| 442 |
+
except ValueError:
|
| 443 |
+
return None
|
| 444 |
+
|
| 445 |
+
def convert(
|
| 446 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 447 |
+
) -> t.Any:
|
| 448 |
+
if isinstance(value, datetime):
|
| 449 |
+
return value
|
| 450 |
+
|
| 451 |
+
for format in self.formats:
|
| 452 |
+
converted = self._try_to_convert_date(value, format)
|
| 453 |
+
|
| 454 |
+
if converted is not None:
|
| 455 |
+
return converted
|
| 456 |
+
|
| 457 |
+
formats_str = ", ".join(map(repr, self.formats))
|
| 458 |
+
self.fail(
|
| 459 |
+
ngettext(
|
| 460 |
+
"{value!r} does not match the format {format}.",
|
| 461 |
+
"{value!r} does not match the formats {formats}.",
|
| 462 |
+
len(self.formats),
|
| 463 |
+
).format(value=value, format=formats_str, formats=formats_str),
|
| 464 |
+
param,
|
| 465 |
+
ctx,
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
def __repr__(self) -> str:
|
| 469 |
+
return "DateTime"
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
class _NumberParamTypeBase(ParamType):
|
| 473 |
+
_number_class: t.ClassVar[type[t.Any]]
|
| 474 |
+
|
| 475 |
+
def convert(
|
| 476 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 477 |
+
) -> t.Any:
|
| 478 |
+
try:
|
| 479 |
+
return self._number_class(value)
|
| 480 |
+
except ValueError:
|
| 481 |
+
self.fail(
|
| 482 |
+
_("{value!r} is not a valid {number_type}.").format(
|
| 483 |
+
value=value, number_type=self.name
|
| 484 |
+
),
|
| 485 |
+
param,
|
| 486 |
+
ctx,
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
class _NumberRangeBase(_NumberParamTypeBase):
|
| 491 |
+
def __init__(
|
| 492 |
+
self,
|
| 493 |
+
min: float | None = None,
|
| 494 |
+
max: float | None = None,
|
| 495 |
+
min_open: bool = False,
|
| 496 |
+
max_open: bool = False,
|
| 497 |
+
clamp: bool = False,
|
| 498 |
+
) -> None:
|
| 499 |
+
self.min = min
|
| 500 |
+
self.max = max
|
| 501 |
+
self.min_open = min_open
|
| 502 |
+
self.max_open = max_open
|
| 503 |
+
self.clamp = clamp
|
| 504 |
+
|
| 505 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 506 |
+
info_dict = super().to_info_dict()
|
| 507 |
+
info_dict.update(
|
| 508 |
+
min=self.min,
|
| 509 |
+
max=self.max,
|
| 510 |
+
min_open=self.min_open,
|
| 511 |
+
max_open=self.max_open,
|
| 512 |
+
clamp=self.clamp,
|
| 513 |
+
)
|
| 514 |
+
return info_dict
|
| 515 |
+
|
| 516 |
+
def convert(
|
| 517 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 518 |
+
) -> t.Any:
|
| 519 |
+
import operator
|
| 520 |
+
|
| 521 |
+
rv = super().convert(value, param, ctx)
|
| 522 |
+
lt_min: bool = self.min is not None and (
|
| 523 |
+
operator.le if self.min_open else operator.lt
|
| 524 |
+
)(rv, self.min)
|
| 525 |
+
gt_max: bool = self.max is not None and (
|
| 526 |
+
operator.ge if self.max_open else operator.gt
|
| 527 |
+
)(rv, self.max)
|
| 528 |
+
|
| 529 |
+
if self.clamp:
|
| 530 |
+
if lt_min:
|
| 531 |
+
return self._clamp(self.min, 1, self.min_open) # type: ignore
|
| 532 |
+
|
| 533 |
+
if gt_max:
|
| 534 |
+
return self._clamp(self.max, -1, self.max_open) # type: ignore
|
| 535 |
+
|
| 536 |
+
if lt_min or gt_max:
|
| 537 |
+
self.fail(
|
| 538 |
+
_("{value} is not in the range {range}.").format(
|
| 539 |
+
value=rv, range=self._describe_range()
|
| 540 |
+
),
|
| 541 |
+
param,
|
| 542 |
+
ctx,
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
return rv
|
| 546 |
+
|
| 547 |
+
def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float:
|
| 548 |
+
"""Find the valid value to clamp to bound in the given
|
| 549 |
+
direction.
|
| 550 |
+
|
| 551 |
+
:param bound: The boundary value.
|
| 552 |
+
:param dir: 1 or -1 indicating the direction to move.
|
| 553 |
+
:param open: If true, the range does not include the bound.
|
| 554 |
+
"""
|
| 555 |
+
raise NotImplementedError
|
| 556 |
+
|
| 557 |
+
def _describe_range(self) -> str:
|
| 558 |
+
"""Describe the range for use in help text."""
|
| 559 |
+
if self.min is None:
|
| 560 |
+
op = "<" if self.max_open else "<="
|
| 561 |
+
return f"x{op}{self.max}"
|
| 562 |
+
|
| 563 |
+
if self.max is None:
|
| 564 |
+
op = ">" if self.min_open else ">="
|
| 565 |
+
return f"x{op}{self.min}"
|
| 566 |
+
|
| 567 |
+
lop = "<" if self.min_open else "<="
|
| 568 |
+
rop = "<" if self.max_open else "<="
|
| 569 |
+
return f"{self.min}{lop}x{rop}{self.max}"
|
| 570 |
+
|
| 571 |
+
def __repr__(self) -> str:
|
| 572 |
+
clamp = " clamped" if self.clamp else ""
|
| 573 |
+
return f"<{type(self).__name__} {self._describe_range()}{clamp}>"
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
class IntParamType(_NumberParamTypeBase):
|
| 577 |
+
name = "integer"
|
| 578 |
+
_number_class = int
|
| 579 |
+
|
| 580 |
+
def __repr__(self) -> str:
|
| 581 |
+
return "INT"
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
class IntRange(_NumberRangeBase, IntParamType):
|
| 585 |
+
"""Restrict an :data:`click.INT` value to a range of accepted
|
| 586 |
+
values. See :ref:`ranges`.
|
| 587 |
+
|
| 588 |
+
If ``min`` or ``max`` are not passed, any value is accepted in that
|
| 589 |
+
direction. If ``min_open`` or ``max_open`` are enabled, the
|
| 590 |
+
corresponding boundary is not included in the range.
|
| 591 |
+
|
| 592 |
+
If ``clamp`` is enabled, a value outside the range is clamped to the
|
| 593 |
+
boundary instead of failing.
|
| 594 |
+
|
| 595 |
+
.. versionchanged:: 8.0
|
| 596 |
+
Added the ``min_open`` and ``max_open`` parameters.
|
| 597 |
+
"""
|
| 598 |
+
|
| 599 |
+
name = "integer range"
|
| 600 |
+
|
| 601 |
+
def _clamp( # type: ignore
|
| 602 |
+
self, bound: int, dir: t.Literal[1, -1], open: bool
|
| 603 |
+
) -> int:
|
| 604 |
+
if not open:
|
| 605 |
+
return bound
|
| 606 |
+
|
| 607 |
+
return bound + dir
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
class FloatParamType(_NumberParamTypeBase):
|
| 611 |
+
name = "float"
|
| 612 |
+
_number_class = float
|
| 613 |
+
|
| 614 |
+
def __repr__(self) -> str:
|
| 615 |
+
return "FLOAT"
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
class FloatRange(_NumberRangeBase, FloatParamType):
|
| 619 |
+
"""Restrict a :data:`click.FLOAT` value to a range of accepted
|
| 620 |
+
values. See :ref:`ranges`.
|
| 621 |
+
|
| 622 |
+
If ``min`` or ``max`` are not passed, any value is accepted in that
|
| 623 |
+
direction. If ``min_open`` or ``max_open`` are enabled, the
|
| 624 |
+
corresponding boundary is not included in the range.
|
| 625 |
+
|
| 626 |
+
If ``clamp`` is enabled, a value outside the range is clamped to the
|
| 627 |
+
boundary instead of failing. This is not supported if either
|
| 628 |
+
boundary is marked ``open``.
|
| 629 |
+
|
| 630 |
+
.. versionchanged:: 8.0
|
| 631 |
+
Added the ``min_open`` and ``max_open`` parameters.
|
| 632 |
+
"""
|
| 633 |
+
|
| 634 |
+
name = "float range"
|
| 635 |
+
|
| 636 |
+
def __init__(
|
| 637 |
+
self,
|
| 638 |
+
min: float | None = None,
|
| 639 |
+
max: float | None = None,
|
| 640 |
+
min_open: bool = False,
|
| 641 |
+
max_open: bool = False,
|
| 642 |
+
clamp: bool = False,
|
| 643 |
+
) -> None:
|
| 644 |
+
super().__init__(
|
| 645 |
+
min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
if (min_open or max_open) and clamp:
|
| 649 |
+
raise TypeError("Clamping is not supported for open bounds.")
|
| 650 |
+
|
| 651 |
+
def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float:
|
| 652 |
+
if not open:
|
| 653 |
+
return bound
|
| 654 |
+
|
| 655 |
+
# Could use math.nextafter here, but clamping an
|
| 656 |
+
# open float range doesn't seem to be particularly useful. It's
|
| 657 |
+
# left up to the user to write a callback to do it if needed.
|
| 658 |
+
raise RuntimeError("Clamping is not supported for open bounds.")
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
class BoolParamType(ParamType):
|
| 662 |
+
name = "boolean"
|
| 663 |
+
|
| 664 |
+
bool_states: dict[str, bool] = {
|
| 665 |
+
"1": True,
|
| 666 |
+
"0": False,
|
| 667 |
+
"yes": True,
|
| 668 |
+
"no": False,
|
| 669 |
+
"true": True,
|
| 670 |
+
"false": False,
|
| 671 |
+
"on": True,
|
| 672 |
+
"off": False,
|
| 673 |
+
"t": True,
|
| 674 |
+
"f": False,
|
| 675 |
+
"y": True,
|
| 676 |
+
"n": False,
|
| 677 |
+
# Absence of value is considered False.
|
| 678 |
+
"": False,
|
| 679 |
+
}
|
| 680 |
+
"""A mapping of string values to boolean states.
|
| 681 |
+
|
| 682 |
+
Mapping is inspired by :py:attr:`configparser.ConfigParser.BOOLEAN_STATES`
|
| 683 |
+
and extends it.
|
| 684 |
+
|
| 685 |
+
.. caution::
|
| 686 |
+
String values are lower-cased, as the ``str_to_bool`` comparison function
|
| 687 |
+
below is case-insensitive.
|
| 688 |
+
|
| 689 |
+
.. warning::
|
| 690 |
+
The mapping is not exhaustive, and does not cover all possible boolean strings
|
| 691 |
+
representations. It will remains as it is to avoid endless bikeshedding.
|
| 692 |
+
|
| 693 |
+
Future work my be considered to make this mapping user-configurable from public
|
| 694 |
+
API.
|
| 695 |
+
"""
|
| 696 |
+
|
| 697 |
+
@staticmethod
|
| 698 |
+
def str_to_bool(value: str | bool) -> bool | None:
|
| 699 |
+
"""Convert a string to a boolean value.
|
| 700 |
+
|
| 701 |
+
If the value is already a boolean, it is returned as-is. If the value is a
|
| 702 |
+
string, it is stripped of whitespaces and lower-cased, then checked against
|
| 703 |
+
the known boolean states pre-defined in the `BoolParamType.bool_states` mapping
|
| 704 |
+
above.
|
| 705 |
+
|
| 706 |
+
Returns `None` if the value does not match any known boolean state.
|
| 707 |
+
"""
|
| 708 |
+
if isinstance(value, bool):
|
| 709 |
+
return value
|
| 710 |
+
return BoolParamType.bool_states.get(value.strip().lower())
|
| 711 |
+
|
| 712 |
+
def convert(
|
| 713 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 714 |
+
) -> bool:
|
| 715 |
+
normalized = self.str_to_bool(value)
|
| 716 |
+
if normalized is None:
|
| 717 |
+
self.fail(
|
| 718 |
+
_(
|
| 719 |
+
"{value!r} is not a valid boolean. Recognized values: {states}"
|
| 720 |
+
).format(value=value, states=", ".join(sorted(self.bool_states))),
|
| 721 |
+
param,
|
| 722 |
+
ctx,
|
| 723 |
+
)
|
| 724 |
+
return normalized
|
| 725 |
+
|
| 726 |
+
def __repr__(self) -> str:
|
| 727 |
+
return "BOOL"
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
class UUIDParameterType(ParamType):
|
| 731 |
+
name = "uuid"
|
| 732 |
+
|
| 733 |
+
def convert(
|
| 734 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 735 |
+
) -> t.Any:
|
| 736 |
+
import uuid
|
| 737 |
+
|
| 738 |
+
if isinstance(value, uuid.UUID):
|
| 739 |
+
return value
|
| 740 |
+
|
| 741 |
+
value = value.strip()
|
| 742 |
+
|
| 743 |
+
try:
|
| 744 |
+
return uuid.UUID(value)
|
| 745 |
+
except ValueError:
|
| 746 |
+
self.fail(
|
| 747 |
+
_("{value!r} is not a valid UUID.").format(value=value), param, ctx
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
def __repr__(self) -> str:
|
| 751 |
+
return "UUID"
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
class File(ParamType):
|
| 755 |
+
"""Declares a parameter to be a file for reading or writing. The file
|
| 756 |
+
is automatically closed once the context tears down (after the command
|
| 757 |
+
finished working).
|
| 758 |
+
|
| 759 |
+
Files can be opened for reading or writing. The special value ``-``
|
| 760 |
+
indicates stdin or stdout depending on the mode.
|
| 761 |
+
|
| 762 |
+
By default, the file is opened for reading text data, but it can also be
|
| 763 |
+
opened in binary mode or for writing. The encoding parameter can be used
|
| 764 |
+
to force a specific encoding.
|
| 765 |
+
|
| 766 |
+
The `lazy` flag controls if the file should be opened immediately or upon
|
| 767 |
+
first IO. The default is to be non-lazy for standard input and output
|
| 768 |
+
streams as well as files opened for reading, `lazy` otherwise. When opening a
|
| 769 |
+
file lazily for reading, it is still opened temporarily for validation, but
|
| 770 |
+
will not be held open until first IO. lazy is mainly useful when opening
|
| 771 |
+
for writing to avoid creating the file until it is needed.
|
| 772 |
+
|
| 773 |
+
Files can also be opened atomically in which case all writes go into a
|
| 774 |
+
separate file in the same folder and upon completion the file will
|
| 775 |
+
be moved over to the original location. This is useful if a file
|
| 776 |
+
regularly read by other users is modified.
|
| 777 |
+
|
| 778 |
+
See :ref:`file-args` for more information.
|
| 779 |
+
|
| 780 |
+
.. versionchanged:: 2.0
|
| 781 |
+
Added the ``atomic`` parameter.
|
| 782 |
+
"""
|
| 783 |
+
|
| 784 |
+
name = "filename"
|
| 785 |
+
envvar_list_splitter: t.ClassVar[str] = os.path.pathsep
|
| 786 |
+
|
| 787 |
+
def __init__(
|
| 788 |
+
self,
|
| 789 |
+
mode: str = "r",
|
| 790 |
+
encoding: str | None = None,
|
| 791 |
+
errors: str | None = "strict",
|
| 792 |
+
lazy: bool | None = None,
|
| 793 |
+
atomic: bool = False,
|
| 794 |
+
) -> None:
|
| 795 |
+
self.mode = mode
|
| 796 |
+
self.encoding = encoding
|
| 797 |
+
self.errors = errors
|
| 798 |
+
self.lazy = lazy
|
| 799 |
+
self.atomic = atomic
|
| 800 |
+
|
| 801 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 802 |
+
info_dict = super().to_info_dict()
|
| 803 |
+
info_dict.update(mode=self.mode, encoding=self.encoding)
|
| 804 |
+
return info_dict
|
| 805 |
+
|
| 806 |
+
def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool:
|
| 807 |
+
if self.lazy is not None:
|
| 808 |
+
return self.lazy
|
| 809 |
+
if os.fspath(value) == "-":
|
| 810 |
+
return False
|
| 811 |
+
elif "w" in self.mode:
|
| 812 |
+
return True
|
| 813 |
+
return False
|
| 814 |
+
|
| 815 |
+
def convert(
|
| 816 |
+
self,
|
| 817 |
+
value: str | os.PathLike[str] | t.IO[t.Any],
|
| 818 |
+
param: Parameter | None,
|
| 819 |
+
ctx: Context | None,
|
| 820 |
+
) -> t.IO[t.Any]:
|
| 821 |
+
if _is_file_like(value):
|
| 822 |
+
return value
|
| 823 |
+
|
| 824 |
+
value = t.cast("str | os.PathLike[str]", value)
|
| 825 |
+
|
| 826 |
+
try:
|
| 827 |
+
lazy = self.resolve_lazy_flag(value)
|
| 828 |
+
|
| 829 |
+
if lazy:
|
| 830 |
+
lf = LazyFile(
|
| 831 |
+
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
| 832 |
+
)
|
| 833 |
+
|
| 834 |
+
if ctx is not None:
|
| 835 |
+
ctx.call_on_close(lf.close_intelligently)
|
| 836 |
+
|
| 837 |
+
return t.cast("t.IO[t.Any]", lf)
|
| 838 |
+
|
| 839 |
+
f, should_close = open_stream(
|
| 840 |
+
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
| 841 |
+
)
|
| 842 |
+
|
| 843 |
+
# If a context is provided, we automatically close the file
|
| 844 |
+
# at the end of the context execution (or flush out). If a
|
| 845 |
+
# context does not exist, it's the caller's responsibility to
|
| 846 |
+
# properly close the file. This for instance happens when the
|
| 847 |
+
# type is used with prompts.
|
| 848 |
+
if ctx is not None:
|
| 849 |
+
if should_close:
|
| 850 |
+
ctx.call_on_close(safecall(f.close))
|
| 851 |
+
else:
|
| 852 |
+
ctx.call_on_close(safecall(f.flush))
|
| 853 |
+
|
| 854 |
+
return f
|
| 855 |
+
except OSError as e:
|
| 856 |
+
self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx)
|
| 857 |
+
|
| 858 |
+
def shell_complete(
|
| 859 |
+
self, ctx: Context, param: Parameter, incomplete: str
|
| 860 |
+
) -> list[CompletionItem]:
|
| 861 |
+
"""Return a special completion marker that tells the completion
|
| 862 |
+
system to use the shell to provide file path completions.
|
| 863 |
+
|
| 864 |
+
:param ctx: Invocation context for this command.
|
| 865 |
+
:param param: The parameter that is requesting completion.
|
| 866 |
+
:param incomplete: Value being completed. May be empty.
|
| 867 |
+
|
| 868 |
+
.. versionadded:: 8.0
|
| 869 |
+
"""
|
| 870 |
+
from click.shell_completion import CompletionItem
|
| 871 |
+
|
| 872 |
+
return [CompletionItem(incomplete, type="file")]
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
def _is_file_like(value: t.Any) -> te.TypeGuard[t.IO[t.Any]]:
|
| 876 |
+
return hasattr(value, "read") or hasattr(value, "write")
|
| 877 |
+
|
| 878 |
+
|
| 879 |
+
class Path(ParamType):
|
| 880 |
+
"""The ``Path`` type is similar to the :class:`File` type, but
|
| 881 |
+
returns the filename instead of an open file. Various checks can be
|
| 882 |
+
enabled to validate the type of file and permissions.
|
| 883 |
+
|
| 884 |
+
:param exists: The file or directory needs to exist for the value to
|
| 885 |
+
be valid. If this is not set to ``True``, and the file does not
|
| 886 |
+
exist, then all further checks are silently skipped.
|
| 887 |
+
:param file_okay: Allow a file as a value.
|
| 888 |
+
:param dir_okay: Allow a directory as a value.
|
| 889 |
+
:param readable: if true, a readable check is performed.
|
| 890 |
+
:param writable: if true, a writable check is performed.
|
| 891 |
+
:param executable: if true, an executable check is performed.
|
| 892 |
+
:param resolve_path: Make the value absolute and resolve any
|
| 893 |
+
symlinks. A ``~`` is not expanded, as this is supposed to be
|
| 894 |
+
done by the shell only.
|
| 895 |
+
:param allow_dash: Allow a single dash as a value, which indicates
|
| 896 |
+
a standard stream (but does not open it). Use
|
| 897 |
+
:func:`~click.open_file` to handle opening this value.
|
| 898 |
+
:param path_type: Convert the incoming path value to this type. If
|
| 899 |
+
``None``, keep Python's default, which is ``str``. Useful to
|
| 900 |
+
convert to :class:`pathlib.Path`.
|
| 901 |
+
|
| 902 |
+
.. versionchanged:: 8.1
|
| 903 |
+
Added the ``executable`` parameter.
|
| 904 |
+
|
| 905 |
+
.. versionchanged:: 8.0
|
| 906 |
+
Allow passing ``path_type=pathlib.Path``.
|
| 907 |
+
|
| 908 |
+
.. versionchanged:: 6.0
|
| 909 |
+
Added the ``allow_dash`` parameter.
|
| 910 |
+
"""
|
| 911 |
+
|
| 912 |
+
envvar_list_splitter: t.ClassVar[str] = os.path.pathsep
|
| 913 |
+
|
| 914 |
+
def __init__(
|
| 915 |
+
self,
|
| 916 |
+
exists: bool = False,
|
| 917 |
+
file_okay: bool = True,
|
| 918 |
+
dir_okay: bool = True,
|
| 919 |
+
writable: bool = False,
|
| 920 |
+
readable: bool = True,
|
| 921 |
+
resolve_path: bool = False,
|
| 922 |
+
allow_dash: bool = False,
|
| 923 |
+
path_type: type[t.Any] | None = None,
|
| 924 |
+
executable: bool = False,
|
| 925 |
+
):
|
| 926 |
+
self.exists = exists
|
| 927 |
+
self.file_okay = file_okay
|
| 928 |
+
self.dir_okay = dir_okay
|
| 929 |
+
self.readable = readable
|
| 930 |
+
self.writable = writable
|
| 931 |
+
self.executable = executable
|
| 932 |
+
self.resolve_path = resolve_path
|
| 933 |
+
self.allow_dash = allow_dash
|
| 934 |
+
self.type = path_type
|
| 935 |
+
|
| 936 |
+
if self.file_okay and not self.dir_okay:
|
| 937 |
+
self.name: str = _("file")
|
| 938 |
+
elif self.dir_okay and not self.file_okay:
|
| 939 |
+
self.name = _("directory")
|
| 940 |
+
else:
|
| 941 |
+
self.name = _("path")
|
| 942 |
+
|
| 943 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 944 |
+
info_dict = super().to_info_dict()
|
| 945 |
+
info_dict.update(
|
| 946 |
+
exists=self.exists,
|
| 947 |
+
file_okay=self.file_okay,
|
| 948 |
+
dir_okay=self.dir_okay,
|
| 949 |
+
writable=self.writable,
|
| 950 |
+
readable=self.readable,
|
| 951 |
+
allow_dash=self.allow_dash,
|
| 952 |
+
)
|
| 953 |
+
return info_dict
|
| 954 |
+
|
| 955 |
+
def coerce_path_result(
|
| 956 |
+
self, value: str | os.PathLike[str]
|
| 957 |
+
) -> str | bytes | os.PathLike[str]:
|
| 958 |
+
if self.type is not None and not isinstance(value, self.type):
|
| 959 |
+
if self.type is str:
|
| 960 |
+
return os.fsdecode(value)
|
| 961 |
+
elif self.type is bytes:
|
| 962 |
+
return os.fsencode(value)
|
| 963 |
+
else:
|
| 964 |
+
return t.cast("os.PathLike[str]", self.type(value))
|
| 965 |
+
|
| 966 |
+
return value
|
| 967 |
+
|
| 968 |
+
def convert(
|
| 969 |
+
self,
|
| 970 |
+
value: str | os.PathLike[str],
|
| 971 |
+
param: Parameter | None,
|
| 972 |
+
ctx: Context | None,
|
| 973 |
+
) -> str | bytes | os.PathLike[str]:
|
| 974 |
+
rv = value
|
| 975 |
+
|
| 976 |
+
is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
|
| 977 |
+
|
| 978 |
+
if not is_dash:
|
| 979 |
+
if self.resolve_path:
|
| 980 |
+
rv = os.path.realpath(rv)
|
| 981 |
+
|
| 982 |
+
try:
|
| 983 |
+
st = os.stat(rv)
|
| 984 |
+
except OSError:
|
| 985 |
+
if not self.exists:
|
| 986 |
+
return self.coerce_path_result(rv)
|
| 987 |
+
self.fail(
|
| 988 |
+
_("{name} {filename!r} does not exist.").format(
|
| 989 |
+
name=self.name.title(), filename=format_filename(value)
|
| 990 |
+
),
|
| 991 |
+
param,
|
| 992 |
+
ctx,
|
| 993 |
+
)
|
| 994 |
+
|
| 995 |
+
if not self.file_okay and stat.S_ISREG(st.st_mode):
|
| 996 |
+
self.fail(
|
| 997 |
+
_("{name} {filename!r} is a file.").format(
|
| 998 |
+
name=self.name.title(), filename=format_filename(value)
|
| 999 |
+
),
|
| 1000 |
+
param,
|
| 1001 |
+
ctx,
|
| 1002 |
+
)
|
| 1003 |
+
if not self.dir_okay and stat.S_ISDIR(st.st_mode):
|
| 1004 |
+
self.fail(
|
| 1005 |
+
_("{name} {filename!r} is a directory.").format(
|
| 1006 |
+
name=self.name.title(), filename=format_filename(value)
|
| 1007 |
+
),
|
| 1008 |
+
param,
|
| 1009 |
+
ctx,
|
| 1010 |
+
)
|
| 1011 |
+
|
| 1012 |
+
if self.readable and not os.access(rv, os.R_OK):
|
| 1013 |
+
self.fail(
|
| 1014 |
+
_("{name} {filename!r} is not readable.").format(
|
| 1015 |
+
name=self.name.title(), filename=format_filename(value)
|
| 1016 |
+
),
|
| 1017 |
+
param,
|
| 1018 |
+
ctx,
|
| 1019 |
+
)
|
| 1020 |
+
|
| 1021 |
+
if self.writable and not os.access(rv, os.W_OK):
|
| 1022 |
+
self.fail(
|
| 1023 |
+
_("{name} {filename!r} is not writable.").format(
|
| 1024 |
+
name=self.name.title(), filename=format_filename(value)
|
| 1025 |
+
),
|
| 1026 |
+
param,
|
| 1027 |
+
ctx,
|
| 1028 |
+
)
|
| 1029 |
+
|
| 1030 |
+
if self.executable and not os.access(value, os.X_OK):
|
| 1031 |
+
self.fail(
|
| 1032 |
+
_("{name} {filename!r} is not executable.").format(
|
| 1033 |
+
name=self.name.title(), filename=format_filename(value)
|
| 1034 |
+
),
|
| 1035 |
+
param,
|
| 1036 |
+
ctx,
|
| 1037 |
+
)
|
| 1038 |
+
|
| 1039 |
+
return self.coerce_path_result(rv)
|
| 1040 |
+
|
| 1041 |
+
def shell_complete(
|
| 1042 |
+
self, ctx: Context, param: Parameter, incomplete: str
|
| 1043 |
+
) -> list[CompletionItem]:
|
| 1044 |
+
"""Return a special completion marker that tells the completion
|
| 1045 |
+
system to use the shell to provide path completions for only
|
| 1046 |
+
directories or any paths.
|
| 1047 |
+
|
| 1048 |
+
:param ctx: Invocation context for this command.
|
| 1049 |
+
:param param: The parameter that is requesting completion.
|
| 1050 |
+
:param incomplete: Value being completed. May be empty.
|
| 1051 |
+
|
| 1052 |
+
.. versionadded:: 8.0
|
| 1053 |
+
"""
|
| 1054 |
+
from click.shell_completion import CompletionItem
|
| 1055 |
+
|
| 1056 |
+
type = "dir" if self.dir_okay and not self.file_okay else "file"
|
| 1057 |
+
return [CompletionItem(incomplete, type=type)]
|
| 1058 |
+
|
| 1059 |
+
|
| 1060 |
+
class Tuple(CompositeParamType):
|
| 1061 |
+
"""The default behavior of Click is to apply a type on a value directly.
|
| 1062 |
+
This works well in most cases, except for when `nargs` is set to a fixed
|
| 1063 |
+
count and different types should be used for different items. In this
|
| 1064 |
+
case the :class:`Tuple` type can be used. This type can only be used
|
| 1065 |
+
if `nargs` is set to a fixed number.
|
| 1066 |
+
|
| 1067 |
+
For more information see :ref:`tuple-type`.
|
| 1068 |
+
|
| 1069 |
+
This can be selected by using a Python tuple literal as a type.
|
| 1070 |
+
|
| 1071 |
+
:param types: a list of types that should be used for the tuple items.
|
| 1072 |
+
"""
|
| 1073 |
+
|
| 1074 |
+
def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType]) -> None:
|
| 1075 |
+
self.types: cabc.Sequence[ParamType] = [convert_type(ty) for ty in types]
|
| 1076 |
+
|
| 1077 |
+
def to_info_dict(self) -> dict[str, t.Any]:
|
| 1078 |
+
info_dict = super().to_info_dict()
|
| 1079 |
+
info_dict["types"] = [t.to_info_dict() for t in self.types]
|
| 1080 |
+
return info_dict
|
| 1081 |
+
|
| 1082 |
+
@property
|
| 1083 |
+
def name(self) -> str: # type: ignore
|
| 1084 |
+
return f"<{' '.join(ty.name for ty in self.types)}>"
|
| 1085 |
+
|
| 1086 |
+
@property
|
| 1087 |
+
def arity(self) -> int: # type: ignore
|
| 1088 |
+
return len(self.types)
|
| 1089 |
+
|
| 1090 |
+
def convert(
|
| 1091 |
+
self, value: t.Any, param: Parameter | None, ctx: Context | None
|
| 1092 |
+
) -> t.Any:
|
| 1093 |
+
len_type = len(self.types)
|
| 1094 |
+
len_value = len(value)
|
| 1095 |
+
|
| 1096 |
+
if len_value != len_type:
|
| 1097 |
+
self.fail(
|
| 1098 |
+
ngettext(
|
| 1099 |
+
"{len_type} values are required, but {len_value} was given.",
|
| 1100 |
+
"{len_type} values are required, but {len_value} were given.",
|
| 1101 |
+
len_value,
|
| 1102 |
+
).format(len_type=len_type, len_value=len_value),
|
| 1103 |
+
param=param,
|
| 1104 |
+
ctx=ctx,
|
| 1105 |
+
)
|
| 1106 |
+
|
| 1107 |
+
return tuple(
|
| 1108 |
+
ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False)
|
| 1109 |
+
)
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType:
|
| 1113 |
+
"""Find the most appropriate :class:`ParamType` for the given Python
|
| 1114 |
+
type. If the type isn't provided, it can be inferred from a default
|
| 1115 |
+
value.
|
| 1116 |
+
"""
|
| 1117 |
+
guessed_type = False
|
| 1118 |
+
|
| 1119 |
+
if ty is None and default is not None:
|
| 1120 |
+
if isinstance(default, (tuple, list)):
|
| 1121 |
+
# If the default is empty, ty will remain None and will
|
| 1122 |
+
# return STRING.
|
| 1123 |
+
if default:
|
| 1124 |
+
item = default[0]
|
| 1125 |
+
|
| 1126 |
+
# A tuple of tuples needs to detect the inner types.
|
| 1127 |
+
# Can't call convert recursively because that would
|
| 1128 |
+
# incorrectly unwind the tuple to a single type.
|
| 1129 |
+
if isinstance(item, (tuple, list)):
|
| 1130 |
+
ty = tuple(map(type, item))
|
| 1131 |
+
else:
|
| 1132 |
+
ty = type(item)
|
| 1133 |
+
else:
|
| 1134 |
+
ty = type(default)
|
| 1135 |
+
|
| 1136 |
+
guessed_type = True
|
| 1137 |
+
|
| 1138 |
+
if isinstance(ty, tuple):
|
| 1139 |
+
return Tuple(ty)
|
| 1140 |
+
|
| 1141 |
+
if isinstance(ty, ParamType):
|
| 1142 |
+
return ty
|
| 1143 |
+
|
| 1144 |
+
if ty is str or ty is None:
|
| 1145 |
+
return STRING
|
| 1146 |
+
|
| 1147 |
+
if ty is int:
|
| 1148 |
+
return INT
|
| 1149 |
+
|
| 1150 |
+
if ty is float:
|
| 1151 |
+
return FLOAT
|
| 1152 |
+
|
| 1153 |
+
if ty is bool:
|
| 1154 |
+
return BOOL
|
| 1155 |
+
|
| 1156 |
+
if guessed_type:
|
| 1157 |
+
return STRING
|
| 1158 |
+
|
| 1159 |
+
if __debug__:
|
| 1160 |
+
try:
|
| 1161 |
+
if issubclass(ty, ParamType):
|
| 1162 |
+
raise AssertionError(
|
| 1163 |
+
f"Attempted to use an uninstantiated parameter type ({ty})."
|
| 1164 |
+
)
|
| 1165 |
+
except TypeError:
|
| 1166 |
+
# ty is an instance (correct), so issubclass fails.
|
| 1167 |
+
pass
|
| 1168 |
+
|
| 1169 |
+
return FuncParamType(ty)
|
| 1170 |
+
|
| 1171 |
+
|
| 1172 |
+
#: A dummy parameter type that just does nothing. From a user's
|
| 1173 |
+
#: perspective this appears to just be the same as `STRING` but
|
| 1174 |
+
#: internally no string conversion takes place if the input was bytes.
|
| 1175 |
+
#: This is usually useful when working with file paths as they can
|
| 1176 |
+
#: appear in bytes and unicode.
|
| 1177 |
+
#:
|
| 1178 |
+
#: For path related uses the :class:`Path` type is a better choice but
|
| 1179 |
+
#: there are situations where an unprocessed type is useful which is why
|
| 1180 |
+
#: it is is provided.
|
| 1181 |
+
#:
|
| 1182 |
+
#: .. versionadded:: 4.0
|
| 1183 |
+
UNPROCESSED = UnprocessedParamType()
|
| 1184 |
+
|
| 1185 |
+
#: A unicode string parameter type which is the implicit default. This
|
| 1186 |
+
#: can also be selected by using ``str`` as type.
|
| 1187 |
+
STRING = StringParamType()
|
| 1188 |
+
|
| 1189 |
+
#: An integer parameter. This can also be selected by using ``int`` as
|
| 1190 |
+
#: type.
|
| 1191 |
+
INT = IntParamType()
|
| 1192 |
+
|
| 1193 |
+
#: A floating point value parameter. This can also be selected by using
|
| 1194 |
+
#: ``float`` as type.
|
| 1195 |
+
FLOAT = FloatParamType()
|
| 1196 |
+
|
| 1197 |
+
#: A boolean parameter. This is the default for boolean flags. This can
|
| 1198 |
+
#: also be selected by using ``bool`` as a type.
|
| 1199 |
+
BOOL = BoolParamType()
|
| 1200 |
+
|
| 1201 |
+
#: A UUID parameter.
|
| 1202 |
+
UUID = UUIDParameterType()
|
| 1203 |
+
|
| 1204 |
+
|
| 1205 |
+
class OptionHelpExtra(t.TypedDict, total=False):
|
| 1206 |
+
envvars: tuple[str, ...]
|
| 1207 |
+
default: str
|
| 1208 |
+
range: str
|
| 1209 |
+
required: str
|
env/lib/python3.13/site-packages/click/utils.py
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import collections.abc as cabc
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import sys
|
| 7 |
+
import typing as t
|
| 8 |
+
from functools import update_wrapper
|
| 9 |
+
from types import ModuleType
|
| 10 |
+
from types import TracebackType
|
| 11 |
+
|
| 12 |
+
from ._compat import _default_text_stderr
|
| 13 |
+
from ._compat import _default_text_stdout
|
| 14 |
+
from ._compat import _find_binary_writer
|
| 15 |
+
from ._compat import auto_wrap_for_ansi
|
| 16 |
+
from ._compat import binary_streams
|
| 17 |
+
from ._compat import open_stream
|
| 18 |
+
from ._compat import should_strip_ansi
|
| 19 |
+
from ._compat import strip_ansi
|
| 20 |
+
from ._compat import text_streams
|
| 21 |
+
from ._compat import WIN
|
| 22 |
+
from .globals import resolve_color_default
|
| 23 |
+
|
| 24 |
+
if t.TYPE_CHECKING:
|
| 25 |
+
import typing_extensions as te
|
| 26 |
+
|
| 27 |
+
P = te.ParamSpec("P")
|
| 28 |
+
|
| 29 |
+
R = t.TypeVar("R")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _posixify(name: str) -> str:
|
| 33 |
+
return "-".join(name.split()).lower()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]:
|
| 37 |
+
"""Wraps a function so that it swallows exceptions."""
|
| 38 |
+
|
| 39 |
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
|
| 40 |
+
try:
|
| 41 |
+
return func(*args, **kwargs)
|
| 42 |
+
except Exception:
|
| 43 |
+
pass
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
return update_wrapper(wrapper, func)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def make_str(value: t.Any) -> str:
|
| 50 |
+
"""Converts a value into a valid string."""
|
| 51 |
+
if isinstance(value, bytes):
|
| 52 |
+
try:
|
| 53 |
+
return value.decode(sys.getfilesystemencoding())
|
| 54 |
+
except UnicodeError:
|
| 55 |
+
return value.decode("utf-8", "replace")
|
| 56 |
+
return str(value)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def make_default_short_help(help: str, max_length: int = 45) -> str:
|
| 60 |
+
"""Returns a condensed version of help string."""
|
| 61 |
+
# Consider only the first paragraph.
|
| 62 |
+
paragraph_end = help.find("\n\n")
|
| 63 |
+
|
| 64 |
+
if paragraph_end != -1:
|
| 65 |
+
help = help[:paragraph_end]
|
| 66 |
+
|
| 67 |
+
# Collapse newlines, tabs, and spaces.
|
| 68 |
+
words = help.split()
|
| 69 |
+
|
| 70 |
+
if not words:
|
| 71 |
+
return ""
|
| 72 |
+
|
| 73 |
+
# The first paragraph started with a "no rewrap" marker, ignore it.
|
| 74 |
+
if words[0] == "\b":
|
| 75 |
+
words = words[1:]
|
| 76 |
+
|
| 77 |
+
total_length = 0
|
| 78 |
+
last_index = len(words) - 1
|
| 79 |
+
|
| 80 |
+
for i, word in enumerate(words):
|
| 81 |
+
total_length += len(word) + (i > 0)
|
| 82 |
+
|
| 83 |
+
if total_length > max_length: # too long, truncate
|
| 84 |
+
break
|
| 85 |
+
|
| 86 |
+
if word[-1] == ".": # sentence end, truncate without "..."
|
| 87 |
+
return " ".join(words[: i + 1])
|
| 88 |
+
|
| 89 |
+
if total_length == max_length and i != last_index:
|
| 90 |
+
break # not at sentence end, truncate with "..."
|
| 91 |
+
else:
|
| 92 |
+
return " ".join(words) # no truncation needed
|
| 93 |
+
|
| 94 |
+
# Account for the length of the suffix.
|
| 95 |
+
total_length += len("...")
|
| 96 |
+
|
| 97 |
+
# remove words until the length is short enough
|
| 98 |
+
while i > 0:
|
| 99 |
+
total_length -= len(words[i]) + (i > 0)
|
| 100 |
+
|
| 101 |
+
if total_length <= max_length:
|
| 102 |
+
break
|
| 103 |
+
|
| 104 |
+
i -= 1
|
| 105 |
+
|
| 106 |
+
return " ".join(words[:i]) + "..."
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class LazyFile:
|
| 110 |
+
"""A lazy file works like a regular file but it does not fully open
|
| 111 |
+
the file but it does perform some basic checks early to see if the
|
| 112 |
+
filename parameter does make sense. This is useful for safely opening
|
| 113 |
+
files for writing.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
filename: str | os.PathLike[str],
|
| 119 |
+
mode: str = "r",
|
| 120 |
+
encoding: str | None = None,
|
| 121 |
+
errors: str | None = "strict",
|
| 122 |
+
atomic: bool = False,
|
| 123 |
+
):
|
| 124 |
+
self.name: str = os.fspath(filename)
|
| 125 |
+
self.mode = mode
|
| 126 |
+
self.encoding = encoding
|
| 127 |
+
self.errors = errors
|
| 128 |
+
self.atomic = atomic
|
| 129 |
+
self._f: t.IO[t.Any] | None
|
| 130 |
+
self.should_close: bool
|
| 131 |
+
|
| 132 |
+
if self.name == "-":
|
| 133 |
+
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
|
| 134 |
+
else:
|
| 135 |
+
if "r" in mode:
|
| 136 |
+
# Open and close the file in case we're opening it for
|
| 137 |
+
# reading so that we can catch at least some errors in
|
| 138 |
+
# some cases early.
|
| 139 |
+
open(filename, mode).close()
|
| 140 |
+
self._f = None
|
| 141 |
+
self.should_close = True
|
| 142 |
+
|
| 143 |
+
def __getattr__(self, name: str) -> t.Any:
|
| 144 |
+
return getattr(self.open(), name)
|
| 145 |
+
|
| 146 |
+
def __repr__(self) -> str:
|
| 147 |
+
if self._f is not None:
|
| 148 |
+
return repr(self._f)
|
| 149 |
+
return f"<unopened file '{format_filename(self.name)}' {self.mode}>"
|
| 150 |
+
|
| 151 |
+
def open(self) -> t.IO[t.Any]:
|
| 152 |
+
"""Opens the file if it's not yet open. This call might fail with
|
| 153 |
+
a :exc:`FileError`. Not handling this error will produce an error
|
| 154 |
+
that Click shows.
|
| 155 |
+
"""
|
| 156 |
+
if self._f is not None:
|
| 157 |
+
return self._f
|
| 158 |
+
try:
|
| 159 |
+
rv, self.should_close = open_stream(
|
| 160 |
+
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
|
| 161 |
+
)
|
| 162 |
+
except OSError as e:
|
| 163 |
+
from .exceptions import FileError
|
| 164 |
+
|
| 165 |
+
raise FileError(self.name, hint=e.strerror) from e
|
| 166 |
+
self._f = rv
|
| 167 |
+
return rv
|
| 168 |
+
|
| 169 |
+
def close(self) -> None:
|
| 170 |
+
"""Closes the underlying file, no matter what."""
|
| 171 |
+
if self._f is not None:
|
| 172 |
+
self._f.close()
|
| 173 |
+
|
| 174 |
+
def close_intelligently(self) -> None:
|
| 175 |
+
"""This function only closes the file if it was opened by the lazy
|
| 176 |
+
file wrapper. For instance this will never close stdin.
|
| 177 |
+
"""
|
| 178 |
+
if self.should_close:
|
| 179 |
+
self.close()
|
| 180 |
+
|
| 181 |
+
def __enter__(self) -> LazyFile:
|
| 182 |
+
return self
|
| 183 |
+
|
| 184 |
+
def __exit__(
|
| 185 |
+
self,
|
| 186 |
+
exc_type: type[BaseException] | None,
|
| 187 |
+
exc_value: BaseException | None,
|
| 188 |
+
tb: TracebackType | None,
|
| 189 |
+
) -> None:
|
| 190 |
+
self.close_intelligently()
|
| 191 |
+
|
| 192 |
+
def __iter__(self) -> cabc.Iterator[t.AnyStr]:
|
| 193 |
+
self.open()
|
| 194 |
+
return iter(self._f) # type: ignore
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class KeepOpenFile:
|
| 198 |
+
def __init__(self, file: t.IO[t.Any]) -> None:
|
| 199 |
+
self._file: t.IO[t.Any] = file
|
| 200 |
+
|
| 201 |
+
def __getattr__(self, name: str) -> t.Any:
|
| 202 |
+
return getattr(self._file, name)
|
| 203 |
+
|
| 204 |
+
def __enter__(self) -> KeepOpenFile:
|
| 205 |
+
return self
|
| 206 |
+
|
| 207 |
+
def __exit__(
|
| 208 |
+
self,
|
| 209 |
+
exc_type: type[BaseException] | None,
|
| 210 |
+
exc_value: BaseException | None,
|
| 211 |
+
tb: TracebackType | None,
|
| 212 |
+
) -> None:
|
| 213 |
+
pass
|
| 214 |
+
|
| 215 |
+
def __repr__(self) -> str:
|
| 216 |
+
return repr(self._file)
|
| 217 |
+
|
| 218 |
+
def __iter__(self) -> cabc.Iterator[t.AnyStr]:
|
| 219 |
+
return iter(self._file)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def echo(
|
| 223 |
+
message: t.Any | None = None,
|
| 224 |
+
file: t.IO[t.Any] | None = None,
|
| 225 |
+
nl: bool = True,
|
| 226 |
+
err: bool = False,
|
| 227 |
+
color: bool | None = None,
|
| 228 |
+
) -> None:
|
| 229 |
+
"""Print a message and newline to stdout or a file. This should be
|
| 230 |
+
used instead of :func:`print` because it provides better support
|
| 231 |
+
for different data, files, and environments.
|
| 232 |
+
|
| 233 |
+
Compared to :func:`print`, this does the following:
|
| 234 |
+
|
| 235 |
+
- Ensures that the output encoding is not misconfigured on Linux.
|
| 236 |
+
- Supports Unicode in the Windows console.
|
| 237 |
+
- Supports writing to binary outputs, and supports writing bytes
|
| 238 |
+
to text outputs.
|
| 239 |
+
- Supports colors and styles on Windows.
|
| 240 |
+
- Removes ANSI color and style codes if the output does not look
|
| 241 |
+
like an interactive terminal.
|
| 242 |
+
- Always flushes the output.
|
| 243 |
+
|
| 244 |
+
:param message: The string or bytes to output. Other objects are
|
| 245 |
+
converted to strings.
|
| 246 |
+
:param file: The file to write to. Defaults to ``stdout``.
|
| 247 |
+
:param err: Write to ``stderr`` instead of ``stdout``.
|
| 248 |
+
:param nl: Print a newline after the message. Enabled by default.
|
| 249 |
+
:param color: Force showing or hiding colors and other styles. By
|
| 250 |
+
default Click will remove color if the output does not look like
|
| 251 |
+
an interactive terminal.
|
| 252 |
+
|
| 253 |
+
.. versionchanged:: 6.0
|
| 254 |
+
Support Unicode output on the Windows console. Click does not
|
| 255 |
+
modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()``
|
| 256 |
+
will still not support Unicode.
|
| 257 |
+
|
| 258 |
+
.. versionchanged:: 4.0
|
| 259 |
+
Added the ``color`` parameter.
|
| 260 |
+
|
| 261 |
+
.. versionadded:: 3.0
|
| 262 |
+
Added the ``err`` parameter.
|
| 263 |
+
|
| 264 |
+
.. versionchanged:: 2.0
|
| 265 |
+
Support colors on Windows if colorama is installed.
|
| 266 |
+
"""
|
| 267 |
+
if file is None:
|
| 268 |
+
if err:
|
| 269 |
+
file = _default_text_stderr()
|
| 270 |
+
else:
|
| 271 |
+
file = _default_text_stdout()
|
| 272 |
+
|
| 273 |
+
# There are no standard streams attached to write to. For example,
|
| 274 |
+
# pythonw on Windows.
|
| 275 |
+
if file is None:
|
| 276 |
+
return
|
| 277 |
+
|
| 278 |
+
# Convert non bytes/text into the native string type.
|
| 279 |
+
if message is not None and not isinstance(message, (str, bytes, bytearray)):
|
| 280 |
+
out: str | bytes | bytearray | None = str(message)
|
| 281 |
+
else:
|
| 282 |
+
out = message
|
| 283 |
+
|
| 284 |
+
if nl:
|
| 285 |
+
out = out or ""
|
| 286 |
+
if isinstance(out, str):
|
| 287 |
+
out += "\n"
|
| 288 |
+
else:
|
| 289 |
+
out += b"\n"
|
| 290 |
+
|
| 291 |
+
if not out:
|
| 292 |
+
file.flush()
|
| 293 |
+
return
|
| 294 |
+
|
| 295 |
+
# If there is a message and the value looks like bytes, we manually
|
| 296 |
+
# need to find the binary stream and write the message in there.
|
| 297 |
+
# This is done separately so that most stream types will work as you
|
| 298 |
+
# would expect. Eg: you can write to StringIO for other cases.
|
| 299 |
+
if isinstance(out, (bytes, bytearray)):
|
| 300 |
+
binary_file = _find_binary_writer(file)
|
| 301 |
+
|
| 302 |
+
if binary_file is not None:
|
| 303 |
+
file.flush()
|
| 304 |
+
binary_file.write(out)
|
| 305 |
+
binary_file.flush()
|
| 306 |
+
return
|
| 307 |
+
|
| 308 |
+
# ANSI style code support. For no message or bytes, nothing happens.
|
| 309 |
+
# When outputting to a file instead of a terminal, strip codes.
|
| 310 |
+
else:
|
| 311 |
+
color = resolve_color_default(color)
|
| 312 |
+
|
| 313 |
+
if should_strip_ansi(file, color):
|
| 314 |
+
out = strip_ansi(out)
|
| 315 |
+
elif WIN:
|
| 316 |
+
if auto_wrap_for_ansi is not None:
|
| 317 |
+
file = auto_wrap_for_ansi(file, color) # type: ignore
|
| 318 |
+
elif not color:
|
| 319 |
+
out = strip_ansi(out)
|
| 320 |
+
|
| 321 |
+
file.write(out) # type: ignore
|
| 322 |
+
file.flush()
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO:
|
| 326 |
+
"""Returns a system stream for byte processing.
|
| 327 |
+
|
| 328 |
+
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
| 329 |
+
``'stdout'`` and ``'stderr'``
|
| 330 |
+
"""
|
| 331 |
+
opener = binary_streams.get(name)
|
| 332 |
+
if opener is None:
|
| 333 |
+
raise TypeError(f"Unknown standard stream '{name}'")
|
| 334 |
+
return opener()
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def get_text_stream(
|
| 338 |
+
name: t.Literal["stdin", "stdout", "stderr"],
|
| 339 |
+
encoding: str | None = None,
|
| 340 |
+
errors: str | None = "strict",
|
| 341 |
+
) -> t.TextIO:
|
| 342 |
+
"""Returns a system stream for text processing. This usually returns
|
| 343 |
+
a wrapped stream around a binary stream returned from
|
| 344 |
+
:func:`get_binary_stream` but it also can take shortcuts for already
|
| 345 |
+
correctly configured streams.
|
| 346 |
+
|
| 347 |
+
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
| 348 |
+
``'stdout'`` and ``'stderr'``
|
| 349 |
+
:param encoding: overrides the detected default encoding.
|
| 350 |
+
:param errors: overrides the default error mode.
|
| 351 |
+
"""
|
| 352 |
+
opener = text_streams.get(name)
|
| 353 |
+
if opener is None:
|
| 354 |
+
raise TypeError(f"Unknown standard stream '{name}'")
|
| 355 |
+
return opener(encoding, errors)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def open_file(
|
| 359 |
+
filename: str | os.PathLike[str],
|
| 360 |
+
mode: str = "r",
|
| 361 |
+
encoding: str | None = None,
|
| 362 |
+
errors: str | None = "strict",
|
| 363 |
+
lazy: bool = False,
|
| 364 |
+
atomic: bool = False,
|
| 365 |
+
) -> t.IO[t.Any]:
|
| 366 |
+
"""Open a file, with extra behavior to handle ``'-'`` to indicate
|
| 367 |
+
a standard stream, lazy open on write, and atomic write. Similar to
|
| 368 |
+
the behavior of the :class:`~click.File` param type.
|
| 369 |
+
|
| 370 |
+
If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is
|
| 371 |
+
wrapped so that using it in a context manager will not close it.
|
| 372 |
+
This makes it possible to use the function without accidentally
|
| 373 |
+
closing a standard stream:
|
| 374 |
+
|
| 375 |
+
.. code-block:: python
|
| 376 |
+
|
| 377 |
+
with open_file(filename) as f:
|
| 378 |
+
...
|
| 379 |
+
|
| 380 |
+
:param filename: The name or Path of the file to open, or ``'-'`` for
|
| 381 |
+
``stdin``/``stdout``.
|
| 382 |
+
:param mode: The mode in which to open the file.
|
| 383 |
+
:param encoding: The encoding to decode or encode a file opened in
|
| 384 |
+
text mode.
|
| 385 |
+
:param errors: The error handling mode.
|
| 386 |
+
:param lazy: Wait to open the file until it is accessed. For read
|
| 387 |
+
mode, the file is temporarily opened to raise access errors
|
| 388 |
+
early, then closed until it is read again.
|
| 389 |
+
:param atomic: Write to a temporary file and replace the given file
|
| 390 |
+
on close.
|
| 391 |
+
|
| 392 |
+
.. versionadded:: 3.0
|
| 393 |
+
"""
|
| 394 |
+
if lazy:
|
| 395 |
+
return t.cast(
|
| 396 |
+
"t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic)
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
|
| 400 |
+
|
| 401 |
+
if not should_close:
|
| 402 |
+
f = t.cast("t.IO[t.Any]", KeepOpenFile(f))
|
| 403 |
+
|
| 404 |
+
return f
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def format_filename(
|
| 408 |
+
filename: str | bytes | os.PathLike[str] | os.PathLike[bytes],
|
| 409 |
+
shorten: bool = False,
|
| 410 |
+
) -> str:
|
| 411 |
+
"""Format a filename as a string for display. Ensures the filename can be
|
| 412 |
+
displayed by replacing any invalid bytes or surrogate escapes in the name
|
| 413 |
+
with the replacement character ``�``.
|
| 414 |
+
|
| 415 |
+
Invalid bytes or surrogate escapes will raise an error when written to a
|
| 416 |
+
stream with ``errors="strict"``. This will typically happen with ``stdout``
|
| 417 |
+
when the locale is something like ``en_GB.UTF-8``.
|
| 418 |
+
|
| 419 |
+
Many scenarios *are* safe to write surrogates though, due to PEP 538 and
|
| 420 |
+
PEP 540, including:
|
| 421 |
+
|
| 422 |
+
- Writing to ``stderr``, which uses ``errors="backslashreplace"``.
|
| 423 |
+
- The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens
|
| 424 |
+
stdout and stderr with ``errors="surrogateescape"``.
|
| 425 |
+
- None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``.
|
| 426 |
+
- Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``.
|
| 427 |
+
Python opens stdout and stderr with ``errors="surrogateescape"``.
|
| 428 |
+
|
| 429 |
+
:param filename: formats a filename for UI display. This will also convert
|
| 430 |
+
the filename into unicode without failing.
|
| 431 |
+
:param shorten: this optionally shortens the filename to strip of the
|
| 432 |
+
path that leads up to it.
|
| 433 |
+
"""
|
| 434 |
+
if shorten:
|
| 435 |
+
filename = os.path.basename(filename)
|
| 436 |
+
else:
|
| 437 |
+
filename = os.fspath(filename)
|
| 438 |
+
|
| 439 |
+
if isinstance(filename, bytes):
|
| 440 |
+
filename = filename.decode(sys.getfilesystemencoding(), "replace")
|
| 441 |
+
else:
|
| 442 |
+
filename = filename.encode("utf-8", "surrogateescape").decode(
|
| 443 |
+
"utf-8", "replace"
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
return filename
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str:
|
| 450 |
+
r"""Returns the config folder for the application. The default behavior
|
| 451 |
+
is to return whatever is most appropriate for the operating system.
|
| 452 |
+
|
| 453 |
+
To give you an idea, for an app called ``"Foo Bar"``, something like
|
| 454 |
+
the following folders could be returned:
|
| 455 |
+
|
| 456 |
+
Mac OS X:
|
| 457 |
+
``~/Library/Application Support/Foo Bar``
|
| 458 |
+
Mac OS X (POSIX):
|
| 459 |
+
``~/.foo-bar``
|
| 460 |
+
Unix:
|
| 461 |
+
``~/.config/foo-bar``
|
| 462 |
+
Unix (POSIX):
|
| 463 |
+
``~/.foo-bar``
|
| 464 |
+
Windows (roaming):
|
| 465 |
+
``C:\Users\<user>\AppData\Roaming\Foo Bar``
|
| 466 |
+
Windows (not roaming):
|
| 467 |
+
``C:\Users\<user>\AppData\Local\Foo Bar``
|
| 468 |
+
|
| 469 |
+
.. versionadded:: 2.0
|
| 470 |
+
|
| 471 |
+
:param app_name: the application name. This should be properly capitalized
|
| 472 |
+
and can contain whitespace.
|
| 473 |
+
:param roaming: controls if the folder should be roaming or not on Windows.
|
| 474 |
+
Has no effect otherwise.
|
| 475 |
+
:param force_posix: if this is set to `True` then on any POSIX system the
|
| 476 |
+
folder will be stored in the home folder with a leading
|
| 477 |
+
dot instead of the XDG config home or darwin's
|
| 478 |
+
application support folder.
|
| 479 |
+
"""
|
| 480 |
+
if WIN:
|
| 481 |
+
key = "APPDATA" if roaming else "LOCALAPPDATA"
|
| 482 |
+
folder = os.environ.get(key)
|
| 483 |
+
if folder is None:
|
| 484 |
+
folder = os.path.expanduser("~")
|
| 485 |
+
return os.path.join(folder, app_name)
|
| 486 |
+
if force_posix:
|
| 487 |
+
return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}"))
|
| 488 |
+
if sys.platform == "darwin":
|
| 489 |
+
return os.path.join(
|
| 490 |
+
os.path.expanduser("~/Library/Application Support"), app_name
|
| 491 |
+
)
|
| 492 |
+
return os.path.join(
|
| 493 |
+
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
| 494 |
+
_posixify(app_name),
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
class PacifyFlushWrapper:
|
| 499 |
+
"""This wrapper is used to catch and suppress BrokenPipeErrors resulting
|
| 500 |
+
from ``.flush()`` being called on broken pipe during the shutdown/final-GC
|
| 501 |
+
of the Python interpreter. Notably ``.flush()`` is always called on
|
| 502 |
+
``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
|
| 503 |
+
other cleanup code, and the case where the underlying file is not a broken
|
| 504 |
+
pipe, all calls and attributes are proxied.
|
| 505 |
+
"""
|
| 506 |
+
|
| 507 |
+
def __init__(self, wrapped: t.IO[t.Any]) -> None:
|
| 508 |
+
self.wrapped = wrapped
|
| 509 |
+
|
| 510 |
+
def flush(self) -> None:
|
| 511 |
+
try:
|
| 512 |
+
self.wrapped.flush()
|
| 513 |
+
except OSError as e:
|
| 514 |
+
import errno
|
| 515 |
+
|
| 516 |
+
if e.errno != errno.EPIPE:
|
| 517 |
+
raise
|
| 518 |
+
|
| 519 |
+
def __getattr__(self, attr: str) -> t.Any:
|
| 520 |
+
return getattr(self.wrapped, attr)
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _detect_program_name(
|
| 524 |
+
path: str | None = None, _main: ModuleType | None = None
|
| 525 |
+
) -> str:
|
| 526 |
+
"""Determine the command used to run the program, for use in help
|
| 527 |
+
text. If a file or entry point was executed, the file name is
|
| 528 |
+
returned. If ``python -m`` was used to execute a module or package,
|
| 529 |
+
``python -m name`` is returned.
|
| 530 |
+
|
| 531 |
+
This doesn't try to be too precise, the goal is to give a concise
|
| 532 |
+
name for help text. Files are only shown as their name without the
|
| 533 |
+
path. ``python`` is only shown for modules, and the full path to
|
| 534 |
+
``sys.executable`` is not shown.
|
| 535 |
+
|
| 536 |
+
:param path: The Python file being executed. Python puts this in
|
| 537 |
+
``sys.argv[0]``, which is used by default.
|
| 538 |
+
:param _main: The ``__main__`` module. This should only be passed
|
| 539 |
+
during internal testing.
|
| 540 |
+
|
| 541 |
+
.. versionadded:: 8.0
|
| 542 |
+
Based on command args detection in the Werkzeug reloader.
|
| 543 |
+
|
| 544 |
+
:meta private:
|
| 545 |
+
"""
|
| 546 |
+
if _main is None:
|
| 547 |
+
_main = sys.modules["__main__"]
|
| 548 |
+
|
| 549 |
+
if not path:
|
| 550 |
+
path = sys.argv[0]
|
| 551 |
+
|
| 552 |
+
# The value of __package__ indicates how Python was called. It may
|
| 553 |
+
# not exist if a setuptools script is installed as an egg. It may be
|
| 554 |
+
# set incorrectly for entry points created with pip on Windows.
|
| 555 |
+
# It is set to "" inside a Shiv or PEX zipapp.
|
| 556 |
+
if getattr(_main, "__package__", None) in {None, ""} or (
|
| 557 |
+
os.name == "nt"
|
| 558 |
+
and _main.__package__ == ""
|
| 559 |
+
and not os.path.exists(path)
|
| 560 |
+
and os.path.exists(f"{path}.exe")
|
| 561 |
+
):
|
| 562 |
+
# Executed a file, like "python app.py".
|
| 563 |
+
return os.path.basename(path)
|
| 564 |
+
|
| 565 |
+
# Executed a module, like "python -m example".
|
| 566 |
+
# Rewritten by Python from "-m script" to "/path/to/script.py".
|
| 567 |
+
# Need to look at main module to determine how it was executed.
|
| 568 |
+
py_module = t.cast(str, _main.__package__)
|
| 569 |
+
name = os.path.splitext(os.path.basename(path))[0]
|
| 570 |
+
|
| 571 |
+
# A submodule like "example.cli".
|
| 572 |
+
if name != "__main__":
|
| 573 |
+
py_module = f"{py_module}.{name}"
|
| 574 |
+
|
| 575 |
+
return f"python -m {py_module.lstrip('.')}"
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
def _expand_args(
|
| 579 |
+
args: cabc.Iterable[str],
|
| 580 |
+
*,
|
| 581 |
+
user: bool = True,
|
| 582 |
+
env: bool = True,
|
| 583 |
+
glob_recursive: bool = True,
|
| 584 |
+
) -> list[str]:
|
| 585 |
+
"""Simulate Unix shell expansion with Python functions.
|
| 586 |
+
|
| 587 |
+
See :func:`glob.glob`, :func:`os.path.expanduser`, and
|
| 588 |
+
:func:`os.path.expandvars`.
|
| 589 |
+
|
| 590 |
+
This is intended for use on Windows, where the shell does not do any
|
| 591 |
+
expansion. It may not exactly match what a Unix shell would do.
|
| 592 |
+
|
| 593 |
+
:param args: List of command line arguments to expand.
|
| 594 |
+
:param user: Expand user home directory.
|
| 595 |
+
:param env: Expand environment variables.
|
| 596 |
+
:param glob_recursive: ``**`` matches directories recursively.
|
| 597 |
+
|
| 598 |
+
.. versionchanged:: 8.1
|
| 599 |
+
Invalid glob patterns are treated as empty expansions rather
|
| 600 |
+
than raising an error.
|
| 601 |
+
|
| 602 |
+
.. versionadded:: 8.0
|
| 603 |
+
|
| 604 |
+
:meta private:
|
| 605 |
+
"""
|
| 606 |
+
from glob import glob
|
| 607 |
+
|
| 608 |
+
out = []
|
| 609 |
+
|
| 610 |
+
for arg in args:
|
| 611 |
+
if user:
|
| 612 |
+
arg = os.path.expanduser(arg)
|
| 613 |
+
|
| 614 |
+
if env:
|
| 615 |
+
arg = os.path.expandvars(arg)
|
| 616 |
+
|
| 617 |
+
try:
|
| 618 |
+
matches = glob(arg, recursive=glob_recursive)
|
| 619 |
+
except re.error:
|
| 620 |
+
matches = []
|
| 621 |
+
|
| 622 |
+
if not matches:
|
| 623 |
+
out.append(arg)
|
| 624 |
+
else:
|
| 625 |
+
out.extend(matches)
|
| 626 |
+
|
| 627 |
+
return out
|
env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (c) 2018, Tzu-ping Chung <uranusjr@gmail.com>
|
| 2 |
+
|
| 3 |
+
Permission to use, copy, modify, and distribute this software for any
|
| 4 |
+
purpose with or without fee is hereby granted, provided that the above
|
| 5 |
+
copyright notice and this permission notice appear in all copies.
|
| 6 |
+
|
| 7 |
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
| 8 |
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
| 9 |
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
| 10 |
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
| 11 |
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
| 12 |
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
| 13 |
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/METADATA
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: shellingham
|
| 3 |
+
Version: 1.5.4
|
| 4 |
+
Summary: Tool to Detect Surrounding Shell
|
| 5 |
+
Home-page: https://github.com/sarugaku/shellingham
|
| 6 |
+
Author: Tzu-ping Chung
|
| 7 |
+
Author-email: uranusjr@gmail.com
|
| 8 |
+
License: ISC License
|
| 9 |
+
Keywords: shell
|
| 10 |
+
Classifier: Development Status :: 3 - Alpha
|
| 11 |
+
Classifier: Environment :: Console
|
| 12 |
+
Classifier: Intended Audience :: Developers
|
| 13 |
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
| 14 |
+
Classifier: Operating System :: OS Independent
|
| 15 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 16 |
+
Classifier: Programming Language :: Python :: 3.7
|
| 17 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 18 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 22 |
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
| 23 |
+
Requires-Python: >=3.7
|
| 24 |
+
Description-Content-Type: text/x-rst
|
| 25 |
+
License-File: LICENSE
|
| 26 |
+
|
| 27 |
+
=============================================
|
| 28 |
+
Shellingham: Tool to Detect Surrounding Shell
|
| 29 |
+
=============================================
|
| 30 |
+
|
| 31 |
+
.. image:: https://img.shields.io/pypi/v/shellingham.svg
|
| 32 |
+
:target: https://pypi.org/project/shellingham/
|
| 33 |
+
|
| 34 |
+
Shellingham detects what shell the current Python executable is running in.
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
Usage
|
| 38 |
+
=====
|
| 39 |
+
|
| 40 |
+
.. code-block:: python
|
| 41 |
+
|
| 42 |
+
>>> import shellingham
|
| 43 |
+
>>> shellingham.detect_shell()
|
| 44 |
+
('bash', '/bin/bash')
|
| 45 |
+
|
| 46 |
+
``detect_shell`` pokes around the process's running environment to determine
|
| 47 |
+
what shell it is run in. It returns a 2-tuple:
|
| 48 |
+
|
| 49 |
+
* The shell name, always lowercased.
|
| 50 |
+
* The command used to run the shell.
|
| 51 |
+
|
| 52 |
+
``ShellDetectionFailure`` is raised if ``detect_shell`` fails to detect the
|
| 53 |
+
surrounding shell.
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
Notes
|
| 57 |
+
=====
|
| 58 |
+
|
| 59 |
+
* The shell name is always lowercased.
|
| 60 |
+
* On Windows, the shell name is the name of the executable, minus the file
|
| 61 |
+
extension.
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
Notes for Application Developers
|
| 65 |
+
================================
|
| 66 |
+
|
| 67 |
+
Remember, your application's user is not necessarily using a shell.
|
| 68 |
+
Shellingham raises ``ShellDetectionFailure`` if there is no shell to detect,
|
| 69 |
+
but *your application should almost never do this to your user*.
|
| 70 |
+
|
| 71 |
+
A practical approach to this is to wrap ``detect_shell`` in a try block, and
|
| 72 |
+
provide a sane default on failure
|
| 73 |
+
|
| 74 |
+
.. code-block:: python
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
shell = shellingham.detect_shell()
|
| 78 |
+
except shellingham.ShellDetectionFailure:
|
| 79 |
+
shell = provide_default()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
There are a few choices for you to choose from.
|
| 83 |
+
|
| 84 |
+
* The POSIX standard mandates the environment variable ``SHELL`` to refer to
|
| 85 |
+
"the user's preferred command language interpreter". This is always available
|
| 86 |
+
(even if the user is not in an interactive session), and likely the correct
|
| 87 |
+
choice to launch an interactive sub-shell with.
|
| 88 |
+
* A command ``sh`` is almost guaranteed to exist, likely at ``/bin/sh``, since
|
| 89 |
+
several POSIX tools rely on it. This should be suitable if you want to run a
|
| 90 |
+
(possibly non-interactive) script.
|
| 91 |
+
* All versions of DOS and Windows have an environment variable ``COMSPEC``.
|
| 92 |
+
This can always be used to launch a usable command prompt (e.g. `cmd.exe` on
|
| 93 |
+
Windows).
|
| 94 |
+
|
| 95 |
+
Here's a simple implementation to provide a default shell
|
| 96 |
+
|
| 97 |
+
.. code-block:: python
|
| 98 |
+
|
| 99 |
+
import os
|
| 100 |
+
|
| 101 |
+
def provide_default():
|
| 102 |
+
if os.name == 'posix':
|
| 103 |
+
return os.environ['SHELL']
|
| 104 |
+
elif os.name == 'nt':
|
| 105 |
+
return os.environ['COMSPEC']
|
| 106 |
+
raise NotImplementedError(f'OS {os.name!r} support not available')
|
env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/RECORD
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
shellingham-1.5.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
shellingham-1.5.4.dist-info/LICENSE,sha256=84j9OMrRMRLB3A9mm76A5_hFQe26-3LzAw0sp2QsPJ0,751
|
| 3 |
+
shellingham-1.5.4.dist-info/METADATA,sha256=GD2AIgo3STJieVc53TV8xbs_Sb05DMkZjVGA5UUaB_o,3461
|
| 4 |
+
shellingham-1.5.4.dist-info/RECORD,,
|
| 5 |
+
shellingham-1.5.4.dist-info/WHEEL,sha256=iYlv5fX357PQyRT2o6tw1bN-YcKFFHKqB_LwHO5wP-g,110
|
| 6 |
+
shellingham-1.5.4.dist-info/top_level.txt,sha256=uKMQL5AKxPi4O9_Rbd838QeEs4ImpGQKNbEDZYqgBgk,12
|
| 7 |
+
shellingham-1.5.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
| 8 |
+
shellingham/__init__.py,sha256=pAKXUPKUdwyErC0ZjS-5w-fRdSbmdcfvnpt_x1yWqtA,635
|
| 9 |
+
shellingham/__pycache__/__init__.cpython-313.pyc,,
|
| 10 |
+
shellingham/__pycache__/_core.cpython-313.pyc,,
|
| 11 |
+
shellingham/__pycache__/nt.cpython-313.pyc,,
|
| 12 |
+
shellingham/_core.py,sha256=v-CTr_7F7cJAtNnzpa1N_Hl8afkY5yiDA4joGmsUBu0,300
|
| 13 |
+
shellingham/nt.py,sha256=m6J6SuwyqVVlxXT9Bc-9F_1x-T5u0gCFFrRAF2LIkeg,4516
|
| 14 |
+
shellingham/posix/__init__.py,sha256=pB69qtvZJ_yIf48nl4-ZfS3wLwwuXuknXOZhBnC2T1o,3129
|
| 15 |
+
shellingham/posix/__pycache__/__init__.cpython-313.pyc,,
|
| 16 |
+
shellingham/posix/__pycache__/_core.cpython-313.pyc,,
|
| 17 |
+
shellingham/posix/__pycache__/proc.cpython-313.pyc,,
|
| 18 |
+
shellingham/posix/__pycache__/ps.cpython-313.pyc,,
|
| 19 |
+
shellingham/posix/_core.py,sha256=_v18UaXbzr4muNhr3-mH1FdSdjZ_dOXQrtUyomIbKYQ,81
|
| 20 |
+
shellingham/posix/proc.py,sha256=nSUxIuQSotvaDW76i0oTQAM9aZ9PXBLFAEktWljSKCo,2659
|
| 21 |
+
shellingham/posix/ps.py,sha256=NGmDKCukhNp0lahwYCaMXphBYaVbhbiR9BtE0OkT8qU,1770
|
env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: bdist_wheel (0.41.2)
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py2-none-any
|
| 5 |
+
Tag: py3-none-any
|
| 6 |
+
|
env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
shellingham
|
env/lib/python3.13/site-packages/shellingham-1.5.4.dist-info/zip-safe
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
env/lib/python3.13/site-packages/tqdm-4.67.1.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
env/lib/python3.13/site-packages/tqdm-4.67.1.dist-info/RECORD
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
../../../bin/tqdm,sha256=CbWuHrvyWyMkCZcwsU1AQGJnOp3zzMmQKJ-YUqOuPPI,256
|
| 2 |
+
tqdm-4.67.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 3 |
+
tqdm-4.67.1.dist-info/LICENCE,sha256=3DMlLoKQFeOxUAhvubOkD2rW-zLC9GEM6BL6Z301mGo,1985
|
| 4 |
+
tqdm-4.67.1.dist-info/METADATA,sha256=aIoWMt9SWhmP7FLc_vsSRtMerO6cA1qsrC1-r42P9mk,57675
|
| 5 |
+
tqdm-4.67.1.dist-info/RECORD,,
|
| 6 |
+
tqdm-4.67.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
| 7 |
+
tqdm-4.67.1.dist-info/entry_points.txt,sha256=ReJCH7Ui3Zyh6M16E4OhsZ1oU7WtMXCfbtoyBhGO29Y,39
|
| 8 |
+
tqdm-4.67.1.dist-info/top_level.txt,sha256=NLiUJNfmc9At15s7JURiwvqMEjUi9G5PMGRrmMYzNSM,5
|
| 9 |
+
tqdm/__init__.py,sha256=9mQNYSSqP99JasubEC1POJLMmhkkBH6cJZxPIR5G2pQ,1572
|
| 10 |
+
tqdm/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
|
| 11 |
+
tqdm/__pycache__/__init__.cpython-313.pyc,,
|
| 12 |
+
tqdm/__pycache__/__main__.cpython-313.pyc,,
|
| 13 |
+
tqdm/__pycache__/_dist_ver.cpython-313.pyc,,
|
| 14 |
+
tqdm/__pycache__/_main.cpython-313.pyc,,
|
| 15 |
+
tqdm/__pycache__/_monitor.cpython-313.pyc,,
|
| 16 |
+
tqdm/__pycache__/_tqdm.cpython-313.pyc,,
|
| 17 |
+
tqdm/__pycache__/_tqdm_gui.cpython-313.pyc,,
|
| 18 |
+
tqdm/__pycache__/_tqdm_notebook.cpython-313.pyc,,
|
| 19 |
+
tqdm/__pycache__/_tqdm_pandas.cpython-313.pyc,,
|
| 20 |
+
tqdm/__pycache__/_utils.cpython-313.pyc,,
|
| 21 |
+
tqdm/__pycache__/asyncio.cpython-313.pyc,,
|
| 22 |
+
tqdm/__pycache__/auto.cpython-313.pyc,,
|
| 23 |
+
tqdm/__pycache__/autonotebook.cpython-313.pyc,,
|
| 24 |
+
tqdm/__pycache__/cli.cpython-313.pyc,,
|
| 25 |
+
tqdm/__pycache__/dask.cpython-313.pyc,,
|
| 26 |
+
tqdm/__pycache__/gui.cpython-313.pyc,,
|
| 27 |
+
tqdm/__pycache__/keras.cpython-313.pyc,,
|
| 28 |
+
tqdm/__pycache__/notebook.cpython-313.pyc,,
|
| 29 |
+
tqdm/__pycache__/rich.cpython-313.pyc,,
|
| 30 |
+
tqdm/__pycache__/std.cpython-313.pyc,,
|
| 31 |
+
tqdm/__pycache__/tk.cpython-313.pyc,,
|
| 32 |
+
tqdm/__pycache__/utils.cpython-313.pyc,,
|
| 33 |
+
tqdm/__pycache__/version.cpython-313.pyc,,
|
| 34 |
+
tqdm/_dist_ver.py,sha256=m5AdYI-jB-v6P0VJ_70isH_p24EzSOGSwVvuAZmkmKY,23
|
| 35 |
+
tqdm/_main.py,sha256=9ySvgmi_2Sw4CAo5UDW0Q2dxfTryboEWGHohfCJz0sA,283
|
| 36 |
+
tqdm/_monitor.py,sha256=Uku-DPWgzJ7dO5CK08xKJK-E_F6qQ-JB3ksuXczSYR0,3699
|
| 37 |
+
tqdm/_tqdm.py,sha256=LfLCuJ6bpsVo9xilmtBXyEm1vGnUCFrliW85j3J-nD4,283
|
| 38 |
+
tqdm/_tqdm_gui.py,sha256=03Hc8KayxJveieI5-0-2NGiDpLvw9jZekofJUV7CCwk,287
|
| 39 |
+
tqdm/_tqdm_notebook.py,sha256=BuHiLuxu6uEfZFaPJW3RPpPaxaVctEQA3kdSJSDL1hw,307
|
| 40 |
+
tqdm/_tqdm_pandas.py,sha256=c9jptUgigN6axRDhRd4Rif98Tmxeopc1nFNFhIpbFUE,888
|
| 41 |
+
tqdm/_utils.py,sha256=_4E73bfDj4f1s3sM42NLHNrZDOkijZoWq-n6xWLkdZ8,553
|
| 42 |
+
tqdm/asyncio.py,sha256=Kp2rSkNRf9KRqa3d9YpgeZQ7L7EZf2Ki4bSc7UPIyoo,2757
|
| 43 |
+
tqdm/auto.py,sha256=nDZflj6p2zKkjBCNBourrhS81zYfZy1_dQvbckrdW8o,871
|
| 44 |
+
tqdm/autonotebook.py,sha256=Yb9F5uaiBPhfbDDFpbtoG8I2YUw3uQJ89rUDLbfR6ws,956
|
| 45 |
+
tqdm/cli.py,sha256=SbKlN8QyZ2ogenqt-wT_p6_sx2OOdCjCyhoZBFnlmyI,11010
|
| 46 |
+
tqdm/completion.sh,sha256=j79KbSmpIj_E11jfTfBXrGnUTzKXVpQ1vGVQvsyDRl4,946
|
| 47 |
+
tqdm/contrib/__init__.py,sha256=OgSwVXm-vlDJ-2imtoQ9z8qdom4snMSRztH72KMA82A,2494
|
| 48 |
+
tqdm/contrib/__pycache__/__init__.cpython-313.pyc,,
|
| 49 |
+
tqdm/contrib/__pycache__/bells.cpython-313.pyc,,
|
| 50 |
+
tqdm/contrib/__pycache__/concurrent.cpython-313.pyc,,
|
| 51 |
+
tqdm/contrib/__pycache__/discord.cpython-313.pyc,,
|
| 52 |
+
tqdm/contrib/__pycache__/itertools.cpython-313.pyc,,
|
| 53 |
+
tqdm/contrib/__pycache__/logging.cpython-313.pyc,,
|
| 54 |
+
tqdm/contrib/__pycache__/slack.cpython-313.pyc,,
|
| 55 |
+
tqdm/contrib/__pycache__/telegram.cpython-313.pyc,,
|
| 56 |
+
tqdm/contrib/__pycache__/utils_worker.cpython-313.pyc,,
|
| 57 |
+
tqdm/contrib/bells.py,sha256=Yx1HqGCmHrESCAO700j5wE__JCleNODJxedh1ijPLD0,837
|
| 58 |
+
tqdm/contrib/concurrent.py,sha256=K1yjloKS5WRNFyjLRth0DmU5PAnDbF0A-GD27N-J4a8,3986
|
| 59 |
+
tqdm/contrib/discord.py,sha256=MtVIL1s_dxH21G4sL8FBgQ4Wei23ho9Ek5T-AommvNc,5243
|
| 60 |
+
tqdm/contrib/itertools.py,sha256=WdKKQU5eSzsqHu29SN_oH12huYZo0Jihqoi9-nVhwz4,774
|
| 61 |
+
tqdm/contrib/logging.py,sha256=NsYtnKttj2mMrGm58mEdo5a9DP_2vv8pZyrimSuWulA,3760
|
| 62 |
+
tqdm/contrib/slack.py,sha256=eP_Mr5sQonYniHxxQNGue3jk2JkIPmPWFZqIYxnOui0,4007
|
| 63 |
+
tqdm/contrib/telegram.py,sha256=vn_9SATMbbwn2PAbzSDyOX6av3eBB01QBug11P4H-Og,5008
|
| 64 |
+
tqdm/contrib/utils_worker.py,sha256=HJP5Mz1S1xyzEke2JaqJ2sYLHXADYoo2epT5AzQ38eA,1207
|
| 65 |
+
tqdm/dask.py,sha256=9Ei58eVqTossRLhAfWyUFCduXYKjmLmwkaXIy-CHYfs,1319
|
| 66 |
+
tqdm/gui.py,sha256=STIB3K8iDzDgkNUqWIpvcI_u0OGtbGNy5NwpALXhfWs,5479
|
| 67 |
+
tqdm/keras.py,sha256=op9sBkb6q6c6dw2wJ0SD2ZwpPK7yM1Vbg4l1Qiy3MIo,4373
|
| 68 |
+
tqdm/notebook.py,sha256=GtZ3IapLL1v8WNDaTSvPw0bJGTyfp71Vfz5HDnAzx1M,10895
|
| 69 |
+
tqdm/rich.py,sha256=YyMPkEHVyYUVUR3adJKbVX26iTmNKpNMf3DEqmm-m60,5021
|
| 70 |
+
tqdm/std.py,sha256=tWjz6-QCa92aqYjz7PIdkLUCAfiy-lJZheBtZyIIyO0,57461
|
| 71 |
+
tqdm/tk.py,sha256=Gu0uwXwLCGPRGHORdi3WvBLGiseUp_xxX_h_gp9VpK0,6701
|
| 72 |
+
tqdm/tqdm.1,sha256=aILyUPk2S4OPe_uWy2P4AMjUf0oQ6PUW0nLYXB-BWwI,7889
|
| 73 |
+
tqdm/utils.py,sha256=6E0BQw3Sg7uGWKBM_cDn3P42tXswRhzkggbhBgLDjl8,11821
|
| 74 |
+
tqdm/version.py,sha256=-1yWjfu3P0eghVsysHH07fbzdiADNRdzRtYPqOaqR2A,333
|
env/lib/python3.13/site-packages/tqdm-4.67.1.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (75.6.0)
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
| 5 |
+
|
env/lib/python3.13/site-packages/typer/__main__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .cli import main
|
| 2 |
+
|
| 3 |
+
main()
|
env/lib/python3.13/site-packages/typer/_completion_classes.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.util
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
from typing import Any, Dict, List, Tuple
|
| 6 |
+
|
| 7 |
+
import click
|
| 8 |
+
import click.parser
|
| 9 |
+
import click.shell_completion
|
| 10 |
+
|
| 11 |
+
from ._completion_shared import (
|
| 12 |
+
COMPLETION_SCRIPT_BASH,
|
| 13 |
+
COMPLETION_SCRIPT_FISH,
|
| 14 |
+
COMPLETION_SCRIPT_POWER_SHELL,
|
| 15 |
+
COMPLETION_SCRIPT_ZSH,
|
| 16 |
+
Shells,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from click.shell_completion import split_arg_string as click_split_arg_string
|
| 21 |
+
except ImportError: # pragma: no cover
|
| 22 |
+
# TODO: when removing support for Click < 8.2, remove this import
|
| 23 |
+
from click.parser import ( # type: ignore[no-redef]
|
| 24 |
+
split_arg_string as click_split_arg_string,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
import shellingham
|
| 29 |
+
except ImportError: # pragma: no cover
|
| 30 |
+
shellingham = None
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _sanitize_help_text(text: str) -> str:
|
| 34 |
+
"""Sanitizes the help text by removing rich tags"""
|
| 35 |
+
if not importlib.util.find_spec("rich"):
|
| 36 |
+
return text
|
| 37 |
+
from . import rich_utils
|
| 38 |
+
|
| 39 |
+
return rich_utils.rich_render_text(text)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class BashComplete(click.shell_completion.BashComplete):
|
| 43 |
+
name = Shells.bash.value
|
| 44 |
+
source_template = COMPLETION_SCRIPT_BASH
|
| 45 |
+
|
| 46 |
+
def source_vars(self) -> Dict[str, Any]:
|
| 47 |
+
return {
|
| 48 |
+
"complete_func": self.func_name,
|
| 49 |
+
"autocomplete_var": self.complete_var,
|
| 50 |
+
"prog_name": self.prog_name,
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def get_completion_args(self) -> Tuple[List[str], str]:
|
| 54 |
+
cwords = click_split_arg_string(os.environ["COMP_WORDS"])
|
| 55 |
+
cword = int(os.environ["COMP_CWORD"])
|
| 56 |
+
args = cwords[1:cword]
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
incomplete = cwords[cword]
|
| 60 |
+
except IndexError:
|
| 61 |
+
incomplete = ""
|
| 62 |
+
|
| 63 |
+
return args, incomplete
|
| 64 |
+
|
| 65 |
+
def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
|
| 66 |
+
# TODO: Explore replicating the new behavior from Click, with item types and
|
| 67 |
+
# triggering completion for files and directories
|
| 68 |
+
# return f"{item.type},{item.value}"
|
| 69 |
+
return f"{item.value}"
|
| 70 |
+
|
| 71 |
+
def complete(self) -> str:
|
| 72 |
+
args, incomplete = self.get_completion_args()
|
| 73 |
+
completions = self.get_completions(args, incomplete)
|
| 74 |
+
out = [self.format_completion(item) for item in completions]
|
| 75 |
+
return "\n".join(out)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class ZshComplete(click.shell_completion.ZshComplete):
|
| 79 |
+
name = Shells.zsh.value
|
| 80 |
+
source_template = COMPLETION_SCRIPT_ZSH
|
| 81 |
+
|
| 82 |
+
def source_vars(self) -> Dict[str, Any]:
|
| 83 |
+
return {
|
| 84 |
+
"complete_func": self.func_name,
|
| 85 |
+
"autocomplete_var": self.complete_var,
|
| 86 |
+
"prog_name": self.prog_name,
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
def get_completion_args(self) -> Tuple[List[str], str]:
|
| 90 |
+
completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
|
| 91 |
+
cwords = click_split_arg_string(completion_args)
|
| 92 |
+
args = cwords[1:]
|
| 93 |
+
if args and not completion_args.endswith(" "):
|
| 94 |
+
incomplete = args[-1]
|
| 95 |
+
args = args[:-1]
|
| 96 |
+
else:
|
| 97 |
+
incomplete = ""
|
| 98 |
+
return args, incomplete
|
| 99 |
+
|
| 100 |
+
def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
|
| 101 |
+
def escape(s: str) -> str:
|
| 102 |
+
return (
|
| 103 |
+
s.replace('"', '""')
|
| 104 |
+
.replace("'", "''")
|
| 105 |
+
.replace("$", "\\$")
|
| 106 |
+
.replace("`", "\\`")
|
| 107 |
+
.replace(":", r"\\:")
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# TODO: Explore replicating the new behavior from Click, pay attention to
|
| 111 |
+
# the difference with and without escape
|
| 112 |
+
# return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}"
|
| 113 |
+
if item.help:
|
| 114 |
+
return f'"{escape(item.value)}":"{_sanitize_help_text(escape(item.help))}"'
|
| 115 |
+
else:
|
| 116 |
+
return f'"{escape(item.value)}"'
|
| 117 |
+
|
| 118 |
+
def complete(self) -> str:
|
| 119 |
+
args, incomplete = self.get_completion_args()
|
| 120 |
+
completions = self.get_completions(args, incomplete)
|
| 121 |
+
res = [self.format_completion(item) for item in completions]
|
| 122 |
+
if res:
|
| 123 |
+
args_str = "\n".join(res)
|
| 124 |
+
return f"_arguments '*: :(({args_str}))'"
|
| 125 |
+
else:
|
| 126 |
+
return "_files"
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class FishComplete(click.shell_completion.FishComplete):
|
| 130 |
+
name = Shells.fish.value
|
| 131 |
+
source_template = COMPLETION_SCRIPT_FISH
|
| 132 |
+
|
| 133 |
+
def source_vars(self) -> Dict[str, Any]:
|
| 134 |
+
return {
|
| 135 |
+
"complete_func": self.func_name,
|
| 136 |
+
"autocomplete_var": self.complete_var,
|
| 137 |
+
"prog_name": self.prog_name,
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
def get_completion_args(self) -> Tuple[List[str], str]:
|
| 141 |
+
completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
|
| 142 |
+
cwords = click_split_arg_string(completion_args)
|
| 143 |
+
args = cwords[1:]
|
| 144 |
+
if args and not completion_args.endswith(" "):
|
| 145 |
+
incomplete = args[-1]
|
| 146 |
+
args = args[:-1]
|
| 147 |
+
else:
|
| 148 |
+
incomplete = ""
|
| 149 |
+
return args, incomplete
|
| 150 |
+
|
| 151 |
+
def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
|
| 152 |
+
# TODO: Explore replicating the new behavior from Click, pay attention to
|
| 153 |
+
# the difference with and without formatted help
|
| 154 |
+
# if item.help:
|
| 155 |
+
# return f"{item.type},{item.value}\t{item.help}"
|
| 156 |
+
|
| 157 |
+
# return f"{item.type},{item.value}
|
| 158 |
+
if item.help:
|
| 159 |
+
formatted_help = re.sub(r"\s", " ", item.help)
|
| 160 |
+
return f"{item.value}\t{_sanitize_help_text(formatted_help)}"
|
| 161 |
+
else:
|
| 162 |
+
return f"{item.value}"
|
| 163 |
+
|
| 164 |
+
def complete(self) -> str:
|
| 165 |
+
complete_action = os.getenv("_TYPER_COMPLETE_FISH_ACTION", "")
|
| 166 |
+
args, incomplete = self.get_completion_args()
|
| 167 |
+
completions = self.get_completions(args, incomplete)
|
| 168 |
+
show_args = [self.format_completion(item) for item in completions]
|
| 169 |
+
if complete_action == "get-args":
|
| 170 |
+
if show_args:
|
| 171 |
+
return "\n".join(show_args)
|
| 172 |
+
elif complete_action == "is-args":
|
| 173 |
+
if show_args:
|
| 174 |
+
# Activate complete args (no files)
|
| 175 |
+
sys.exit(0)
|
| 176 |
+
else:
|
| 177 |
+
# Deactivate complete args (allow files)
|
| 178 |
+
sys.exit(1)
|
| 179 |
+
return "" # pragma: no cover
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class PowerShellComplete(click.shell_completion.ShellComplete):
|
| 183 |
+
name = Shells.powershell.value
|
| 184 |
+
source_template = COMPLETION_SCRIPT_POWER_SHELL
|
| 185 |
+
|
| 186 |
+
def source_vars(self) -> Dict[str, Any]:
|
| 187 |
+
return {
|
| 188 |
+
"complete_func": self.func_name,
|
| 189 |
+
"autocomplete_var": self.complete_var,
|
| 190 |
+
"prog_name": self.prog_name,
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
def get_completion_args(self) -> Tuple[List[str], str]:
|
| 194 |
+
completion_args = os.getenv("_TYPER_COMPLETE_ARGS", "")
|
| 195 |
+
incomplete = os.getenv("_TYPER_COMPLETE_WORD_TO_COMPLETE", "")
|
| 196 |
+
cwords = click_split_arg_string(completion_args)
|
| 197 |
+
args = cwords[1:-1] if incomplete else cwords[1:]
|
| 198 |
+
return args, incomplete
|
| 199 |
+
|
| 200 |
+
def format_completion(self, item: click.shell_completion.CompletionItem) -> str:
|
| 201 |
+
return f"{item.value}:::{_sanitize_help_text(item.help) if item.help else ' '}"
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def completion_init() -> None:
|
| 205 |
+
click.shell_completion.add_completion_class(BashComplete, Shells.bash.value)
|
| 206 |
+
click.shell_completion.add_completion_class(ZshComplete, Shells.zsh.value)
|
| 207 |
+
click.shell_completion.add_completion_class(FishComplete, Shells.fish.value)
|
| 208 |
+
click.shell_completion.add_completion_class(
|
| 209 |
+
PowerShellComplete, Shells.powershell.value
|
| 210 |
+
)
|
| 211 |
+
click.shell_completion.add_completion_class(PowerShellComplete, Shells.pwsh.value)
|
env/lib/python3.13/site-packages/typer/_typing.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copied from pydantic 1.9.2 (the latest version to support python 3.6.)
|
| 2 |
+
# https://github.com/pydantic/pydantic/blob/v1.9.2/pydantic/typing.py
|
| 3 |
+
# Reduced drastically to only include Typer-specific 3.8+ functionality
|
| 4 |
+
# mypy: ignore-errors
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
from typing import (
|
| 8 |
+
Any,
|
| 9 |
+
Callable,
|
| 10 |
+
Optional,
|
| 11 |
+
Tuple,
|
| 12 |
+
Type,
|
| 13 |
+
Union,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
if sys.version_info >= (3, 9):
|
| 17 |
+
from typing import Annotated, Literal, get_args, get_origin, get_type_hints
|
| 18 |
+
else:
|
| 19 |
+
from typing_extensions import (
|
| 20 |
+
Annotated,
|
| 21 |
+
Literal,
|
| 22 |
+
get_args,
|
| 23 |
+
get_origin,
|
| 24 |
+
get_type_hints,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
if sys.version_info < (3, 10):
|
| 28 |
+
|
| 29 |
+
def is_union(tp: Optional[Type[Any]]) -> bool:
|
| 30 |
+
return tp is Union
|
| 31 |
+
|
| 32 |
+
else:
|
| 33 |
+
import types
|
| 34 |
+
|
| 35 |
+
def is_union(tp: Optional[Type[Any]]) -> bool:
|
| 36 |
+
return tp is Union or tp is types.UnionType # noqa: E721
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
__all__ = (
|
| 40 |
+
"NoneType",
|
| 41 |
+
"is_none_type",
|
| 42 |
+
"is_callable_type",
|
| 43 |
+
"is_literal_type",
|
| 44 |
+
"all_literal_values",
|
| 45 |
+
"is_union",
|
| 46 |
+
"Annotated",
|
| 47 |
+
"Literal",
|
| 48 |
+
"get_args",
|
| 49 |
+
"get_origin",
|
| 50 |
+
"get_type_hints",
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
NoneType = None.__class__
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
NONE_TYPES: Tuple[Any, Any, Any] = (None, NoneType, Literal[None])
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if sys.version_info[:2] == (3, 8):
|
| 61 |
+
# We can use the fast implementation for 3.8 but there is a very weird bug
|
| 62 |
+
# where it can fail for `Literal[None]`.
|
| 63 |
+
# We just need to redefine a useless `Literal[None]` inside the function body to fix this
|
| 64 |
+
|
| 65 |
+
def is_none_type(type_: Any) -> bool:
|
| 66 |
+
Literal[None] # fix edge case
|
| 67 |
+
for none_type in NONE_TYPES:
|
| 68 |
+
if type_ is none_type:
|
| 69 |
+
return True
|
| 70 |
+
return False
|
| 71 |
+
|
| 72 |
+
else:
|
| 73 |
+
|
| 74 |
+
def is_none_type(type_: Any) -> bool:
|
| 75 |
+
for none_type in NONE_TYPES:
|
| 76 |
+
if type_ is none_type:
|
| 77 |
+
return True
|
| 78 |
+
return False
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def is_callable_type(type_: Type[Any]) -> bool:
|
| 82 |
+
return type_ is Callable or get_origin(type_) is Callable
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def is_literal_type(type_: Type[Any]) -> bool:
|
| 86 |
+
import typing_extensions
|
| 87 |
+
|
| 88 |
+
return get_origin(type_) in (Literal, typing_extensions.Literal)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
|
| 92 |
+
return get_args(type_)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
|
| 96 |
+
"""
|
| 97 |
+
This method is used to retrieve all Literal values as
|
| 98 |
+
Literal can be used recursively (see https://www.python.org/dev/peps/pep-0586)
|
| 99 |
+
e.g. `Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]`
|
| 100 |
+
"""
|
| 101 |
+
if not is_literal_type(type_):
|
| 102 |
+
return (type_,)
|
| 103 |
+
|
| 104 |
+
values = literal_values(type_)
|
| 105 |
+
return tuple(x for value in values for x in all_literal_values(value))
|
env/lib/python3.13/site-packages/typer/cli.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.util
|
| 2 |
+
import re
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any, List, Optional
|
| 6 |
+
|
| 7 |
+
import click
|
| 8 |
+
import typer
|
| 9 |
+
import typer.core
|
| 10 |
+
from click import Command, Group, Option
|
| 11 |
+
|
| 12 |
+
from . import __version__
|
| 13 |
+
from .core import HAS_RICH
|
| 14 |
+
|
| 15 |
+
default_app_names = ("app", "cli", "main")
|
| 16 |
+
default_func_names = ("main", "cli", "app")
|
| 17 |
+
|
| 18 |
+
app = typer.Typer()
|
| 19 |
+
utils_app = typer.Typer(help="Extra utility commands for Typer apps.")
|
| 20 |
+
app.add_typer(utils_app, name="utils")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class State:
|
| 24 |
+
def __init__(self) -> None:
|
| 25 |
+
self.app: Optional[str] = None
|
| 26 |
+
self.func: Optional[str] = None
|
| 27 |
+
self.file: Optional[Path] = None
|
| 28 |
+
self.module: Optional[str] = None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
state = State()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def maybe_update_state(ctx: click.Context) -> None:
|
| 35 |
+
path_or_module = ctx.params.get("path_or_module")
|
| 36 |
+
if path_or_module:
|
| 37 |
+
file_path = Path(path_or_module)
|
| 38 |
+
if file_path.exists() and file_path.is_file():
|
| 39 |
+
state.file = file_path
|
| 40 |
+
else:
|
| 41 |
+
if not re.fullmatch(r"[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*", path_or_module):
|
| 42 |
+
typer.echo(
|
| 43 |
+
f"Not a valid file or Python module: {path_or_module}", err=True
|
| 44 |
+
)
|
| 45 |
+
sys.exit(1)
|
| 46 |
+
state.module = path_or_module
|
| 47 |
+
app_name = ctx.params.get("app")
|
| 48 |
+
if app_name:
|
| 49 |
+
state.app = app_name
|
| 50 |
+
func_name = ctx.params.get("func")
|
| 51 |
+
if func_name:
|
| 52 |
+
state.func = func_name
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class TyperCLIGroup(typer.core.TyperGroup):
|
| 56 |
+
def list_commands(self, ctx: click.Context) -> List[str]:
|
| 57 |
+
self.maybe_add_run(ctx)
|
| 58 |
+
return super().list_commands(ctx)
|
| 59 |
+
|
| 60 |
+
def get_command(self, ctx: click.Context, name: str) -> Optional[Command]:
|
| 61 |
+
self.maybe_add_run(ctx)
|
| 62 |
+
return super().get_command(ctx, name)
|
| 63 |
+
|
| 64 |
+
def invoke(self, ctx: click.Context) -> Any:
|
| 65 |
+
self.maybe_add_run(ctx)
|
| 66 |
+
return super().invoke(ctx)
|
| 67 |
+
|
| 68 |
+
def maybe_add_run(self, ctx: click.Context) -> None:
|
| 69 |
+
maybe_update_state(ctx)
|
| 70 |
+
maybe_add_run_to_cli(self)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def get_typer_from_module(module: Any) -> Optional[typer.Typer]:
|
| 74 |
+
# Try to get defined app
|
| 75 |
+
if state.app:
|
| 76 |
+
obj = getattr(module, state.app, None)
|
| 77 |
+
if not isinstance(obj, typer.Typer):
|
| 78 |
+
typer.echo(f"Not a Typer object: --app {state.app}", err=True)
|
| 79 |
+
sys.exit(1)
|
| 80 |
+
return obj
|
| 81 |
+
# Try to get defined function
|
| 82 |
+
if state.func:
|
| 83 |
+
func_obj = getattr(module, state.func, None)
|
| 84 |
+
if not callable(func_obj):
|
| 85 |
+
typer.echo(f"Not a function: --func {state.func}", err=True)
|
| 86 |
+
sys.exit(1)
|
| 87 |
+
sub_app = typer.Typer()
|
| 88 |
+
sub_app.command()(func_obj)
|
| 89 |
+
return sub_app
|
| 90 |
+
# Iterate and get a default object to use as CLI
|
| 91 |
+
local_names = dir(module)
|
| 92 |
+
local_names_set = set(local_names)
|
| 93 |
+
# Try to get a default Typer app
|
| 94 |
+
for name in default_app_names:
|
| 95 |
+
if name in local_names_set:
|
| 96 |
+
obj = getattr(module, name, None)
|
| 97 |
+
if isinstance(obj, typer.Typer):
|
| 98 |
+
return obj
|
| 99 |
+
# Try to get any Typer app
|
| 100 |
+
for name in local_names_set - set(default_app_names):
|
| 101 |
+
obj = getattr(module, name)
|
| 102 |
+
if isinstance(obj, typer.Typer):
|
| 103 |
+
return obj
|
| 104 |
+
# Try to get a default function
|
| 105 |
+
for func_name in default_func_names:
|
| 106 |
+
func_obj = getattr(module, func_name, None)
|
| 107 |
+
if callable(func_obj):
|
| 108 |
+
sub_app = typer.Typer()
|
| 109 |
+
sub_app.command()(func_obj)
|
| 110 |
+
return sub_app
|
| 111 |
+
# Try to get any func app
|
| 112 |
+
for func_name in local_names_set - set(default_func_names):
|
| 113 |
+
func_obj = getattr(module, func_name)
|
| 114 |
+
if callable(func_obj):
|
| 115 |
+
sub_app = typer.Typer()
|
| 116 |
+
sub_app.command()(func_obj)
|
| 117 |
+
return sub_app
|
| 118 |
+
return None
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def get_typer_from_state() -> Optional[typer.Typer]:
|
| 122 |
+
spec = None
|
| 123 |
+
if state.file:
|
| 124 |
+
module_name = state.file.name
|
| 125 |
+
spec = importlib.util.spec_from_file_location(module_name, str(state.file))
|
| 126 |
+
elif state.module:
|
| 127 |
+
spec = importlib.util.find_spec(state.module)
|
| 128 |
+
if spec is None:
|
| 129 |
+
if state.file:
|
| 130 |
+
typer.echo(f"Could not import as Python file: {state.file}", err=True)
|
| 131 |
+
else:
|
| 132 |
+
typer.echo(f"Could not import as Python module: {state.module}", err=True)
|
| 133 |
+
sys.exit(1)
|
| 134 |
+
module = importlib.util.module_from_spec(spec)
|
| 135 |
+
spec.loader.exec_module(module) # type: ignore
|
| 136 |
+
obj = get_typer_from_module(module)
|
| 137 |
+
return obj
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def maybe_add_run_to_cli(cli: click.Group) -> None:
|
| 141 |
+
if "run" not in cli.commands:
|
| 142 |
+
if state.file or state.module:
|
| 143 |
+
obj = get_typer_from_state()
|
| 144 |
+
if obj:
|
| 145 |
+
obj._add_completion = False
|
| 146 |
+
click_obj = typer.main.get_command(obj)
|
| 147 |
+
click_obj.name = "run"
|
| 148 |
+
if not click_obj.help:
|
| 149 |
+
click_obj.help = "Run the provided Typer app."
|
| 150 |
+
cli.add_command(click_obj)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def print_version(ctx: click.Context, param: Option, value: bool) -> None:
|
| 154 |
+
if not value or ctx.resilient_parsing:
|
| 155 |
+
return
|
| 156 |
+
typer.echo(f"Typer version: {__version__}")
|
| 157 |
+
raise typer.Exit()
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@app.callback(cls=TyperCLIGroup, no_args_is_help=True)
|
| 161 |
+
def callback(
|
| 162 |
+
ctx: typer.Context,
|
| 163 |
+
*,
|
| 164 |
+
path_or_module: str = typer.Argument(None),
|
| 165 |
+
app: str = typer.Option(None, help="The typer app object/variable to use."),
|
| 166 |
+
func: str = typer.Option(None, help="The function to convert to Typer."),
|
| 167 |
+
version: bool = typer.Option(
|
| 168 |
+
False,
|
| 169 |
+
"--version",
|
| 170 |
+
help="Print version and exit.",
|
| 171 |
+
callback=print_version,
|
| 172 |
+
),
|
| 173 |
+
) -> None:
|
| 174 |
+
"""
|
| 175 |
+
Run Typer scripts with completion, without having to create a package.
|
| 176 |
+
|
| 177 |
+
You probably want to install completion for the typer command:
|
| 178 |
+
|
| 179 |
+
$ typer --install-completion
|
| 180 |
+
|
| 181 |
+
https://typer.tiangolo.com/
|
| 182 |
+
"""
|
| 183 |
+
maybe_update_state(ctx)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def get_docs_for_click(
|
| 187 |
+
*,
|
| 188 |
+
obj: Command,
|
| 189 |
+
ctx: typer.Context,
|
| 190 |
+
indent: int = 0,
|
| 191 |
+
name: str = "",
|
| 192 |
+
call_prefix: str = "",
|
| 193 |
+
title: Optional[str] = None,
|
| 194 |
+
) -> str:
|
| 195 |
+
docs = "#" * (1 + indent)
|
| 196 |
+
command_name = name or obj.name
|
| 197 |
+
if call_prefix:
|
| 198 |
+
command_name = f"{call_prefix} {command_name}"
|
| 199 |
+
if not title:
|
| 200 |
+
title = f"`{command_name}`" if command_name else "CLI"
|
| 201 |
+
docs += f" {title}\n\n"
|
| 202 |
+
if obj.help:
|
| 203 |
+
docs += f"{_parse_html(obj.help)}\n\n"
|
| 204 |
+
usage_pieces = obj.collect_usage_pieces(ctx)
|
| 205 |
+
if usage_pieces:
|
| 206 |
+
docs += "**Usage**:\n\n"
|
| 207 |
+
docs += "```console\n"
|
| 208 |
+
docs += "$ "
|
| 209 |
+
if command_name:
|
| 210 |
+
docs += f"{command_name} "
|
| 211 |
+
docs += f"{' '.join(usage_pieces)}\n"
|
| 212 |
+
docs += "```\n\n"
|
| 213 |
+
args = []
|
| 214 |
+
opts = []
|
| 215 |
+
for param in obj.get_params(ctx):
|
| 216 |
+
rv = param.get_help_record(ctx)
|
| 217 |
+
if rv is not None:
|
| 218 |
+
if param.param_type_name == "argument":
|
| 219 |
+
args.append(rv)
|
| 220 |
+
elif param.param_type_name == "option":
|
| 221 |
+
opts.append(rv)
|
| 222 |
+
if args:
|
| 223 |
+
docs += "**Arguments**:\n\n"
|
| 224 |
+
for arg_name, arg_help in args:
|
| 225 |
+
docs += f"* `{arg_name}`"
|
| 226 |
+
if arg_help:
|
| 227 |
+
docs += f": {_parse_html(arg_help)}"
|
| 228 |
+
docs += "\n"
|
| 229 |
+
docs += "\n"
|
| 230 |
+
if opts:
|
| 231 |
+
docs += "**Options**:\n\n"
|
| 232 |
+
for opt_name, opt_help in opts:
|
| 233 |
+
docs += f"* `{opt_name}`"
|
| 234 |
+
if opt_help:
|
| 235 |
+
docs += f": {_parse_html(opt_help)}"
|
| 236 |
+
docs += "\n"
|
| 237 |
+
docs += "\n"
|
| 238 |
+
if obj.epilog:
|
| 239 |
+
docs += f"{obj.epilog}\n\n"
|
| 240 |
+
if isinstance(obj, Group):
|
| 241 |
+
group = obj
|
| 242 |
+
commands = group.list_commands(ctx)
|
| 243 |
+
if commands:
|
| 244 |
+
docs += "**Commands**:\n\n"
|
| 245 |
+
for command in commands:
|
| 246 |
+
command_obj = group.get_command(ctx, command)
|
| 247 |
+
assert command_obj
|
| 248 |
+
docs += f"* `{command_obj.name}`"
|
| 249 |
+
command_help = command_obj.get_short_help_str()
|
| 250 |
+
if command_help:
|
| 251 |
+
docs += f": {_parse_html(command_help)}"
|
| 252 |
+
docs += "\n"
|
| 253 |
+
docs += "\n"
|
| 254 |
+
for command in commands:
|
| 255 |
+
command_obj = group.get_command(ctx, command)
|
| 256 |
+
assert command_obj
|
| 257 |
+
use_prefix = ""
|
| 258 |
+
if command_name:
|
| 259 |
+
use_prefix += f"{command_name}"
|
| 260 |
+
docs += get_docs_for_click(
|
| 261 |
+
obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix
|
| 262 |
+
)
|
| 263 |
+
return docs
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _parse_html(input_text: str) -> str:
|
| 267 |
+
if not HAS_RICH: # pragma: no cover
|
| 268 |
+
return input_text
|
| 269 |
+
from . import rich_utils
|
| 270 |
+
|
| 271 |
+
return rich_utils.rich_to_html(input_text)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
@utils_app.command()
|
| 275 |
+
def docs(
|
| 276 |
+
ctx: typer.Context,
|
| 277 |
+
name: str = typer.Option("", help="The name of the CLI program to use in docs."),
|
| 278 |
+
output: Optional[Path] = typer.Option(
|
| 279 |
+
None,
|
| 280 |
+
help="An output file to write docs to, like README.md.",
|
| 281 |
+
file_okay=True,
|
| 282 |
+
dir_okay=False,
|
| 283 |
+
),
|
| 284 |
+
title: Optional[str] = typer.Option(
|
| 285 |
+
None,
|
| 286 |
+
help="The title for the documentation page. If not provided, the name of "
|
| 287 |
+
"the program is used.",
|
| 288 |
+
),
|
| 289 |
+
) -> None:
|
| 290 |
+
"""
|
| 291 |
+
Generate Markdown docs for a Typer app.
|
| 292 |
+
"""
|
| 293 |
+
typer_obj = get_typer_from_state()
|
| 294 |
+
if not typer_obj:
|
| 295 |
+
typer.echo("No Typer app found", err=True)
|
| 296 |
+
raise typer.Abort()
|
| 297 |
+
click_obj = typer.main.get_command(typer_obj)
|
| 298 |
+
docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title)
|
| 299 |
+
clean_docs = f"{docs.strip()}\n"
|
| 300 |
+
if output:
|
| 301 |
+
output.write_text(clean_docs)
|
| 302 |
+
typer.echo(f"Docs saved to: {output}")
|
| 303 |
+
else:
|
| 304 |
+
typer.echo(clean_docs)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def main() -> Any:
|
| 308 |
+
return app()
|
env/lib/python3.13/site-packages/typer/colors.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Variable names to colors, just for completion
|
| 2 |
+
BLACK = "black"
|
| 3 |
+
RED = "red"
|
| 4 |
+
GREEN = "green"
|
| 5 |
+
YELLOW = "yellow"
|
| 6 |
+
BLUE = "blue"
|
| 7 |
+
MAGENTA = "magenta"
|
| 8 |
+
CYAN = "cyan"
|
| 9 |
+
WHITE = "white"
|
| 10 |
+
|
| 11 |
+
RESET = "reset"
|
| 12 |
+
|
| 13 |
+
BRIGHT_BLACK = "bright_black"
|
| 14 |
+
BRIGHT_RED = "bright_red"
|
| 15 |
+
BRIGHT_GREEN = "bright_green"
|
| 16 |
+
BRIGHT_YELLOW = "bright_yellow"
|
| 17 |
+
BRIGHT_BLUE = "bright_blue"
|
| 18 |
+
BRIGHT_MAGENTA = "bright_magenta"
|
| 19 |
+
BRIGHT_CYAN = "bright_cyan"
|
| 20 |
+
BRIGHT_WHITE = "bright_white"
|
env/lib/python3.13/site-packages/typer/models.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
import io
|
| 3 |
+
from typing import (
|
| 4 |
+
TYPE_CHECKING,
|
| 5 |
+
Any,
|
| 6 |
+
Callable,
|
| 7 |
+
Dict,
|
| 8 |
+
List,
|
| 9 |
+
Optional,
|
| 10 |
+
Sequence,
|
| 11 |
+
Type,
|
| 12 |
+
TypeVar,
|
| 13 |
+
Union,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
import click
|
| 17 |
+
import click.shell_completion
|
| 18 |
+
|
| 19 |
+
if TYPE_CHECKING: # pragma: no cover
|
| 20 |
+
from .core import TyperCommand, TyperGroup
|
| 21 |
+
from .main import Typer
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
NoneType = type(None)
|
| 25 |
+
|
| 26 |
+
AnyType = Type[Any]
|
| 27 |
+
|
| 28 |
+
Required = ...
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Context(click.Context):
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class FileText(io.TextIOWrapper):
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class FileTextWrite(FileText):
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class FileBinaryRead(io.BufferedReader):
|
| 44 |
+
pass
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class FileBinaryWrite(io.BufferedWriter):
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class CallbackParam(click.Parameter):
|
| 52 |
+
pass
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class DefaultPlaceholder:
|
| 56 |
+
"""
|
| 57 |
+
You shouldn't use this class directly.
|
| 58 |
+
|
| 59 |
+
It's used internally to recognize when a default value has been overwritten, even
|
| 60 |
+
if the new value is `None`.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(self, value: Any):
|
| 64 |
+
self.value = value
|
| 65 |
+
|
| 66 |
+
def __bool__(self) -> bool:
|
| 67 |
+
return bool(self.value)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
DefaultType = TypeVar("DefaultType")
|
| 71 |
+
|
| 72 |
+
CommandFunctionType = TypeVar("CommandFunctionType", bound=Callable[..., Any])
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def Default(value: DefaultType) -> DefaultType:
|
| 76 |
+
"""
|
| 77 |
+
You shouldn't use this function directly.
|
| 78 |
+
|
| 79 |
+
It's used internally to recognize when a default value has been overwritten, even
|
| 80 |
+
if the new value is `None`.
|
| 81 |
+
"""
|
| 82 |
+
return DefaultPlaceholder(value) # type: ignore
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class CommandInfo:
|
| 86 |
+
def __init__(
|
| 87 |
+
self,
|
| 88 |
+
name: Optional[str] = None,
|
| 89 |
+
*,
|
| 90 |
+
cls: Optional[Type["TyperCommand"]] = None,
|
| 91 |
+
context_settings: Optional[Dict[Any, Any]] = None,
|
| 92 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 93 |
+
help: Optional[str] = None,
|
| 94 |
+
epilog: Optional[str] = None,
|
| 95 |
+
short_help: Optional[str] = None,
|
| 96 |
+
options_metavar: str = "[OPTIONS]",
|
| 97 |
+
add_help_option: bool = True,
|
| 98 |
+
no_args_is_help: bool = False,
|
| 99 |
+
hidden: bool = False,
|
| 100 |
+
deprecated: bool = False,
|
| 101 |
+
# Rich settings
|
| 102 |
+
rich_help_panel: Union[str, None] = None,
|
| 103 |
+
):
|
| 104 |
+
self.name = name
|
| 105 |
+
self.cls = cls
|
| 106 |
+
self.context_settings = context_settings
|
| 107 |
+
self.callback = callback
|
| 108 |
+
self.help = help
|
| 109 |
+
self.epilog = epilog
|
| 110 |
+
self.short_help = short_help
|
| 111 |
+
self.options_metavar = options_metavar
|
| 112 |
+
self.add_help_option = add_help_option
|
| 113 |
+
self.no_args_is_help = no_args_is_help
|
| 114 |
+
self.hidden = hidden
|
| 115 |
+
self.deprecated = deprecated
|
| 116 |
+
# Rich settings
|
| 117 |
+
self.rich_help_panel = rich_help_panel
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class TyperInfo:
|
| 121 |
+
def __init__(
|
| 122 |
+
self,
|
| 123 |
+
typer_instance: Optional["Typer"] = Default(None),
|
| 124 |
+
*,
|
| 125 |
+
name: Optional[str] = Default(None),
|
| 126 |
+
cls: Optional[Type["TyperGroup"]] = Default(None),
|
| 127 |
+
invoke_without_command: bool = Default(False),
|
| 128 |
+
no_args_is_help: bool = Default(False),
|
| 129 |
+
subcommand_metavar: Optional[str] = Default(None),
|
| 130 |
+
chain: bool = Default(False),
|
| 131 |
+
result_callback: Optional[Callable[..., Any]] = Default(None),
|
| 132 |
+
# Command
|
| 133 |
+
context_settings: Optional[Dict[Any, Any]] = Default(None),
|
| 134 |
+
callback: Optional[Callable[..., Any]] = Default(None),
|
| 135 |
+
help: Optional[str] = Default(None),
|
| 136 |
+
epilog: Optional[str] = Default(None),
|
| 137 |
+
short_help: Optional[str] = Default(None),
|
| 138 |
+
options_metavar: str = Default("[OPTIONS]"),
|
| 139 |
+
add_help_option: bool = Default(True),
|
| 140 |
+
hidden: bool = Default(False),
|
| 141 |
+
deprecated: bool = Default(False),
|
| 142 |
+
# Rich settings
|
| 143 |
+
rich_help_panel: Union[str, None] = Default(None),
|
| 144 |
+
):
|
| 145 |
+
self.typer_instance = typer_instance
|
| 146 |
+
self.name = name
|
| 147 |
+
self.cls = cls
|
| 148 |
+
self.invoke_without_command = invoke_without_command
|
| 149 |
+
self.no_args_is_help = no_args_is_help
|
| 150 |
+
self.subcommand_metavar = subcommand_metavar
|
| 151 |
+
self.chain = chain
|
| 152 |
+
self.result_callback = result_callback
|
| 153 |
+
self.context_settings = context_settings
|
| 154 |
+
self.callback = callback
|
| 155 |
+
self.help = help
|
| 156 |
+
self.epilog = epilog
|
| 157 |
+
self.short_help = short_help
|
| 158 |
+
self.options_metavar = options_metavar
|
| 159 |
+
self.add_help_option = add_help_option
|
| 160 |
+
self.hidden = hidden
|
| 161 |
+
self.deprecated = deprecated
|
| 162 |
+
self.rich_help_panel = rich_help_panel
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class ParameterInfo:
|
| 166 |
+
def __init__(
|
| 167 |
+
self,
|
| 168 |
+
*,
|
| 169 |
+
default: Optional[Any] = None,
|
| 170 |
+
param_decls: Optional[Sequence[str]] = None,
|
| 171 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 172 |
+
metavar: Optional[str] = None,
|
| 173 |
+
expose_value: bool = True,
|
| 174 |
+
is_eager: bool = False,
|
| 175 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 176 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 177 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 178 |
+
shell_complete: Optional[
|
| 179 |
+
Callable[
|
| 180 |
+
[click.Context, click.Parameter, str],
|
| 181 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 182 |
+
]
|
| 183 |
+
] = None,
|
| 184 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 185 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 186 |
+
# Custom type
|
| 187 |
+
parser: Optional[Callable[[str], Any]] = None,
|
| 188 |
+
click_type: Optional[click.ParamType] = None,
|
| 189 |
+
# TyperArgument
|
| 190 |
+
show_default: Union[bool, str] = True,
|
| 191 |
+
show_choices: bool = True,
|
| 192 |
+
show_envvar: bool = True,
|
| 193 |
+
help: Optional[str] = None,
|
| 194 |
+
hidden: bool = False,
|
| 195 |
+
# Choice
|
| 196 |
+
case_sensitive: bool = True,
|
| 197 |
+
# Numbers
|
| 198 |
+
min: Optional[Union[int, float]] = None,
|
| 199 |
+
max: Optional[Union[int, float]] = None,
|
| 200 |
+
clamp: bool = False,
|
| 201 |
+
# DateTime
|
| 202 |
+
formats: Optional[List[str]] = None,
|
| 203 |
+
# File
|
| 204 |
+
mode: Optional[str] = None,
|
| 205 |
+
encoding: Optional[str] = None,
|
| 206 |
+
errors: Optional[str] = "strict",
|
| 207 |
+
lazy: Optional[bool] = None,
|
| 208 |
+
atomic: bool = False,
|
| 209 |
+
# Path
|
| 210 |
+
exists: bool = False,
|
| 211 |
+
file_okay: bool = True,
|
| 212 |
+
dir_okay: bool = True,
|
| 213 |
+
writable: bool = False,
|
| 214 |
+
readable: bool = True,
|
| 215 |
+
resolve_path: bool = False,
|
| 216 |
+
allow_dash: bool = False,
|
| 217 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 218 |
+
# Rich settings
|
| 219 |
+
rich_help_panel: Union[str, None] = None,
|
| 220 |
+
):
|
| 221 |
+
# Check if user has provided multiple custom parsers
|
| 222 |
+
if parser and click_type:
|
| 223 |
+
raise ValueError(
|
| 224 |
+
"Multiple custom type parsers provided. "
|
| 225 |
+
"`parser` and `click_type` may not both be provided."
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
self.default = default
|
| 229 |
+
self.param_decls = param_decls
|
| 230 |
+
self.callback = callback
|
| 231 |
+
self.metavar = metavar
|
| 232 |
+
self.expose_value = expose_value
|
| 233 |
+
self.is_eager = is_eager
|
| 234 |
+
self.envvar = envvar
|
| 235 |
+
self.shell_complete = shell_complete
|
| 236 |
+
self.autocompletion = autocompletion
|
| 237 |
+
self.default_factory = default_factory
|
| 238 |
+
# Custom type
|
| 239 |
+
self.parser = parser
|
| 240 |
+
self.click_type = click_type
|
| 241 |
+
# TyperArgument
|
| 242 |
+
self.show_default = show_default
|
| 243 |
+
self.show_choices = show_choices
|
| 244 |
+
self.show_envvar = show_envvar
|
| 245 |
+
self.help = help
|
| 246 |
+
self.hidden = hidden
|
| 247 |
+
# Choice
|
| 248 |
+
self.case_sensitive = case_sensitive
|
| 249 |
+
# Numbers
|
| 250 |
+
self.min = min
|
| 251 |
+
self.max = max
|
| 252 |
+
self.clamp = clamp
|
| 253 |
+
# DateTime
|
| 254 |
+
self.formats = formats
|
| 255 |
+
# File
|
| 256 |
+
self.mode = mode
|
| 257 |
+
self.encoding = encoding
|
| 258 |
+
self.errors = errors
|
| 259 |
+
self.lazy = lazy
|
| 260 |
+
self.atomic = atomic
|
| 261 |
+
# Path
|
| 262 |
+
self.exists = exists
|
| 263 |
+
self.file_okay = file_okay
|
| 264 |
+
self.dir_okay = dir_okay
|
| 265 |
+
self.writable = writable
|
| 266 |
+
self.readable = readable
|
| 267 |
+
self.resolve_path = resolve_path
|
| 268 |
+
self.allow_dash = allow_dash
|
| 269 |
+
self.path_type = path_type
|
| 270 |
+
# Rich settings
|
| 271 |
+
self.rich_help_panel = rich_help_panel
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
class OptionInfo(ParameterInfo):
|
| 275 |
+
def __init__(
|
| 276 |
+
self,
|
| 277 |
+
*,
|
| 278 |
+
# ParameterInfo
|
| 279 |
+
default: Optional[Any] = None,
|
| 280 |
+
param_decls: Optional[Sequence[str]] = None,
|
| 281 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 282 |
+
metavar: Optional[str] = None,
|
| 283 |
+
expose_value: bool = True,
|
| 284 |
+
is_eager: bool = False,
|
| 285 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 286 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 287 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 288 |
+
shell_complete: Optional[
|
| 289 |
+
Callable[
|
| 290 |
+
[click.Context, click.Parameter, str],
|
| 291 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 292 |
+
]
|
| 293 |
+
] = None,
|
| 294 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 295 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 296 |
+
# Custom type
|
| 297 |
+
parser: Optional[Callable[[str], Any]] = None,
|
| 298 |
+
click_type: Optional[click.ParamType] = None,
|
| 299 |
+
# Option
|
| 300 |
+
show_default: Union[bool, str] = True,
|
| 301 |
+
prompt: Union[bool, str] = False,
|
| 302 |
+
confirmation_prompt: bool = False,
|
| 303 |
+
prompt_required: bool = True,
|
| 304 |
+
hide_input: bool = False,
|
| 305 |
+
# TODO: remove is_flag and flag_value in a future release
|
| 306 |
+
is_flag: Optional[bool] = None,
|
| 307 |
+
flag_value: Optional[Any] = None,
|
| 308 |
+
count: bool = False,
|
| 309 |
+
allow_from_autoenv: bool = True,
|
| 310 |
+
help: Optional[str] = None,
|
| 311 |
+
hidden: bool = False,
|
| 312 |
+
show_choices: bool = True,
|
| 313 |
+
show_envvar: bool = True,
|
| 314 |
+
# Choice
|
| 315 |
+
case_sensitive: bool = True,
|
| 316 |
+
# Numbers
|
| 317 |
+
min: Optional[Union[int, float]] = None,
|
| 318 |
+
max: Optional[Union[int, float]] = None,
|
| 319 |
+
clamp: bool = False,
|
| 320 |
+
# DateTime
|
| 321 |
+
formats: Optional[List[str]] = None,
|
| 322 |
+
# File
|
| 323 |
+
mode: Optional[str] = None,
|
| 324 |
+
encoding: Optional[str] = None,
|
| 325 |
+
errors: Optional[str] = "strict",
|
| 326 |
+
lazy: Optional[bool] = None,
|
| 327 |
+
atomic: bool = False,
|
| 328 |
+
# Path
|
| 329 |
+
exists: bool = False,
|
| 330 |
+
file_okay: bool = True,
|
| 331 |
+
dir_okay: bool = True,
|
| 332 |
+
writable: bool = False,
|
| 333 |
+
readable: bool = True,
|
| 334 |
+
resolve_path: bool = False,
|
| 335 |
+
allow_dash: bool = False,
|
| 336 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 337 |
+
# Rich settings
|
| 338 |
+
rich_help_panel: Union[str, None] = None,
|
| 339 |
+
):
|
| 340 |
+
super().__init__(
|
| 341 |
+
default=default,
|
| 342 |
+
param_decls=param_decls,
|
| 343 |
+
callback=callback,
|
| 344 |
+
metavar=metavar,
|
| 345 |
+
expose_value=expose_value,
|
| 346 |
+
is_eager=is_eager,
|
| 347 |
+
envvar=envvar,
|
| 348 |
+
shell_complete=shell_complete,
|
| 349 |
+
autocompletion=autocompletion,
|
| 350 |
+
default_factory=default_factory,
|
| 351 |
+
# Custom type
|
| 352 |
+
parser=parser,
|
| 353 |
+
click_type=click_type,
|
| 354 |
+
# TyperArgument
|
| 355 |
+
show_default=show_default,
|
| 356 |
+
show_choices=show_choices,
|
| 357 |
+
show_envvar=show_envvar,
|
| 358 |
+
help=help,
|
| 359 |
+
hidden=hidden,
|
| 360 |
+
# Choice
|
| 361 |
+
case_sensitive=case_sensitive,
|
| 362 |
+
# Numbers
|
| 363 |
+
min=min,
|
| 364 |
+
max=max,
|
| 365 |
+
clamp=clamp,
|
| 366 |
+
# DateTime
|
| 367 |
+
formats=formats,
|
| 368 |
+
# File
|
| 369 |
+
mode=mode,
|
| 370 |
+
encoding=encoding,
|
| 371 |
+
errors=errors,
|
| 372 |
+
lazy=lazy,
|
| 373 |
+
atomic=atomic,
|
| 374 |
+
# Path
|
| 375 |
+
exists=exists,
|
| 376 |
+
file_okay=file_okay,
|
| 377 |
+
dir_okay=dir_okay,
|
| 378 |
+
writable=writable,
|
| 379 |
+
readable=readable,
|
| 380 |
+
resolve_path=resolve_path,
|
| 381 |
+
allow_dash=allow_dash,
|
| 382 |
+
path_type=path_type,
|
| 383 |
+
# Rich settings
|
| 384 |
+
rich_help_panel=rich_help_panel,
|
| 385 |
+
)
|
| 386 |
+
if is_flag is not None or flag_value is not None:
|
| 387 |
+
import warnings
|
| 388 |
+
|
| 389 |
+
warnings.warn(
|
| 390 |
+
"The 'is_flag' and 'flag_value' parameters are not supported by Typer "
|
| 391 |
+
"and will be removed entirely in a future release.",
|
| 392 |
+
DeprecationWarning,
|
| 393 |
+
stacklevel=2,
|
| 394 |
+
)
|
| 395 |
+
self.prompt = prompt
|
| 396 |
+
self.confirmation_prompt = confirmation_prompt
|
| 397 |
+
self.prompt_required = prompt_required
|
| 398 |
+
self.hide_input = hide_input
|
| 399 |
+
self.count = count
|
| 400 |
+
self.allow_from_autoenv = allow_from_autoenv
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
class ArgumentInfo(ParameterInfo):
|
| 404 |
+
def __init__(
|
| 405 |
+
self,
|
| 406 |
+
*,
|
| 407 |
+
# ParameterInfo
|
| 408 |
+
default: Optional[Any] = None,
|
| 409 |
+
param_decls: Optional[Sequence[str]] = None,
|
| 410 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 411 |
+
metavar: Optional[str] = None,
|
| 412 |
+
expose_value: bool = True,
|
| 413 |
+
is_eager: bool = False,
|
| 414 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 415 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 416 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 417 |
+
shell_complete: Optional[
|
| 418 |
+
Callable[
|
| 419 |
+
[click.Context, click.Parameter, str],
|
| 420 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 421 |
+
]
|
| 422 |
+
] = None,
|
| 423 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 424 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 425 |
+
# Custom type
|
| 426 |
+
parser: Optional[Callable[[str], Any]] = None,
|
| 427 |
+
click_type: Optional[click.ParamType] = None,
|
| 428 |
+
# TyperArgument
|
| 429 |
+
show_default: Union[bool, str] = True,
|
| 430 |
+
show_choices: bool = True,
|
| 431 |
+
show_envvar: bool = True,
|
| 432 |
+
help: Optional[str] = None,
|
| 433 |
+
hidden: bool = False,
|
| 434 |
+
# Choice
|
| 435 |
+
case_sensitive: bool = True,
|
| 436 |
+
# Numbers
|
| 437 |
+
min: Optional[Union[int, float]] = None,
|
| 438 |
+
max: Optional[Union[int, float]] = None,
|
| 439 |
+
clamp: bool = False,
|
| 440 |
+
# DateTime
|
| 441 |
+
formats: Optional[List[str]] = None,
|
| 442 |
+
# File
|
| 443 |
+
mode: Optional[str] = None,
|
| 444 |
+
encoding: Optional[str] = None,
|
| 445 |
+
errors: Optional[str] = "strict",
|
| 446 |
+
lazy: Optional[bool] = None,
|
| 447 |
+
atomic: bool = False,
|
| 448 |
+
# Path
|
| 449 |
+
exists: bool = False,
|
| 450 |
+
file_okay: bool = True,
|
| 451 |
+
dir_okay: bool = True,
|
| 452 |
+
writable: bool = False,
|
| 453 |
+
readable: bool = True,
|
| 454 |
+
resolve_path: bool = False,
|
| 455 |
+
allow_dash: bool = False,
|
| 456 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 457 |
+
# Rich settings
|
| 458 |
+
rich_help_panel: Union[str, None] = None,
|
| 459 |
+
):
|
| 460 |
+
super().__init__(
|
| 461 |
+
default=default,
|
| 462 |
+
param_decls=param_decls,
|
| 463 |
+
callback=callback,
|
| 464 |
+
metavar=metavar,
|
| 465 |
+
expose_value=expose_value,
|
| 466 |
+
is_eager=is_eager,
|
| 467 |
+
envvar=envvar,
|
| 468 |
+
shell_complete=shell_complete,
|
| 469 |
+
autocompletion=autocompletion,
|
| 470 |
+
default_factory=default_factory,
|
| 471 |
+
# Custom type
|
| 472 |
+
parser=parser,
|
| 473 |
+
click_type=click_type,
|
| 474 |
+
# TyperArgument
|
| 475 |
+
show_default=show_default,
|
| 476 |
+
show_choices=show_choices,
|
| 477 |
+
show_envvar=show_envvar,
|
| 478 |
+
help=help,
|
| 479 |
+
hidden=hidden,
|
| 480 |
+
# Choice
|
| 481 |
+
case_sensitive=case_sensitive,
|
| 482 |
+
# Numbers
|
| 483 |
+
min=min,
|
| 484 |
+
max=max,
|
| 485 |
+
clamp=clamp,
|
| 486 |
+
# DateTime
|
| 487 |
+
formats=formats,
|
| 488 |
+
# File
|
| 489 |
+
mode=mode,
|
| 490 |
+
encoding=encoding,
|
| 491 |
+
errors=errors,
|
| 492 |
+
lazy=lazy,
|
| 493 |
+
atomic=atomic,
|
| 494 |
+
# Path
|
| 495 |
+
exists=exists,
|
| 496 |
+
file_okay=file_okay,
|
| 497 |
+
dir_okay=dir_okay,
|
| 498 |
+
writable=writable,
|
| 499 |
+
readable=readable,
|
| 500 |
+
resolve_path=resolve_path,
|
| 501 |
+
allow_dash=allow_dash,
|
| 502 |
+
path_type=path_type,
|
| 503 |
+
# Rich settings
|
| 504 |
+
rich_help_panel=rich_help_panel,
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
class ParamMeta:
|
| 509 |
+
empty = inspect.Parameter.empty
|
| 510 |
+
|
| 511 |
+
def __init__(
|
| 512 |
+
self,
|
| 513 |
+
*,
|
| 514 |
+
name: str,
|
| 515 |
+
default: Any = inspect.Parameter.empty,
|
| 516 |
+
annotation: Any = inspect.Parameter.empty,
|
| 517 |
+
) -> None:
|
| 518 |
+
self.name = name
|
| 519 |
+
self.default = default
|
| 520 |
+
self.annotation = annotation
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
class DeveloperExceptionConfig:
|
| 524 |
+
def __init__(
|
| 525 |
+
self,
|
| 526 |
+
*,
|
| 527 |
+
pretty_exceptions_enable: bool = True,
|
| 528 |
+
pretty_exceptions_show_locals: bool = True,
|
| 529 |
+
pretty_exceptions_short: bool = True,
|
| 530 |
+
) -> None:
|
| 531 |
+
self.pretty_exceptions_enable = pretty_exceptions_enable
|
| 532 |
+
self.pretty_exceptions_show_locals = pretty_exceptions_show_locals
|
| 533 |
+
self.pretty_exceptions_short = pretty_exceptions_short
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
class TyperPath(click.Path):
|
| 537 |
+
# Overwrite Click's behaviour to be compatible with Typer's autocompletion system
|
| 538 |
+
def shell_complete(
|
| 539 |
+
self, ctx: click.Context, param: click.Parameter, incomplete: str
|
| 540 |
+
) -> List[click.shell_completion.CompletionItem]:
|
| 541 |
+
"""Return an empty list so that the autocompletion functionality
|
| 542 |
+
will work properly from the commandline.
|
| 543 |
+
"""
|
| 544 |
+
return []
|
env/lib/python3.13/site-packages/typer/params.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, Union, overload
|
| 2 |
+
|
| 3 |
+
import click
|
| 4 |
+
|
| 5 |
+
from .models import ArgumentInfo, OptionInfo
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING: # pragma: no cover
|
| 8 |
+
import click.shell_completion
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# Overload for Option created with custom type 'parser'
|
| 12 |
+
@overload
|
| 13 |
+
def Option(
|
| 14 |
+
# Parameter
|
| 15 |
+
default: Optional[Any] = ...,
|
| 16 |
+
*param_decls: str,
|
| 17 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 18 |
+
metavar: Optional[str] = None,
|
| 19 |
+
expose_value: bool = True,
|
| 20 |
+
is_eager: bool = False,
|
| 21 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 22 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 23 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 24 |
+
shell_complete: Optional[
|
| 25 |
+
Callable[
|
| 26 |
+
[click.Context, click.Parameter, str],
|
| 27 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 28 |
+
]
|
| 29 |
+
] = None,
|
| 30 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 31 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 32 |
+
# Custom type
|
| 33 |
+
parser: Optional[Callable[[str], Any]] = None,
|
| 34 |
+
# Option
|
| 35 |
+
show_default: Union[bool, str] = True,
|
| 36 |
+
prompt: Union[bool, str] = False,
|
| 37 |
+
confirmation_prompt: bool = False,
|
| 38 |
+
prompt_required: bool = True,
|
| 39 |
+
hide_input: bool = False,
|
| 40 |
+
# TODO: remove is_flag and flag_value in a future release
|
| 41 |
+
is_flag: Optional[bool] = None,
|
| 42 |
+
flag_value: Optional[Any] = None,
|
| 43 |
+
count: bool = False,
|
| 44 |
+
allow_from_autoenv: bool = True,
|
| 45 |
+
help: Optional[str] = None,
|
| 46 |
+
hidden: bool = False,
|
| 47 |
+
show_choices: bool = True,
|
| 48 |
+
show_envvar: bool = True,
|
| 49 |
+
# Choice
|
| 50 |
+
case_sensitive: bool = True,
|
| 51 |
+
# Numbers
|
| 52 |
+
min: Optional[Union[int, float]] = None,
|
| 53 |
+
max: Optional[Union[int, float]] = None,
|
| 54 |
+
clamp: bool = False,
|
| 55 |
+
# DateTime
|
| 56 |
+
formats: Optional[List[str]] = None,
|
| 57 |
+
# File
|
| 58 |
+
mode: Optional[str] = None,
|
| 59 |
+
encoding: Optional[str] = None,
|
| 60 |
+
errors: Optional[str] = "strict",
|
| 61 |
+
lazy: Optional[bool] = None,
|
| 62 |
+
atomic: bool = False,
|
| 63 |
+
# Path
|
| 64 |
+
exists: bool = False,
|
| 65 |
+
file_okay: bool = True,
|
| 66 |
+
dir_okay: bool = True,
|
| 67 |
+
writable: bool = False,
|
| 68 |
+
readable: bool = True,
|
| 69 |
+
resolve_path: bool = False,
|
| 70 |
+
allow_dash: bool = False,
|
| 71 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 72 |
+
# Rich settings
|
| 73 |
+
rich_help_panel: Union[str, None] = None,
|
| 74 |
+
) -> Any: ...
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# Overload for Option created with custom type 'click_type'
|
| 78 |
+
@overload
|
| 79 |
+
def Option(
|
| 80 |
+
# Parameter
|
| 81 |
+
default: Optional[Any] = ...,
|
| 82 |
+
*param_decls: str,
|
| 83 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 84 |
+
metavar: Optional[str] = None,
|
| 85 |
+
expose_value: bool = True,
|
| 86 |
+
is_eager: bool = False,
|
| 87 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 88 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 89 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 90 |
+
shell_complete: Optional[
|
| 91 |
+
Callable[
|
| 92 |
+
[click.Context, click.Parameter, str],
|
| 93 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 94 |
+
]
|
| 95 |
+
] = None,
|
| 96 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 97 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 98 |
+
# Custom type
|
| 99 |
+
click_type: Optional[click.ParamType] = None,
|
| 100 |
+
# Option
|
| 101 |
+
show_default: Union[bool, str] = True,
|
| 102 |
+
prompt: Union[bool, str] = False,
|
| 103 |
+
confirmation_prompt: bool = False,
|
| 104 |
+
prompt_required: bool = True,
|
| 105 |
+
hide_input: bool = False,
|
| 106 |
+
# TODO: remove is_flag and flag_value in a future release
|
| 107 |
+
is_flag: Optional[bool] = None,
|
| 108 |
+
flag_value: Optional[Any] = None,
|
| 109 |
+
count: bool = False,
|
| 110 |
+
allow_from_autoenv: bool = True,
|
| 111 |
+
help: Optional[str] = None,
|
| 112 |
+
hidden: bool = False,
|
| 113 |
+
show_choices: bool = True,
|
| 114 |
+
show_envvar: bool = True,
|
| 115 |
+
# Choice
|
| 116 |
+
case_sensitive: bool = True,
|
| 117 |
+
# Numbers
|
| 118 |
+
min: Optional[Union[int, float]] = None,
|
| 119 |
+
max: Optional[Union[int, float]] = None,
|
| 120 |
+
clamp: bool = False,
|
| 121 |
+
# DateTime
|
| 122 |
+
formats: Optional[List[str]] = None,
|
| 123 |
+
# File
|
| 124 |
+
mode: Optional[str] = None,
|
| 125 |
+
encoding: Optional[str] = None,
|
| 126 |
+
errors: Optional[str] = "strict",
|
| 127 |
+
lazy: Optional[bool] = None,
|
| 128 |
+
atomic: bool = False,
|
| 129 |
+
# Path
|
| 130 |
+
exists: bool = False,
|
| 131 |
+
file_okay: bool = True,
|
| 132 |
+
dir_okay: bool = True,
|
| 133 |
+
writable: bool = False,
|
| 134 |
+
readable: bool = True,
|
| 135 |
+
resolve_path: bool = False,
|
| 136 |
+
allow_dash: bool = False,
|
| 137 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 138 |
+
# Rich settings
|
| 139 |
+
rich_help_panel: Union[str, None] = None,
|
| 140 |
+
) -> Any: ...
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def Option(
|
| 144 |
+
# Parameter
|
| 145 |
+
default: Optional[Any] = ...,
|
| 146 |
+
*param_decls: str,
|
| 147 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 148 |
+
metavar: Optional[str] = None,
|
| 149 |
+
expose_value: bool = True,
|
| 150 |
+
is_eager: bool = False,
|
| 151 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 152 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 153 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 154 |
+
shell_complete: Optional[
|
| 155 |
+
Callable[
|
| 156 |
+
[click.Context, click.Parameter, str],
|
| 157 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 158 |
+
]
|
| 159 |
+
] = None,
|
| 160 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 161 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 162 |
+
# Custom type
|
| 163 |
+
parser: Optional[Callable[[str], Any]] = None,
|
| 164 |
+
click_type: Optional[click.ParamType] = None,
|
| 165 |
+
# Option
|
| 166 |
+
show_default: Union[bool, str] = True,
|
| 167 |
+
prompt: Union[bool, str] = False,
|
| 168 |
+
confirmation_prompt: bool = False,
|
| 169 |
+
prompt_required: bool = True,
|
| 170 |
+
hide_input: bool = False,
|
| 171 |
+
# TODO: remove is_flag and flag_value in a future release
|
| 172 |
+
is_flag: Optional[bool] = None,
|
| 173 |
+
flag_value: Optional[Any] = None,
|
| 174 |
+
count: bool = False,
|
| 175 |
+
allow_from_autoenv: bool = True,
|
| 176 |
+
help: Optional[str] = None,
|
| 177 |
+
hidden: bool = False,
|
| 178 |
+
show_choices: bool = True,
|
| 179 |
+
show_envvar: bool = True,
|
| 180 |
+
# Choice
|
| 181 |
+
case_sensitive: bool = True,
|
| 182 |
+
# Numbers
|
| 183 |
+
min: Optional[Union[int, float]] = None,
|
| 184 |
+
max: Optional[Union[int, float]] = None,
|
| 185 |
+
clamp: bool = False,
|
| 186 |
+
# DateTime
|
| 187 |
+
formats: Optional[List[str]] = None,
|
| 188 |
+
# File
|
| 189 |
+
mode: Optional[str] = None,
|
| 190 |
+
encoding: Optional[str] = None,
|
| 191 |
+
errors: Optional[str] = "strict",
|
| 192 |
+
lazy: Optional[bool] = None,
|
| 193 |
+
atomic: bool = False,
|
| 194 |
+
# Path
|
| 195 |
+
exists: bool = False,
|
| 196 |
+
file_okay: bool = True,
|
| 197 |
+
dir_okay: bool = True,
|
| 198 |
+
writable: bool = False,
|
| 199 |
+
readable: bool = True,
|
| 200 |
+
resolve_path: bool = False,
|
| 201 |
+
allow_dash: bool = False,
|
| 202 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 203 |
+
# Rich settings
|
| 204 |
+
rich_help_panel: Union[str, None] = None,
|
| 205 |
+
) -> Any:
|
| 206 |
+
return OptionInfo(
|
| 207 |
+
# Parameter
|
| 208 |
+
default=default,
|
| 209 |
+
param_decls=param_decls,
|
| 210 |
+
callback=callback,
|
| 211 |
+
metavar=metavar,
|
| 212 |
+
expose_value=expose_value,
|
| 213 |
+
is_eager=is_eager,
|
| 214 |
+
envvar=envvar,
|
| 215 |
+
shell_complete=shell_complete,
|
| 216 |
+
autocompletion=autocompletion,
|
| 217 |
+
default_factory=default_factory,
|
| 218 |
+
# Custom type
|
| 219 |
+
parser=parser,
|
| 220 |
+
click_type=click_type,
|
| 221 |
+
# Option
|
| 222 |
+
show_default=show_default,
|
| 223 |
+
prompt=prompt,
|
| 224 |
+
confirmation_prompt=confirmation_prompt,
|
| 225 |
+
prompt_required=prompt_required,
|
| 226 |
+
hide_input=hide_input,
|
| 227 |
+
is_flag=is_flag,
|
| 228 |
+
flag_value=flag_value,
|
| 229 |
+
count=count,
|
| 230 |
+
allow_from_autoenv=allow_from_autoenv,
|
| 231 |
+
help=help,
|
| 232 |
+
hidden=hidden,
|
| 233 |
+
show_choices=show_choices,
|
| 234 |
+
show_envvar=show_envvar,
|
| 235 |
+
# Choice
|
| 236 |
+
case_sensitive=case_sensitive,
|
| 237 |
+
# Numbers
|
| 238 |
+
min=min,
|
| 239 |
+
max=max,
|
| 240 |
+
clamp=clamp,
|
| 241 |
+
# DateTime
|
| 242 |
+
formats=formats,
|
| 243 |
+
# File
|
| 244 |
+
mode=mode,
|
| 245 |
+
encoding=encoding,
|
| 246 |
+
errors=errors,
|
| 247 |
+
lazy=lazy,
|
| 248 |
+
atomic=atomic,
|
| 249 |
+
# Path
|
| 250 |
+
exists=exists,
|
| 251 |
+
file_okay=file_okay,
|
| 252 |
+
dir_okay=dir_okay,
|
| 253 |
+
writable=writable,
|
| 254 |
+
readable=readable,
|
| 255 |
+
resolve_path=resolve_path,
|
| 256 |
+
allow_dash=allow_dash,
|
| 257 |
+
path_type=path_type,
|
| 258 |
+
# Rich settings
|
| 259 |
+
rich_help_panel=rich_help_panel,
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
# Overload for Argument created with custom type 'parser'
|
| 264 |
+
@overload
|
| 265 |
+
def Argument(
|
| 266 |
+
# Parameter
|
| 267 |
+
default: Optional[Any] = ...,
|
| 268 |
+
*,
|
| 269 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 270 |
+
metavar: Optional[str] = None,
|
| 271 |
+
expose_value: bool = True,
|
| 272 |
+
is_eager: bool = False,
|
| 273 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 274 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 275 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 276 |
+
shell_complete: Optional[
|
| 277 |
+
Callable[
|
| 278 |
+
[click.Context, click.Parameter, str],
|
| 279 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 280 |
+
]
|
| 281 |
+
] = None,
|
| 282 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 283 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 284 |
+
# Custom type
|
| 285 |
+
parser: Optional[Callable[[str], Any]] = None,
|
| 286 |
+
# TyperArgument
|
| 287 |
+
show_default: Union[bool, str] = True,
|
| 288 |
+
show_choices: bool = True,
|
| 289 |
+
show_envvar: bool = True,
|
| 290 |
+
help: Optional[str] = None,
|
| 291 |
+
hidden: bool = False,
|
| 292 |
+
# Choice
|
| 293 |
+
case_sensitive: bool = True,
|
| 294 |
+
# Numbers
|
| 295 |
+
min: Optional[Union[int, float]] = None,
|
| 296 |
+
max: Optional[Union[int, float]] = None,
|
| 297 |
+
clamp: bool = False,
|
| 298 |
+
# DateTime
|
| 299 |
+
formats: Optional[List[str]] = None,
|
| 300 |
+
# File
|
| 301 |
+
mode: Optional[str] = None,
|
| 302 |
+
encoding: Optional[str] = None,
|
| 303 |
+
errors: Optional[str] = "strict",
|
| 304 |
+
lazy: Optional[bool] = None,
|
| 305 |
+
atomic: bool = False,
|
| 306 |
+
# Path
|
| 307 |
+
exists: bool = False,
|
| 308 |
+
file_okay: bool = True,
|
| 309 |
+
dir_okay: bool = True,
|
| 310 |
+
writable: bool = False,
|
| 311 |
+
readable: bool = True,
|
| 312 |
+
resolve_path: bool = False,
|
| 313 |
+
allow_dash: bool = False,
|
| 314 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 315 |
+
# Rich settings
|
| 316 |
+
rich_help_panel: Union[str, None] = None,
|
| 317 |
+
) -> Any: ...
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
# Overload for Argument created with custom type 'click_type'
|
| 321 |
+
@overload
|
| 322 |
+
def Argument(
|
| 323 |
+
# Parameter
|
| 324 |
+
default: Optional[Any] = ...,
|
| 325 |
+
*,
|
| 326 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 327 |
+
metavar: Optional[str] = None,
|
| 328 |
+
expose_value: bool = True,
|
| 329 |
+
is_eager: bool = False,
|
| 330 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 331 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 332 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 333 |
+
shell_complete: Optional[
|
| 334 |
+
Callable[
|
| 335 |
+
[click.Context, click.Parameter, str],
|
| 336 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 337 |
+
]
|
| 338 |
+
] = None,
|
| 339 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 340 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 341 |
+
# Custom type
|
| 342 |
+
click_type: Optional[click.ParamType] = None,
|
| 343 |
+
# TyperArgument
|
| 344 |
+
show_default: Union[bool, str] = True,
|
| 345 |
+
show_choices: bool = True,
|
| 346 |
+
show_envvar: bool = True,
|
| 347 |
+
help: Optional[str] = None,
|
| 348 |
+
hidden: bool = False,
|
| 349 |
+
# Choice
|
| 350 |
+
case_sensitive: bool = True,
|
| 351 |
+
# Numbers
|
| 352 |
+
min: Optional[Union[int, float]] = None,
|
| 353 |
+
max: Optional[Union[int, float]] = None,
|
| 354 |
+
clamp: bool = False,
|
| 355 |
+
# DateTime
|
| 356 |
+
formats: Optional[List[str]] = None,
|
| 357 |
+
# File
|
| 358 |
+
mode: Optional[str] = None,
|
| 359 |
+
encoding: Optional[str] = None,
|
| 360 |
+
errors: Optional[str] = "strict",
|
| 361 |
+
lazy: Optional[bool] = None,
|
| 362 |
+
atomic: bool = False,
|
| 363 |
+
# Path
|
| 364 |
+
exists: bool = False,
|
| 365 |
+
file_okay: bool = True,
|
| 366 |
+
dir_okay: bool = True,
|
| 367 |
+
writable: bool = False,
|
| 368 |
+
readable: bool = True,
|
| 369 |
+
resolve_path: bool = False,
|
| 370 |
+
allow_dash: bool = False,
|
| 371 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 372 |
+
# Rich settings
|
| 373 |
+
rich_help_panel: Union[str, None] = None,
|
| 374 |
+
) -> Any: ...
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def Argument(
|
| 378 |
+
# Parameter
|
| 379 |
+
default: Optional[Any] = ...,
|
| 380 |
+
*,
|
| 381 |
+
callback: Optional[Callable[..., Any]] = None,
|
| 382 |
+
metavar: Optional[str] = None,
|
| 383 |
+
expose_value: bool = True,
|
| 384 |
+
is_eager: bool = False,
|
| 385 |
+
envvar: Optional[Union[str, List[str]]] = None,
|
| 386 |
+
# Note that shell_complete is not fully supported and will be removed in future versions
|
| 387 |
+
# TODO: Remove shell_complete in a future version (after 0.16.0)
|
| 388 |
+
shell_complete: Optional[
|
| 389 |
+
Callable[
|
| 390 |
+
[click.Context, click.Parameter, str],
|
| 391 |
+
Union[List["click.shell_completion.CompletionItem"], List[str]],
|
| 392 |
+
]
|
| 393 |
+
] = None,
|
| 394 |
+
autocompletion: Optional[Callable[..., Any]] = None,
|
| 395 |
+
default_factory: Optional[Callable[[], Any]] = None,
|
| 396 |
+
# Custom type
|
| 397 |
+
parser: Optional[Callable[[str], Any]] = None,
|
| 398 |
+
click_type: Optional[click.ParamType] = None,
|
| 399 |
+
# TyperArgument
|
| 400 |
+
show_default: Union[bool, str] = True,
|
| 401 |
+
show_choices: bool = True,
|
| 402 |
+
show_envvar: bool = True,
|
| 403 |
+
help: Optional[str] = None,
|
| 404 |
+
hidden: bool = False,
|
| 405 |
+
# Choice
|
| 406 |
+
case_sensitive: bool = True,
|
| 407 |
+
# Numbers
|
| 408 |
+
min: Optional[Union[int, float]] = None,
|
| 409 |
+
max: Optional[Union[int, float]] = None,
|
| 410 |
+
clamp: bool = False,
|
| 411 |
+
# DateTime
|
| 412 |
+
formats: Optional[List[str]] = None,
|
| 413 |
+
# File
|
| 414 |
+
mode: Optional[str] = None,
|
| 415 |
+
encoding: Optional[str] = None,
|
| 416 |
+
errors: Optional[str] = "strict",
|
| 417 |
+
lazy: Optional[bool] = None,
|
| 418 |
+
atomic: bool = False,
|
| 419 |
+
# Path
|
| 420 |
+
exists: bool = False,
|
| 421 |
+
file_okay: bool = True,
|
| 422 |
+
dir_okay: bool = True,
|
| 423 |
+
writable: bool = False,
|
| 424 |
+
readable: bool = True,
|
| 425 |
+
resolve_path: bool = False,
|
| 426 |
+
allow_dash: bool = False,
|
| 427 |
+
path_type: Union[None, Type[str], Type[bytes]] = None,
|
| 428 |
+
# Rich settings
|
| 429 |
+
rich_help_panel: Union[str, None] = None,
|
| 430 |
+
) -> Any:
|
| 431 |
+
return ArgumentInfo(
|
| 432 |
+
# Parameter
|
| 433 |
+
default=default,
|
| 434 |
+
# Arguments can only have one param declaration
|
| 435 |
+
# it will be generated from the param name
|
| 436 |
+
param_decls=None,
|
| 437 |
+
callback=callback,
|
| 438 |
+
metavar=metavar,
|
| 439 |
+
expose_value=expose_value,
|
| 440 |
+
is_eager=is_eager,
|
| 441 |
+
envvar=envvar,
|
| 442 |
+
shell_complete=shell_complete,
|
| 443 |
+
autocompletion=autocompletion,
|
| 444 |
+
default_factory=default_factory,
|
| 445 |
+
# Custom type
|
| 446 |
+
parser=parser,
|
| 447 |
+
click_type=click_type,
|
| 448 |
+
# TyperArgument
|
| 449 |
+
show_default=show_default,
|
| 450 |
+
show_choices=show_choices,
|
| 451 |
+
show_envvar=show_envvar,
|
| 452 |
+
help=help,
|
| 453 |
+
hidden=hidden,
|
| 454 |
+
# Choice
|
| 455 |
+
case_sensitive=case_sensitive,
|
| 456 |
+
# Numbers
|
| 457 |
+
min=min,
|
| 458 |
+
max=max,
|
| 459 |
+
clamp=clamp,
|
| 460 |
+
# DateTime
|
| 461 |
+
formats=formats,
|
| 462 |
+
# File
|
| 463 |
+
mode=mode,
|
| 464 |
+
encoding=encoding,
|
| 465 |
+
errors=errors,
|
| 466 |
+
lazy=lazy,
|
| 467 |
+
atomic=atomic,
|
| 468 |
+
# Path
|
| 469 |
+
exists=exists,
|
| 470 |
+
file_okay=file_okay,
|
| 471 |
+
dir_okay=dir_okay,
|
| 472 |
+
writable=writable,
|
| 473 |
+
readable=readable,
|
| 474 |
+
resolve_path=resolve_path,
|
| 475 |
+
allow_dash=allow_dash,
|
| 476 |
+
path_type=path_type,
|
| 477 |
+
# Rich settings
|
| 478 |
+
rich_help_panel=rich_help_panel,
|
| 479 |
+
)
|
env/lib/python3.13/site-packages/typer/utils.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
import sys
|
| 3 |
+
from copy import copy
|
| 4 |
+
from typing import Any, Callable, Dict, List, Tuple, Type, cast
|
| 5 |
+
|
| 6 |
+
from ._typing import Annotated, get_args, get_origin, get_type_hints
|
| 7 |
+
from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _param_type_to_user_string(param_type: Type[ParameterInfo]) -> str:
|
| 11 |
+
# Render a `ParameterInfo` subclass for use in error messages.
|
| 12 |
+
# User code doesn't call `*Info` directly, so errors should present the classes how
|
| 13 |
+
# they were (probably) defined in the user code.
|
| 14 |
+
if param_type is OptionInfo:
|
| 15 |
+
return "`Option`"
|
| 16 |
+
elif param_type is ArgumentInfo:
|
| 17 |
+
return "`Argument`"
|
| 18 |
+
# This line shouldn't be reachable during normal use.
|
| 19 |
+
return f"`{param_type.__name__}`" # pragma: no cover
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class AnnotatedParamWithDefaultValueError(Exception):
|
| 23 |
+
argument_name: str
|
| 24 |
+
param_type: Type[ParameterInfo]
|
| 25 |
+
|
| 26 |
+
def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
|
| 27 |
+
self.argument_name = argument_name
|
| 28 |
+
self.param_type = param_type
|
| 29 |
+
|
| 30 |
+
def __str__(self) -> str:
|
| 31 |
+
param_type_str = _param_type_to_user_string(self.param_type)
|
| 32 |
+
return (
|
| 33 |
+
f"{param_type_str} default value cannot be set in `Annotated`"
|
| 34 |
+
f" for {self.argument_name!r}. Set the default value with `=` instead."
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class MixedAnnotatedAndDefaultStyleError(Exception):
|
| 39 |
+
argument_name: str
|
| 40 |
+
annotated_param_type: Type[ParameterInfo]
|
| 41 |
+
default_param_type: Type[ParameterInfo]
|
| 42 |
+
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
argument_name: str,
|
| 46 |
+
annotated_param_type: Type[ParameterInfo],
|
| 47 |
+
default_param_type: Type[ParameterInfo],
|
| 48 |
+
):
|
| 49 |
+
self.argument_name = argument_name
|
| 50 |
+
self.annotated_param_type = annotated_param_type
|
| 51 |
+
self.default_param_type = default_param_type
|
| 52 |
+
|
| 53 |
+
def __str__(self) -> str:
|
| 54 |
+
annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type)
|
| 55 |
+
default_param_type_str = _param_type_to_user_string(self.default_param_type)
|
| 56 |
+
msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and"
|
| 57 |
+
if self.annotated_param_type is self.default_param_type:
|
| 58 |
+
msg += " default value"
|
| 59 |
+
else:
|
| 60 |
+
msg += f" {default_param_type_str} as a default value"
|
| 61 |
+
msg += f" together for {self.argument_name!r}"
|
| 62 |
+
return msg
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class MultipleTyperAnnotationsError(Exception):
|
| 66 |
+
argument_name: str
|
| 67 |
+
|
| 68 |
+
def __init__(self, argument_name: str):
|
| 69 |
+
self.argument_name = argument_name
|
| 70 |
+
|
| 71 |
+
def __str__(self) -> str:
|
| 72 |
+
return (
|
| 73 |
+
"Cannot specify multiple `Annotated` Typer arguments"
|
| 74 |
+
f" for {self.argument_name!r}"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class DefaultFactoryAndDefaultValueError(Exception):
|
| 79 |
+
argument_name: str
|
| 80 |
+
param_type: Type[ParameterInfo]
|
| 81 |
+
|
| 82 |
+
def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
|
| 83 |
+
self.argument_name = argument_name
|
| 84 |
+
self.param_type = param_type
|
| 85 |
+
|
| 86 |
+
def __str__(self) -> str:
|
| 87 |
+
param_type_str = _param_type_to_user_string(self.param_type)
|
| 88 |
+
return (
|
| 89 |
+
"Cannot specify `default_factory` and a default value together"
|
| 90 |
+
f" for {param_type_str}"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _split_annotation_from_typer_annotations(
|
| 95 |
+
base_annotation: Type[Any],
|
| 96 |
+
) -> Tuple[Type[Any], List[ParameterInfo]]:
|
| 97 |
+
if get_origin(base_annotation) is not Annotated:
|
| 98 |
+
return base_annotation, []
|
| 99 |
+
base_annotation, *maybe_typer_annotations = get_args(base_annotation)
|
| 100 |
+
return base_annotation, [
|
| 101 |
+
annotation
|
| 102 |
+
for annotation in maybe_typer_annotations
|
| 103 |
+
if isinstance(annotation, ParameterInfo)
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]:
|
| 108 |
+
if sys.version_info >= (3, 10):
|
| 109 |
+
signature = inspect.signature(func, eval_str=True)
|
| 110 |
+
else:
|
| 111 |
+
signature = inspect.signature(func)
|
| 112 |
+
|
| 113 |
+
type_hints = get_type_hints(func)
|
| 114 |
+
params = {}
|
| 115 |
+
for param in signature.parameters.values():
|
| 116 |
+
annotation, typer_annotations = _split_annotation_from_typer_annotations(
|
| 117 |
+
param.annotation,
|
| 118 |
+
)
|
| 119 |
+
if len(typer_annotations) > 1:
|
| 120 |
+
raise MultipleTyperAnnotationsError(param.name)
|
| 121 |
+
|
| 122 |
+
default = param.default
|
| 123 |
+
if typer_annotations:
|
| 124 |
+
# It's something like `my_param: Annotated[str, Argument()]`
|
| 125 |
+
[parameter_info] = typer_annotations
|
| 126 |
+
|
| 127 |
+
# Forbid `my_param: Annotated[str, Argument()] = Argument("...")`
|
| 128 |
+
if isinstance(param.default, ParameterInfo):
|
| 129 |
+
raise MixedAnnotatedAndDefaultStyleError(
|
| 130 |
+
argument_name=param.name,
|
| 131 |
+
annotated_param_type=type(parameter_info),
|
| 132 |
+
default_param_type=type(param.default),
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
parameter_info = copy(parameter_info)
|
| 136 |
+
|
| 137 |
+
# When used as a default, `Option` takes a default value and option names
|
| 138 |
+
# as positional arguments:
|
| 139 |
+
# `Option(some_value, "--some-argument", "-s")`
|
| 140 |
+
# When used in `Annotated` (ie, what this is handling), `Option` just takes
|
| 141 |
+
# option names as positional arguments:
|
| 142 |
+
# `Option("--some-argument", "-s")`
|
| 143 |
+
# In this case, the `default` attribute of `parameter_info` is actually
|
| 144 |
+
# meant to be the first item of `param_decls`.
|
| 145 |
+
if (
|
| 146 |
+
isinstance(parameter_info, OptionInfo)
|
| 147 |
+
and parameter_info.default is not ...
|
| 148 |
+
):
|
| 149 |
+
parameter_info.param_decls = (
|
| 150 |
+
cast(str, parameter_info.default),
|
| 151 |
+
*(parameter_info.param_decls or ()),
|
| 152 |
+
)
|
| 153 |
+
parameter_info.default = ...
|
| 154 |
+
|
| 155 |
+
# Forbid `my_param: Annotated[str, Argument('some-default')]`
|
| 156 |
+
if parameter_info.default is not ...:
|
| 157 |
+
raise AnnotatedParamWithDefaultValueError(
|
| 158 |
+
param_type=type(parameter_info),
|
| 159 |
+
argument_name=param.name,
|
| 160 |
+
)
|
| 161 |
+
if param.default is not param.empty:
|
| 162 |
+
# Put the parameter's default (set by `=`) into `parameter_info`, where
|
| 163 |
+
# typer can find it.
|
| 164 |
+
parameter_info.default = param.default
|
| 165 |
+
|
| 166 |
+
default = parameter_info
|
| 167 |
+
elif param.name in type_hints:
|
| 168 |
+
# Resolve forward references.
|
| 169 |
+
annotation = type_hints[param.name]
|
| 170 |
+
|
| 171 |
+
if isinstance(default, ParameterInfo):
|
| 172 |
+
parameter_info = copy(default)
|
| 173 |
+
# Click supports `default` as either
|
| 174 |
+
# - an actual value; or
|
| 175 |
+
# - a factory function (returning a default value.)
|
| 176 |
+
# The two are not interchangeable for static typing, so typer allows
|
| 177 |
+
# specifying `default_factory`. Move the `default_factory` into `default`
|
| 178 |
+
# so click can find it.
|
| 179 |
+
if parameter_info.default is ... and parameter_info.default_factory:
|
| 180 |
+
parameter_info.default = parameter_info.default_factory
|
| 181 |
+
elif parameter_info.default_factory:
|
| 182 |
+
raise DefaultFactoryAndDefaultValueError(
|
| 183 |
+
argument_name=param.name, param_type=type(parameter_info)
|
| 184 |
+
)
|
| 185 |
+
default = parameter_info
|
| 186 |
+
|
| 187 |
+
params[param.name] = ParamMeta(
|
| 188 |
+
name=param.name, default=default, annotation=annotation
|
| 189 |
+
)
|
| 190 |
+
return params
|