diff --git a/typer/__init__.py b/typer/__init__.py index e3185b3fca..12386c4ff3 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -8,23 +8,17 @@ from ._click.exceptions import Abort as Abort from ._click.exceptions import BadParameter as BadParameter from ._click.exceptions import Exit as Exit -from ._click.termui import clear as clear from ._click.termui import confirm as confirm -from ._click.termui import echo_via_pager as echo_via_pager -from ._click.termui import edit as edit from ._click.termui import getchar as getchar -from ._click.termui import pause as pause from ._click.termui import progressbar as progressbar from ._click.termui import prompt as prompt from ._click.termui import secho as secho from ._click.termui import style as style -from ._click.termui import unstyle as unstyle from ._click.utils import echo as echo from ._click.utils import format_filename as format_filename from ._click.utils import get_app_dir as get_app_dir from ._click.utils import get_binary_stream as get_binary_stream from ._click.utils import get_text_stream as get_text_stream -from ._click.utils import open_file as open_file from .main import Typer as Typer from .main import launch as launch from .main import run as run diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py index 60bd5427ea..3b9455b78a 100644 --- a/typer/_click/__init__.py +++ b/typer/_click/__init__.py @@ -6,7 +6,6 @@ from .core import Argument as Argument from .core import Command as Command -from .core import CommandCollection as CommandCollection from .core import Context as Context from .core import Group as Group from .core import Option as Option @@ -25,23 +24,17 @@ from .formatting import HelpFormatter as HelpFormatter from .formatting import wrap_text as wrap_text from .globals import get_current_context as get_current_context -from .termui import clear as clear from .termui import confirm as confirm -from .termui import echo_via_pager as echo_via_pager -from .termui import edit as edit from .termui import getchar as getchar from .termui import launch as launch -from .termui import pause as pause from .termui import progressbar as progressbar from .termui import prompt as prompt from .termui import secho as secho from .termui import style as style -from .termui import unstyle as unstyle from .types import BOOL as BOOL from .types import FLOAT as FLOAT from .types import INT as INT from .types import STRING as STRING -from .types import UNPROCESSED as UNPROCESSED from .types import UUID as UUID from .types import Choice as Choice from .types import DateTime as DateTime @@ -56,4 +49,3 @@ from .utils import get_app_dir as get_app_dir from .utils import get_binary_stream as get_binary_stream from .utils import get_text_stream as get_text_stream -from .utils import open_file as open_file diff --git a/typer/_click/_termui_impl.py b/typer/_click/_termui_impl.py index 3b280648de..a64c8fe2c8 100644 --- a/typer/_click/_termui_impl.py +++ b/typer/_click/_termui_impl.py @@ -10,13 +10,10 @@ import contextlib import math import os -import shlex import sys import time import typing as t -from gettext import gettext as _ from io import StringIO -from pathlib import Path from types import TracebackType from ._compat import ( @@ -25,11 +22,8 @@ _default_text_stdout, get_best_encoding, isatty, - open_stream, - strip_ansi, term_len, ) -from .exceptions import ClickException from .utils import echo V = t.TypeVar("V") @@ -368,313 +362,6 @@ def generator(self) -> cabc.Iterator[V]: self.render_progress() -def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None: - """Decide what method to use for paging through text.""" - stdout = _default_text_stdout() - - # There are no standard streams attached to write to. For example, - # pythonw on Windows. - if stdout is None: - stdout = StringIO() - - if not isatty(sys.stdin) or not isatty(stdout): - return _nullpager(stdout, generator, color) - - # Split and normalize the pager command into parts. - pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False) - if pager_cmd_parts: - if WIN: - if _tempfilepager(generator, pager_cmd_parts, color): - return - elif _pipepager(generator, pager_cmd_parts, color): - return - - if os.environ.get("TERM") in ("dumb", "emacs"): - return _nullpager(stdout, generator, color) - if (WIN or sys.platform.startswith("os2")) and _tempfilepager( - generator, ["more"], color - ): - return - if _pipepager(generator, ["less"], color): - return - - import tempfile - - fd, filename = tempfile.mkstemp() - os.close(fd) - try: - if _pipepager(generator, ["more"], color): - return - return _nullpager(stdout, generator, color) - finally: - os.unlink(filename) - - -def _pipepager( - generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None -) -> bool: - """Page through text by feeding it to another program. Invoking a - pager through this might support colors. - - Returns `True` if the command was found, `False` otherwise and thus another - pager should be attempted. - """ - # Split the command into the invoked CLI and its parameters. - if not cmd_parts: - return False - - import shutil - - cmd = cmd_parts[0] - cmd_params = cmd_parts[1:] - - cmd_filepath = shutil.which(cmd) - if not cmd_filepath: - return False - - # Produces a normalized absolute path string. - # multi-call binaries such as busybox derive their identity from the symlink - # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox) - cmd_path = Path(cmd_filepath).absolute() - cmd_name = cmd_path.name - - import subprocess - - # Make a local copy of the environment to not affect the global one. - env = dict(os.environ) - - # If we're piping to less and the user hasn't decided on colors, we enable - # them by default we find the -R flag in the command line arguments. - if color is None and cmd_name == "less": - less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_params)}" - if not less_flags: - env["LESS"] = "-R" - color = True - elif "r" in less_flags or "R" in less_flags: - color = True - - c = subprocess.Popen( - [str(cmd_path)] + cmd_params, - shell=False, - stdin=subprocess.PIPE, - env=env, - errors="replace", - text=True, - ) - assert c.stdin is not None - try: - for text in generator: - if not color: - text = strip_ansi(text) - - c.stdin.write(text) - except BrokenPipeError: - # In case the pager exited unexpectedly, ignore the broken pipe error. - pass - except Exception as e: - # In case there is an exception we want to close the pager immediately - # and let the caller handle it. - # Otherwise the pager will keep running, and the user may not notice - # the error message, or worse yet it may leave the terminal in a broken state. - c.terminate() - raise e - finally: - # We must close stdin and wait for the pager to exit before we continue - try: - c.stdin.close() - # Close implies flush, so it might throw a BrokenPipeError if the pager - # process exited already. - except BrokenPipeError: - pass - - # Less doesn't respect ^C, but catches it for its own UI purposes (aborting - # search or other commands inside less). - # - # That means when the user hits ^C, the parent process (click) terminates, - # but less is still alive, paging the output and messing up the terminal. - # - # If the user wants to make the pager exit on ^C, they should set - # `LESS='-K'`. It's not our decision to make. - while True: - try: - c.wait() - except KeyboardInterrupt: - pass - else: - break - - return True - - -def _tempfilepager( - generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None -) -> bool: - """Page through text by invoking a program on a temporary file. - - Returns `True` if the command was found, `False` otherwise and thus another - pager should be attempted. - """ - # Split the command into the invoked CLI and its parameters. - if not cmd_parts: - return False - - import shutil - - cmd = cmd_parts[0] - - cmd_filepath = shutil.which(cmd) - if not cmd_filepath: - return False - # Produces a normalized absolute path string. - # multi-call binaries such as busybox derive their identity from the symlink - # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox) - cmd_path = Path(cmd_filepath).absolute() - - import subprocess - import tempfile - - fd, filename = tempfile.mkstemp() - # TODO: This never terminates if the passed generator never terminates. - text = "".join(generator) - if not color: - text = strip_ansi(text) - encoding = get_best_encoding(sys.stdout) - with open_stream(filename, "wb")[0] as f: - f.write(text.encode(encoding)) - try: - subprocess.call([str(cmd_path), filename]) - except OSError: - # Command not found - pass - finally: - os.close(fd) - os.unlink(filename) - - return True - - -def _nullpager( - stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None -) -> None: - """Simply print unformatted text. This is the ultimate fallback.""" - for text in generator: - if not color: - text = strip_ansi(text) - stream.write(text) - - -class Editor: - def __init__( - self, - editor: str | None = None, - env: cabc.Mapping[str, str] | None = None, - require_save: bool = True, - extension: str = ".txt", - ) -> None: - self.editor = editor - self.env = env - self.require_save = require_save - self.extension = extension - - def get_editor(self) -> str: - if self.editor is not None: - return self.editor - for key in "VISUAL", "EDITOR": - rv = os.environ.get(key) - if rv: - return rv - if WIN: - return "notepad" - - from shutil import which - - for editor in "sensible-editor", "vim", "nano": - if which(editor) is not None: - return editor - return "vi" - - def edit_files(self, filenames: cabc.Iterable[str]) -> None: - import subprocess - - editor = self.get_editor() - environ: dict[str, str] | None = None - - if self.env: - environ = os.environ.copy() - environ.update(self.env) - - exc_filename = " ".join(f'"{filename}"' for filename in filenames) - - try: - c = subprocess.Popen( - args=f"{editor} {exc_filename}", env=environ, shell=True - ) - exit_code = c.wait() - if exit_code != 0: - raise ClickException( - _("{editor}: Editing failed").format(editor=editor) - ) - except OSError as e: - raise ClickException( - _("{editor}: Editing failed: {e}").format(editor=editor, e=e) - ) from e - - @t.overload - def edit(self, text: bytes | bytearray) -> bytes | None: ... - - # We cannot know whether or not the type expected is str or bytes when None - # is passed, so str is returned as that was what was done before. - @t.overload - def edit(self, text: str | None) -> str | None: ... - - def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None: - import tempfile - - if text is None: - data: bytes | bytearray = b"" - elif isinstance(text, (bytes, bytearray)): - data = text - else: - if text and not text.endswith("\n"): - text += "\n" - - if WIN: - data = text.replace("\n", "\r\n").encode("utf-8-sig") - else: - data = text.encode("utf-8") - - fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) - f: t.BinaryIO - - try: - with os.fdopen(fd, "wb") as f: - f.write(data) - - # If the filesystem resolution is 1 second, like Mac OS - # 10.12 Extended, or 2 seconds, like FAT32, and the editor - # closes very fast, require_save can fail. Set the modified - # time to be 2 seconds in the past to work around this. - os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2)) - # Depending on the resolution, the exact value might not be - # recorded, so get the new recorded value. - timestamp = os.path.getmtime(name) - - self.edit_files((name,)) - - if self.require_save and os.path.getmtime(name) == timestamp: - return None - - with open(name, "rb") as f: - rv = f.read() - - if isinstance(text, (bytes, bytearray)): - return rv - - return rv.decode("utf-8-sig").replace("\r\n", "\n") - finally: - os.unlink(name) - - def open_url(url: str, wait: bool = False, locate: bool = False) -> int: import subprocess diff --git a/typer/_click/core.py b/typer/_click/core.py index abc49dcd61..3d9f439dd2 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -65,26 +65,6 @@ def _complete_visible_commands( yield name, command -def _check_nested_chain( - base_command: Group, cmd_name: str, cmd: Command, register: bool = False -) -> None: - if not base_command.chain or not isinstance(cmd, Group): - return - - if register: - message = ( - f"It is not possible to add the group {cmd_name!r} to another" - f" group {base_command.name!r} that is in chain mode." - ) - else: - message = ( - f"Found the group {cmd_name!r} as subcommand to another group " - f" {base_command.name!r} that is in chain mode. This is not supported." - ) - - raise RuntimeError(message) - - def batch(iterable: cabc.Iterable[V], batch_size: int) -> list[tuple[V, ...]]: return list(zip(*repeat(iter(iterable), batch_size), strict=False)) @@ -447,27 +427,6 @@ def protected_args(self) -> list[str]: ) return self._protected_args - def to_info_dict(self) -> dict[str, t.Any]: - """Gather information that could be useful for a tool generating - user-facing documentation. This traverses the entire CLI - structure. - - .. code-block:: python - - with Context(cli) as ctx: - info = ctx.to_info_dict() - - .. versionadded:: 8.0 - """ - return { - "command": self.command.to_info_dict(self), - "info_name": self.info_name, - "allow_extra_args": self.allow_extra_args, - "allow_interspersed_args": self.allow_interspersed_args, - "ignore_unknown_options": self.ignore_unknown_options, - "auto_envvar_prefix": self.auto_envvar_prefix, - } - def __enter__(self) -> Context: self._depth += 1 push_context(self) @@ -893,11 +852,6 @@ class Command: its deprecation in --help. The message can be customized by using a string as the value. - .. versionchanged:: 8.2 - This is the base class for all commands, not ``BaseCommand``. - ``deprecated`` can be set to a string as well to customize the - deprecation message. - .. versionchanged:: 8.1 ``help``, ``epilog``, and ``short_help`` are stored unprocessed, all formatting is done when outputting help text, not at init, @@ -971,17 +925,6 @@ def __init__( self.hidden = hidden self.deprecated = deprecated - def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: - return { - "name": self.name, - "params": [param.to_info_dict() for param in self.get_params(ctx)], - "help": self.help, - "epilog": self.epilog, - "short_help": self.short_help, - "hidden": self.hidden, - "deprecated": self.deprecated, - } - def __repr__(self) -> str: return f"<{self.__class__.__name__} {self.name}>" @@ -1480,21 +1423,6 @@ def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: return self.main(*args, **kwargs) -class _FakeSubclassCheck(type): - def __subclasscheck__(cls, subclass: type) -> bool: - return issubclass(subclass, cls.__bases__[0]) - - def __instancecheck__(cls, instance: t.Any) -> bool: - return isinstance(instance, cls.__bases__[0]) - - -class _BaseCommand(Command, metaclass=_FakeSubclassCheck): - """ - .. deprecated:: 8.2 - Will be removed in Click 9.0. Use ``Command`` instead. - """ - - class Group(Command): """A group is a command that nests other commands (or more groups). @@ -1520,9 +1448,6 @@ class Group(Command): .. versionchanged:: 8.0 The ``commands`` argument can be a list of command objects. - - .. versionchanged:: 8.2 - Merged with and replaces the ``MultiCommand`` base class. """ allow_extra_args = True @@ -1596,24 +1521,6 @@ def __init__( "A group in chain mode cannot have optional arguments." ) - def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: - info_dict = super().to_info_dict(ctx) - commands = {} - - for name in self.list_commands(ctx): - command = self.get_command(ctx, name) - - if command is None: - continue - - sub_ctx = ctx._make_sub_context(command) - - with sub_ctx.scope(cleanup=False): - commands[name] = command.to_info_dict(sub_ctx) - - info_dict.update(commands=commands, chain=self.chain) - return info_dict - def add_command(self, cmd: Command, name: str | None = None) -> None: """Registers another :class:`Command` with this group. If the name is not provided, the name of the command is used. @@ -1621,7 +1528,6 @@ def add_command(self, cmd: Command, name: str | None = None) -> None: name = name or cmd.name if name is None: raise TypeError("Command has no name.") - _check_nested_chain(self, name, cmd, register=True) self.commands[name] = cmd def result_callback(self, replace: bool = False) -> t.Callable[[F], F]: @@ -1845,69 +1751,6 @@ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: return results -class _MultiCommand(Group, metaclass=_FakeSubclassCheck): - """ - .. deprecated:: 8.2 - Will be removed in Click 9.0. Use ``Group`` instead. - """ - - -class CommandCollection(Group): - """A :class:`Group` that looks up subcommands on other groups. If a command - is not found on this group, each registered source is checked in order. - Parameters on a source are not added to this group, and a source's callback - is not invoked when invoking its commands. In other words, this "flattens" - commands in many groups into this one group. - - :param name: The name of the group command. - :param sources: A list of :class:`Group` objects to look up commands from. - :param kwargs: Other arguments passed to :class:`Group`. - - .. versionchanged:: 8.2 - This is a subclass of ``Group``. Commands are looked up first on this - group, then each of its sources. - """ - - def __init__( - self, - name: str | None = None, - sources: list[Group] | None = None, - **kwargs: t.Any, - ) -> None: - super().__init__(name, **kwargs) - #: The list of registered groups. - self.sources: list[Group] = sources or [] - - def add_source(self, group: Group) -> None: - """Add a group as a source of commands.""" - self.sources.append(group) - - def get_command(self, ctx: Context, cmd_name: str) -> Command | None: - rv = super().get_command(ctx, cmd_name) - - if rv is not None: - return rv - - for source in self.sources: - rv = source.get_command(ctx, cmd_name) - - if rv is not None: - if self.chain: - _check_nested_chain(self, cmd_name, rv) - - return rv - - return None - - def list_commands(self, ctx: Context) -> list[str]: - rv: set[str] = set(super().list_commands(ctx)) - - for source in self.sources: - rv.update(source.list_commands(ctx)) - - return sorted(rv) - - def _check_iter(value: t.Any) -> cabc.Iterator[t.Any]: """Check if the value is iterable but not a string. Raises a type error, or return an iterator over the value. @@ -2082,34 +1925,6 @@ def __init__( f"{self.param_type_name} cannot be required." ) - def to_info_dict(self) -> dict[str, t.Any]: - """Gather information that could be useful for a tool generating - user-facing documentation. - - Use :meth:`click.Context.to_info_dict` to traverse the entire - CLI structure. - - .. versionchanged:: 8.3.0 - Returns ``None`` for the :attr:`default` if it was not set. - - .. versionadded:: 8.0 - """ - return { - "name": self.name, - "param_type_name": self.param_type_name, - "opts": self.opts, - "secondary_opts": self.secondary_opts, - "type": self.type.to_info_dict(), - "required": self.required, - "nargs": self.nargs, - "multiple": self.multiple, - # We explicitly hide the :attr:`UNSET` value to the user, as we choose to - # make it an implementation detail. And because ``to_info_dict`` has been - # designed for documentation purposes, we return ``None`` instead. - "default": self.default if self.default is not UNSET else None, - "envvar": self.envvar, - } - def __repr__(self) -> str: return f"<{self.__class__.__name__} {self.name}>" @@ -2757,25 +2572,6 @@ def __init__( if self.is_flag: raise TypeError("'count' is not valid with 'is_flag'.") - def to_info_dict(self) -> dict[str, t.Any]: - """ - .. versionchanged:: 8.3.0 - Returns ``None`` for the :attr:`flag_value` if it was not set. - """ - info_dict = super().to_info_dict() - info_dict.update( - help=self.help, - prompt=self.prompt, - is_flag=self.is_flag, - # We explicitly hide the :attr:`UNSET` value to the user, as we choose to - # make it an implementation detail. And because ``to_info_dict`` has been - # designed for documentation purposes, we return ``None`` instead. - flag_value=self.flag_value if self.flag_value is not UNSET else None, - count=self.count, - hidden=self.hidden, - ) - return info_dict - def get_error_hint(self, ctx: Context) -> str: result = super().get_error_hint(ctx) if self.show_envvar and self.envvar is not None: @@ -3283,27 +3079,3 @@ def get_error_hint(self, ctx: Context) -> str: def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) - - -def __getattr__(name: str) -> object: - import warnings - - if name == "BaseCommand": - warnings.warn( - "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" - " 'Command' instead.", - DeprecationWarning, - stacklevel=2, - ) - return _BaseCommand - - if name == "MultiCommand": - warnings.warn( - "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" - " 'Group' instead.", - DeprecationWarning, - stacklevel=2, - ) - return _MultiCommand - - raise AttributeError(name) diff --git a/typer/_click/termui.py b/typer/_click/termui.py index d2331f0d7f..228cd138a4 100644 --- a/typer/_click/termui.py +++ b/typer/_click/termui.py @@ -1,15 +1,11 @@ from __future__ import annotations import collections.abc as cabc -import inspect import io -import itertools -import sys import typing as t from contextlib import AbstractContextManager from gettext import gettext as _ -from ._compat import isatty, strip_ansi from .exceptions import Abort, UsageError from .globals import resolve_color_default from .types import Choice, ParamType, convert_type @@ -253,38 +249,6 @@ def confirm( return rv -def echo_via_pager( - text_or_generator: cabc.Iterable[str] | t.Callable[[], cabc.Iterable[str]] | str, - color: bool | None = None, -) -> None: - """This function takes a text and shows it via an environment specific - pager on stdout. - - .. versionchanged:: 3.0 - Added the `color` flag. - - :param text_or_generator: the text to page, or alternatively, a - generator emitting the text to page. - :param color: controls if the pager supports ANSI colors or not. The - default is autodetection. - """ - color = resolve_color_default(color) - - if inspect.isgeneratorfunction(text_or_generator): - i = t.cast("t.Callable[[], cabc.Iterable[str]]", text_or_generator)() - elif isinstance(text_or_generator, str): - i = [text_or_generator] - else: - i = iter(t.cast("cabc.Iterable[str]", text_or_generator)) - - # convert every element of i to a text type if necessary - text_generator = (el if isinstance(el, str) else str(el) for el in i) - - from ._termui_impl import pager - - return pager(itertools.chain(text_generator, "\n"), color) - - @t.overload def progressbar( *, @@ -485,20 +449,6 @@ def progressbar( ) -def clear() -> None: - """Clears the terminal screen. This will have the effect of clearing - the whole visible space of the terminal and moving the cursor to the - top left. This does not do anything if not connected to a terminal. - - .. versionadded:: 2.0 - """ - if not isatty(sys.stdout): - return - - # ANSI escape \033[2J clears the screen, \033[1;1H moves the cursor - echo("\033[2J\033[1;1H", nl=False) - - def _interpret_color(color: int | tuple[int, int, int] | str, offset: int = 0) -> str: if isinstance(color, int): return f"{38 + offset};5;{color:d}" @@ -639,18 +589,6 @@ def style( return "".join(bits) -def unstyle(text: str) -> str: - """Removes ANSI styling information from a string. Usually it's not - necessary to use this function as Click's echo function will - automatically remove styling if necessary. - - .. versionadded:: 2.0 - - :param text: the text to remove style information from. - """ - return strip_ansi(text) - - def secho( message: t.Any | None = None, file: t.IO[t.AnyStr] | None = None, @@ -685,95 +623,6 @@ def secho( return echo(message, file=file, nl=nl, err=err, color=color) -@t.overload -def edit( - text: bytes | bytearray, - editor: str | None = None, - env: cabc.Mapping[str, str] | None = None, - require_save: bool = False, - extension: str = ".txt", -) -> bytes | None: ... - - -@t.overload -def edit( - text: str, - editor: str | None = None, - env: cabc.Mapping[str, str] | None = None, - require_save: bool = True, - extension: str = ".txt", -) -> str | None: ... - - -@t.overload -def edit( - text: None = None, - editor: str | None = None, - env: cabc.Mapping[str, str] | None = None, - require_save: bool = True, - extension: str = ".txt", - filename: str | cabc.Iterable[str] | None = None, -) -> None: ... - - -def edit( - text: str | bytes | bytearray | None = None, - editor: str | None = None, - env: cabc.Mapping[str, str] | None = None, - require_save: bool = True, - extension: str = ".txt", - filename: str | cabc.Iterable[str] | None = None, -) -> str | bytes | bytearray | None: - r"""Edits the given text in the defined editor. If an editor is given - (should be the full path to the executable but the regular operating - system search path is used for finding the executable) it overrides - the detected editor. Optionally, some environment variables can be - used. If the editor is closed without changes, `None` is returned. In - case a file is edited directly the return value is always `None` and - `require_save` and `extension` are ignored. - - If the editor cannot be opened a :exc:`UsageError` is raised. - - Note for Windows: to simplify cross-platform usage, the newlines are - automatically converted from POSIX to Windows and vice versa. As such, - the message here will have ``\n`` as newline markers. - - :param text: the text to edit. - :param editor: optionally the editor to use. Defaults to automatic - detection. - :param env: environment variables to forward to the editor. - :param require_save: if this is true, then not saving in the editor - will make the return value become `None`. - :param extension: the extension to tell the editor about. This defaults - to `.txt` but changing this might change syntax - highlighting. - :param filename: if provided it will edit this file instead of the - provided text contents. It will not use a temporary - file as an indirection in that case. If the editor supports - editing multiple files at once, a sequence of files may be - passed as well. Invoke `click.file` once per file instead - if multiple files cannot be managed at once or editing the - files serially is desired. - - .. versionchanged:: 8.2.0 - ``filename`` now accepts any ``Iterable[str]`` in addition to a ``str`` - if the ``editor`` supports editing multiple files at once. - - """ - from ._termui_impl import Editor - - ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension) - - if filename is None: - return ed.edit(text) - - if isinstance(filename, str): - filename = (filename,) - - ed.edit_files(filenames=filename) - return None - - def launch(url: str, wait: bool = False, locate: bool = False) -> int: """This function launches the given URL (or filename) in the default viewer application for this file type. If this is an executable, it @@ -842,37 +691,3 @@ def raw_terminal() -> AbstractContextManager[int]: from ._termui_impl import raw_terminal as f return f() - - -def pause(info: str | None = None, err: bool = False) -> None: - """This command stops execution and waits for the user to press any - key to continue. This is similar to the Windows batch "pause" - command. If the program is not run through a terminal, this command - will instead do nothing. - - .. versionadded:: 2.0 - - .. versionadded:: 4.0 - Added the `err` parameter. - - :param info: The message to print before pausing. Defaults to - ``"Press any key to continue..."``. - :param err: if set to message goes to ``stderr`` instead of - ``stdout``, the same as with echo. - """ - if not isatty(sys.stdin) or not isatty(sys.stdout): - return - - if info is None: - info = _("Press any key to continue...") - - try: - if info: - echo(info, nl=False, err=err) - try: - getchar() - except (KeyboardInterrupt, EOFError): - pass - finally: - if info: - echo(err=err) diff --git a/typer/_click/types.py b/typer/_click/types.py index 54be66cc1d..01b4f33bfd 100644 --- a/typer/_click/types.py +++ b/typer/_click/types.py @@ -55,27 +55,6 @@ class ParamType: #: Windows). envvar_list_splitter: t.ClassVar[str | None] = None - def to_info_dict(self) -> dict[str, t.Any]: - """Gather information that could be useful for a tool generating - user-facing documentation. - - Use :meth:`click.Context.to_info_dict` to traverse the entire - CLI structure. - - .. versionadded:: 8.0 - """ - # The class name without the "ParamType" suffix. - param_type = type(self).__name__.partition("ParamType")[0] - param_type = param_type.partition("ParameterType")[0] - - # Custom subclasses might not remember to set a name. - if hasattr(self, "name"): - name = self.name - else: - name = param_type - - return {"param_type": param_type, "name": name} - def __call__( self, value: t.Any, @@ -169,11 +148,6 @@ def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None: self.name: str = func.__name__ self.func = func - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["func"] = self.func - return info_dict - def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None ) -> t.Any: @@ -188,18 +162,6 @@ def convert( self.fail(value, param, ctx) -class UnprocessedParamType(ParamType): - name = "text" - - def convert( - self, value: t.Any, param: Parameter | None, ctx: Context | None - ) -> t.Any: - return value - - def __repr__(self) -> str: - return "UNPROCESSED" - - class StringParamType(ParamType): name = "text" @@ -257,12 +219,6 @@ def __init__( self.choices: cabc.Sequence[ParamTypeValue] = tuple(choices) self.case_sensitive = case_sensitive - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["choices"] = self.choices - info_dict["case_sensitive"] = self.case_sensitive - return info_dict - def _normalized_mapping( self, ctx: Context | None = None ) -> cabc.Mapping[ParamTypeValue, str]: @@ -424,11 +380,6 @@ def __init__(self, formats: cabc.Sequence[str] | None = None): "%Y-%m-%d %H:%M:%S", ] - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["formats"] = self.formats - return info_dict - def get_metavar(self, param: Parameter, ctx: Context) -> str | None: return f"[{'|'.join(self.formats)}]" @@ -498,17 +449,6 @@ def __init__( self.max_open = max_open self.clamp = clamp - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict.update( - min=self.min, - max=self.max, - min_open=self.min_open, - max_open=self.max_open, - clamp=self.clamp, - ) - return info_dict - def convert( self, value: t.Any, param: Parameter | None, ctx: Context | None ) -> t.Any: @@ -794,11 +734,6 @@ def __init__( self.lazy = lazy self.atomic = atomic - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict.update(mode=self.mode, encoding=self.encoding) - return info_dict - def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: if self.lazy is not None: return self.lazy @@ -889,8 +824,7 @@ class Path(ParamType): symlinks. A ``~`` is not expanded, as this is supposed to be done by the shell only. :param allow_dash: Allow a single dash as a value, which indicates - a standard stream (but does not open it). Use - :func:`~click.open_file` to handle opening this value. + a standard stream (but does not open it). :param path_type: Convert the incoming path value to this type. If ``None``, keep Python's default, which is ``str``. Useful to convert to :class:`pathlib.Path`. @@ -936,18 +870,6 @@ def __init__( else: self.name = _("path") - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict.update( - exists=self.exists, - file_okay=self.file_okay, - dir_okay=self.dir_okay, - writable=self.writable, - readable=self.readable, - allow_dash=self.allow_dash, - ) - return info_dict - def coerce_path_result( self, value: str | os.PathLike[str] ) -> str | bytes | os.PathLike[str]: @@ -1070,11 +992,6 @@ class Tuple(CompositeParamType): def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType]) -> None: self.types: cabc.Sequence[ParamType] = [convert_type(ty) for ty in types] - def to_info_dict(self) -> dict[str, t.Any]: - info_dict = super().to_info_dict() - info_dict["types"] = [t.to_info_dict() for t in self.types] - return info_dict - @property def name(self) -> str: # type: ignore return f"<{' '.join(ty.name for ty in self.types)}>" @@ -1165,19 +1082,6 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: return FuncParamType(ty) -#: A dummy parameter type that just does nothing. From a user's -#: perspective this appears to just be the same as `STRING` but -#: internally no string conversion takes place if the input was bytes. -#: This is usually useful when working with file paths as they can -#: appear in bytes and unicode. -#: -#: For path related uses the :class:`Path` type is a better choice but -#: there are situations where an unprocessed type is useful which is why -#: it is is provided. -#: -#: .. versionadded:: 4.0 -UNPROCESSED = UnprocessedParamType() - #: A unicode string parameter type which is the implicit default. This #: can also be selected by using ``str`` as type. STRING = StringParamType() diff --git a/typer/_click/utils.py b/typer/_click/utils.py index 17a2349195..9869a0e778 100644 --- a/typer/_click/utils.py +++ b/typer/_click/utils.py @@ -195,31 +195,6 @@ def __iter__(self) -> cabc.Iterator[t.AnyStr]: return iter(self._f) # type: ignore -class KeepOpenFile: - def __init__(self, file: t.IO[t.Any]) -> None: - self._file: t.IO[t.Any] = file - - def __getattr__(self, name: str) -> t.Any: - return getattr(self._file, name) - - def __enter__(self) -> KeepOpenFile: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - tb: TracebackType | None, - ) -> None: - pass - - def __repr__(self) -> str: - return repr(self._file) - - def __iter__(self) -> cabc.Iterator[t.AnyStr]: - return iter(self._file) - - def echo( message: t.Any | None = None, file: t.IO[t.Any] | None = None, @@ -356,55 +331,6 @@ def get_text_stream( return opener(encoding, errors) -def open_file( - filename: str | os.PathLike[str], - mode: str = "r", - encoding: str | None = None, - errors: str | None = "strict", - lazy: bool = False, - atomic: bool = False, -) -> t.IO[t.Any]: - """Open a file, with extra behavior to handle ``'-'`` to indicate - a standard stream, lazy open on write, and atomic write. Similar to - the behavior of the :class:`~click.File` param type. - - If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is - wrapped so that using it in a context manager will not close it. - This makes it possible to use the function without accidentally - closing a standard stream: - - .. code-block:: python - - with open_file(filename) as f: - ... - - :param filename: The name or Path of the file to open, or ``'-'`` for - ``stdin``/``stdout``. - :param mode: The mode in which to open the file. - :param encoding: The encoding to decode or encode a file opened in - text mode. - :param errors: The error handling mode. - :param lazy: Wait to open the file until it is accessed. For read - mode, the file is temporarily opened to raise access errors - early, then closed until it is read again. - :param atomic: Write to a temporary file and replace the given file - on close. - - .. versionadded:: 3.0 - """ - if lazy: - return t.cast( - "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic) - ) - - f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) - - if not should_close: - f = t.cast("t.IO[t.Any]", KeepOpenFile(f)) - - return f - - def format_filename( filename: str | bytes | os.PathLike[str] | os.PathLike[bytes], shorten: bool = False, diff --git a/typer/_completion_classes.py b/typer/_completion_classes.py index 9a0a52b836..1f61846eed 100644 --- a/typer/_completion_classes.py +++ b/typer/_completion_classes.py @@ -6,7 +6,6 @@ from . import _click from ._click.shell_completion import split_arg_string as click_split_arg_string - from ._completion_shared import ( COMPLETION_SCRIPT_BASH, COMPLETION_SCRIPT_FISH, diff --git a/uv.lock b/uv.lock index bbbdc556ad..8b9f518ee7 100644 --- a/uv.lock +++ b/uv.lock @@ -1888,7 +1888,7 @@ tests = [ [package.metadata] requires-dist = [ { name = "annotated-doc", specifier = ">=0.0.2" }, - { name = "rich", specifier = ">=10.11.0" }, + { name = "rich", specifier = ">=12.3.0" }, { name = "shellingham", specifier = ">=1.3.0" }, ]