From 5e717591f3ee79bb987ef0c80ce3fe4759b2ef62 Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 15:26:52 -0600 Subject: [PATCH 1/8] annotate run functions --- invoke/_types.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++ invoke/context.py | 3 ++ invoke/runners.py | 2 + 3 files changed, 104 insertions(+) create mode 100644 invoke/_types.py diff --git a/invoke/_types.py b/invoke/_types.py new file mode 100644 index 000000000..be4b641e6 --- /dev/null +++ b/invoke/_types.py @@ -0,0 +1,99 @@ +from typing import IO, TYPE_CHECKING, Union, overload, cast, Optional, Dict +from typing_extensions import Protocol, TypedDict, Unpack, Literal + + +if TYPE_CHECKING: + from invoke.runners import Promise, Result + from invoke.watchers import StreamWatcher + + +def annotate_run_function(func: "_RunFunctionImpl") -> "RunFunction": + """Add standard run function annotations to a function.""" + return cast(RunFunction, func) + + +class _RunFunctionImpl(Protocol): + + def __call__( + self, command: str, **kwargs: Unpack["RunParams"] + ) -> Optional[Result]: ... + + +class _BaseRunParams(TypedDict, total=False): + dry: bool + echo: bool + echo_format: str + echo_stdin: Optional[bool] + encoding: Optional[str] + err_stream: IO + env: Dict[str, str] + fallback: bool + hide: Optional[bool] + in_stream: Optional[IO] + out_stream: IO + pty: bool + replace_env: bool + shell: str + timeout: Optional[int] + warn: bool + watchers: list[StreamWatcher] + + +class RunParams(_BaseRunParams, total=False): + """Parameters for Runner.run""" + + asynchronous: bool + disown: bool + + +class RunFunction(Protocol): + """A function that runs a command.""" + + @overload + def __call__( + self, + command: str, + *, + disown: Literal[True], + **kwargs: Unpack[_BaseRunParams], + ) -> None: ... + + @overload + def __call__( + self, + command: str, + *, + disown: bool, + **kwargs: Unpack[_BaseRunParams], + ) -> Optional[Result]: ... + + @overload + def __call__( + self, + command: str, + *, + asynchronous: Literal[True], + **kwargs: Unpack[_BaseRunParams], + ) -> Promise: ... + + @overload + def __call__( + self, + command: str, + *, + asynchronous: bool, + **kwargs: Unpack[_BaseRunParams], + ) -> Union[Promise, Result]: ... + + @overload + def __call__( + self, + command: str, + **kwargs: Unpack[_BaseRunParams], + ) -> Result: ... + + def __call__( + self, + command: str, + **kwargs: Unpack[RunParams], + ) -> Optional[Result]: ... diff --git a/invoke/context.py b/invoke/context.py index e9beaf4d1..7d47f3f4d 100644 --- a/invoke/context.py +++ b/invoke/context.py @@ -14,6 +14,7 @@ ) from unittest.mock import Mock +from ._types import annotate_run_function from .config import Config, DataProxy from .exceptions import Failure, AuthFailure, ResponseNotAccepted from .runners import Result @@ -87,6 +88,7 @@ def config(self, value: Config) -> None: # runtime. self._set(_config=value) + @annotate_run_function def run(self, command: str, **kwargs: Any) -> Optional[Result]: """ Execute a local shell command, honoring config options. @@ -112,6 +114,7 @@ def _run( command = self._prefix_commands(command) return runner.run(command, **kwargs) + @annotate_run_function def sudo(self, command: str, **kwargs: Any) -> Optional[Result]: """ Execute a shell command via ``sudo`` with password auto-response. diff --git a/invoke/runners.py b/invoke/runners.py index f1c888f44..1cd1a9182 100644 --- a/invoke/runners.py +++ b/invoke/runners.py @@ -36,6 +36,7 @@ except ImportError: termios = None # type: ignore[assignment] +from ._types import annotate_run_function from .exceptions import ( UnexpectedExit, Failure, @@ -122,6 +123,7 @@ def __init__(self, context: "Context") -> None: self._asynchronous = False self._disowned = False + @annotate_run_function def run(self, command: str, **kwargs: Any) -> Optional["Result"]: """ Execute ``command``, returning an instance of `Result` once complete. From 2ac285f8371bc77bf8e3bc11b5cbf7e122db7a7a Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 15:46:20 -0600 Subject: [PATCH 2/8] typing-extensions as dev dep --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 1bf0ad732..dc89f6f7e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -21,3 +21,4 @@ icecream>=2.1 # typing mypy==0.971 types-PyYAML==6.0.12.4 +typing-extensions>=4,<5 From c18974bd1c1b59a72d2d0619c5a85d32ad264e95 Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 15:50:28 -0600 Subject: [PATCH 3/8] lazily reference types --- invoke/_types.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/invoke/_types.py b/invoke/_types.py index be4b641e6..22dfdd4dc 100644 --- a/invoke/_types.py +++ b/invoke/_types.py @@ -16,7 +16,7 @@ class _RunFunctionImpl(Protocol): def __call__( self, command: str, **kwargs: Unpack["RunParams"] - ) -> Optional[Result]: ... + ) -> Optional["Result"]: ... class _BaseRunParams(TypedDict, total=False): @@ -36,7 +36,7 @@ class _BaseRunParams(TypedDict, total=False): shell: str timeout: Optional[int] warn: bool - watchers: list[StreamWatcher] + watchers: list["StreamWatcher"] class RunParams(_BaseRunParams, total=False): @@ -65,7 +65,7 @@ def __call__( *, disown: bool, **kwargs: Unpack[_BaseRunParams], - ) -> Optional[Result]: ... + ) -> Optional["Result"]: ... @overload def __call__( @@ -74,7 +74,7 @@ def __call__( *, asynchronous: Literal[True], **kwargs: Unpack[_BaseRunParams], - ) -> Promise: ... + ) -> "Promise": ... @overload def __call__( @@ -83,17 +83,17 @@ def __call__( *, asynchronous: bool, **kwargs: Unpack[_BaseRunParams], - ) -> Union[Promise, Result]: ... + ) -> Union["Promise", "Result"]: ... @overload def __call__( self, command: str, **kwargs: Unpack[_BaseRunParams], - ) -> Result: ... + ) -> "Result": ... def __call__( self, command: str, **kwargs: Unpack[RunParams], - ) -> Optional[Result]: ... + ) -> Optional["Result"]: ... From d4244e916443c4de8aca7f3775aca8d49a8383db Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 15:52:24 -0600 Subject: [PATCH 4/8] fix lint --- invoke/_types.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/invoke/_types.py b/invoke/_types.py index 22dfdd4dc..24847bd7e 100644 --- a/invoke/_types.py +++ b/invoke/_types.py @@ -16,7 +16,8 @@ class _RunFunctionImpl(Protocol): def __call__( self, command: str, **kwargs: Unpack["RunParams"] - ) -> Optional["Result"]: ... + ) -> Optional["Result"]: + ... class _BaseRunParams(TypedDict, total=False): @@ -56,7 +57,8 @@ def __call__( *, disown: Literal[True], **kwargs: Unpack[_BaseRunParams], - ) -> None: ... + ) -> None: + ... @overload def __call__( @@ -65,7 +67,8 @@ def __call__( *, disown: bool, **kwargs: Unpack[_BaseRunParams], - ) -> Optional["Result"]: ... + ) -> Optional["Result"]: + ... @overload def __call__( @@ -74,7 +77,8 @@ def __call__( *, asynchronous: Literal[True], **kwargs: Unpack[_BaseRunParams], - ) -> "Promise": ... + ) -> "Promise": + ... @overload def __call__( @@ -83,17 +87,20 @@ def __call__( *, asynchronous: bool, **kwargs: Unpack[_BaseRunParams], - ) -> Union["Promise", "Result"]: ... + ) -> Union["Promise", "Result"]: + ... @overload def __call__( self, command: str, **kwargs: Unpack[_BaseRunParams], - ) -> "Result": ... + ) -> "Result": + ... def __call__( self, command: str, **kwargs: Unpack[RunParams], - ) -> Optional["Result"]: ... + ) -> Optional["Result"]: + ... From b2797b775d58b5aac7300db287441c1bf87871bf Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 15:53:39 -0600 Subject: [PATCH 5/8] use sequence instead of list --- invoke/_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invoke/_types.py b/invoke/_types.py index 24847bd7e..28150f626 100644 --- a/invoke/_types.py +++ b/invoke/_types.py @@ -1,4 +1,4 @@ -from typing import IO, TYPE_CHECKING, Union, overload, cast, Optional, Dict +from typing import IO, TYPE_CHECKING, Union, Sequence, overload, cast, Optional, Dict from typing_extensions import Protocol, TypedDict, Unpack, Literal @@ -37,7 +37,7 @@ class _BaseRunParams(TypedDict, total=False): shell: str timeout: Optional[int] warn: bool - watchers: list["StreamWatcher"] + watchers: Sequence["StreamWatcher"] class RunParams(_BaseRunParams, total=False): From ba2b81aaffb3543ba9623e6672bf39888aac771c Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 16:02:31 -0600 Subject: [PATCH 6/8] fix long import --- invoke/_types.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/invoke/_types.py b/invoke/_types.py index 28150f626..c5d8bd1f0 100644 --- a/invoke/_types.py +++ b/invoke/_types.py @@ -1,4 +1,13 @@ -from typing import IO, TYPE_CHECKING, Union, Sequence, overload, cast, Optional, Dict +from typing import ( + IO, + TYPE_CHECKING, + Union, + Sequence, + overload, + cast, + Optional, + Dict, +) from typing_extensions import Protocol, TypedDict, Unpack, Literal @@ -13,7 +22,6 @@ def annotate_run_function(func: "_RunFunctionImpl") -> "RunFunction": class _RunFunctionImpl(Protocol): - def __call__( self, command: str, **kwargs: Unpack["RunParams"] ) -> Optional["Result"]: From 1e229c09877755b8daa5119b4692f5b922dd75b7 Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 16:17:01 -0600 Subject: [PATCH 7/8] fix types --- dev-requirements.txt | 2 +- invoke/_types.py | 11 +++-------- invoke/collection.py | 2 +- invoke/context.py | 22 ++++++++-------------- invoke/runners.py | 40 ++++++++++++---------------------------- invoke/util.py | 4 ++-- 6 files changed, 27 insertions(+), 54 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index dc89f6f7e..d49745734 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -19,6 +19,6 @@ setuptools>56 # Debuggery icecream>=2.1 # typing -mypy==0.971 +mypy==1.10.0 types-PyYAML==6.0.12.4 typing-extensions>=4,<5 diff --git a/invoke/_types.py b/invoke/_types.py index c5d8bd1f0..abc9316c1 100644 --- a/invoke/_types.py +++ b/invoke/_types.py @@ -1,6 +1,8 @@ from typing import ( IO, TYPE_CHECKING, + Any, + Callable, Union, Sequence, overload, @@ -16,18 +18,11 @@ from invoke.watchers import StreamWatcher -def annotate_run_function(func: "_RunFunctionImpl") -> "RunFunction": +def annotate_run_function(func: Callable[..., Any]) -> "RunFunction": """Add standard run function annotations to a function.""" return cast(RunFunction, func) -class _RunFunctionImpl(Protocol): - def __call__( - self, command: str, **kwargs: Unpack["RunParams"] - ) -> Optional["Result"]: - ... - - class _BaseRunParams(TypedDict, total=False): dry: bool echo: bool diff --git a/invoke/collection.py b/invoke/collection.py index 23dcff928..eadf32504 100644 --- a/invoke/collection.py +++ b/invoke/collection.py @@ -266,7 +266,7 @@ def add_task( name = task.name # XXX https://github.com/python/mypy/issues/1424 elif hasattr(task.body, "func_name"): - name = task.body.func_name # type: ignore + name = task.body.func_name elif hasattr(task.body, "__name__"): name = task.__name__ else: diff --git a/invoke/context.py b/invoke/context.py index 7d47f3f4d..6c3e5029a 100644 --- a/invoke/context.py +++ b/invoke/context.py @@ -108,9 +108,7 @@ def run(self, command: str, **kwargs: Any) -> Optional[Result]: # NOTE: broken out of run() to allow for runner class injection in # Fabric/etc, which needs to juggle multiple runner class types (local and # remote). - def _run( - self, runner: "Runner", command: str, **kwargs: Any - ) -> Optional[Result]: + def _run(self, runner: "Runner", command: str, **kwargs: Any) -> Optional[Result]: command = self._prefix_commands(command) return runner.run(command, **kwargs) @@ -188,9 +186,7 @@ def sudo(self, command: str, **kwargs: Any) -> Optional[Result]: return self._sudo(runner, command, **kwargs) # NOTE: this is for runner injection; see NOTE above _run(). - def _sudo( - self, runner: "Runner", command: str, **kwargs: Any - ) -> Optional[Result]: + def _sudo(self, runner: "Runner", command: str, **kwargs: Any) -> Optional[Result]: prompt = self.config.sudo.prompt password = kwargs.pop("password", self.config.sudo.password) user = kwargs.pop("user", self.config.sudo.user) @@ -488,9 +484,7 @@ def __init__(self, config: Optional[Config] = None, **kwargs: Any) -> None: if isinstance(results, dict): for key, value in results.items(): results[key] = self._normalize(value) - elif isinstance(results, singletons) or hasattr( - results, "__iter__" - ): + elif isinstance(results, singletons) or hasattr(results, "__iter__"): results = self._normalize(results) # Unknown input value: cry else: @@ -551,23 +545,23 @@ def _yield_result(self, attname: str, command: str) -> Result: # raise_from(NotImplementedError(command), None) raise NotImplementedError(command) - def run(self, command: str, *args: Any, **kwargs: Any) -> Result: + @annotate_run_function + def run(self, command: str, **kwargs: Any) -> Result: # TODO: perform more convenience stuff associating args/kwargs with the # result? E.g. filling in .command, etc? Possibly useful for debugging # if one hits unexpected-order problems with what they passed in to # __init__. return self._yield_result("__run", command) - def sudo(self, command: str, *args: Any, **kwargs: Any) -> Result: + @annotate_run_function + def sudo(self, command: str, **kwargs: Any) -> Result: # TODO: this completely nukes the top-level behavior of sudo(), which # could be good or bad, depending. Most of the time I think it's good. # No need to supply dummy password config, etc. # TODO: see the TODO from run() re: injecting arg/kwarg values return self._yield_result("__sudo", command) - def set_result_for( - self, attname: str, command: str, result: Result - ) -> None: + def set_result_for(self, attname: str, command: str, result: Result) -> None: """ Modify the stored mock results for given ``attname`` (e.g. ``run``). diff --git a/invoke/runners.py b/invoke/runners.py index 1cd1a9182..f1fe05d85 100644 --- a/invoke/runners.py +++ b/invoke/runners.py @@ -409,9 +409,7 @@ def _setup(self, command: str, kwargs: Any) -> None: # Normalize kwargs w/ config; sets self.opts, self.streams self._unify_kwargs_with_config(kwargs) # Environment setup - self.env = self.generate_env( - self.opts["env"], self.opts["replace_env"] - ) + self.env = self.generate_env(self.opts["env"], self.opts["replace_env"]) # Arrive at final encoding if neither config nor kwargs had one self.encoding = self.opts["encoding"] or self.default_encoding() # Echo running command (wants to be early to be included in dry-run) @@ -546,7 +544,9 @@ def _unify_kwargs_with_config(self, kwargs: Any) -> None: self._asynchronous = opts["asynchronous"] self._disowned = opts["disown"] if self._asynchronous and self._disowned: - err = "Cannot give both 'asynchronous' and 'disown' at the same time!" # noqa + err = ( + "Cannot give both 'asynchronous' and 'disown' at the same time!" # noqa + ) raise ValueError(err) # If hide was True, turn off echoing if opts["hide"] is True: @@ -602,9 +602,7 @@ def _collate_result(self, watcher_errors: List[WatcherError]) -> "Result": # TODO: as noted elsewhere, I kinda hate this. Consider changing # generate_result()'s API in next major rev so we can tidy up. result = self.generate_result( - **dict( - self.result_kwargs, stdout=stdout, stderr=stderr, exited=exited - ) + **dict(self.result_kwargs, stdout=stdout, stderr=stderr, exited=exited) ) return result @@ -755,9 +753,7 @@ def _handle_output( # Run our specific buffer through the autoresponder framework self.respond(buffer_) - def handle_stdout( - self, buffer_: List[str], hide: bool, output: IO - ) -> None: + def handle_stdout(self, buffer_: List[str], hide: bool, output: IO) -> None: """ Read process' stdout, storing into a buffer & printing/parsing. @@ -774,13 +770,9 @@ def handle_stdout( .. versionadded:: 1.0 """ - self._handle_output( - buffer_, hide, output, reader=self.read_proc_stdout - ) + self._handle_output(buffer_, hide, output, reader=self.read_proc_stdout) - def handle_stderr( - self, buffer_: List[str], hide: bool, output: IO - ) -> None: + def handle_stderr(self, buffer_: List[str], hide: bool, output: IO) -> None: """ Read process' stderr, storing into a buffer & printing/parsing. @@ -789,9 +781,7 @@ def handle_stderr( .. versionadded:: 1.0 """ - self._handle_output( - buffer_, hide, output, reader=self.read_proc_stderr - ) + self._handle_output(buffer_, hide, output, reader=self.read_proc_stderr) def read_our_stdin(self, input_: IO) -> Optional[str]: """ @@ -940,9 +930,7 @@ def respond(self, buffer_: List[str]) -> None: for response in watcher.submit(stream): self.write_proc_stdin(response) - def generate_env( - self, env: Dict[str, Any], replace_env: bool - ) -> Dict[str, Any]: + def generate_env(self, env: Dict[str, Any], replace_env: bool) -> Dict[str, Any]: """ Return a suitable environment dict based on user input & behavior. @@ -1283,9 +1271,7 @@ def _write_proc_stdin(self, data: bytes) -> None: elif self.process and self.process.stdin: fd = self.process.stdin.fileno() else: - raise SubprocessPipeError( - "Unable to write to missing subprocess or stdin!" - ) + raise SubprocessPipeError("Unable to write to missing subprocess or stdin!") # Try to write, ignoring broken pipes if encountered (implies child # process exited before the process piping stdin to us finished; # there's nothing we can do about that!) @@ -1303,9 +1289,7 @@ def close_proc_stdin(self) -> None: elif self.process and self.process.stdin: self.process.stdin.close() else: - raise SubprocessPipeError( - "Unable to close missing subprocess or stdin!" - ) + raise SubprocessPipeError("Unable to close missing subprocess or stdin!") def start(self, command: str, shell: str, env: Dict[str, Any]) -> None: if self.using_pty: diff --git a/invoke/util.py b/invoke/util.py index df29c841a..7184ea30e 100644 --- a/invoke/util.py +++ b/invoke/util.py @@ -191,7 +191,7 @@ def run(self) -> None: # doesn't appear to be the case, then assume we're being used # directly and just use super() ourselves. # XXX https://github.com/python/mypy/issues/1424 - if hasattr(self, "_run") and callable(self._run): # type: ignore + if hasattr(self, "_run") and callable(self._run): # TODO: this could be: # - io worker with no 'result' (always local) # - tunnel worker, also with no 'result' (also always local) @@ -206,7 +206,7 @@ def run(self) -> None: # and let it continue acting like a normal thread (meh) # - assume the run/sudo/etc case will use a queue inside its # worker body, orthogonal to how exception handling works - self._run() # type: ignore + self._run() else: super().run() except BaseException: From dce7a8cf651775064ff204134623f0d80f3a4adf Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Mon, 3 Jun 2024 16:26:41 -0600 Subject: [PATCH 8/8] conditionally define type annotations --- invoke/_types.py | 177 +++++++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 90 deletions(-) diff --git a/invoke/_types.py b/invoke/_types.py index abc9316c1..3ee457704 100644 --- a/invoke/_types.py +++ b/invoke/_types.py @@ -8,102 +8,99 @@ overload, cast, Optional, - Dict, + Mapping, ) -from typing_extensions import Protocol, TypedDict, Unpack, Literal - if TYPE_CHECKING: from invoke.runners import Promise, Result from invoke.watchers import StreamWatcher + from typing_extensions import Protocol, TypedDict, Unpack, Literal + + class _BaseRunParams(TypedDict, total=False): + dry: bool + echo: bool + echo_format: str + echo_stdin: Optional[bool] + encoding: Optional[str] + err_stream: IO + env: Mapping[str, str] + fallback: bool + hide: Optional[bool] + in_stream: Optional[IO] + out_stream: IO + pty: bool + replace_env: bool + shell: str + timeout: Optional[int] + warn: bool + watchers: Sequence["StreamWatcher"] + + class RunParams(_BaseRunParams, total=False): + """Parameters for Runner.run""" + + asynchronous: bool + disown: bool + + class RunFunction(Protocol): + """A function that runs a command.""" + + @overload + def __call__( + self, + command: str, + *, + disown: Literal[True], + **kwargs: Unpack[_BaseRunParams], + ) -> None: + ... + + @overload + def __call__( + self, + command: str, + *, + disown: bool, + **kwargs: Unpack[_BaseRunParams], + ) -> Optional["Result"]: + ... + + @overload + def __call__( + self, + command: str, + *, + asynchronous: Literal[True], + **kwargs: Unpack[_BaseRunParams], + ) -> "Promise": + ... + + @overload + def __call__( + self, + command: str, + *, + asynchronous: bool, + **kwargs: Unpack[_BaseRunParams], + ) -> Union["Promise", "Result"]: + ... + + @overload + def __call__( + self, + command: str, + **kwargs: Unpack[_BaseRunParams], + ) -> "Result": + ... + + def __call__( + self, + command: str, + **kwargs: Unpack[RunParams], + ) -> Optional["Result"]: + ... + def annotate_run_function(func: Callable[..., Any]) -> "RunFunction": """Add standard run function annotations to a function.""" - return cast(RunFunction, func) - - -class _BaseRunParams(TypedDict, total=False): - dry: bool - echo: bool - echo_format: str - echo_stdin: Optional[bool] - encoding: Optional[str] - err_stream: IO - env: Dict[str, str] - fallback: bool - hide: Optional[bool] - in_stream: Optional[IO] - out_stream: IO - pty: bool - replace_env: bool - shell: str - timeout: Optional[int] - warn: bool - watchers: Sequence["StreamWatcher"] - - -class RunParams(_BaseRunParams, total=False): - """Parameters for Runner.run""" - - asynchronous: bool - disown: bool - - -class RunFunction(Protocol): - """A function that runs a command.""" - - @overload - def __call__( - self, - command: str, - *, - disown: Literal[True], - **kwargs: Unpack[_BaseRunParams], - ) -> None: - ... - - @overload - def __call__( - self, - command: str, - *, - disown: bool, - **kwargs: Unpack[_BaseRunParams], - ) -> Optional["Result"]: - ... - - @overload - def __call__( - self, - command: str, - *, - asynchronous: Literal[True], - **kwargs: Unpack[_BaseRunParams], - ) -> "Promise": - ... - - @overload - def __call__( - self, - command: str, - *, - asynchronous: bool, - **kwargs: Unpack[_BaseRunParams], - ) -> Union["Promise", "Result"]: - ... - - @overload - def __call__( - self, - command: str, - **kwargs: Unpack[_BaseRunParams], - ) -> "Result": - ... - - def __call__( - self, - command: str, - **kwargs: Unpack[RunParams], - ) -> Optional["Result"]: - ... + return cast("RunFunction", func)