summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--README.rst4
-rw-r--r--codecov.yml1
-rw-r--r--paramiko/client.py1
-rw-r--r--paramiko/common.py22
-rw-r--r--paramiko/file.py6
-rw-r--r--paramiko/hostkeys.py7
-rw-r--r--paramiko/kex_gss.py2
-rw-r--r--paramiko/primes.py1
-rw-r--r--paramiko/py3compat.py11
-rw-r--r--paramiko/resource.py71
-rw-r--r--paramiko/sftp_client.py11
-rw-r--r--paramiko/transport.py18
-rw-r--r--paramiko/win_pageant.py2
-rw-r--r--sites/www/changelog.rst27
-rw-r--r--tests/__init__.py36
-rw-r--r--tests/test_client.py15
-rwxr-xr-xtests/test_file.py57
-rwxr-xr-xtests/test_sftp.py30
-rw-r--r--tests/test_transport.py69
20 files changed, 243 insertions, 152 deletions
diff --git a/.travis.yml b/.travis.yml
index c8faf0a2..7ca8db09 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,7 +15,7 @@ install:
# Self-install for setup.py-driven deps
- pip install -e .
# Dev (doc/test running) requirements
- - pip install coveralls # For coveralls.io specifically
+ - pip install codecov # For codecov specifically
- pip install -r dev-requirements.txt
script:
# Main tests, w/ coverage!
@@ -33,4 +33,4 @@ notifications:
on_failure: change
email: false
after_success:
- - coveralls
+ - codecov
diff --git a/README.rst b/README.rst
index e267f69a..399dceb9 100644
--- a/README.rst
+++ b/README.rst
@@ -6,8 +6,8 @@ Paramiko
.. image:: https://travis-ci.org/paramiko/paramiko.svg?branch=master
:target: https://travis-ci.org/paramiko/paramiko
-.. image:: https://coveralls.io/repos/paramiko/paramiko/badge.svg?branch=master&service=github
- :target: https://coveralls.io/github/paramiko/paramiko?branch=master
+.. image:: https://codecov.io/gh/paramiko/paramiko/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/paramiko/paramiko
:Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..69cb7601
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1 @@
+comment: false
diff --git a/paramiko/client.py b/paramiko/client.py
index 55365705..32878c72 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -34,7 +34,6 @@ from paramiko.dsskey import DSSKey
from paramiko.ecdsakey import ECDSAKey
from paramiko.hostkeys import HostKeys
from paramiko.py3compat import string_types
-from paramiko.resource import ResourceManager
from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import (
SSHException, BadHostKeyException, NoValidConnectionsError
diff --git a/paramiko/common.py b/paramiko/common.py
index 556f046a..0012372a 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -20,9 +20,7 @@
Common constants and global variables.
"""
import logging
-from paramiko.py3compat import (
- byte_chr, PY2, bytes_types, string_types, b, long,
-)
+from paramiko.py3compat import byte_chr, PY2, bytes_types, text_type, long
MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, \
MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT = range(1, 7)
@@ -163,14 +161,16 @@ else:
def asbytes(s):
- if not isinstance(s, bytes_types):
- if isinstance(s, string_types):
- s = b(s)
- else:
- try:
- s = s.asbytes()
- except Exception:
- raise Exception('Unknown type')
+ """Coerce to bytes if possible or return unchanged."""
+ if isinstance(s, bytes_types):
+ return s
+ if isinstance(s, text_type):
+ # Accept text and encode as utf-8 for compatibility only.
+ return s.encode("utf-8")
+ asbytes = getattr(s, "asbytes", None)
+ if asbytes is not None:
+ return asbytes()
+ # May be an object that implements the buffer api, let callers handle.
return s
diff --git a/paramiko/file.py b/paramiko/file.py
index 5212091a..a1bdafbe 100644
--- a/paramiko/file.py
+++ b/paramiko/file.py
@@ -18,7 +18,7 @@
from paramiko.common import (
linefeed_byte_value, crlf, cr_byte, linefeed_byte, cr_byte_value,
)
-from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types
+from paramiko.py3compat import BytesIO, PY2, u, bytes_types, text_type
from paramiko.util import ClosingContextManager
@@ -391,7 +391,9 @@ class BufferedFile (ClosingContextManager):
:param data: ``str``/``bytes`` data to write
"""
- data = b(data)
+ if isinstance(data, text_type):
+ # Accept text and encode as utf-8 for compatibility only.
+ data = data.encode('utf-8')
if self._closed:
raise IOError('File is closed')
if not (self._flags & self.FLAG_WRITE):
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 008ba592..c873f58b 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -20,17 +20,12 @@
import binascii
import os
+from collections import MutableMapping
from hashlib import sha1
from hmac import HMAC
from paramiko.py3compat import b, u, encodebytes, decodebytes
-try:
- from collections import MutableMapping
-except ImportError:
- # noinspection PyUnresolvedReferences
- from UserDict import DictMixin as MutableMapping
-
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.util import get_logger, constant_time_bytes_eq
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index ba24c0a0..3406babb 100644
--- a/paramiko/kex_gss.py
+++ b/paramiko/kex_gss.py
@@ -40,7 +40,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`.
import os
from hashlib import sha1
-from paramiko.common import * # noqa
+from paramiko.common import DEBUG, max_byte, zero_byte
from paramiko import util
from paramiko.message import Message
from paramiko.py3compat import byte_chr, byte_mask, byte_ord
diff --git a/paramiko/primes.py b/paramiko/primes.py
index 48a34e53..65617914 100644
--- a/paramiko/primes.py
+++ b/paramiko/primes.py
@@ -25,7 +25,6 @@ import os
from paramiko import util
from paramiko.py3compat import byte_mask, long
from paramiko.ssh_exception import SSHException
-from paramiko.common import * # noqa
def _roll_random(n):
diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py
index 095b0d09..6703ace8 100644
--- a/paramiko/py3compat.py
+++ b/paramiko/py3compat.py
@@ -65,15 +65,8 @@ if PY2:
return s
- try:
- import cStringIO
-
- StringIO = cStringIO.StringIO # NOQA
- except ImportError:
- import StringIO
-
- StringIO = StringIO.StringIO # NOQA
-
+ import cStringIO
+ StringIO = cStringIO.StringIO
BytesIO = StringIO
diff --git a/paramiko/resource.py b/paramiko/resource.py
deleted file mode 100644
index 5fed22ad..00000000
--- a/paramiko/resource.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
-#
-# This file is part of paramiko.
-#
-# Paramiko is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation; either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
-
-"""
-Resource manager.
-"""
-
-import weakref
-
-
-class ResourceManager (object):
- """
- A registry of objects and resources that should be closed when those
- objects are deleted.
-
- This is meant to be a safer alternative to Python's ``__del__`` method,
- which can cause reference cycles to never be collected. Objects registered
- with the ResourceManager can be collected but still free resources when
- they die.
-
- Resources are registered using `register`, and when an object is garbage
- collected, each registered resource is closed by having its ``close()``
- method called. Multiple resources may be registered per object, but a
- resource will only be closed once, even if multiple objects register it.
- (The last object to register it wins.)
- """
-
- def __init__(self):
- self._table = {}
-
- def register(self, obj, resource):
- """
- Register a resource to be closed with an object is collected.
-
- When the given ``obj`` is garbage-collected by the Python interpreter,
- the ``resource`` will be closed by having its ``close()`` method
- called. Any exceptions are ignored.
-
- :param object obj: the object to track
- :param object resource:
- the resource to close when the object is collected
- """
- def callback(ref):
- try:
- resource.close()
- except:
- pass
- del self._table[id(resource)]
-
- # keep the weakref in a table so it sticks around long enough to get
- # its callback called. :)
- self._table[id(resource)] = weakref.ref(obj, callback)
-
-
-# singleton
-ResourceManager = ResourceManager()
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 12fccb2f..ee5ab073 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -28,9 +28,7 @@ from paramiko import util
from paramiko.channel import Channel
from paramiko.message import Message
from paramiko.common import INFO, DEBUG, o777
-from paramiko.py3compat import (
- bytestring, b, u, long, string_types, bytes_types,
-)
+from paramiko.py3compat import bytestring, b, u, long
from paramiko.sftp import (
BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READDIR, CMD_NAME,
CMD_CLOSE, SFTP_FLAG_READ, SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
@@ -758,13 +756,12 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
msg.add_int64(item)
elif isinstance(item, int):
msg.add_int(item)
- elif isinstance(item, (string_types, bytes_types)):
- msg.add_string(item)
elif isinstance(item, SFTPAttributes):
item._pack(msg)
else:
- raise Exception(
- 'unknown type for %r type %r' % (item, type(item)))
+ # For all other types, rely on as_string() to either coerce
+ # to bytes before writing or raise a suitable exception.
+ msg.add_string(item)
num = self.request_number
self._expecting[num] = fileobj
self.request_number += 1
diff --git a/paramiko/transport.py b/paramiko/transport.py
index f0952e1b..d26af833 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -107,9 +107,9 @@ class Transport(threading.Thread, ClosingContextManager):
'aes192-ctr',
'aes256-ctr',
'aes128-cbc',
- 'blowfish-cbc',
'aes192-cbc',
'aes256-cbc',
+ 'blowfish-cbc',
'3des-cbc',
)
_preferred_macs = (
@@ -277,7 +277,6 @@ class Transport(threading.Thread, ClosingContextManager):
arguments.
"""
self.active = False
- self._sshclient = None
if isinstance(sock, string_types):
# convert "host:port" into (host, port)
@@ -311,14 +310,9 @@ class Transport(threading.Thread, ClosingContextManager):
threading.Thread.__init__(self)
self.setDaemon(True)
self.sock = sock
- # Python < 2.3 doesn't have the settimeout method - RogerB
- try:
- # we set the timeout so we can check self.active periodically to
- # see if we should bail. socket.timeout exception is never
- # propagated.
- self.sock.settimeout(self._active_check_timeout)
- except AttributeError:
- pass
+ # we set the timeout so we can check self.active periodically to
+ # see if we should bail. socket.timeout exception is never propagated.
+ self.sock.settimeout(self._active_check_timeout)
# negotiated crypto parameters
self.packetizer = Packetizer(sock)
@@ -651,9 +645,6 @@ class Transport(threading.Thread, ClosingContextManager):
Transport._modulus_pack = None
return False
- def set_sshclient(self, sshclient):
- self._sshclient = sshclient
-
def close(self):
"""
Close this session, and any open channels that are tied to it.
@@ -664,7 +655,6 @@ class Transport(threading.Thread, ClosingContextManager):
for chan in list(self._channels.values()):
chan._unlink()
self.sock.close()
- self._sshclient = None
def get_remote_server_key(self):
"""
diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py
index c8c2c7bc..fda3b9c1 100644
--- a/paramiko/win_pageant.py
+++ b/paramiko/win_pageant.py
@@ -25,7 +25,7 @@ import array
import ctypes.wintypes
import platform
import struct
-from paramiko.util import * # noqa
+from paramiko.common import zero_byte
from paramiko.py3compat import b
try:
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 6ab67985..3b2a7500 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -3,9 +3,19 @@ Changelog
=========
* :bug:`865` SSHClient requests the type of host key it has (e.g. from
- known_hosts) and does not consider a different type to be a "Missing"
- host key. This fixes a common case where an ecdsa key is in known_hosts and
- the server also has an rsa host key. Thanks to Pierce Lopez.
+ known_hosts) and does not consider a different type to be a "Missing" host
+ key. This fixes a common case where an ECDSA key is in known_hosts and the
+ server also has an RSA host key. Thanks to Pierce Lopez.
+* :support:`906 (1.18+)` Clean up a handful of outdated imports and related
+ tweaks. Thanks to Pierce Lopez.
+* :bug:`984` Enhance default cipher preference order such that
+ ``aes(192|256)-cbc`` are preferred over ``blowfish-cbc``. Thanks to Alex
+ Gaynor.
+* :bug:`971 (1.17+)` Allow any type implementing the buffer API to be used with
+ `BufferedFile <paramiko.file.BufferedFile>`, `Channel
+ <paramiko.channel.Channel>`, and `SFTPFile <paramiko.sftp_file.SFTPFile>`.
+ This resolves a regression introduced in 1.13 with the Python 3 porting
+ changes, when using types such as ``memoryview``. Credit: Martin Packman.
* :bug:`741` (also :issue:`809`, :issue:`772`; all via :issue:`912`) Writing
encrypted/password-protected private key files was silently broken since 2.0
due to an incorrect API call; this has been fixed.
@@ -16,6 +26,9 @@ Changelog
Thanks to ``@virlos`` for the original report, Chris Harris and ``@ibuler``
for initial draft PRs, and ``@jhgorrell`` for the final patch.
+* :support:`956 (1.17+)` Switch code coverage service from coveralls.io to
+ codecov.io (& then disable the latter's auto-comments.) Thanks to Nikolai
+ Røed Kristiansen for the patch.
* :bug:`983` Move ``sha1`` above the now-arguably-broken ``md5`` in the list of
preferred MAC algorithms, as an incremental security improvement for users
whose target systems offer both. Credit: Pierce Lopez.
@@ -40,8 +53,12 @@ Changelog
(i.e. passes the maintainer's preferred `flake8 <http://flake8.pycqa.org/>`_
configuration) and add a ``flake8`` step to the Travis config. Big thanks to
Dorian Pula!
-* :bug:`683` Make ``util.log_to_file`` append instead of replace. Thanks
- to ``@vlcinsky`` for the report.
+* :bug:`949 (1.17+)` SSHClient and Transport could cause a memory leak if
+ there's a connection problem or protocol error, even if ``Transport.close()``
+ is called. Thanks Kyle Agronick for the discovery and investigation, and
+ Pierce Lopez for assistance.
+* :bug:`683 (1.17+)` Make ``util.log_to_file`` append instead of replace.
+ Thanks to ``@vlcinsky`` for the report.
* :release:`2.1.2 <2017-02-20>`
* :release:`2.0.5 <2017-02-20>`
* :release:`1.18.2 <2017-02-20>`
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29b..8878f14d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2017 Martin Packman <gzlist@googlemail.com>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""Base classes and helpers for testing paramiko."""
+
+import unittest
+
+from paramiko.py3compat import (
+ builtins,
+ )
+
+
+def skipUnlessBuiltin(name):
+ """Skip decorated test if builtin name does not exist."""
+ if getattr(builtins, name, None) is None:
+ skip = getattr(unittest, "skip", None)
+ if skip is None:
+ # Python 2.6 pseudo-skip
+ return lambda func: None
+ return skip("No builtin " + repr(name))
+ return lambda func: func
diff --git a/tests/test_client.py b/tests/test_client.py
index 3adb9665..2288cf57 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -35,7 +35,7 @@ import time
from tests.util import test_path
import paramiko
-from paramiko.common import PY2
+from paramiko.py3compat import PY2, b
from paramiko.ssh_exception import SSHException
@@ -296,13 +296,10 @@ class SSHClientTest (unittest.TestCase):
verify that when an SSHClient is collected, its transport (and the
transport's packetizer) is closed.
"""
- # Unclear why this is borked on Py3, but it is, and does not seem worth
- # pursuing at the moment. Skipped on PyPy because it fails on travis
- # for unknown reasons, works fine locally.
- # XXX: It's the release of the references to e.g packetizer that fails
- # in py3...
- if not PY2 or platform.python_implementation() == "PyPy":
+ # Skipped on PyPy because it fails on travis for unknown reasons
+ if platform.python_implementation() == "PyPy":
return
+
threading.Thread(target=self._run).start()
self.tc = paramiko.SSHClient()
@@ -320,8 +317,8 @@ class SSHClientTest (unittest.TestCase):
del self.tc
# force a collection to see whether the SSHClient object is deallocated
- # correctly. 2 GCs are needed to make sure it's really collected on
- # PyPy
+ # 2 GCs are needed on PyPy, time is needed for Python 3
+ time.sleep(0.3)
gc.collect()
gc.collect()
diff --git a/tests/test_file.py b/tests/test_file.py
index 7fab6985..b33ecd51 100755
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -21,10 +21,14 @@ Some unit tests for the BufferedFile abstraction.
"""
import unittest
-from paramiko.file import BufferedFile
-from paramiko.common import linefeed_byte, crlf, cr_byte
import sys
+from paramiko.common import linefeed_byte, crlf, cr_byte
+from paramiko.file import BufferedFile
+from paramiko.py3compat import BytesIO
+
+from tests import skipUnlessBuiltin
+
class LoopbackFile (BufferedFile):
"""
@@ -33,19 +37,16 @@ class LoopbackFile (BufferedFile):
def __init__(self, mode='r', bufsize=-1):
BufferedFile.__init__(self)
self._set_mode(mode, bufsize)
- self.buffer = bytes()
+ self.buffer = BytesIO()
+ self.offset = 0
def _read(self, size):
- if len(self.buffer) == 0:
- return None
- if size > len(self.buffer):
- size = len(self.buffer)
- data = self.buffer[:size]
- self.buffer = self.buffer[size:]
+ data = self.buffer.getvalue()[self.offset:self.offset+size]
+ self.offset += len(data)
return data
def _write(self, data):
- self.buffer += data
+ self.buffer.write(data)
return len(data)
@@ -187,6 +188,42 @@ class BufferedFileTest (unittest.TestCase):
self.assertEqual(data, b'hello')
f.close()
+ def test_write_bad_type(self):
+ with LoopbackFile('wb') as f:
+ self.assertRaises(TypeError, f.write, object())
+
+ def test_write_unicode_as_binary(self):
+ text = u"\xa7 why is writing text to a binary file allowed?\n"
+ with LoopbackFile('rb+') as f:
+ f.write(text)
+ self.assertEqual(f.read(), text.encode("utf-8"))
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_bytearray(self):
+ with LoopbackFile('rb+') as f:
+ f.write(bytearray(12))
+ self.assertEqual(f.read(), 12 * b"\0")
+
+ @skipUnlessBuiltin('buffer')
+ def test_write_buffer(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile('rb+') as f:
+ for offset in offsets:
+ f.write(buffer(data, offset, 8))
+ self.assertEqual(f.read(), data)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_memoryview(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile('rb+') as f:
+ view = memoryview(data)
+ for offset in offsets:
+ f.write(view[offset:offset+8])
+ self.assertEqual(f.read(), data)
+
+
if __name__ == '__main__':
from unittest import main
main()
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index d3064fff..98a9cebb 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -35,6 +35,7 @@ from tempfile import mkstemp
import paramiko
from paramiko.py3compat import PY2, b, u, StringIO
from paramiko.common import o777, o600, o666, o644
+from tests import skipUnlessBuiltin
from tests.stub_sftp import StubServer, StubSFTPServer
from tests.loop import LoopSocket
from tests.util import test_path
@@ -817,6 +818,35 @@ class SFTPTest (unittest.TestCase):
sftp_attributes = SFTPAttributes()
self.assertEqual(str(sftp_attributes), "?--------- 1 0 0 0 (unknown date) ?")
+ @skipUnlessBuiltin('buffer')
+ def test_write_buffer(self):
+ """Test write() using a buffer instance."""
+ data = 3 * b'A potentially large block of data to chunk up.\n'
+ try:
+ with sftp.open('%s/write_buffer' % FOLDER, 'wb') as f:
+ for offset in range(0, len(data), 8):
+ f.write(buffer(data, offset, 8))
+
+ with sftp.open('%s/write_buffer' % FOLDER, 'rb') as f:
+ self.assertEqual(f.read(), data)
+ finally:
+ sftp.remove('%s/write_buffer' % FOLDER)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_memoryview(self):
+ """Test write() using a memoryview instance."""
+ data = 3 * b'A potentially large block of data to chunk up.\n'
+ try:
+ with sftp.open('%s/write_memoryview' % FOLDER, 'wb') as f:
+ view = memoryview(data)
+ for offset in range(0, len(data), 8):
+ f.write(view[offset:offset+8])
+
+ with sftp.open('%s/write_memoryview' % FOLDER, 'rb') as f:
+ self.assertEqual(f.read(), data)
+ finally:
+ sftp.remove('%s/write_memoryview' % FOLDER)
+
if __name__ == '__main__':
SFTPTest.init_loopback()
diff --git a/tests/test_transport.py b/tests/test_transport.py
index c426cef1..3e352919 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -43,6 +43,7 @@ from paramiko.common import (
)
from paramiko.py3compat import bytes
from paramiko.message import Message
+from tests import skipUnlessBuiltin
from tests.loop import LoopSocket
from tests.util import test_path
@@ -858,3 +859,71 @@ class TransportTest(unittest.TestCase):
self.assertEqual([chan], r)
self.assertEqual([], w)
self.assertEqual([], e)
+
+ def test_channel_send_misc(self):
+ """
+ verify behaviours sending various instances to a channel
+ """
+ self.setup_test_server()
+ text = u"\xa7 slice me nicely"
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # TypeError raised on non string or buffer type
+ self.assertRaises(TypeError, chan.send, object())
+ self.assertRaises(TypeError, chan.sendall, object())
+
+ # sendall() accepts a unicode instance
+ chan.sendall(text)
+ expected = text.encode("utf-8")
+ self.assertEqual(sfile.read(len(expected)), expected)
+
+ @skipUnlessBuiltin('buffer')
+ def test_channel_send_buffer(self):
+ """
+ verify sending buffer instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b'some test data\n whole'
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts buffer instances
+ sent = 0
+ while sent < len(data):
+ sent += chan.send(buffer(data, sent, 8))
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a buffer instance
+ chan.sendall(buffer(data))
+ self.assertEqual(sfile.read(len(data)), data)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_channel_send_memoryview(self):
+ """
+ verify sending memoryview instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b'some test data\n whole'
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts memoryview slices
+ sent = 0
+ view = memoryview(data)
+ while sent < len(view):
+ sent += chan.send(view[sent:sent+8])
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a memoryview instance
+ chan.sendall(memoryview(data))
+ self.assertEqual(sfile.read(len(data)), data)