py3: misc fixes for the server

20 out of 34 tests for the server now pass
This commit is contained in:
Kovid Goyal 2019-04-14 19:22:50 +05:30
parent 2e8f88394b
commit 333dab88c2
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 60 additions and 33 deletions

View File

@ -68,12 +68,18 @@ def as_bytestring(x):
return x return x
def as_unicodestring(x):
if isinstance(x, bytes):
x = x.decode('utf-8')
return x
def md5_hex(s): def md5_hex(s):
return md5(as_bytestring(s)).hexdigest().decode('ascii') return as_unicodestring(md5(as_bytestring(s)).hexdigest())
def sha256_hex(s): def sha256_hex(s):
return sha256(as_bytestring(s)).hexdigest().decode('ascii') return as_unicodestring(sha256(as_bytestring(s)).hexdigest())
def base64_decode(s): def base64_decode(s):

View File

@ -9,7 +9,7 @@ __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import os, errno import os, errno
from io import BytesIO from io import BytesIO
from threading import Lock from threading import Lock
from polyglot.builtins import map from polyglot.builtins import map, unicode_type
from functools import partial from functools import partial
from calibre import fit_image, sanitize_file_name from calibre import fit_image, sanitize_file_name
@ -167,7 +167,7 @@ def book_filename(rd, book_id, mi, fmt, as_encoded_unicode=False):
if as_encoded_unicode: if as_encoded_unicode:
# See https://tools.ietf.org/html/rfc6266 # See https://tools.ietf.org/html/rfc6266
fname = sanitize_file_name(fname).encode('utf-8') fname = sanitize_file_name(fname).encode('utf-8')
fname = quote(fname).decode('ascii') fname = unicode_type(quote(fname))
else: else:
fname = ascii_filename(fname).replace('"', '_') fname = ascii_filename(fname).replace('"', '_')
return fname return fname

View File

@ -266,6 +266,7 @@ class HTTPRequest(Connection):
try: try:
method, uri, req_protocol = line.strip().split(b' ', 2) method, uri, req_protocol = line.strip().split(b' ', 2)
req_protocol = req_protocol.decode('ascii')
rp = int(req_protocol[5]), int(req_protocol[7]) rp = int(req_protocol[5]), int(req_protocol[7])
self.method = method.decode('ascii').upper() self.method = method.decode('ascii').upper()
except Exception: except Exception:

View File

@ -13,7 +13,7 @@ from itertools import chain, repeat
from operator import itemgetter from operator import itemgetter
from functools import wraps from functools import wraps
from polyglot.builtins import iteritems, itervalues, reraise, map, is_py3 from polyglot.builtins import iteritems, itervalues, reraise, map, is_py3, unicode_type, string_or_bytes
from calibre import guess_type, force_unicode from calibre import guess_type, force_unicode
from calibre.constants import __version__, plugins, ispy3 from calibre.constants import __version__, plugins, ispy3
@ -247,8 +247,7 @@ class RequestData(object): # {{{
def filesystem_file_with_custom_etag(self, output, *etag_parts): def filesystem_file_with_custom_etag(self, output, *etag_parts):
etag = hashlib.sha1() etag = hashlib.sha1()
string = type('') tuple(map(lambda x:etag.update(unicode_type(x).encode('utf-8')), etag_parts))
tuple(map(lambda x:etag.update(string(x)), etag_parts))
return ETaggedFile(output, etag.hexdigest()) return ETaggedFile(output, etag.hexdigest())
def filesystem_file_with_constant_etag(self, output, etag_as_hexencoded_string): def filesystem_file_with_constant_etag(self, output, etag_as_hexencoded_string):
@ -312,7 +311,10 @@ class ReadableOutput(object):
def filesystem_file_output(output, outheaders, stat_result): def filesystem_file_output(output, outheaders, stat_result):
etag = getattr(output, 'etag', None) etag = getattr(output, 'etag', None)
if etag is None: if etag is None:
etag = hashlib.sha1(type('')(stat_result.st_mtime) + force_unicode(output.name or '')).hexdigest() oname = output.name or ''
if not isinstance(oname, string_or_bytes):
oname = unicode_type(oname)
etag = hashlib.sha1((unicode_type(stat_result.st_mtime) + force_unicode(oname)).encode('utf-8')).hexdigest()
else: else:
output = output.output output = output.output
etag = '"%s"' % etag etag = '"%s"' % etag
@ -356,7 +358,7 @@ class GeneratedOutput(object):
class StaticOutput(object): class StaticOutput(object):
def __init__(self, data): def __init__(self, data):
if isinstance(data, type('')): if isinstance(data, unicode_type):
data = data.encode('utf-8') data = data.encode('utf-8')
self.data = data self.data = data
self.etag = '"%s"' % hashlib.sha1(data).hexdigest() self.etag = '"%s"' % hashlib.sha1(data).hexdigest()
@ -649,12 +651,17 @@ class HTTPConnection(HTTPRequest):
if stat_result is not None: if stat_result is not None:
output = filesystem_file_output(output, outheaders, stat_result) output = filesystem_file_output(output, outheaders, stat_result)
if 'Content-Type' not in outheaders: if 'Content-Type' not in outheaders:
mt = guess_type(output.name)[0] output_name = output.name
if not isinstance(output_name, string_or_bytes):
output_name = unicode_type(output_name)
mt = guess_type(output_name)[0]
if mt: if mt:
if mt in {'text/plain', 'text/html', 'application/javascript', 'text/css'}: if mt in {'text/plain', 'text/html', 'application/javascript', 'text/css'}:
mt += '; charset=UTF-8' mt += '; charset=UTF-8'
outheaders['Content-Type'] = mt outheaders['Content-Type'] = mt
elif isinstance(output, (bytes, type(''))): else:
outheaders['Content-Type'] = 'application/octet-stream'
elif isinstance(output, string_or_bytes):
output = dynamic_output(output, outheaders) output = dynamic_output(output, outheaders)
elif hasattr(output, 'read'): elif hasattr(output, 'read'):
output = ReadableOutput(output) output = ReadableOutput(output)

