Merge pull request #1379 from scrapy/py3-utils-python

PY3 port scrapy.utils.python
This commit is contained in:
Mikhail Korobov 2015-07-24 12:17:16 +02:00
commit 335aa5f276
3 changed files with 90 additions and 42 deletions

View File

@ -23,8 +23,12 @@ def flatten(x):
>>> [1, 2, [3,4], (5,6)]
[1, 2, [3, 4], (5, 6)]
>>> flatten([[[1,2,3], (42,None)], [4,5], [6], 7, (8,9,10)])
[1, 2, 3, 42, None, 4, 5, 6, 7, 8, 9, 10]"""
[1, 2, 3, 42, None, 4, 5, 6, 7, 8, 9, 10]
>>> flatten(["foo", "bar"])
['foo', 'bar']
>>> flatten(["foo", ["baz", 42], "bar"])
['foo', 'baz', 42, 'bar']
"""
return list(iflatten(x))
@ -32,15 +36,38 @@ def iflatten(x):
"""iflatten(sequence) -> iterator
Similar to ``.flatten()``, but returns iterator instead"""
for el in x:
if hasattr(el, "__iter__"):
if is_listlike(el):
for el_ in flatten(el):
yield el_
else:
yield el
def is_listlike(x):
"""
>>> is_listlike("foo")
False
>>> is_listlike(5)
False
>>> is_listlike(b"foo")
False
>>> is_listlike([b"foo"])
True
>>> is_listlike((b"foo",))
True
>>> is_listlike({})
True
>>> is_listlike(set())
True
>>> is_listlike((x for x in range(3)))
True
>>> is_listlike(six.moves.xrange(5))
True
"""
return hasattr(x, "__iter__") and not isinstance(x, (six.text_type, bytes))
def unique(list_, key=lambda x: x):
"""efficient function to uniquify a list preserving item order"""
seen = set()
@ -115,11 +142,14 @@ def re_rsearch(pattern, text, chunk_size=1024):
yield (text[offset:], offset)
yield (text, 0)
pattern = re.compile(pattern) if isinstance(pattern, basestring) else pattern
if isinstance(pattern, six.string_types):
pattern = re.compile(pattern)
for chunk, offset in _chunk_iter():
matches = [match for match in pattern.finditer(chunk)]
if matches:
return (offset + matches[-1].span()[0], offset + matches[-1].span()[1])
start, end = matches[-1].span()
return offset + start, offset + end
return None
@ -135,14 +165,16 @@ def memoizemethod_noargs(method):
return cache[self]
return new_method
_BINARYCHARS = set(map(chr, range(32))) - set(["\0", "\t", "\n", "\r"])
_BINARYCHARS = {six.b(chr(i)) for i in range(32)} - {b"\0", b"\t", b"\n", b"\r"}
_BINARYCHARS |= {ord(ch) for ch in _BINARYCHARS}
def isbinarytext(text):
"""Return True if the given text is considered binary, or false
"""Return True if the given text is considered binary, or False
otherwise, by looking for binary bytes at their chars
"""
assert isinstance(text, str), "text must be str, got '%s'" % type(text).__name__
if not isinstance(text, bytes):
raise TypeError("text must be bytes, got '%s'" % type(text).__name__)
return any(c in _BINARYCHARS for c in text)
@ -246,20 +278,22 @@ class WeakKeyCache(object):
return self._weakdict[key]
@deprecated
def stringify_dict(dct_or_tuples, encoding='utf-8', keys_only=True):
"""Return a (new) dict with the unicode keys (and values if, keys_only is
"""Return a (new) dict with unicode keys (and values when "keys_only" is
False) of the given dict converted to strings. `dct_or_tuples` can be a
dict or a list of tuples, like any dict constructor supports.
"""
d = {}
for k, v in six.iteritems(dict(dct_or_tuples)):
k = k.encode(encoding) if isinstance(k, unicode) else k
k = k.encode(encoding) if isinstance(k, six.text_type) else k
if not keys_only:
v = v.encode(encoding) if isinstance(v, unicode) else v
v = v.encode(encoding) if isinstance(v, six.text_type) else v
d[k] = v
return d
@deprecated
def is_writable(path):
"""Return True if the given path can be written (if it exists) or created
(if it doesn't exist)
@ -270,6 +304,7 @@ def is_writable(path):
return os.access(os.path.dirname(path), os.W_OK)
@deprecated
def setattr_default(obj, name, value):
"""Set attribute value, but only if it's not already set. Similar to
setdefault() for dicts.

View File

@ -59,7 +59,6 @@ tests/test_utils_defer.py
tests/test_utils_iterators.py
tests/test_utils_jsonrpc.py
tests/test_utils_log.py
tests/test_utils_python.py
tests/test_utils_reqser.py
tests/test_utils_request.py
tests/test_utils_response.py

View File

@ -2,6 +2,7 @@ import functools
import operator
import unittest
from itertools import count
import six
from scrapy.utils.python import (
memoizemethod_noargs, isbinarytext, equal_attributes,
@ -12,10 +13,10 @@ __doctests__ = ['scrapy.utils.python']
class ToUnicodeTest(unittest.TestCase):
def test_converting_an_utf8_encoded_string_to_unicode(self):
self.assertEqual(to_unicode('lel\xc3\xb1e'), u'lel\xf1e')
self.assertEqual(to_unicode(b'lel\xc3\xb1e'), u'lel\xf1e')
def test_converting_a_latin_1_encoded_string_to_unicode(self):
self.assertEqual(to_unicode('lel\xf1e', 'latin-1'), u'lel\xf1e')
self.assertEqual(to_unicode(b'lel\xf1e', 'latin-1'), u'lel\xf1e')
def test_converting_a_unicode_to_unicode_should_return_the_same_object(self):
self.assertEqual(to_unicode(u'\xf1e\xf1e\xf1e'), u'\xf1e\xf1e\xf1e')
@ -23,28 +24,34 @@ class ToUnicodeTest(unittest.TestCase):
def test_converting_a_strange_object_should_raise_TypeError(self):
self.assertRaises(TypeError, to_unicode, 423)
def test_check_errors_argument_works(self):
self.assertIn(u'\ufffd', to_unicode('a\xedb', 'utf-8', errors='replace'))
def test_errors_argument(self):
self.assertEqual(
to_unicode(b'a\xedb', 'utf-8', errors='replace'),
u'a\ufffdb'
)
class ToBytesTest(unittest.TestCase):
def test_converting_a_unicode_object_to_an_utf_8_encoded_string(self):
self.assertEqual(to_bytes(u'\xa3 49'), '\xc2\xa3 49')
self.assertEqual(to_bytes(u'\xa3 49'), b'\xc2\xa3 49')
def test_converting_a_unicode_object_to_a_latin_1_encoded_string(self):
self.assertEqual(to_bytes(u'\xa3 49', 'latin-1'), '\xa3 49')
self.assertEqual(to_bytes(u'\xa3 49', 'latin-1'), b'\xa3 49')
def test_converting_a_regular_string_to_string_should_return_the_same_object(self):
self.assertEqual(to_bytes('lel\xf1e'), 'lel\xf1e')
def test_converting_a_regular_bytes_to_bytes_should_return_the_same_object(self):
self.assertEqual(to_bytes(b'lel\xf1e'), b'lel\xf1e')
def test_converting_a_strange_object_should_raise_TypeError(self):
self.assertRaises(TypeError, to_bytes, unittest)
def test_check_errors_argument_works(self):
self.assertIn('?', to_bytes(u'a\ufffdb', 'latin-1', errors='replace'))
def test_errors_argument(self):
self.assertEqual(
to_bytes(u'a\ufffdb', 'latin-1', errors='replace'),
b'a?b'
)
class UtilsPythonTestCase(unittest.TestCase):
class MemoizedMethodTest(unittest.TestCase):
def test_memoizemethod_noargs(self):
class A(object):
@ -62,19 +69,23 @@ class UtilsPythonTestCase(unittest.TestCase):
assert one is two
assert one is not three
class IsBinaryTextTest(unittest.TestCase):
def test_isbinarytext(self):
assert not isbinarytext(b"hello")
# basic tests
assert not isbinarytext("hello")
# utf-16 strings contain null bytes
def test_utf_16_strings_contain_null_bytes(self):
assert not isbinarytext(u"hello".encode('utf-16'))
# one with encoding
assert not isbinarytext("<div>Price \xa3</div>")
def test_one_with_encoding(self):
assert not isbinarytext(b"<div>Price \xa3</div>")
# finally some real binary bytes
assert isbinarytext("\x02\xa3")
def test_real_binary_bytes(self):
assert isbinarytext(b"\x02\xa3")
class UtilsPythonTestCase(unittest.TestCase):
def test_equal_attributes(self):
class Obj:
@ -134,29 +145,32 @@ class UtilsPythonTestCase(unittest.TestCase):
del k
self.assertFalse(len(wk._weakdict))
@unittest.skipUnless(six.PY2, "deprecated function")
def test_stringify_dict(self):
d = {'a': 123, u'b': 'c', u'd': u'e', object(): u'e'}
d = {'a': 123, u'b': b'c', u'd': u'e', object(): u'e'}
d2 = stringify_dict(d, keys_only=False)
self.assertEqual(d, d2)
self.failIf(d is d2) # shouldn't modify in place
self.failIf(any(isinstance(x, unicode) for x in d2.keys()))
self.failIf(any(isinstance(x, unicode) for x in d2.values()))
self.failIf(d is d2) # shouldn't modify in place
self.failIf(any(isinstance(x, six.text_type) for x in d2.keys()))
self.failIf(any(isinstance(x, six.text_type) for x in d2.values()))
@unittest.skipUnless(six.PY2, "deprecated function")
def test_stringify_dict_tuples(self):
tuples = [('a', 123), (u'b', 'c'), (u'd', u'e'), (object(), u'e')]
d = dict(tuples)
d2 = stringify_dict(tuples, keys_only=False)
self.assertEqual(d, d2)
self.failIf(d is d2) # shouldn't modify in place
self.failIf(any(isinstance(x, unicode) for x in d2.keys()), d2.keys())
self.failIf(any(isinstance(x, unicode) for x in d2.values()))
self.failIf(d is d2) # shouldn't modify in place
self.failIf(any(isinstance(x, six.text_type) for x in d2.keys()), d2.keys())
self.failIf(any(isinstance(x, six.text_type) for x in d2.values()))
@unittest.skipUnless(six.PY2, "deprecated function")
def test_stringify_dict_keys_only(self):
d = {'a': 123, u'b': 'c', u'd': u'e', object(): u'e'}
d2 = stringify_dict(d)
self.assertEqual(d, d2)
self.failIf(d is d2) # shouldn't modify in place
self.failIf(any(isinstance(x, unicode) for x in d2.keys()))
self.failIf(d is d2) # shouldn't modify in place
self.failIf(any(isinstance(x, six.text_type) for x in d2.keys()))
def test_get_func_args(self):
def f1(a, b, c):
@ -194,7 +208,7 @@ class UtilsPythonTestCase(unittest.TestCase):
self.assertEqual(get_func_args(object), [])
# TODO: how do we fix this to return the actual argument names?
self.assertEqual(get_func_args(unicode.split), [])
self.assertEqual(get_func_args(six.text_type.split), [])
self.assertEqual(get_func_args(" ".join), [])
self.assertEqual(get_func_args(operator.itemgetter(2)), [])