summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2014-09-05 13:23:02 -0700
committerJeff Forcier <jeff@bitprophet.org>2014-09-05 13:23:02 -0700
commit0e784d81131a369da88051e4c12dd6e4b5e3c8b1 (patch)
tree4bec5f731196ced01797d11548dde6ca84f22c9e
parentf258d1e207a21d4342dbc431fcc4a4dafe893e80 (diff)
parent720806734a05d9de0f47588d816de05646a06f0b (diff)
Merge branch 'master' into 184-int
Conflicts: paramiko/config.py tests/test_util.py
-rw-r--r--.travis.yml7
-rw-r--r--README2
-rw-r--r--dev-requirements.txt6
-rw-r--r--paramiko/__init__.py3
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/channel.py14
-rw-r--r--paramiko/config.py20
-rw-r--r--paramiko/file.py23
-rw-r--r--paramiko/hostkeys.py2
-rw-r--r--paramiko/py3compat.py4
-rw-r--r--paramiko/sftp_client.py79
-rw-r--r--paramiko/transport.py3
-rw-r--r--setup.py10
-rw-r--r--sites/shared_conf.py2
-rw-r--r--sites/www/changelog.rst53
-rw-r--r--sites/www/conf.py3
-rw-r--r--sites/www/contact.rst1
-rw-r--r--sites/www/contributing.rst16
-rw-r--r--sites/www/faq.rst17
-rw-r--r--sites/www/installing.rst8
-rw-r--r--tasks.py4
-rwxr-xr-xtest.py5
-rwxr-xr-xtests/test_file.py55
-rwxr-xr-xtests/test_sftp.py63
-rw-r--r--tests/test_sftp_big.py4
-rw-r--r--tests/test_util.py6
-rw-r--r--tox.ini2
27 files changed, 324 insertions, 90 deletions
diff --git a/.travis.yml b/.travis.yml
index 7042570f..3f6f7331 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,7 @@ python:
- "2.7"
- "3.2"
- "3.3"
+ - "3.4"
install:
# Self-install for setup.py-driven deps
- pip install -e .
@@ -17,8 +18,9 @@ script:
# Run 'docs' first since its objects.inv is referred to by 'www'.
# Also force warnings to be errors since most of them tend to be actual
# problems.
- - invoke docs -o -W
- - invoke www -o -W
+ # Finally, skip them under Python 3.2 due to sphinx shenanigans
+ - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke docs -o -W || true"
+ - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke www -o -W || true"
notifications:
irc:
channels: "irc.freenode.org#paramiko"
@@ -26,7 +28,6 @@ notifications:
- "%{repository}@%{branch}: %{message} (%{build_url})"
on_success: change
on_failure: change
- use_notice: true
email: false
after_success:
- coveralls
diff --git a/README b/README
index 9305bfb3..61c5c852 100644
--- a/README
+++ b/README
@@ -35,7 +35,7 @@ Requirements
------------
- Python 2.6 or better <http://www.python.org/> - this includes Python
- 3.3 and higher as well.
+ 3.2 and higher as well.
- pycrypto 2.1 or better <https://www.dlitz.net/software/pycrypto/>
- ecdsa 0.9 or better <https://pypi.python.org/pypi/ecdsa>
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 91ae8549..7a0ccbc5 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,9 +1,9 @@
# Older junk
tox>=1.4,<1.5
# For newer tasks like building Sphinx docs.
-# NOTE: Requires Python >=2.6
-invoke>=0.7.0
+invoke>=0.7.0,<0.8
invocations>=0.5.0
sphinx>=1.1.3
-alabaster>=0.4.0
+alabaster>=0.6.1
releases>=0.5.2
+wheel==0.23.0
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index b1d9aaa9..65f6f8a2 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -17,14 +17,13 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
import sys
+from paramiko._version import __version__, __version_info__
if sys.version_info < (2, 6):
raise RuntimeError('You need Python 2.6+ for this module.')
__author__ = "Jeff Forcier <jeff@bitprophet.org>"
-__version__ = "1.13.0"
-__version_info__ = tuple([ int(d) for d in __version__.split(".") ])
__license__ = "GNU Lesser General Public License (LGPL)"
diff --git a/paramiko/_version.py b/paramiko/_version.py
new file mode 100644
index 00000000..a7857b09
--- /dev/null
+++ b/paramiko/_version.py
@@ -0,0 +1,2 @@
+__version_info__ = (1, 15, 0)
+__version__ = '.'.join(map(str, __version_info__))
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 583809d5..49d8dd6e 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -280,7 +280,7 @@ class Channel (object):
def recv_exit_status(self):
"""
Return the exit status from the process on the server. This is
- mostly useful for retrieving the reults of an `exec_command`.
+ mostly useful for retrieving the results of an `exec_command`.
If the command hasn't finished yet, this method will wait until
it does, or until the channel is closed. If no exit status is
provided by the server, -1 is returned.
@@ -330,7 +330,7 @@ class Channel (object):
If you omit the auth_cookie, a new secure random 128-bit value will be
generated, used, and returned. You will need to use this value to
verify incoming x11 requests and replace them with the actual local
- x11 cookie (which requires some knoweldge of the x11 protocol).
+ x11 cookie (which requires some knowledge of the x11 protocol).
If a handler is passed in, the handler is called from another thread
whenever a new x11 connection arrives. The default handler queues up
@@ -339,7 +339,7 @@ class Channel (object):
handler(channel: Channel, (address: str, port: int))
- :param int screen_number: the x11 screen number (0, 10, etc)
+ :param int screen_number: the x11 screen number (0, 10, etc.)
:param str auth_protocol:
the name of the X11 authentication method used; if none is given,
``"MIT-MAGIC-COOKIE-1"`` is used
@@ -744,10 +744,10 @@ class Channel (object):
:raises socket.timeout:
if sending stalled for longer than the timeout set by `settimeout`.
:raises socket.error:
- if an error occured before the entire string was sent.
+ if an error occurred before the entire string was sent.
.. note::
- If the channel is closed while only part of the data hase been
+ If the channel is closed while only part of the data has been
sent, there is no way to determine how much data (if any) was sent.
This is irritating, but identically follows Python's API.
"""
@@ -771,7 +771,7 @@ class Channel (object):
:raises socket.timeout:
if sending stalled for longer than the timeout set by `settimeout`.
:raises socket.error:
- if an error occured before the entire string was sent.
+ if an error occurred before the entire string was sent.
.. versionadded:: 1.1
"""
@@ -812,7 +812,7 @@ class Channel (object):
def fileno(self):
"""
Returns an OS-level file descriptor which can be used for polling, but
- but not for reading or writing. This is primaily to allow Python's
+ but not for reading or writing. This is primarily to allow Python's
``select`` module to work.
The first time ``fileno`` is called on a channel, a pipe is created to
diff --git a/paramiko/config.py b/paramiko/config.py
index 5abf181f..20ca4aa7 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -57,8 +57,8 @@ class SSHConfig (object):
host = {"host": ['*'], "config": {}}
for line in file_obj:
- line = line.rstrip('\n').lstrip()
- if (line == '') or (line[0] == '#'):
+ line = line.rstrip('\r\n').lstrip()
+ if not line or line.startswith('#'):
continue
match = re.match(self.SETTINGS_REGEX, line)
@@ -107,8 +107,10 @@ class SSHConfig (object):
:param str hostname: the hostname to lookup
"""
- matches = [config for config in self._config if
- self._allowed(hostname, config['host'])]
+ matches = [
+ config for config in self._config
+ if self._allowed(config['host'], hostname)
+ ]
ret = {}
for match in matches:
@@ -124,7 +126,7 @@ class SSHConfig (object):
ret = self._expand_variables(ret, hostname)
return ret
- def _allowed(self, hostname, hosts):
+ def _allowed(self, hosts, hostname):
match = False
for host in hosts:
if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]):
@@ -196,10 +198,12 @@ class SSHConfig (object):
for find, replace in replacements[k]:
if isinstance(config[k], list):
for item in range(len(config[k])):
- config[k][item] = config[k][item].\
- replace(find, str(replace))
+ if find in config[k][item]:
+ config[k][item] = config[k][item].\
+ replace(find, str(replace))
else:
- config[k] = config[k].replace(find, str(replace))
+ if find in config[k]:
+ config[k] = config[k].replace(find, str(replace))
return config
def _get_hosts(self, host):
diff --git a/paramiko/file.py b/paramiko/file.py
index f57aa79f..2238f0bf 100644
--- a/paramiko/file.py
+++ b/paramiko/file.py
@@ -124,9 +124,15 @@ class BufferedFile (object):
file first). If the ``size`` argument is negative or omitted, read all
the remaining data in the file.
+ .. note::
+ ``'b'`` mode flag is ignored (``self.FLAG_BINARY`` in
+ ``self._flags``), because SSH treats all files as binary, since we
+ have no idea what encoding the file is in, or even if the file is
+ text data.
+
:param int size: maximum number of bytes to read
:return:
- data read from the file (as a `str`), or an empty string if EOF was
+ data read from the file (as bytes), or an empty string if EOF was
encountered immediately
"""
if self._closed:
@@ -148,12 +154,12 @@ class BufferedFile (object):
result += new_data
self._realpos += len(new_data)
self._pos += len(new_data)
- return result if self._flags & self.FLAG_BINARY else u(result)
+ return result
if size <= len(self._rbuffer):
result = self._rbuffer[:size]
self._rbuffer = self._rbuffer[size:]
self._pos += len(result)
- return result if self._flags & self.FLAG_BINARY else u(result)
+ return result
while len(self._rbuffer) < size:
read_size = size - len(self._rbuffer)
if self._flags & self.FLAG_BUFFERED:
@@ -169,7 +175,7 @@ class BufferedFile (object):
result = self._rbuffer[:size]
self._rbuffer = self._rbuffer[size:]
self._pos += len(result)
- return result if self._flags & self.FLAG_BINARY else u(result)
+ return result
def readline(self, size=None):
"""
@@ -186,8 +192,12 @@ class BufferedFile (object):
:param int size: maximum length of returned string.
:return:
- next line of the file (`str`), or an empty string if the end of the
+ next line of the file, or an empty string if the end of the
file has been reached.
+
+ If the file was opened in binary (``'b'``) mode: bytes are returned
+ Else: the encoding of the file is assumed to be UTF-8 and character
+ strings (`str`) are returned
"""
# it's almost silly how complex this function is.
if self._closed:
@@ -277,7 +287,8 @@ class BufferedFile (object):
Set the file's current position, like stdio's ``fseek``. Not all file
objects support seeking.
- .. note:: If a file is opened in append mode (``'a'`` or ``'a+'``), any seek
+ .. note::
+ If a file is opened in append mode (``'a'`` or ``'a+'``), any seek
operations will be undone at the next write (as the file position
will move back to the end of the file).
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index c0caeda9..cd65e77c 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -179,7 +179,7 @@ class HostKeys (MutableMapping):
entries = []
for e in self._entries:
for h in e.hostnames:
- if h.startswith('|1|') and constant_time_bytes_eq(self.hash_host(hostname, h), h) or h == hostname:
+ if h.startswith('|1|') and not hostname.startswith('|1|') and constant_time_bytes_eq(self.hash_host(hostname, h), h) or h == hostname:
entries.append(e)
if len(entries) == 0:
return None
diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py
index 8842b988..57c096b2 100644
--- a/paramiko/py3compat.py
+++ b/paramiko/py3compat.py
@@ -39,6 +39,8 @@ if PY2:
return s
elif isinstance(s, unicode):
return s.encode(encoding)
+ elif isinstance(s, buffer):
+ return s
else:
raise TypeError("Expected unicode or bytes, got %r" % s)
@@ -49,6 +51,8 @@ if PY2:
return s.decode(encoding)
elif isinstance(s, unicode):
return s
+ elif isinstance(s, buffer):
+ return s.decode(encoding)
else:
raise TypeError("Expected unicode or bytes, got %r" % s)
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 1caaf165..3e85a8c9 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -196,6 +196,71 @@ class SFTPClient(BaseSFTP):
self._request(CMD_CLOSE, handle)
return filelist
+ def listdir_iter(self, path='.', read_aheads=50):
+ """
+ Generator version of `.listdir_attr`.
+
+ See the API docs for `.listdir_attr` for overall details.
+
+ This function adds one more kwarg on top of `.listdir_attr`:
+ ``read_aheads``, an integer controlling how many
+ ``SSH_FXP_READDIR`` requests are made to the server. The default of 50
+ should suffice for most file listings as each request/response cycle
+ may contain multiple files (dependant on server implementation.)
+
+ .. versionadded:: 1.15
+ """
+ path = self._adjust_cwd(path)
+ self._log(DEBUG, 'listdir(%r)' % path)
+ t, msg = self._request(CMD_OPENDIR, path)
+
+ if t != CMD_HANDLE:
+ raise SFTPError('Expected handle')
+
+ handle = msg.get_string()
+
+ nums = list()
+ while True:
+ try:
+ # Send out a bunch of readdir requests so that we can read the
+ # responses later on Section 6.7 of the SSH file transfer RFC
+ # explains this
+ # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
+ for i in range(read_aheads):
+ num = self._async_request(type(None), CMD_READDIR, handle)
+ nums.append(num)
+
+
+ # For each of our sent requests
+ # Read and parse the corresponding packets
+ # If we're at the end of our queued requests, then fire off
+ # some more requests
+ # Exit the loop when we've reached the end of the directory
+ # handle
+ for num in nums:
+ t, pkt_data = self._read_packet()
+ msg = Message(pkt_data)
+ new_num = msg.get_int()
+ if num == new_num:
+ if t == CMD_STATUS:
+ self._convert_status(msg)
+ count = msg.get_int()
+ for i in range(count):
+ filename = msg.get_text()
+ longname = msg.get_text()
+ attr = SFTPAttributes._from_msg(
+ msg, filename, longname)
+ if (filename != '.') and (filename != '..'):
+ yield attr
+
+ # If we've hit the end of our queued requests, reset nums.
+ nums = list()
+
+ except EOFError:
+ self._request(CMD_CLOSE, handle)
+ return
+
+
def open(self, filename, mode='r', bufsize=-1):
"""
Open a file on the remote server. The arguments are the same as for
@@ -534,9 +599,7 @@ class SFTPClient(BaseSFTP):
an `.SFTPAttributes` object containing attributes about the given
file.
- .. versionadded:: 1.4
- .. versionchanged:: 1.7.4
- Began returning rich attribute objects.
+ .. versionadded:: 1.10
"""
with self.file(remotepath, 'wb') as fr:
fr.set_pipelined(True)
@@ -566,7 +629,9 @@ class SFTPClient(BaseSFTP):
The SFTP operations use pipelining for speed.
:param str localpath: the local file to copy
- :param str remotepath: the destination path on the SFTP server
+ :param str remotepath: the destination path on the SFTP server. Note
+ that the filename should be included. Only specifying a directory
+ may result in an error.
:param callable callback:
optional callback function (form: ``func(int, int)``) that accepts
the bytes transferred so far and the total bytes to be transferred
@@ -584,7 +649,7 @@ class SFTPClient(BaseSFTP):
"""
file_size = os.stat(localpath).st_size
with open(localpath, 'rb') as fl:
- return self.putfo(fl, remotepath, os.stat(localpath).st_size, callback, confirm)
+ return self.putfo(fl, remotepath, file_size, callback, confirm)
def getfo(self, remotepath, fl, callback=None):
"""
@@ -601,9 +666,7 @@ class SFTPClient(BaseSFTP):
the bytes transferred so far and the total bytes to be transferred
:return: the `number <int>` of bytes written to the opened file object
- .. versionadded:: 1.4
- .. versionchanged:: 1.7.4
- Added the ``callable`` param.
+ .. versionadded:: 1.10
"""
with self.open(remotepath, 'rb') as fr:
file_size = self.stat(remotepath).st_size
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 406626a7..1a33e1ae 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -164,6 +164,8 @@ class Transport (threading.Thread):
:param socket sock:
a socket or socket-like object to create the session over.
"""
+ self.active = False
+
if isinstance(sock, string_types):
# convert "host:port" into (host, port)
hl = sock.split(':', 1)
@@ -219,7 +221,6 @@ class Transport (threading.Thread):
self.H = None
self.K = None
- self.active = False
self.initial_kex_done = False
self.in_kex = False
self.authenticated = False
diff --git a/setup.py b/setup.py
index 4a858326..13386c8e 100644
--- a/setup.py
+++ b/setup.py
@@ -54,9 +54,16 @@ if sys.platform == 'darwin':
setup_helper.install_custom_make_tarball()
+# Version info -- read without importing
+_locals = {}
+with open('paramiko/_version.py') as fp:
+ exec(fp.read(), None, _locals)
+version = _locals['__version__']
+
+
setup(
name = "paramiko",
- version = "1.13.0",
+ version = version,
description = "SSH2 protocol library",
long_description = longdesc,
author = "Jeff Forcier",
@@ -79,6 +86,7 @@ setup(
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
],
**kw
)
diff --git a/sites/shared_conf.py b/sites/shared_conf.py
index 69908388..4a6a5c4e 100644
--- a/sites/shared_conf.py
+++ b/sites/shared_conf.py
@@ -12,7 +12,7 @@ html_theme_options = {
'description': "A Python implementation of SSHv2.",
'github_user': 'paramiko',
'github_repo': 'paramiko',
- 'gittip_user': 'bitprophet',
+ 'gratipay_user': 'bitprophet',
'analytics_id': 'UA-18486793-2',
'travis_button': True,
}
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index c9b7bcaf..e18b5368 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,59 @@
Changelog
=========
+* :feature:`131` Add a `~paramiko.sftp_client.SFTPClient.listdir_iter` method
+ to `~paramiko.sftp_client.SFTPClient` allowing for more efficient,
+ async/generator based file listings. Thanks to John Begeman.
+* :support:`378 backported` Minor code cleanup in the SSH config module
+ courtesy of Olle Lundberg.
+* :support:`249` Consolidate version information into one spot. Thanks to Gabi
+ Davar for the reminder.
+* :release:`1.14.1 <2014-08-25>`
+* :release:`1.13.2 <2014-08-25>`
+* :bug:`376` Be less aggressive about expanding variables in ``ssh_config``
+ files, which results in a speedup of SSH config parsing. Credit to Olle
+ Lundberg.
+* :support:`324 backported` A bevvy of documentation typo fixes, courtesy of Roy
+ Wellington.
+* :bug:`312` `paramiko.transport.Transport` had a bug in its ``__repr__`` which
+ surfaces during errors encountered within its ``__init__``, causing
+ problematic tracebacks in such situations. Thanks to Simon Percivall for
+ catch & patch.
+* :bug:`272` Fix a bug where ``known_hosts`` parsing hashed the input hostname
+ as well as the hostnames from the ``known_hosts`` file, on every comparison.
+ Thanks to ``@sigmunau`` for final patch and ``@ostacey`` for the original
+ report.
+* :bug:`239` Add Windows-style CRLF support to SSH config file parsing. Props
+ to Christopher Swenson.
+* :support:`229 backported` Fix a couple of incorrectly-copied docstrings' ``..
+ versionadded::`` RST directives. Thanks to Aarni Koskela for the catch.
+* :support:`169 backported` Minor refactor of
+ `paramiko.sftp_client.SFTPClient.put` thanks to Abhinav Upadhyay.
+* :bug:`285` (also :issue:`352`) Update our Python 3 ``b()`` compatibility shim
+ to handle ``buffer`` objects correctly; this fixes a frequently reported
+ issue affecting many users, including users of the ``bzr`` software suite.
+ Thanks to ``@basictheprogram`` for the initial report, Jelmer Vernooij for
+ the fix and Andrew Starr-Bochicchio & Jeremy T. Bouse (among others) for
+ discussion & feedback.
+* :support:`371` Add Travis support & docs update for Python 3.4. Thanks to
+ Olle Lundberg.
+* :release:`1.14.0 <2014-05-07>`
+* :release:`1.13.1 <2014-05-07>`
+* :release:`1.12.4 <2014-05-07>`
+* :release:`1.11.6 <2014-05-07>`
+* :bug:`-` `paramiko.file.BufferedFile.read` incorrectly returned text strings
+ after the Python 3 migration, despite bytes being more appropriate for file
+ contents (which may be binary or of an unknown encoding.) This has been
+ addressed.
+
+ .. note::
+ `paramiko.file.BufferedFile.readline` continues to return strings, not
+ bytes, as "lines" only make sense for textual data. It assumes UTF-8 by
+ default.
+
+ This should fix `this issue raised on the Obnam mailing list
+ <http://comments.gmane.org/gmane.comp.sysutils.backup.obnam/252>`_. Thanks
+ to Antoine Brenner for the patch.
* :bug:`-` Added self.args for exception classes. Used for unpickling. Related
to (`Fabric #986 <https://github.com/fabric/fabric/issues/986>`_, `Fabric
#714 <https://github.com/fabric/fabric/issues/714>`_). Thanks to Alex
diff --git a/sites/www/conf.py b/sites/www/conf.py
index bdb5929a..0b0fb85c 100644
--- a/sites/www/conf.py
+++ b/sites/www/conf.py
@@ -12,13 +12,10 @@ extensions.append('releases')
releases_release_uri = "https://github.com/paramiko/paramiko/tree/v%s"
releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s"
-# Intersphinx for referencing API/usage docs
-extensions.append('sphinx.ext.intersphinx')
# Default is 'local' building, but reference the public docs site when building
# under RTD.
target = join(dirname(__file__), '..', 'docs', '_build')
if os.environ.get('READTHEDOCS') == 'True':
- # TODO: switch to docs.paramiko.org post go-live of sphinx API docs
target = 'http://docs.paramiko.org/en/latest/'
intersphinx_mapping['docs'] = (target, None)
diff --git a/sites/www/contact.rst b/sites/www/contact.rst
index 2b6583f5..7e6c947e 100644
--- a/sites/www/contact.rst
+++ b/sites/www/contact.rst
@@ -9,3 +9,4 @@ following ways:
* Mailing list: ``paramiko@librelist.com`` (see `the LibreList homepage
<http://librelist.com>`_ for usage details).
* This website - a blog section is forthcoming.
+* Submit contributions on Github - see the :doc:`contributing` page.
diff --git a/sites/www/contributing.rst b/sites/www/contributing.rst
index 634c2b26..a44414e8 100644
--- a/sites/www/contributing.rst
+++ b/sites/www/contributing.rst
@@ -5,18 +5,22 @@ Contributing
How to get the code
===================
-Our primary Git repository is on Github at `paramiko/paramiko
-<https://github.com/paramiko/paramiko>`_; please follow their instructions for
-cloning to your local system. (If you intend to submit patches/pull requests,
-we recommend forking first, then cloning your fork. Github has excellent
-documentation for all this.)
+Our primary Git repository is on Github at `paramiko/paramiko`_;
+please follow their instructions for cloning to your local system. (If you
+intend to submit patches/pull requests, we recommend forking first, then
+cloning your fork. Github has excellent documentation for all this.)
How to submit bug reports or new code
=====================================
Please see `this project-agnostic contribution guide
-<http://contribution-guide.org>`_ - we follow it explicitly.
+<http://contribution-guide.org>`_ - we follow it explicitly. Again, our code
+repository and bug tracker is `on Github`_.
Our current changelog is located in ``sites/www/changelog.rst`` - the top
level files like ``ChangeLog.*`` and ``NEWS`` are historical only.
+
+
+.. _paramiko/paramiko:
+.. _on Github: https://github.com/paramiko/paramiko
diff --git a/sites/www/faq.rst b/sites/www/faq.rst
index a7e80014..a5d9b383 100644
--- a/sites/www/faq.rst
+++ b/sites/www/faq.rst
@@ -7,3 +7,20 @@ Which version should I use? I see multiple active releases.
Please see :ref:`the installation docs <release-lines>` which have an explicit
section about this topic.
+
+Paramiko doesn't work with my Cisco, Windows or other non-Unix system!
+======================================================================
+
+In an ideal world, the developers would love to support every possible target
+system. Unfortunately, volunteer development time and access to non-mainstream
+platforms are limited, meaning that we can only fully support standard OpenSSH
+implementations such as those found on the average Linux distribution (as well
+as on Mac OS X and \*BSD.)
+
+Because of this, **we typically close bug reports for nonstandard SSH
+implementations or host systems**.
+
+However, **closed does not imply locked** - affected users can still post
+comments on such tickets - and **we will always consider actual patch
+submissions for these issues**, provided they can get +1s from similarly
+affected users and are proven to not break existing functionality.
diff --git a/sites/www/installing.rst b/sites/www/installing.rst
index 74c5c6e8..052825c4 100644
--- a/sites/www/installing.rst
+++ b/sites/www/installing.rst
@@ -16,12 +16,12 @@ via `pip <http://pip-installer.org>`_::
Users who want the bleeding edge can install the development version via
``pip install paramiko==dev``.
-We currently support **Python 2.6, 2.7 and 3.3** (Python **3.2** should also
+We currently support **Python 2.6, 2.7 and 3.3+** (Python **3.2** should also
work but has a less-strong compatibility guarantee from us.) Users on Python
2.5 or older are urged to upgrade.
-Paramiko has two dependencies: the pure-Python ECDSA module `ecdsa`, and the
-PyCrypto C extension. `ecdsa` is easily installable from wherever you
+Paramiko has two dependencies: the pure-Python ECDSA module ``ecdsa``, and the
+PyCrypto C extension. ``ecdsa`` is easily installable from wherever you
obtained Paramiko's package; PyCrypto may require more work. Read on for
details.
@@ -32,7 +32,7 @@ Release lines
Users desiring stability may wish to pin themselves to a specific release line
once they first start using Paramiko; to assist in this, we guarantee bugfixes
-for at least the last 2-3 releases including the latest stable one. This currently means Paramiko **1.11** through **1.13**.
+for the last 2-3 releases including the latest stable one.
If you're unsure which version to install, we have suggestions:
diff --git a/tasks.py b/tasks.py
index 38282b8e..94bf0aa2 100644
--- a/tasks.py
+++ b/tasks.py
@@ -36,8 +36,10 @@ def coverage(ctx):
# Until we stop bundling docs w/ releases. Need to discover use cases first.
-@task('docs') # Will invoke the API doc site build
+@task
def release(ctx):
+ # Build docs first. Use terribad workaround pending invoke #146
+ ctx.run("inv docs")
# Move the built docs into where Epydocs used to live
target = 'docs'
rmtree(target, ignore_errors=True)
diff --git a/test.py b/test.py
index 2b3d4ed4..92a3e6d0 100755
--- a/test.py
+++ b/test.py
@@ -149,10 +149,7 @@ def main():
# TODO: make that not a problem, jeez
for thread in threading.enumerate():
if thread is not threading.currentThread():
- if PY2:
- thread._Thread__stop()
- else:
- thread._stop()
+ thread.join(timeout=1)
# Exit correctly
if not result.wasSuccessful():
sys.exit(1)
diff --git a/tests/test_file.py b/tests/test_file.py
index e11d7fd5..22a34aca 100755
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -23,6 +23,7 @@ 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
class LoopbackFile (BufferedFile):
@@ -53,7 +54,7 @@ class BufferedFileTest (unittest.TestCase):
def test_1_simple(self):
f = LoopbackFile('r')
try:
- f.write('hi')
+ f.write(b'hi')
self.assertTrue(False, 'no exception on write to read-only file')
except:
pass
@@ -69,7 +70,7 @@ class BufferedFileTest (unittest.TestCase):
def test_2_readline(self):
f = LoopbackFile('r+U')
- f.write('First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.')
+ f.write(b'First line.\nSecond line.\r\nThird line.\nFinal line non-terminated.')
self.assertEqual(f.readline(), 'First line.\n')
# universal newline mode should convert this linefeed:
self.assertEqual(f.readline(), 'Second line.\n')
@@ -93,9 +94,9 @@ class BufferedFileTest (unittest.TestCase):
try to trick the linefeed detector.
"""
f = LoopbackFile('r+U')
- f.write('First line.\r')
+ f.write(b'First line.\r')
self.assertEqual(f.readline(), 'First line.\n')
- f.write('\nSecond.\r\n')
+ f.write(b'\nSecond.\r\n')
self.assertEqual(f.readline(), 'Second.\n')
f.close()
self.assertEqual(f.newlines, crlf)
@@ -105,7 +106,7 @@ class BufferedFileTest (unittest.TestCase):
verify that write buffering is on.
"""
f = LoopbackFile('r+', 1)
- f.write('Complete line.\nIncomplete line.')
+ f.write(b'Complete line.\nIncomplete line.')
self.assertEqual(f.readline(), 'Complete line.\n')
self.assertEqual(f.readline(), '')
f.write('..\n')
@@ -118,12 +119,12 @@ class BufferedFileTest (unittest.TestCase):
"""
f = LoopbackFile('r+', 512)
f.write('Not\nquite\n512 bytes.\n')
- self.assertEqual(f.read(1), '')
+ self.assertEqual(f.read(1), b'')
f.flush()
- self.assertEqual(f.read(5), 'Not\nq')
- self.assertEqual(f.read(10), 'uite\n512 b')
- self.assertEqual(f.read(9), 'ytes.\n')
- self.assertEqual(f.read(3), '')
+ self.assertEqual(f.read(5), b'Not\nq')
+ self.assertEqual(f.read(10), b'uite\n512 b')
+ self.assertEqual(f.read(9), b'ytes.\n')
+ self.assertEqual(f.read(3), b'')
f.close()
def test_6_buffering(self):
@@ -131,12 +132,12 @@ class BufferedFileTest (unittest.TestCase):
verify that flushing happens automatically on buffer crossing.
"""
f = LoopbackFile('r+', 16)
- f.write('Too small.')
- self.assertEqual(f.read(4), '')
- f.write(' ')
- self.assertEqual(f.read(4), '')
- f.write('Enough.')
- self.assertEqual(f.read(20), 'Too small. Enough.')
+ f.write(b'Too small.')
+ self.assertEqual(f.read(4), b'')
+ f.write(b' ')
+ self.assertEqual(f.read(4), b'')
+ f.write(b'Enough.')
+ self.assertEqual(f.read(20), b'Too small. Enough.')
f.close()
def test_7_read_all(self):
@@ -144,9 +145,23 @@ class BufferedFileTest (unittest.TestCase):
verify that read(-1) returns everything left in the file.
"""
f = LoopbackFile('r+', 16)
- f.write('The first thing you need to do is open your eyes. ')
- f.write('Then, you need to close them again.\n')
+ f.write(b'The first thing you need to do is open your eyes. ')
+ f.write(b'Then, you need to close them again.\n')
s = f.read(-1)
- self.assertEqual(s, 'The first thing you need to do is open your eyes. Then, you ' +
- 'need to close them again.\n')
+ self.assertEqual(s, b'The first thing you need to do is open your eyes. Then, you ' +
+ b'need to close them again.\n')
f.close()
+
+ def test_8_buffering(self):
+ """
+ verify that buffered objects can be written
+ """
+ if sys.version_info[0] == 2:
+ f = LoopbackFile('r+', 16)
+ f.write(buffer(b'Too small.'))
+ f.close()
+
+if __name__ == '__main__':
+ from unittest import main
+ main()
+
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index e0534eb0..1ae9781d 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -67,6 +67,18 @@ liver insulin receptors. Their sensitivity to insulin is, however, similarly
decreased compared with chicken.
'''
+
+# Here is how unicode characters are encoded over 1 to 6 bytes in utf-8
+# U-00000000 - U-0000007F: 0xxxxxxx
+# U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
+# U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
+# U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+# U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+# U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+# Note that: hex(int('11000011',2)) == '0xc3'
+# Thus, the following 2-bytes sequence is not valid utf8: "invalid continuation byte"
+NON_UTF8_DATA = b'\xC3\xC3'
+
FOLDER = os.environ.get('TEST_FOLDER', 'temp-testing000')
sftp = None
@@ -267,8 +279,8 @@ class SFTPTest (unittest.TestCase):
def test_7_listdir(self):
"""
- verify that a folder can be created, a bunch of files can be placed in it,
- and those files show up in sftp.listdir.
+ verify that a folder can be created, a bunch of files can be placed in
+ it, and those files show up in sftp.listdir.
"""
try:
sftp.open(FOLDER + '/duck.txt', 'w').close()
@@ -286,6 +298,26 @@ class SFTPTest (unittest.TestCase):
sftp.remove(FOLDER + '/fish.txt')
sftp.remove(FOLDER + '/tertiary.py')
+ def test_7_5_listdir_iter(self):
+ """
+ listdir_iter version of above test
+ """
+ try:
+ sftp.open(FOLDER + '/duck.txt', 'w').close()
+ sftp.open(FOLDER + '/fish.txt', 'w').close()
+ sftp.open(FOLDER + '/tertiary.py', 'w').close()
+
+ x = [x.filename for x in sftp.listdir_iter(FOLDER)]
+ self.assertEqual(len(x), 3)
+ self.assertTrue('duck.txt' in x)
+ self.assertTrue('fish.txt' in x)
+ self.assertTrue('tertiary.py' in x)
+ self.assertTrue('random' not in x)
+ finally:
+ sftp.remove(FOLDER + '/duck.txt')
+ sftp.remove(FOLDER + '/fish.txt')
+ sftp.remove(FOLDER + '/tertiary.py')
+
def test_8_setstat(self):
"""
verify that the setstat functions (chown, chmod, utime, truncate) work.
@@ -405,7 +437,7 @@ class SFTPTest (unittest.TestCase):
self.assertEqual(sftp.stat(FOLDER + '/testing.txt').st_size, 13)
with sftp.open(FOLDER + '/testing.txt', 'r') as f:
data = f.read(20)
- self.assertEqual(data, 'hello kiddy.\n')
+ self.assertEqual(data, b'hello kiddy.\n')
finally:
sftp.remove(FOLDER + '/testing.txt')
@@ -466,8 +498,8 @@ class SFTPTest (unittest.TestCase):
f.write('?\n')
with sftp.open(FOLDER + '/happy.txt', 'r') as f:
- self.assertEqual(f.readline(), 'full line?\n')
- self.assertEqual(f.read(7), 'partial')
+ self.assertEqual(f.readline(), u('full line?\n'))
+ self.assertEqual(f.read(7), b'partial')
finally:
try:
sftp.remove(FOLDER + '/happy.txt')
@@ -662,8 +694,8 @@ class SFTPTest (unittest.TestCase):
fd, localname = mkstemp()
os.close(fd)
- text = 'All I wanted was a plastic bunny rabbit.\n'
- with open(localname, 'w') as f:
+ text = b'All I wanted was a plastic bunny rabbit.\n'
+ with open(localname, 'wb') as f:
f.write(text)
saved_progress = []
@@ -747,6 +779,23 @@ class SFTPTest (unittest.TestCase):
sftp.remove(FOLDER + '/test%file')
+ def test_O_non_utf8_data(self):
+ """Test write() and read() of non utf8 data"""
+ try:
+ with sftp.open('%s/nonutf8data' % FOLDER, 'w') as f:
+ f.write(NON_UTF8_DATA)
+ with sftp.open('%s/nonutf8data' % FOLDER, 'r') as f:
+ data = f.read()
+ self.assertEqual(data, NON_UTF8_DATA)
+ with sftp.open('%s/nonutf8data' % FOLDER, 'wb') as f:
+ f.write(NON_UTF8_DATA)
+ with sftp.open('%s/nonutf8data' % FOLDER, 'rb') as f:
+ data = f.read()
+ self.assertEqual(data, NON_UTF8_DATA)
+ finally:
+ sftp.remove('%s/nonutf8data' % FOLDER)
+
+
if __name__ == '__main__':
SFTPTest.init_loopback()
# logging is required by test_N_file_with_percent
diff --git a/tests/test_sftp_big.py b/tests/test_sftp_big.py
index 521fbdc8..abed27b8 100644
--- a/tests/test_sftp_big.py
+++ b/tests/test_sftp_big.py
@@ -85,7 +85,7 @@ class BigSFTPTest (unittest.TestCase):
write a 1MB file with no buffering.
"""
sftp = get_sftp()
- kblob = (1024 * 'x')
+ kblob = (1024 * b'x')
start = time.time()
try:
with sftp.open('%s/hongry.txt' % FOLDER, 'w') as f:
@@ -231,7 +231,7 @@ class BigSFTPTest (unittest.TestCase):
without using it, to verify that paramiko doesn't get confused.
"""
sftp = get_sftp()
- kblob = (1024 * 'x')
+ kblob = (1024 * b'x')
try:
with sftp.open('%s/hongry.txt' % FOLDER, 'w') as f:
f.set_pipelined(True)
diff --git a/tests/test_util.py b/tests/test_util.py
index 8142d416..394ea553 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -334,6 +334,12 @@ IdentityFile something_%l_using_fqdn
config = paramiko.util.parse_ssh_config(StringIO(test_config))
assert config.lookup('meh') # will die during lookup() if bug regresses
+ def test_13_config_dos_crlf_succeeds(self):
+ config_file = StringIO("host abcqwerty\r\nHostName 127.0.0.1\r\n")
+ config = paramiko.SSHConfig()
+ config.parse(config_file)
+ self.assertEqual(config.lookup("abcqwerty")["hostname"], "127.0.0.1")
+
def test_quoted_host_names(self):
test_config_file = """\
Host "param pam" param "pam"
diff --git a/tox.ini b/tox.ini
index 43c391dd..83704dfc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py26,py27,py32,py33
+envlist = py26,py27,py32,py33,py34
[testenv]
commands = pip install --use-mirrors -q -r tox-requirements.txt