Skip to content

Commit

Permalink
Merge d17ac87 into bcf960f
Browse files Browse the repository at this point in the history
  • Loading branch information
bz2 committed Jun 8, 2017
2 parents bcf960f + d17ac87 commit b567d54
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.sw[nop]
*.pyc
build/
dist/
Expand Down
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ python:
- "3.4"
- "3.5"
- "3.6"
addons:
apt:
packages:
# Include language pack for locale testing
- language-pack-ko
install:
# Self-install for setup.py-driven deps
- pip install -e .
Expand Down
15 changes: 12 additions & 3 deletions paramiko/py3compat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys
import base64
import time
import sys

__all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 'bytes', 'long', 'input',
'decodebytes', 'encodebytes', 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask',
Expand All @@ -8,6 +9,9 @@
PY2 = sys.version_info[0] < 3

if PY2:
import __builtin__ as builtins
import locale

string_types = basestring
text_type = unicode
bytes_types = str
Expand All @@ -18,8 +22,6 @@
decodebytes = base64.decodestring
encodebytes = base64.encodestring

import __builtin__ as builtins


def bytestring(s): # NOQA
if isinstance(s, unicode):
Expand Down Expand Up @@ -101,6 +103,11 @@ def __len__(self):
# 64-bit
MAXSIZE = int((1 << 63) - 1) # NOQA
del X

def strftime(format, t):
"""Same as time.strftime but returns unicode."""
_, encoding = locale.getlocale(locale.LC_TIME)
return time.strftime(format, t).decode(encoding or 'ascii')
else:
import collections
import struct
Expand Down Expand Up @@ -167,3 +174,5 @@ def get_next(c):
next = next

MAXSIZE = sys.maxsize # NOQA

strftime = time.strftime # NOQA
17 changes: 12 additions & 5 deletions paramiko/sftp_attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import stat
import time
from paramiko.common import x80000000, o700, o70, xffffffff
from paramiko.py3compat import long, b
from paramiko.py3compat import long, PY2, strftime


class SFTPAttributes (object):
Expand Down Expand Up @@ -169,7 +169,7 @@ def _rwx(n, suid, sticky=False):
out += '-xSs'[suid + (n & 1)]
return out

def __str__(self):
def _as_text(self):
"""create a unix-style long description of the file (like ls -l)"""
if self.st_mode is not None:
kind = stat.S_IFMT(self.st_mode)
Expand Down Expand Up @@ -199,11 +199,12 @@ def __str__(self):
# shouldn't really happen
datestr = '(unknown date)'
else:
time_tuple = time.localtime(self.st_mtime)
if abs(time.time() - self.st_mtime) > 15552000:
# (15552000 = 6 months)
datestr = time.strftime('%d %b %Y', time.localtime(self.st_mtime))
datestr = strftime('%d %b %Y', time_tuple)
else:
datestr = time.strftime('%d %b %H:%M', time.localtime(self.st_mtime))
datestr = strftime('%d %b %H:%M', time_tuple)
filename = getattr(self, 'filename', '?')

# not all servers support uid/gid
Expand All @@ -220,4 +221,10 @@ def __str__(self):
return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, size, datestr, filename)

def asbytes(self):
return b(str(self))
return self._as_text().encode('utf-8')

if PY2:
__unicode__ = _as_text
__str__ = asbytes
else:
__str__ = _as_text
87 changes: 87 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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 functools
import locale
import os
import unittest

from paramiko.py3compat import (
builtins,
)


skip = getattr(unittest, "skip", None)
if skip is None:
def skip(reason):
"""Stub skip decorator for Python 2.6 compatibility."""
return lambda func: None


def skipUnlessBuiltin(name):
"""Skip decorated test if builtin name does not exist."""
if getattr(builtins, name, None) is None:
return skip("No builtin " + repr(name))
return lambda func: func


# List of locales which have non-ascii characters in all categories.
# Omits most European languages which for instance may have only some months
# with names that include accented characters.
_non_ascii_locales = [
# East Asian locales
"ja_JP", "ko_KR", "zh_CN", "zh_TW",
# European locales with non-latin alphabets
"el_GR", "ru_RU", "uk_UA",
]
# Also include UTF-8 versions of these locales
_non_ascii_locales.extend([name + ".utf8" for name in _non_ascii_locales])


def requireNonAsciiLocale(category_name="LC_ALL"):
"""Run decorated test under a non-ascii locale or skip if not possible."""
if os.name != "posix":
return skip("Non-posix OSes don't really use C locales")
cat = getattr(locale, category_name)
return functools.partial(_decorate_with_locale, cat, _non_ascii_locales)


def _decorate_with_locale(category, try_locales, test_method):
"""Decorate test_method to run after switching to a different locale."""

def _test_under_locale(testself):
original = locale.setlocale(category)
while try_locales:
try:
locale.setlocale(category, try_locales[0])
except locale.Error:
# Mutating original list is ok, setlocale would keep failing
try_locales.pop(0)
else:
try:
return test_method(testself)
finally:
locale.setlocale(category, original)
skipTest = getattr(testself, "skipTest", None)
if skipTest is not None:
skipTest("No usable locales installed")

functools.update_wrapper(_test_under_locale, test_method)
return _test_under_locale
17 changes: 17 additions & 0 deletions tests/test_sftp.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import paramiko
from paramiko.py3compat import PY2, b, u, StringIO
from paramiko.common import o777, o600, o666, o644
from tests import requireNonAsciiLocale
from tests.stub_sftp import StubServer, StubSFTPServer
from tests.loop import LoopSocket
from tests.util import test_path
Expand Down Expand Up @@ -333,6 +334,16 @@ def test_7_5_listdir_iter(self):
sftp.remove(FOLDER + '/fish.txt')
sftp.remove(FOLDER + '/tertiary.py')

@requireNonAsciiLocale()
def test_listdir_in_locale(self):
"""Test listdir under a locale that uses non-ascii text."""
sftp.open(FOLDER + '/canard.txt', 'w').close()
try:
folder_contents = sftp.listdir(FOLDER)
self.assertEqual(['canard.txt'], folder_contents)
finally:
sftp.remove(FOLDER + '/canard.txt')

def test_8_setstat(self):
"""
verify that the setstat functions (chown, chmod, utime, truncate) work.
Expand Down Expand Up @@ -812,6 +823,12 @@ def test_O_non_utf8_data(self):
finally:
sftp.remove('%s/nonutf8data' % FOLDER)

@requireNonAsciiLocale('LC_TIME')
def test_sftp_attributes_locale_time(self):
"""Test SFTPAttributes under a locale with non-ascii time strings."""
some_stat = os.stat(FOLDER)
sftp_attributes = SFTPAttributes.from_stat(some_stat, u('a_directory'))
self.assertTrue(b'a_directory' in sftp_attributes.asbytes())

def test_sftp_attributes_empty_str(self):
sftp_attributes = SFTPAttributes()
Expand Down

0 comments on commit b567d54

Please sign in to comment.