View File

@ -168,7 +168,7 @@ class ContentTest(LibraryBaseTest):
ae = self.assertEqual ae = self.assertEqual
def a(filename, data=None, status=OK, method='POST', username='12', add_duplicates='n', job_id=1): def a(filename, data=None, status=OK, method='POST', username='12', add_duplicates='n', job_id=1):
r, data = make_request(conn, '/cdb/add-book/{}/{}/{}'.format(job_id, add_duplicates, quote(filename.encode('utf-8')).decode('ascii')), r, data = make_request(conn, '/cdb/add-book/{}/{}/{}'.format(job_id, add_duplicates, quote(filename.encode('utf-8'))),
username=username, password='test', prefix='', method=method, data=data) username=username, password='test', prefix='', method=method, data=data)
ae(status, r.status) ae(status, r.status)
return data return data

View File

@ -43,8 +43,8 @@ class ContentTest(LibraryBaseTest):
missing('/%s/C:/out.html' % prefix, b'Naughty, naughty!') missing('/%s/C:/out.html' % prefix, b'Naughty, naughty!')
def test_response(r): def test_response(r):
self.assertIn(b'max-age=', r.getheader('Cache-Control')) self.assertIn('max-age=', r.getheader('Cache-Control'))
self.assertIn(b'public', r.getheader('Cache-Control')) self.assertIn('public', r.getheader('Cache-Control'))
self.assertIsNotNone(r.getheader('Expires')) self.assertIsNotNone(r.getheader('Expires'))
self.assertIsNotNone(r.getheader('ETag')) self.assertIsNotNone(r.getheader('ETag'))
self.assertIsNotNone(r.getheader('Content-Type')) self.assertIsNotNone(r.getheader('Content-Type'))
@ -182,7 +182,7 @@ class ContentTest(LibraryBaseTest):
self.ae(r.status, http_client.OK) self.ae(r.status, http_client.OK)
self.ae(data, db.cover(2)) self.ae(data, db.cover(2))
self.ae(r.getheader('Used-Cache'), 'no') self.ae(r.getheader('Used-Cache'), 'no')
path = from_hex_unicode(r.getheader('Tempfile')).decode('utf-8') path = from_hex_unicode(r.getheader('Tempfile'))
f, fdata = share_open(path, 'rb'), data f, fdata = share_open(path, 'rb'), data
# Now force an update # Now force an update
change_cover(1) change_cover(1)
@ -190,7 +190,7 @@ class ContentTest(LibraryBaseTest):
self.ae(r.status, http_client.OK) self.ae(r.status, http_client.OK)
self.ae(data, db.cover(2)) self.ae(data, db.cover(2))
self.ae(r.getheader('Used-Cache'), 'no') self.ae(r.getheader('Used-Cache'), 'no')
path = from_hex_unicode(r.getheader('Tempfile')).decode('utf-8') path = from_hex_unicode(r.getheader('Tempfile'))
f2, f2data = share_open(path, 'rb'), data f2, f2data = share_open(path, 'rb'), data
# Do it again # Do it again
change_cover(2) change_cover(2)

View File

@ -175,7 +175,7 @@ class TestHTTP(BaseTest):
self.ae(r.getheader('Content-Length'), str(len(body))) self.ae(r.getheader('Content-Length'), str(len(body)))
self.ae(r.getheader('Content-Type'), 'text/plain; charset=UTF-8') self.ae(r.getheader('Content-Type'), 'text/plain; charset=UTF-8')
self.ae(len(r.getheaders()), 3) self.ae(len(r.getheaders()), 3)
self.ae(r.read(), '') self.ae(r.read(), b'')
conn.request('GET', '/choose') conn.request('GET', '/choose')
r = conn.getresponse() r = conn.getresponse()
self.ae(r.status, http_client.NOT_FOUND) self.ae(r.status, http_client.NOT_FOUND)
@ -200,7 +200,7 @@ class TestHTTP(BaseTest):
r = conn.getresponse() r = conn.getresponse()
self.ae(r.status, http_client.MOVED_PERMANENTLY) self.ae(r.status, http_client.MOVED_PERMANENTLY)
self.ae(r.getheader('Location'), '/somewhere-else') self.ae(r.getheader('Location'), '/somewhere-else')
self.ae('', r.read()) self.ae(b'', r.read())
server.change_handler(lambda data:data.path[0] + data.read().decode('ascii')) server.change_handler(lambda data:data.path[0] + data.read().decode('ascii'))
conn = server.connect(timeout=base_timeout * 5) conn = server.connect(timeout=base_timeout * 5)
@ -277,6 +277,9 @@ class TestHTTP(BaseTest):
for i in range(10): for i in range(10):
conn._HTTPConnection__state = http_client._CS_IDLE conn._HTTPConnection__state = http_client._CS_IDLE
conn.request('GET', '/%d'%i) conn.request('GET', '/%d'%i)
if ispy3:
responses.append(conn.response_class(conn.sock, method=conn._method))
else:
responses.append(conn.response_class(conn.sock, strict=conn.strict, method=conn._method)) responses.append(conn.response_class(conn.sock, strict=conn.strict, method=conn._method))
for i in range(10): for i in range(10):
r = responses[i] r = responses[i]
@ -288,7 +291,7 @@ class TestHTTP(BaseTest):
server.loop.opts.timeout = 10 # ensure socket is not closed because of timeout server.loop.opts.timeout = 10 # ensure socket is not closed because of timeout
conn.request('GET', '/close', headers={'Connection':'close'}) conn.request('GET', '/close', headers={'Connection':'close'})
r = conn.getresponse() r = conn.getresponse()
self.ae(r.status, 200), self.ae(r.read(), 'close') self.ae(r.status, 200), self.ae(r.read(), b'close')
server.loop.wakeup() server.loop.wakeup()
num = 10 num = 10
while num and server.loop.num_active_connections != 0: while num and server.loop.num_active_connections != 0:
@ -302,8 +305,8 @@ class TestHTTP(BaseTest):
conn = server.connect(timeout=1) conn = server.connect(timeout=1)
conn.request('GET', '/something') conn.request('GET', '/something')
r = conn.getresponse() r = conn.getresponse()
self.ae(r.status, 200), self.ae(r.read(), 'something') self.ae(r.status, 200), self.ae(r.read(), b'something')
self.assertIn('Request Timeout', eintr_retry_call(conn.sock.recv, 500)) self.assertIn(b'Request Timeout', eintr_retry_call(conn.sock.recv, 500))
# }}} # }}}
def test_http_response(self): # {{{ def test_http_response(self): # {{{

View File

@ -284,13 +284,13 @@ def get_translator_for_lang(cache, bcp_47_code):
def encode_path(*components): def encode_path(*components):
'Encode the path specified as a list of path components using URL encoding' 'Encode the path specified as a list of path components using URL encoding'
return '/' + '/'.join(urlquote(x.encode('utf-8'), '').decode('ascii') for x in components) return '/' + '/'.join(urlquote(x.encode('utf-8'), '') for x in components)
class Cookie(SimpleCookie): class Cookie(SimpleCookie):
def _BaseCookie__set(self, key, real_value, coded_value): def _BaseCookie__set(self, key, real_value, coded_value):
if not isinstance(key, bytes): if not ispy3 and not isinstance(key, bytes):
key = key.encode('ascii') # Python 2.x cannot handle unicode keys key = key.encode('ascii') # Python 2.x cannot handle unicode keys
return SimpleCookie._BaseCookie__set(self, key, real_value, coded_value) return SimpleCookie._BaseCookie__set(self, key, real_value, coded_value)

View File

@ -295,7 +295,7 @@ class WebSocketConnection(HTTPConnection):
if self.method != 'GET': if self.method != 'GET':
return self.simple_response(http_client.BAD_REQUEST, 'Invalid WebSocket method: %s' % self.method) return self.simple_response(http_client.BAD_REQUEST, 'Invalid WebSocket method: %s' % self.method)
response = HANDSHAKE_STR % as_base64_unicode(sha1(key + GUID_STR).digest()) response = HANDSHAKE_STR % as_base64_unicode(sha1((key + GUID_STR).encode('utf-8')).digest())
self.optimize_for_sending_packet() self.optimize_for_sending_packet()
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.set_state(WRITE, self.upgrade_connection_to_ws, ReadOnlyFileBuffer(response.encode('ascii')), inheaders) self.set_state(WRITE, self.upgrade_connection_to_ws, ReadOnlyFileBuffer(response.encode('ascii')), inheaders)

View File

@ -7,6 +7,7 @@ from __future__ import (unicode_literals, division, absolute_import,
from struct import unpack, error from struct import unpack, error
import os import os
from calibre.utils.speedups import ReadOnlyFileBuffer from calibre.utils.speedups import ReadOnlyFileBuffer
from calibre.constants import ispy3
from polyglot.builtins import string_or_bytes from polyglot.builtins import string_or_bytes
""" Recognize image file formats and sizes based on their first few bytes.""" """ Recognize image file formats and sizes based on their first few bytes."""
@ -125,16 +126,23 @@ def jpeg_dimensions(stream):
raise ValueError('Truncated JPEG data') raise ValueError('Truncated JPEG data')
return ans return ans
x = b'' if ispy3:
def read_byte():
return read(1)[0]
else:
def read_byte():
return ord(read(1)[0])
x = None
while True: while True:
# Find next marker # Find next marker
while x != b'\xff': while x != 0xff:
x = read(1) x = read_byte()
# Soak up padding # Soak up padding
marker = b'\xff' marker = 0xff
while marker == b'\xff': while marker == 0xff:
marker = read(1) marker = read_byte()
q = ord(marker[0]) # [0] needed for memoryview q = marker
if 0xc0 <= q <= 0xcf and q != 0xc4 and q != 0xcc: if 0xc0 <= q <= 0xcf and q != 0xc4 and q != 0xcc:
# SOFn marker # SOFn marker
stream.seek(3, os.SEEK_CUR) stream.seek(3, os.SEEK_CUR)

View File

@ -100,6 +100,8 @@ def svg_path_to_painter_path(d):
while data.tell() < end: while data.tell() < end:
last_cmd = cmd last_cmd = cmd
cmd = data.read(1) if repeated_command is None else repeated_command cmd = data.read(1) if repeated_command is None else repeated_command
if isinstance(cmd, memoryview):
cmd = cmd.tobytes()
repeated_command = None repeated_command = None
if cmd == b' ': if cmd == b' ':
@ -176,7 +178,7 @@ def svg_path_to_painter_path(d):
x1, y1 = x, y x1, y1 = x, y
x, y = parse_floats(2, x, y) x, y = parse_floats(2, x, y)
path.quadTo(x1, y1, x, y) path.quadTo(x1, y1, x, y)
elif cmd[0] in b'-.' or b'0' <= cmd[0] <= b'9': elif cmd[0:1] in b'-.0123456789':
# A new number begins # A new number begins
# In this case, multiple parameters tuples are specified for the last command # In this case, multiple parameters tuples are specified for the last command
# We rewind to reparse data correctly # We rewind to reparse data correctly