I am trying to catch paramiko exceptions but they are still written to stderr.
Is there a way to stop writing there?
EDIT: It happens even before paramiko gets involved:
import pysftp
try:
pysftp.Connection(host="localhost")
except Exception as e:
print(e)
Results in:
Example with proper SFTP params:
UPDATE:
$ pipenv graph
...
pysftp==0.2.9
- paramiko [required: >=1.17, installed: 2.6.0]
...
$ pipenv run python
Python 3.7.3 (default, Jul 19 2019, 11:21:39)
[Clang 11.0.0 (clang-1100.0.28.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pysftp
>>> try:
... pysftp.Connection(host="localhost")
... except Exception as e:
... print(e)
...
No hostkey for host localhost found.
Exception ignored in:
Traceback (most recent call last):
File "/Users/andrei/Work/try/.venv/lib/python3.7/site-packages/pysftp/__init__.py", line 1013, in __del__
self.close()
File "/Users/andrei/Work/try/.venv/lib/python3.7/site-packages/pysftp/__init__.py", line 784, in close
if self._sftp_live:
AttributeError: 'Connection' object has no attribute '_sftp_live'
>>>
解决方案
I want to start by pointing out that PySFTP ([PyPI]: PySFTP) has not been maintain for 3+ years (or has been moved to a different location - which so far is kept secret :) ).
I reproduced the problem. Below is a more verbose version of your code.
code00.py:
#!/usr/bin/env python3
import sys
import pysftp
import traceback
def main(argv):
hostname = argv[0] if argv else "localhost"
print("Attempting to connect to {0:s} ...".format(hostname))
try:
print("----------Before conn----------")
conn = pysftp.Connection(host=hostname)
print("----------After conn----------")
except:
print("----------Before exc print----------")
traceback.print_exc()
print("----------After exc print----------")
finally:
print("----------Finally----------")
print("----------After try / except / finally----------")
if __name__ == "__main__":
print("Python {0:s} {1:d}bit on {2:s}".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
print("pysftp version: {0:s}\n".format(pysftp.__version__))
main(sys.argv[1:])
print("\nDone.")
Output:
[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q058110732]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32
pysftp version: 0.2.9
Attempting to connect to localhost ...
----------Before conn----------
e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py:61: UserWarning: Failed to load HostKeys from C:\Users\cfati\.ssh\known_hosts. You will need to explicitly load HostKeys (cnopts.hostkeys.load(filename)) or disableHostKey checking (cnopts.hostkeys = None).
warnings.warn(wmsg, UserWarning)
----------Before exc print----------
Traceback (most recent call last):
File "code00.py", line 13, in main
conn = pysftp.Connection(host=hostname)
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 132, in __init__
self._tconnect['hostkey'] = self._cnopts.get_hostkey(host)
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 71, in get_hostkey
raise SSHException("No hostkey for host %s found." % host)
paramiko.ssh_exception.SSHException: No hostkey for host localhost found.
----------After exc print----------
Exception ignored in:
Traceback (most recent call last):
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 1013, in __del__
self.close()
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 784, in close
if self._sftp_live:
AttributeError: 'Connection' object has no attribute '_sftp_live'
----------Finally----------
----------After try / except / finally----------
Done.
It's a PySFTP bug:
The Connection object is constructed (__new__)
The initializer (__init__) is called
Somewhere in the initializer an exception occurs
The lines after the exception are not executed
When the object is (automatically) garbage collected (when it goes out of scope, at the end of the except block), in its close method (called by the destructor (__del__)), some attributes are referenced
But since those attributes initialization occurs after the line (raising the exception) from #2.2., they were never initialized, so their reference raises AttributeError
The fix is straightforward: initialize the attributes to some default values, at the initializer's beginning, so their reference doesn't represent a problem if the above scenario happens.
I noticed that you've already submitted an issue on BitBucket.
Considering that:
I created my own repo (out of the above one), and pushed the changes at:
Output (after manually applying the fix to the file installed by pip):
[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q058110732]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32
pysftp version: 0.2.9
Attempting to connect to localhost ...
----------Before conn----------
e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py:61: UserWarning: Failed to load HostKeys from C:\Users\cfati\.ssh\known_hosts. You will need to explicitly load HostKeys (cnopts.hostkeys.load(filename)) or disableHostKey checking (cnopts.hostkeys = None).
warnings.warn(wmsg, UserWarning)
----------Before exc print----------
Traceback (most recent call last):
File "code00.py", line 13, in main
conn = pysftp.Connection(host=hostname)
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 135, in __init__
self._tconnect['hostkey'] = self._cnopts.get_hostkey(host)
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 71, in get_hostkey
raise SSHException("No hostkey for host %s found." % host)
paramiko.ssh_exception.SSHException: No hostkey for host localhost found.
----------After exc print----------
----------Finally----------
----------After try / except / finally----------
Done.
Needless to say, other unhandled exceptions might be raised for different scenarios.
@EDIT0
Apparently, ther's more to this than meets the eye. Besides the PySFTP bug described above, there are 2 more things that pollute stderr.
1. The warning
In my case (as I am on Win and don't have any native SSH tool installed) it pops up every time (unless I create / copy some valid known_hosts file in my home dir), but on Nix systems it most likely won't.
Anyway, the fix for this one (if wanted) is easy, simply suppress the UserWarning for example by setting the %PYTHONWARNINGS% env var to ignore::UserWarning (can also be achieved from code - like in next item's case).
2. Paramiko exceptions
I was able to reproduce this by manually modifying transport.py (and raising socket.timeout).
paramiko.Transport which is initialized by pysftp.Connection._start_transport (called by the initializer) does its work in a thread (by subclassing threading.Thread). Any exception raised in that thread, can't be caught by the calling thread (ours). This is a Python limitation scheduled to be addressed in v3.8 ([Python.Bugs]: threading.Thread should have way to catch an exception thrown within).
For this one, there is a (lame) workaround (gainarie): redirecting stderr. Of course there are other workarounds, but they would imply modifying Paramiko, so I'd advice against them.
Below is an example that redirects stderr to stdout (but you can choose any other file - including /dev/null (or nul on Win)). It does it from code (but it can also be done from interpreter command line) so that it only affects the desired (hot) area(s).
code01.py:
#!/usr/bin/env python3
import sys
import pysftp
import paramiko
import traceback
import threading
_sys_stderr = sys.stderr # For restoring purposes
def main(argv):
hostname = argv[0] if argv else "localhost"
print("Attempting to connect to {0:s} ...".format(hostname))
try:
cnopts = pysftp.CnOpts()
cnopts.hostkeys = None
print("---------- STATS: {0:s} {1:d} ----------".format(__file__, threading.get_ident()))
print("---------- Before conn ----------")
sys.stderr.write("DUMMY TEXT before sent to stderr\n")
sys.stderr = sys.stdout # @TODO - cfati: decomment so that everything from stderr is redirected to stdout
conn = pysftp.Connection(host=hostname, port=22001, username="usr", password="pwd", cnopts=cnopts,)
print("---------- After conn ----------")
except:
sys.stderr = _sys_stderr
print("---------- Before exc tb ----------")
traceback.print_exc(file=sys.stdout)
print("---------- After exc tb ----------")
finally:
sys.stderr = _sys_stderr
print("---------- Finally ----------")
sys.stderr.write("DUMMY TEXT after sent to stderr\n")
print("---------- After try / except / finally ----------")
if __name__ == "__main__":
print("Python {0:s} {1:d}bit on {2:s}".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
print("pysftp version: {0:s}\nparamiko version: {1:s}".format(pysftp.__version__, paramiko.__version__))
main(sys.argv[1:])
print("\nDone.")
Output:
[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q058110732]> sopr.bat
*** Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ***
[prompt]> dir /b
code00.py
code01.py
[prompt]> :: Suppress warning
[prompt]> set PYTHONWARNINGS=ignore::UserWarning
[prompt]> :: Redirect stdout and stderr to different files, so it is obvious which is which
[prompt]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code01.py 1>1.out 2>1.err
[prompt]> type 1.err
DUMMY TEXT before sent to stderr
DUMMY TEXT after sent to stderr
[prompt]> type 1.out
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32
pysftp version: 0.2.9
paramiko version: 2.6.0
Attempting to connect to localhost ...
---------- STATS: code01.py 23016 ----------
---------- Before conn ----------
---------- STATS: e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py 45616 ----------
Exception: Error reading SSH protocol banner
Traceback (most recent call last):
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2212, in _check_banner
raise socket.timeout()
socket.timeout
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2039, in run
self._check_banner()
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2218, in _check_banner
"Error reading SSH protocol banner" + str(e)
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner
---------- Before exc tb ----------
Traceback (most recent call last):
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2212, in _check_banner
raise socket.timeout()
socket.timeout
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "code01.py", line 23, in main
conn = pysftp.Connection(host=hostname, port=22001, username="usr", password="pwd", cnopts=cnopts,)
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\pysftp\__init__.py", line 144, in __init__
self._transport.connect(**self._tconnect)
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 1291, in connect
self.start_client()
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 660, in start_client
raise e
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2039, in run
self._check_banner()
File "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\lib\site-packages\paramiko\transport.py", line 2218, in _check_banner
"Error reading SSH protocol banner" + str(e)
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner
---------- After exc tb ----------
---------- Finally ----------
---------- After try / except / finally ----------
Done.
And how it looks from PyCharm (had to stretch it to the max to fit the whole thing):