summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml7
-rw-r--r--dev-requirements.txt5
-rw-r--r--paramiko/__init__.py3
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/channel.py14
-rw-r--r--paramiko/config.py102
-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.rst38
-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.rst4
-rw-r--r--tasks.py4
-rwxr-xr-xtest.py5
-rwxr-xr-xtests/test_file.py10
-rwxr-xr-xtests/test_sftp.py24
-rw-r--r--tests/test_util.py106
-rw-r--r--tox.ini2
24 files changed, 378 insertions, 85 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/dev-requirements.txt b/dev-requirements.txt
index e9052898..7a0ccbc5 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,10 +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.6.0
+alabaster>=0.6.1
releases>=0.5.2
wheel==0.23.0
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 4c62ad4a..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.14.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 77fa13d7..20ca4aa7 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -27,7 +27,6 @@ import re
import socket
SSH_PORT = 22
-proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)
class SSHConfig (object):
@@ -41,6 +40,8 @@ class SSHConfig (object):
.. versionadded:: 1.6
"""
+ SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
+
def __init__(self):
"""
Create a new OpenSSH config object.
@@ -53,44 +54,39 @@ class SSHConfig (object):
:param file file_obj: a file-like object to read the config file from
"""
+
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
- if '=' in line:
- # Ensure ProxyCommand gets properly split
- if line.lower().strip().startswith('proxycommand'):
- match = proxy_re.match(line)
- key, value = match.group(1).lower(), match.group(2)
- else:
- key, value = line.split('=', 1)
- key = key.strip().lower()
- else:
- # find first whitespace, and split there
- i = 0
- while (i < len(line)) and not line[i].isspace():
- i += 1
- if i == len(line):
- raise Exception('Unparsable line: %r' % line)
- key = line[:i].lower()
- value = line[i:].lstrip()
+ match = re.match(self.SETTINGS_REGEX, line)
+ if not match:
+ raise Exception("Unparsable line %s" % line)
+ key = match.group(1).lower()
+ value = match.group(2)
+
if key == 'host':
self._config.append(host)
- value = value.split()
- host = {key: value, 'config': {}}
- #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be
- # specified multiple times and they should be tried in order
- # of specification.
-
- elif key in ['identityfile', 'localforward', 'remoteforward']:
- if key in host['config']:
- host['config'][key].append(value)
- else:
- host['config'][key] = [value]
- elif key not in host['config']:
- host['config'].update({key: value})
+ host = {
+ 'host': self._get_hosts(value),
+ 'config': {}
+ }
+ else:
+ if value.startswith('"') and value.endswith('"'):
+ value = value[1:-1]
+
+ #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be
+ # specified multiple times and they should be tried in order
+ # of specification.
+ if key in ['identityfile', 'localforward', 'remoteforward']:
+ if key in host['config']:
+ host['config'][key].append(value)
+ else:
+ host['config'][key] = [value]
+ elif key not in host['config']:
+ host['config'][key] = value
self._config.append(host)
def lookup(self, hostname):
@@ -111,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:
@@ -128,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:]):
@@ -200,12 +198,38 @@ 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):
+ """
+ Return a list of host_names from host value.
+ """
+ i, length = 0, len(host)
+ hosts = []
+ while i < length:
+ if host[i] == '"':
+ end = host.find('"', i + 1)
+ if end < 0:
+ raise Exception("Unparsable host %s" % host)
+ hosts.append(host[i + 1:end])
+ i = end + 1
+ elif not host[i].isspace():
+ end = i + 1
+ while end < length and not host[end].isspace() and host[end] != '"':
+ end += 1
+ hosts.append(host[i:end])
+ i = end
+ else:
+ i += 1
+
+ return hosts
+
class LazyFqdn(object):
"""
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 c0f1e579..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.14.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 f8a4d2c1..57f00f12 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,44 @@
Changelog
=========
+* :feature:`184` Support quoted values in SSH config file parsing. Credit to
+ Yan Kalchevskiy.
+* :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>`
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 a28ce6cd..052825c4 100644
--- a/sites/www/installing.rst
+++ b/sites/www/installing.rst
@@ -16,7 +16,7 @@ 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.
@@ -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 c6edd7af..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):
@@ -151,6 +152,15 @@ class BufferedFileTest (unittest.TestCase):
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 2b6aa3b6..1ae9781d 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -279,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()
@@ -298,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.
diff --git a/tests/test_util.py b/tests/test_util.py
index 69c75518..394ea553 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -333,3 +333,109 @@ 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"
+ Port 1111
+
+Host "param2"
+ Port 2222
+
+Host param3 parara
+ Port 3333
+
+Host param4 "p a r" "p" "par" para
+ Port 4444
+"""
+ res = {
+ 'param pam': {'hostname': 'param pam', 'port': '1111'},
+ 'param': {'hostname': 'param', 'port': '1111'},
+ 'pam': {'hostname': 'pam', 'port': '1111'},
+
+ 'param2': {'hostname': 'param2', 'port': '2222'},
+
+ 'param3': {'hostname': 'param3', 'port': '3333'},
+ 'parara': {'hostname': 'parara', 'port': '3333'},
+
+ 'param4': {'hostname': 'param4', 'port': '4444'},
+ 'p a r': {'hostname': 'p a r', 'port': '4444'},
+ 'p': {'hostname': 'p', 'port': '4444'},
+ 'par': {'hostname': 'par', 'port': '4444'},
+ 'para': {'hostname': 'para', 'port': '4444'},
+ }
+ f = StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ for host, values in res.items():
+ self.assertEquals(
+ paramiko.util.lookup_ssh_host_config(host, config),
+ values
+ )
+
+ def test_quoted_params_in_config(self):
+ test_config_file = """\
+Host "param pam" param "pam"
+ IdentityFile id_rsa
+
+Host "param2"
+ IdentityFile "test rsa key"
+
+Host param3 parara
+ IdentityFile id_rsa
+ IdentityFile "test rsa key"
+"""
+ res = {
+ 'param pam': {'hostname': 'param pam', 'identityfile': ['id_rsa']},
+ 'param': {'hostname': 'param', 'identityfile': ['id_rsa']},
+ 'pam': {'hostname': 'pam', 'identityfile': ['id_rsa']},
+
+ 'param2': {'hostname': 'param2', 'identityfile': ['test rsa key']},
+
+ 'param3': {'hostname': 'param3', 'identityfile': ['id_rsa', 'test rsa key']},
+ 'parara': {'hostname': 'parara', 'identityfile': ['id_rsa', 'test rsa key']},
+ }
+ f = StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ for host, values in res.items():
+ self.assertEquals(
+ paramiko.util.lookup_ssh_host_config(host, config),
+ values
+ )
+
+ def test_quoted_host_in_config(self):
+ conf = SSHConfig()
+ correct_data = {
+ 'param': ['param'],
+ '"param"': ['param'],
+
+ 'param pam': ['param', 'pam'],
+ '"param" "pam"': ['param', 'pam'],
+ '"param" pam': ['param', 'pam'],
+ 'param "pam"': ['param', 'pam'],
+
+ 'param "pam" p': ['param', 'pam', 'p'],
+ '"param" pam "p"': ['param', 'pam', 'p'],
+
+ '"pa ram"': ['pa ram'],
+ '"pa ram" pam': ['pa ram', 'pam'],
+ 'param "p a m"': ['param', 'p a m'],
+ }
+ incorrect_data = [
+ 'param"',
+ '"param',
+ 'param "pam',
+ 'param "pam" "p a',
+ ]
+ for host, values in correct_data.items():
+ self.assertEquals(
+ conf._get_hosts(host),
+ values
+ )
+ for host in incorrect_data:
+ self.assertRaises(Exception, conf._get_hosts, host)
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