From fcc8fae8a9bfdffc6eade480debb7ca62f34265c Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 10:39:40 +0100 Subject: [PATCH 01/19] format --- typer/_completion_classes.py | 1 - 1 file changed, 1 deletion(-) 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, From f0494dba5ef40feba9a0db5f9498efa2b58303d5 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 10:49:30 +0100 Subject: [PATCH 02/19] fix leftover issue from merge conflict --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index d9e17e102f..cfede78ca8 100644 --- a/uv.lock +++ b/uv.lock @@ -1892,7 +1892,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" }, ] From c0c050b28b47bfcc81fed2f4448f3fcb1a47a56a Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 11:16:14 +0100 Subject: [PATCH 03/19] remove CommandCollection --- typer/_click/__init__.py | 1 - typer/_click/core.py | 56 ---------------------------------------- 2 files changed, 57 deletions(-) diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py index 60bd5427ea..60a8ee07a6 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 diff --git a/typer/_click/core.py b/typer/_click/core.py index abc49dcd61..3770b5b99b 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -1852,62 +1852,6 @@ class _MultiCommand(Group, metaclass=_FakeSubclassCheck): """ -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. From 2cb575020dc63a8046d898999e049bbf52ad4490 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 11:23:29 +0100 Subject: [PATCH 04/19] remove to_info_dict functions --- typer/_click/core.py | 97 ------------------------------------------- typer/_click/types.py | 78 ---------------------------------- 2 files changed, 175 deletions(-) diff --git a/typer/_click/core.py b/typer/_click/core.py index 3770b5b99b..ce0e051021 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -447,27 +447,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) @@ -971,17 +950,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}>" @@ -1596,24 +1564,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. @@ -2026,34 +1976,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}>" @@ -2701,25 +2623,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: diff --git a/typer/_click/types.py b/typer/_click/types.py index 54be66cc1d..77a18b8f60 100644 --- a/typer/_click/types.py +++ b/typer/_click/types.py @@ -47,35 +47,6 @@ class ParamType: #: the descriptive name of this type name: str - #: if a list of this type is expected and the value is pulled from a - #: string environment variable, this is what splits it up. `None` - #: means any whitespace. For all parameters the general rule is that - #: whitespace splits them up. The exception are paths and files which - #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on - #: Windows). - envvar_list_splitter: 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 +140,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: @@ -257,12 +223,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 +384,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 +453,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 +738,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 @@ -936,18 +875,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 +997,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)}>" From d9a2390dc974d5116dc56a778d3b15100305ff8c Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 11:26:02 +0100 Subject: [PATCH 05/19] restore envvar_list_splitter --- typer/_click/types.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/typer/_click/types.py b/typer/_click/types.py index 77a18b8f60..aeba258722 100644 --- a/typer/_click/types.py +++ b/typer/_click/types.py @@ -47,6 +47,14 @@ class ParamType: #: the descriptive name of this type name: str + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter: t.ClassVar[str | None] = None + def __call__( self, value: t.Any, From 83328a5d71e45832a83efb5f706facbf55fc0737 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 11:58:36 +0100 Subject: [PATCH 06/19] remove open_file --- typer/__init__.py | 1 - typer/_click/__init__.py | 1 - typer/_click/types.py | 3 +-- typer/_click/utils.py | 49 ---------------------------------------- 4 files changed, 1 insertion(+), 53 deletions(-) diff --git a/typer/__init__.py b/typer/__init__.py index e3185b3fca..0badbaf60f 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -24,7 +24,6 @@ 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 60a8ee07a6..59f48e07fb 100644 --- a/typer/_click/__init__.py +++ b/typer/_click/__init__.py @@ -55,4 +55,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/types.py b/typer/_click/types.py index aeba258722..fa4c280c70 100644 --- a/typer/_click/types.py +++ b/typer/_click/types.py @@ -836,8 +836,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`. diff --git a/typer/_click/utils.py b/typer/_click/utils.py index 17a2349195..856ec5058c 100644 --- a/typer/_click/utils.py +++ b/typer/_click/utils.py @@ -356,55 +356,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, From 65747696b4d599276d09338265574c4a8010894c Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 12:12:52 +0100 Subject: [PATCH 07/19] remove UnprocessedParamType --- typer/_click/__init__.py | 1 - typer/_click/types.py | 25 ------------------------- 2 files changed, 26 deletions(-) diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py index 59f48e07fb..76797fc42d 100644 --- a/typer/_click/__init__.py +++ b/typer/_click/__init__.py @@ -40,7 +40,6 @@ 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 diff --git a/typer/_click/types.py b/typer/_click/types.py index fa4c280c70..01b4f33bfd 100644 --- a/typer/_click/types.py +++ b/typer/_click/types.py @@ -162,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" @@ -1094,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() From 71d0e3d4d121f87681b914c65e8777734dc5797c Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 12:22:01 +0100 Subject: [PATCH 08/19] remove KeepOpenFile --- typer/_click/utils.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/typer/_click/utils.py b/typer/_click/utils.py index 856ec5058c..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, From 358962207d527a01f4c320514e7a05642766b37b Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 12:29:14 +0100 Subject: [PATCH 09/19] remove pager functionality --- typer/__init__.py | 1 - typer/_click/__init__.py | 1 - typer/_click/_termui_impl.py | 195 ----------------------------------- typer/_click/termui.py | 32 ------ 4 files changed, 229 deletions(-) diff --git a/typer/__init__.py b/typer/__init__.py index 0badbaf60f..59b88fabd9 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -10,7 +10,6 @@ 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 diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py index 76797fc42d..c58d5d11a7 100644 --- a/typer/_click/__init__.py +++ b/typer/_click/__init__.py @@ -26,7 +26,6 @@ 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 diff --git a/typer/_click/_termui_impl.py b/typer/_click/_termui_impl.py index 3b280648de..e2ca30f3ab 100644 --- a/typer/_click/_termui_impl.py +++ b/typer/_click/_termui_impl.py @@ -368,201 +368,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, diff --git a/typer/_click/termui.py b/typer/_click/termui.py index d2331f0d7f..0fd94d074f 100644 --- a/typer/_click/termui.py +++ b/typer/_click/termui.py @@ -253,38 +253,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( *, From 82aca6eb629fc34e9c0646b18e1bc05d9cd554c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:29:52 +0000 Subject: [PATCH 10/19] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/_click/_termui_impl.py | 4 ---- typer/_click/termui.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/typer/_click/_termui_impl.py b/typer/_click/_termui_impl.py index e2ca30f3ab..acc2db8b2e 100644 --- a/typer/_click/_termui_impl.py +++ b/typer/_click/_termui_impl.py @@ -10,13 +10,11 @@ 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,8 +23,6 @@ _default_text_stdout, get_best_encoding, isatty, - open_stream, - strip_ansi, term_len, ) from .exceptions import ClickException diff --git a/typer/_click/termui.py b/typer/_click/termui.py index 0fd94d074f..d9e1d58ba8 100644 --- a/typer/_click/termui.py +++ b/typer/_click/termui.py @@ -1,9 +1,7 @@ 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 02877ca89ccf792c0af93f1a738ccb33819bea73 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 13:38:45 +0100 Subject: [PATCH 11/19] remove Editor --- typer/__init__.py | 1 - typer/_click/__init__.py | 1 - typer/_click/_termui_impl.py | 114 ----------------------------------- typer/_click/termui.py | 89 --------------------------- 4 files changed, 205 deletions(-) diff --git a/typer/__init__.py b/typer/__init__.py index 59b88fabd9..4812f34488 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -10,7 +10,6 @@ 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 edit as edit from ._click.termui import getchar as getchar from ._click.termui import pause as pause from ._click.termui import progressbar as progressbar diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py index c58d5d11a7..71a1bdf3da 100644 --- a/typer/_click/__init__.py +++ b/typer/_click/__init__.py @@ -26,7 +26,6 @@ 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 edit as edit from .termui import getchar as getchar from .termui import launch as launch from .termui import pause as pause diff --git a/typer/_click/_termui_impl.py b/typer/_click/_termui_impl.py index acc2db8b2e..a64c8fe2c8 100644 --- a/typer/_click/_termui_impl.py +++ b/typer/_click/_termui_impl.py @@ -13,7 +13,6 @@ import sys import time import typing as t -from gettext import gettext as _ from io import StringIO from types import TracebackType @@ -25,7 +24,6 @@ isatty, term_len, ) -from .exceptions import ClickException from .utils import echo V = t.TypeVar("V") @@ -364,118 +362,6 @@ def generator(self) -> cabc.Iterator[V]: self.render_progress() -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/termui.py b/typer/_click/termui.py index d9e1d58ba8..65eaf6594c 100644 --- a/typer/_click/termui.py +++ b/typer/_click/termui.py @@ -651,95 +651,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 From 90fcdd4e9150f0da04bdf3ff3d027e28aa7f73e6 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 14:03:03 +0100 Subject: [PATCH 12/19] remove pause --- typer/__init__.py | 1 - typer/_click/__init__.py | 1 - typer/_click/termui.py | 34 ---------------------------------- 3 files changed, 36 deletions(-) diff --git a/typer/__init__.py b/typer/__init__.py index 4812f34488..940f02943c 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -11,7 +11,6 @@ from ._click.termui import clear as clear from ._click.termui import confirm as confirm 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 diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py index 71a1bdf3da..d83417cad9 100644 --- a/typer/_click/__init__.py +++ b/typer/_click/__init__.py @@ -28,7 +28,6 @@ from .termui import confirm as confirm 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 diff --git a/typer/_click/termui.py b/typer/_click/termui.py index 65eaf6594c..6342f505d4 100644 --- a/typer/_click/termui.py +++ b/typer/_click/termui.py @@ -719,37 +719,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) From f1e8ffb56de2b2f88ea3f0964a7d28aca5a8a1ed Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 14:12:22 +0100 Subject: [PATCH 13/19] remove unstyle and clear --- typer/__init__.py | 2 -- typer/_click/__init__.py | 2 -- typer/_click/termui.py | 28 ---------------------------- 3 files changed, 32 deletions(-) diff --git a/typer/__init__.py b/typer/__init__.py index 940f02943c..12386c4ff3 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -8,14 +8,12 @@ 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 getchar as getchar from ._click.termui import progressbar as progressbar from ._click.termui import prompt as prompt from ._click.termui import secho as secho from ._click.termui import style as style -from ._click.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 diff --git a/typer/_click/__init__.py b/typer/_click/__init__.py index d83417cad9..3b9455b78a 100644 --- a/typer/_click/__init__.py +++ b/typer/_click/__init__.py @@ -24,7 +24,6 @@ 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 getchar as getchar from .termui import launch as launch @@ -32,7 +31,6 @@ 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 diff --git a/typer/_click/termui.py b/typer/_click/termui.py index 6342f505d4..228cd138a4 100644 --- a/typer/_click/termui.py +++ b/typer/_click/termui.py @@ -2,12 +2,10 @@ import collections.abc as cabc import io -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 @@ -451,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}" @@ -605,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, From b1a4ecd66da77b8b88823042ca1b2c6df879685c Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 20:00:25 +0100 Subject: [PATCH 14/19] remove _check_nested_chain --- typer/_click/core.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/typer/_click/core.py b/typer/_click/core.py index ce0e051021..11e0485797 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)) @@ -1571,7 +1551,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]: From 4d8e9cbb2aeeb7267dc6c26a28b1a0d66c265121 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 20:11:25 +0100 Subject: [PATCH 15/19] remove Click's main function --- typer/_click/core.py | 165 ------------------------------------------- 1 file changed, 165 deletions(-) diff --git a/typer/_click/core.py b/typer/_click/core.py index 11e0485797..46b8defc1e 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -1258,171 +1258,6 @@ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: return results - @t.overload - def main( - self, - args: cabc.Sequence[str] | None = None, - prog_name: str | None = None, - complete_var: str | None = None, - standalone_mode: t.Literal[True] = True, - **extra: t.Any, - ) -> t.NoReturn: ... - - @t.overload - def main( - self, - args: cabc.Sequence[str] | None = None, - prog_name: str | None = None, - complete_var: str | None = None, - standalone_mode: bool = ..., - **extra: t.Any, - ) -> t.Any: ... - - def main( - self, - args: cabc.Sequence[str] | None = None, - prog_name: str | None = None, - complete_var: str | None = None, - standalone_mode: bool = True, - windows_expand_args: bool = True, - **extra: t.Any, - ) -> t.Any: - """This is the way to invoke a script with all the bells and - whistles as a command line application. This will always terminate - the application after a call. If this is not wanted, ``SystemExit`` - needs to be caught. - - This method is also available by directly calling the instance of - a :class:`Command`. - - :param args: the arguments that should be used for parsing. If not - provided, ``sys.argv[1:]`` is used. - :param prog_name: the program name that should be used. By default - the program name is constructed by taking the file - name from ``sys.argv[0]``. - :param complete_var: the environment variable that controls the - bash completion support. The default is - ``"__COMPLETE"`` with prog_name in - uppercase. - :param standalone_mode: the default behavior is to invoke the script - in standalone mode. Click will then - handle exceptions and convert them into - error messages and the function will never - return but shut down the interpreter. If - this is set to `False` they will be - propagated to the caller and the return - value of this function is the return value - of :meth:`invoke`. - :param windows_expand_args: Expand glob patterns, user dir, and - env vars in command line args on Windows. - :param extra: extra keyword arguments are forwarded to the context - constructor. See :class:`Context` for more information. - - .. versionchanged:: 8.0.1 - Added the ``windows_expand_args`` parameter to allow - disabling command line arg expansion on Windows. - - .. versionchanged:: 8.0 - When taking arguments from ``sys.argv`` on Windows, glob - patterns, user dir, and env vars are expanded. - - .. versionchanged:: 3.0 - Added the ``standalone_mode`` parameter. - """ - if args is None: - args = sys.argv[1:] - - if os.name == "nt" and windows_expand_args: - args = _expand_args(args) - else: - args = list(args) - - if prog_name is None: - prog_name = _detect_program_name() - - # Process shell completion requests and exit early. - self._main_shell_completion(extra, prog_name, complete_var) - - try: - try: - with self.make_context(prog_name, args, **extra) as ctx: - rv = self.invoke(ctx) - if not standalone_mode: - return rv - # it's not safe to `ctx.exit(rv)` here! - # note that `rv` may actually contain data like "1" which - # has obvious effects - # more subtle case: `rv=[None, None]` can come out of - # chained commands which all returned `None` -- so it's not - # even always obvious that `rv` indicates success/failure - # by its truthiness/falsiness - ctx.exit() - except (EOFError, KeyboardInterrupt) as e: - echo(file=sys.stderr) - raise Abort() from e - except ClickException as e: - if not standalone_mode: - raise - e.show() - sys.exit(e.exit_code) - except OSError as e: - if e.errno == errno.EPIPE: - sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) - sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) - sys.exit(1) - else: - raise - except Exit as e: - if standalone_mode: - sys.exit(e.exit_code) - else: - # in non-standalone mode, return the exit code - # note that this is only reached if `self.invoke` above raises - # an Exit explicitly -- thus bypassing the check there which - # would return its result - # the results of non-standalone execution may therefore be - # somewhat ambiguous: if there are codepaths which lead to - # `ctx.exit(1)` and to `return 1`, the caller won't be able to - # tell the difference between the two - return e.exit_code - except Abort: - if not standalone_mode: - raise - echo(_("Aborted!"), file=sys.stderr) - sys.exit(1) - - def _main_shell_completion( - self, - ctx_args: cabc.MutableMapping[str, t.Any], - prog_name: str, - complete_var: str | None = None, - ) -> None: - """Check if the shell is asking for tab completion, process - that, then exit early. Called from :meth:`main` before the - program is invoked. - - :param prog_name: Name of the executable in the shell. - :param complete_var: Name of the environment variable that holds - the completion instruction. Defaults to - ``_{PROG_NAME}_COMPLETE``. - - .. versionchanged:: 8.2.0 - Dots (``.``) in ``prog_name`` are replaced with underscores (``_``). - """ - if complete_var is None: - complete_name = prog_name.replace("-", "_").replace(".", "_") - complete_var = f"_{complete_name}_COMPLETE".upper() - - instruction = os.environ.get(complete_var) - - if not instruction: - return - - from .shell_completion import shell_complete - - rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) - sys.exit(rv) - def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: """Alias for :meth:`main`.""" return self.main(*args, **kwargs) From bb73b21baf576ac70c933f374e46d0575580ad25 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:12:10 +0000 Subject: [PATCH 16/19] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/_click/core.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/typer/_click/core.py b/typer/_click/core.py index 46b8defc1e..3dfa669e03 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -2,10 +2,8 @@ import collections.abc as cabc import enum -import errno import inspect import os -import sys import typing as t from collections import Counter, abc from contextlib import AbstractContextManager, ExitStack, contextmanager @@ -20,7 +18,6 @@ from .exceptions import ( Abort, BadParameter, - ClickException, Exit, MissingParameter, NoArgsIsHelpError, @@ -31,9 +28,6 @@ from .parser import _OptionParser, _split_opt from .termui import confirm, prompt, style from .utils import ( - PacifyFlushWrapper, - _detect_program_name, - _expand_args, echo, make_default_short_help, make_str, From 0d61e4c28943006fb3d77c5c9b35798d6a84a510 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 20:26:21 +0100 Subject: [PATCH 17/19] Revert "remove Click's main function" This reverts commit 4d8e9cbb2aeeb7267dc6c26a28b1a0d66c265121. --- typer/_click/core.py | 165 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/typer/_click/core.py b/typer/_click/core.py index 46b8defc1e..11e0485797 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -1258,6 +1258,171 @@ def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: return results + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: t.Literal[True] = True, + **extra: t.Any, + ) -> t.NoReturn: ... + + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = ..., + **extra: t.Any, + ) -> t.Any: ... + + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = True, + windows_expand_args: bool = True, + **extra: t.Any, + ) -> t.Any: + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param windows_expand_args: Expand glob patterns, user dir, and + env vars in command line args on Windows. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + + .. versionchanged:: 8.0.1 + Added the ``windows_expand_args`` parameter to allow + disabling command line arg expansion on Windows. + + .. versionchanged:: 8.0 + When taking arguments from ``sys.argv`` on Windows, glob + patterns, user dir, and env vars are expanded. + + .. versionchanged:: 3.0 + Added the ``standalone_mode`` parameter. + """ + if args is None: + args = sys.argv[1:] + + if os.name == "nt" and windows_expand_args: + args = _expand_args(args) + else: + args = list(args) + + if prog_name is None: + prog_name = _detect_program_name() + + # Process shell completion requests and exit early. + self._main_shell_completion(extra, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt) as e: + echo(file=sys.stderr) + raise Abort() from e + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except OSError as e: + if e.errno == errno.EPIPE: + sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo(_("Aborted!"), file=sys.stderr) + sys.exit(1) + + def _main_shell_completion( + self, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str | None = None, + ) -> None: + """Check if the shell is asking for tab completion, process + that, then exit early. Called from :meth:`main` before the + program is invoked. + + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. Defaults to + ``_{PROG_NAME}_COMPLETE``. + + .. versionchanged:: 8.2.0 + Dots (``.``) in ``prog_name`` are replaced with underscores (``_``). + """ + if complete_var is None: + complete_name = prog_name.replace("-", "_").replace(".", "_") + complete_var = f"_{complete_name}_COMPLETE".upper() + + instruction = os.environ.get(complete_var) + + if not instruction: + return + + from .shell_completion import shell_complete + + rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) + sys.exit(rv) + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: """Alias for :meth:`main`.""" return self.main(*args, **kwargs) From c5595d9cd17270ad8a4750808b7d8571f9004be4 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 20:28:27 +0100 Subject: [PATCH 18/19] redo automatic removal of imports --- typer/_click/core.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/typer/_click/core.py b/typer/_click/core.py index 0c11ceb156..11e0485797 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -2,8 +2,10 @@ import collections.abc as cabc import enum +import errno import inspect import os +import sys import typing as t from collections import Counter, abc from contextlib import AbstractContextManager, ExitStack, contextmanager @@ -18,6 +20,7 @@ from .exceptions import ( Abort, BadParameter, + ClickException, Exit, MissingParameter, NoArgsIsHelpError, @@ -28,6 +31,9 @@ from .parser import _OptionParser, _split_opt from .termui import confirm, prompt, style from .utils import ( + PacifyFlushWrapper, + _detect_program_name, + _expand_args, echo, make_default_short_help, make_str, From 7d572f202cb664cdc1a776fb2d7978ae9c6bb5b9 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 23 Feb 2026 20:37:56 +0100 Subject: [PATCH 19/19] remove _BaseCommand, _MultiCommand and _FakeSubclassCheck --- typer/_click/core.py | 54 -------------------------------------------- 1 file changed, 54 deletions(-) diff --git a/typer/_click/core.py b/typer/_click/core.py index 11e0485797..3d9f439dd2 100644 --- a/typer/_click/core.py +++ b/typer/_click/core.py @@ -852,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, @@ -1428,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). @@ -1468,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 @@ -1774,13 +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. - """ - - 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. @@ -3109,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)