-
Notifications
You must be signed in to change notification settings - Fork 153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Return type of create_process is partially unknown #606
Comments
Hello @ronf, it looks like I was incorrect with option 1. As this ends up typing as When
The problem is this does not work when the context manager is used, as it defaults to the first overload. This is likely to be due to microsoft/pyright#2521. @overload
@async_context_manager
async def create_process(self, *args: object,
input: None = None,
stdin: ProcessSource = PIPE,
stdout: ProcessTarget = PIPE,
stderr: ProcessTarget = PIPE,
bufsize: int = io.DEFAULT_BUFFER_SIZE,
send_eof: bool = True, recv_eof: bool = True,
encoding: str,
**kwargs: object) -> SSHClientProcess[str]: ...
@overload
@async_context_manager
async def create_process(self, *args: object,
input: str,
stdin: ProcessSource = PIPE,
stdout: ProcessTarget = PIPE,
stderr: ProcessTarget = PIPE,
bufsize: int = io.DEFAULT_BUFFER_SIZE,
send_eof: bool = True, recv_eof: bool = True,
encoding: str,
**kwargs: object) -> SSHClientProcess[str]: ...
@overload
@async_context_manager
async def create_process(self, *args: object,
input: None = None,
stdin: ProcessSource = PIPE,
stdout: ProcessTarget = PIPE,
stderr: ProcessTarget = PIPE,
bufsize: int = io.DEFAULT_BUFFER_SIZE,
send_eof: bool = True, recv_eof: bool = True,
encoding: None = None,
**kwargs: object) -> SSHClientProcess[bytes]: ...
@overload
@async_context_manager
async def create_process(self, *args: object,
input: bytes,
stdin: ProcessSource = PIPE,
stdout: ProcessTarget = PIPE,
stderr: ProcessTarget = PIPE,
bufsize: int = io.DEFAULT_BUFFER_SIZE,
send_eof: bool = True, recv_eof: bool = True,
encoding: None = None,
**kwargs: object) -> SSHClientProcess[bytes]: ...
# pylint: disable=redefined-builtin
@async_context_manager
async def create_process(self, *args: object,
input: Optional[AnyStr] = None,
stdin: ProcessSource = PIPE,
stdout: ProcessTarget = PIPE,
stderr: ProcessTarget = PIPE,
bufsize: int = io.DEFAULT_BUFFER_SIZE,
send_eof: bool = True, recv_eof: bool = True,
encoding: Optional[str] = None,
**kwargs: object) -> SSHClientProcess[AnyStr]: |
Ah, yeah - I have never been able to get full typing to work through @async_context_manager. You can see python/typing#898 for some earlier attempts at this. I'll take a look again at your version of this change with ParamSpec and see if things have improved since I last attempted this, though. Pyright in general is much stricter than mypy and while I've tried to satisfy it where possible, there are a number of "errors" that pyright reports on AsyncSSH that I haven't been able to resolve without much the code uglier and harder to maintain, and pyright doesn't provide the knobs to disable some of these checks (or at least it didn't the last time I attempted this). So, for now, only mypy is fully supported. |
I see, it looks like I came to the same conclusion as you. I have installed mypy and it looks like it picks up the type as |
Thanks - could you include a code snippet which shows the incomplete type in pyright, so I can try experimenting further with it? |
With my PR, I no longer get an incomplete type with PyLance (which uses pyright). I have been doing something similar to: import asyncssh
async with asyncssh.connect(
"192.168.0.1",
username="user",
password="pass",
# Disable host key checking
known_hosts=None
) as conn:
async with conn.create_process("./proc", encoding="utf-8") as proc:
proc.stdin.write("input-to-proc\n")
await proc.stdin.drain()
out = (await proc.stdout.readline()).strip() |
Can you try the following patch? It adds in the ParamSpec that I mentioned in a link above (pretty much matching your change in misc.py), but it doesn't do the overload on create_process. There are a couple of other fixes in here as well that mypy caught after adding the ParamSpec, suggesting it might be working. --- a/asyncssh/connection.py
+++ b/asyncssh/connection.py
@@ -4796,8 +4796,8 @@ class SSHClientConnection(SSHConnection):
"""
- return await listen_reverse(host, port, tunnel=self,
- **kwargs) # type: ignore
+ return await listen_reverse(host, port,
+ tunnel=self, **kwargs) # type: ignore
@async_context_manager
async def forward_local_port_to_path(
@@ -5038,10 +5038,10 @@ class SSHClientConnection(SSHConnection):
"""
def session_factory(_orig_host: str,
- _orig_port: int) -> Awaitable[SSHUNIXSession]:
+ _orig_port: int) -> Awaitable[SSHTCPSession]:
"""Return an SSHTCPSession used to do remote port forwarding"""
- return cast(Awaitable[SSHUNIXSession],
+ return cast(Awaitable[SSHTCPSession],
self.forward_unix_connection(dest_path))
self.logger.info('Creating remote TCP forwarder from %s to %s',
@@ -5079,10 +5079,10 @@ class SSHClientConnection(SSHConnection):
"""
- def session_factory() -> Awaitable[SSHTCPSession[bytes]]:
+ def session_factory() -> Awaitable[SSHUNIXSession[bytes]]:
"""Return an SSHUNIXSession used to do remote path forwarding"""
- return cast(Awaitable[SSHTCPSession[bytes]],
+ return cast(Awaitable[SSHUNIXSession[bytes]],
self.forward_connection(dest_host, dest_port))
self.logger.info('Creating remote UNIX forwarder from %s to %s',
diff --git a/asyncssh/misc.py b/asyncssh/misc.py
index 22c75d9..f01b9a4 100644
--- a/asyncssh/misc.py
+++ b/asyncssh/misc.py
@@ -29,8 +29,8 @@ from pathlib import Path, PurePath
from random import SystemRandom
from types import TracebackType
from typing import Any, AsyncContextManager, Awaitable, Callable, Dict
-from typing import Generator, Generic, IO, Mapping, Optional, Sequence
-from typing import Tuple, Type, TypeVar, Union, cast, overload
+from typing import Generator, Generic, IO, Mapping, Optional, ParamSpec
+from typing import Sequence, Tuple, Type, TypeVar, Union, cast, overload
from typing_extensions import Literal, Protocol
from .constants import DEFAULT_LANG
@@ -288,10 +288,12 @@ class _ACMWrapper(Generic[_ACM]):
return exit_result
-_ACMCoro = Callable[..., Awaitable[_ACM]]
-_ACMWrapperFunc = Callable[..., _ACMWrapper[_ACM]]
+_ACMParams = ParamSpec('_ACMParams')
+_ACMCoro = Callable[_ACMParams, Awaitable[_ACM]]
+_ACMWrapperFunc = Callable[_ACMParams, _ACMWrapper[_ACM]]
-def async_context_manager(coro: _ACMCoro[_ACM]) -> _ACMWrapperFunc[_ACM]:
+def async_context_manager(coro: _ACMCoro[_ACMParams, _ACM]) -> \
+ _ACMWrapperFunc[_ACMParams, _ACM]:
"""Decorator for functions returning asynchronous context managers
This decorator can be used on functions which return objects
@@ -305,7 +307,8 @@ def async_context_manager(coro: _ACMCoro[_ACM]) -> _ACMWrapperFunc[_ACM]:
"""
@functools.wraps(coro)
- def context_wrapper(*args, **kwargs) -> _ACMWrapper[_ACM]:
+ def context_wrapper(*args: _ACMParams.args,
+ **kwargs: _ACMParams.kwargs) -> _ACMWrapper[_ACM]:
"""Return an async context manager wrapper for this coroutine"""
return _ACMWrapper(coro(*args, **kwargs)) |
Ok - I see now why you were adding the overloads. The above works when "input"is specified, but otherwise the type is not bound, leaving it returning SSHClientProcess[Unknown]. Adding overloads can allow "encoding" to also influence the type, at least when the call passes a string literal or the value I'm also running into a separate issue where I get back the following error when the type ends up as Unknown:
This is on pyright 1.1.335 and Python 3.11.6. It doesn't seem to recognize that a literal string should be allowed when the type is AnyStr. I can work around this by creating a new |
As I dig into this, I've run into a major problem. AsyncSSH allows the encoding to be selected at the connection level or via config files, and so the default value of this argument is actually the empty tuple, which is a placeholder indicating that the encoding should be pulled from "_options", or set to the default of "utf-8" if not specified there. In this case, though, there's no way to infer whether the SSHClientProcess should be set to use Assuming the overloads could be make to work, this change seems like it has limited value, pretty much only helping with |
Thanks for taking look @ronf, it has been greatly appreciated. I did wonder what the default value of tuple was intended to achieve, so that makes sense. Also understandable that the lack of ParamSpec in older Python versions would prevent using it for now. I believe that typing-extensions has a ParamSpec, but I have not tried if it will work effectively with the type checkers. I’m happy to leave this here. It sounds like with the ability to set encoding via options there will not be a way to type this. I haven’t seen the error you mentioned above. Can you share your test code? Potentially one solution could be to type the return with AnyStr. (Option 2 of the original post) but this also feels like a poor option. |
I fiddled a bit with the version of ParamSpec in typing_extensions, and it looks like that did get past that hurdle. It still works to import ParamSpec from there even on Python 3.10 and later (at least for now), so it doesn't even require a conditional. That said, I went back and tried to run mypy on the overloads and I'm getting errors on those. I'll go back and try your version exactly and see if that's still the case, but I'll need to modify it to also handle
Here's the test code I've been using: import asyncio, asyncssh
async def run():
async with asyncssh.connect('localhost') as conn:
async with conn.create_process("cat") as proc:
#reveal_type(proc)
proc.stdin.write("foo\n")
print(await proc.stdout.readline(), end='')
asyncio.run(run()) Regarding option 2, I've run into trouble in the past with using |
Just confirmed that I get mypy errors even with your version of the overloads:
The other error related to calling |
Changing from AnyStr to BytesOrStr didn't help here, and in fact it returned a failure of:
I could switch back to just For now, I think I've going to abandon this, but I saved a copy of my |
Maybe you are searching for $ git status
HEAD detached at v2.14.0
nothing to commit, working tree clean
$ mypy asyncssh|wc -l
3
$ mypy --strict asyncssh|wc -l
320 |
I'm definitely not running with "--strict" here. In fact, to keep the error count manageable, I set "--allow-redefinition". There are a number of places where AsyncSSH allows intentionally flexible typing, but then narrows the type as a function proceeds, and mypy doesn't like redefinitions of that sort. |
Closing due to inactivity. Feel free to reopen this or open a new issue if you need anything else. |
When type checking a program using
create_process
a warning will be shown that its return type is partially unknown.This is because
create_process
(https://github.com/ronf/asyncssh/blob/develop/asyncssh/connection.py#L4057) has been annotated with the return type ofSSHClientProcess
. This error arises asSSHClientProcess
is generic andcreate_process
does not specify the string type.There are likely three options to fix this:
input
parameter.input
.Version information, but this is also currently an issue in develop.
Using VSCode and PyLance for type checking.
The text was updated successfully, but these errors were encountered: