Skip to content

Commit aa3328c

Browse files
authored
More py3 (#400)
* fix img_info tests in py3 * update transforms_t and webapp_t * py3 fixes in parameters.py * py3 fixes in webapp.py * py3 fixes for logging_t and transforms_t * transforms.py: remove unused import and comment * py3: fix resolver tests * py3: fix jp2_extractor_t * py3: authorizer tests pass * py3: fix failures on py3.4 and don't allow py3 test failures in travis anymore * authorizer: require salt config parameter to be bytes; other unicode/bytes updates * add a logger warning call if we get unicode Authorization header and encode it as utf8 * show value of 'salt' parameter if it's a ConfigError in RulesAuthorizer * don't modify dict in place and return it, just modify it; fix logging statement
1 parent e6be685 commit aa3328c

File tree

14 files changed

+122
-101
lines changed

14 files changed

+122
-101
lines changed

.travis.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ python:
2222
- 3.5
2323
- 3.6
2424

25-
matrix:
26-
allow_failures:
27-
- python: 3.4
28-
- python: 3.5
29-
- python: 3.6
30-
3125
install:
3226
- pip install -r requirements_test.txt
3327

loris/authorizer.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,9 @@ def __init__(self, config):
5353

5454
def _strip_empty_fields(self, svc):
5555
# dicts are modified in place
56-
for (k, v) in svc.items():
56+
for k, v in list(svc.items()):
5757
if not v:
5858
del svc[k]
59-
# but return it just in case
60-
return svc
6159

6260
def is_protected(self, info):
6361
"""
@@ -183,7 +181,7 @@ def __init__(self, config):
183181
self.token_secret = config['token_secret']
184182

185183
self.use_jwt = config.get('use_jwt', True)
186-
self.salt = config.get('salt', '')
184+
self.salt = config.get('salt', b'')
187185
self.roles_key = config.get('roles_key', 'roles')
188186
self.id_key = config.get('id_key', 'sub')
189187

@@ -203,6 +201,9 @@ def _validate_config(self, config):
203201
config['use_jwt']
204202
)
205203

204+
if ('salt' in config) and (not isinstance(config['salt'], bytes)):
205+
raise ConfigError('"salt" config parameter must be bytes; got %r (%s)' % (config['salt'], type(config['salt'])))
206+
206207
def kdf(self):
207208
return PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=self.salt,
208209
iterations=100000, backend=default_backend())
@@ -251,22 +252,28 @@ def _roles_from_request(self, request):
251252
origin = request.headers.get('origin', '')
252253
if not origin:
253254
origin = request.headers.get('referer', '*')
254-
origin = self.basic_origin(origin)
255+
origin = self.basic_origin(origin).encode('utf8')
255256

256-
logger.debug("Got basic origin: %s" % origin)
257+
logger.debug('Got basic origin: %s', origin)
257258

258-
if request.path.endswith("info.json"):
259+
if request.path.endswith('info.json'):
259260
token = request.headers.get('Authorization', '')
260-
token = token.replace("Bearer", '')
261+
if not isinstance(token, bytes):
262+
logger.warning('encoding Authorization header as utf8')
263+
token = token.encode('utf8')
264+
token = token.replace(b'Bearer', b'')
261265
cval = token.strip()
262266
if not cval:
263267
return []
264-
secret = "%s-%s" % (self.token_secret, origin)
268+
secret = b'-'.join([self.token_secret, origin])
265269
else:
266270
cval = request.cookies.get(self.cookie_name)
267271
if not cval:
268272
return []
269-
secret = "%s-%s" % (self.cookie_secret, origin)
273+
secret = b'-'.join([self.cookie_secret, origin])
274+
275+
if not isinstance(cval, bytes):
276+
cval = cval.encode('utf8')
270277

271278
if self.use_jwt:
272279
try:
@@ -276,8 +283,7 @@ def _roles_from_request(self, request):
276283
logger.debug(value)
277284
raise AuthorizerException(message="invalidCredentials: expired")
278285
else:
279-
cval = cval.encode('utf-8')
280-
key = base64.urlsafe_b64encode(self.kdf().derive(secret.encode('utf-8')))
286+
key = base64.urlsafe_b64encode(self.kdf().derive(secret))
281287
fern = Fernet(key)
282288
value = fern.decrypt(cval)
283289
if not value.startswith(origin):
@@ -286,6 +292,10 @@ def _roles_from_request(self, request):
286292
else:
287293
value = value[len(origin)+1:]
288294

295+
try:
296+
value = value.decode('utf8')
297+
except AttributeError:
298+
pass
289299
roles = self._roles_from_value(value)
290300
return roles
291301

loris/img_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def _from_jp2(self, fp):
230230
self.extract_jp2(jp2)
231231
except JP2ExtractionError as err:
232232
logger.warning(
233-
"Error extracting JP2 %s: %r", fp, err.message
233+
"Error extracting JP2 %s: %r", fp, str(err)
234234
)
235235
raise ImageInfoException("Invalid JP2 file")
236236

loris/jp2_extractor.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,11 @@ def extract_jp2(self, jp2):
329329

330330
scaleFactors = []
331331

332-
window = deque(jp2.read(2), 2)
332+
window = deque(jp2.read(2), 2)
333333
# start of codestream
334-
while map(ord, window) != [0xFF, 0x4F]: # (SOC - required, see pg 14)
334+
while ((window[0] != b'\xFF') or (window[1] != b'\x4F')): # (SOC - required, see pg 14)
335335
window.append(jp2.read(1))
336-
while map(ord, window) != [0xFF, 0x51]: # (SIZ - required, see pg 14)
336+
while ((window[0] != b'\xFF') or (window[1] != b'\x51')): # (SIZ - required, see pg 14)
337337
window.append(jp2.read(1))
338338
jp2.read(20) # through Lsiz (16), Rsiz (16), Xsiz (32), Ysiz (32), XOsiz (32), YOsiz (32)
339339
tile_width = int(struct.unpack(">I", jp2.read(4))[0]) # XTsiz (32)
@@ -345,10 +345,8 @@ def extract_jp2(self, jp2):
345345
self.tiles[0]['height'] = tile_height
346346
jp2.read(10) # XTOsiz (32), YTOsiz (32), Csiz (16)
347347

348-
window = deque(jp2.read(2), 2)
349-
# while (ord(b) != 0xFF): b = jp2.read(1)
350-
# b = jp2.read(1) # 0x52: The COD marker segment
351-
while map(ord, window) != [0xFF, 0x52]: # (COD - required, see pg 14)
348+
window = deque(jp2.read(2), 2)
349+
while ((window[0] != b'\xFF') or (window[1] != b'\x52')): # (COD - required, see pg 14)
352350
window.append(jp2.read(1))
353351

354352
jp2.read(7) # through Lcod (16), Scod (8), SGcod (32)

loris/parameters.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def _canonicalize(self):
9898
# all adjustments have been made.
9999
if self.mode != FULL_MODE:
100100
px = (self.pixel_x, self.pixel_y, self.pixel_w, self.pixel_h)
101-
self.canonical_uri_value = ','.join(map(str, px))
101+
self.canonical_uri_value = ','.join([str(p) for p in px])
102102
else:
103103
self.canonical_uri_value = FULL_MODE
104104
logger.debug('canonical uri_value for region %s', self.canonical_uri_value)
@@ -150,7 +150,7 @@ def _populate_slots_from_pct(self):
150150
RequestException
151151
'''
152152
# we convert these to pixels and update uri_value
153-
dimensions = map(float, self.uri_value.split(':')[1].split(','))
153+
dimensions = [float(x) for x in self.uri_value.split(':')[1].split(',')]
154154

155155
if len(dimensions) != 4:
156156
raise SyntaxException("Exactly (4) coordinates must be supplied.")
@@ -165,7 +165,7 @@ def _populate_slots_from_pct(self):
165165

166166
# decimals
167167
self.decimal_x, self.decimal_y, self.decimal_w, \
168-
self.decimal_h = map(RegionParameter._pct_to_decimal, dimensions)
168+
self.decimal_h = [RegionParameter._pct_to_decimal(d) for d in dimensions]
169169

170170
# pixels
171171
self.pixel_x = int(floor(self.decimal_x * self.img_info.width))
@@ -179,16 +179,17 @@ def _populate_slots_for_square(self):
179179
RequestException
180180
SyntaxException
181181
'''
182+
#dimensions must be ints, for passing to _populate_slots_from_pixels
182183
if self.img_info.width > self.img_info.height:
183-
offset = (self.img_info.width - self.img_info.height) / 2
184+
offset = (self.img_info.width - self.img_info.height) // 2
184185
dimensions = (offset, 0, self.img_info.height, self.img_info.height)
185186
else:
186-
offset = (self.img_info.height - self.img_info.width) / 2
187+
offset = (self.img_info.height - self.img_info.width) // 2
187188
dimensions = (0, offset, self.img_info.width, self.img_info.width)
188189
return self._populate_slots_from_pixels(dimensions)
189190

190191
def _pixel_dims_to_ints(self):
191-
dimensions = map(int, self.uri_value.split(','))
192+
dimensions = [int(d) for d in self.uri_value.split(',')]
192193
if any(n <= 0 for n in dimensions[2:]):
193194
raise RequestException("Width and height must be greater than 0.")
194195
if len(dimensions) != 4:
@@ -199,10 +200,10 @@ def _populate_slots_from_pixels(self, dimensions):
199200
# pixels
200201
self.pixel_x, self.pixel_y, self.pixel_w, self.pixel_h = dimensions
201202
# decimals
202-
self.decimal_x = self.pixel_x / Decimal(str(self.img_info.width))
203-
self.decimal_y = self.pixel_y / Decimal(str(self.img_info.height))
204-
self.decimal_w = self.pixel_w / Decimal(str(self.img_info.width))
205-
self.decimal_h = self.pixel_h / Decimal(str(self.img_info.height))
203+
self.decimal_x = Decimal(self.pixel_x) / Decimal(str(self.img_info.width))
204+
self.decimal_y = Decimal(self.pixel_y) / Decimal(str(self.img_info.height))
205+
self.decimal_w = Decimal(self.pixel_w) / Decimal(str(self.img_info.width))
206+
self.decimal_h = Decimal(self.pixel_h) / Decimal(str(self.img_info.height))
206207

207208
@staticmethod
208209
def _mode_from_region_segment(region_segment, img_info):
@@ -243,6 +244,7 @@ def _mode_from_region_segment(region_segment, img_info):
243244
def _pct_to_decimal(n):
244245
return Decimal(str(n)) / Decimal('100.0')
245246

247+
246248
class SizeParameter(object):
247249
'''Internal representation of the size slice of an IIIF image URI.
248250
@@ -348,13 +350,13 @@ def _populate_slots_from_pixels(self, region_parameter):
348350

349351
if (not best_fit) and request_w and (not request_h):
350352
self.force_aspect = False
351-
self.w = request_w
353+
self.w = Decimal(request_w)
352354
reduce_by = Decimal(request_w) / region_parameter.pixel_w
353355
self.h = region_parameter.pixel_h * reduce_by
354356

355357
elif (not best_fit) and (not request_w) and request_h:
356358
self.force_aspect = False
357-
self.h = request_h
359+
self.h = Decimal(request_h)
358360
reduce_by = Decimal(request_h) / region_parameter.pixel_h
359361
self.w = region_parameter.pixel_w * reduce_by
360362

@@ -369,17 +371,17 @@ def _populate_slots_from_pixels(self, region_parameter):
369371

370372
elif request_w and request_h:
371373
self.force_aspect = True
372-
self.w = request_w
373-
self.h = request_h
374+
self.w = Decimal(request_w)
375+
self.h = Decimal(request_h)
374376

375377
else: # pragma: no cover
376378
assert False, "Incomplete size data in URI: %r" % self.uri_value
377379

378-
if self.h < 1:
380+
if 0 < self.h < DECIMAL_ONE:
379381
self.h = 1
380382
else:
381383
self.h = int(self.h)
382-
if self.w < 1:
384+
if 0 < self.w < DECIMAL_ONE:
383385
self.w = 1
384386
else:
385387
self.w = int(self.w)

loris/resolver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def _cache_subroot(ident):
304304
@staticmethod
305305
def _ident_file_structure(ident):
306306
file_structure = ''
307-
ident_hash = hashlib.md5(quote_plus(ident)).hexdigest()
307+
ident_hash = hashlib.md5(quote_plus(ident).encode('utf8')).hexdigest()
308308
# First level 2 digit directory then do three digits...
309309
file_structure_list = [ident_hash[0:2]] + [ident_hash[i:i+3] for i in range(2, len(ident_hash), 3)]
310310

loris/webapp.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ def _load_transformers(self):
353353
transformers = {}
354354
for sf in source_formats:
355355
# merge [transforms] options and [transforms][source_format]] options
356-
config = dict(self.app_configs['transforms'][sf].items() + global_tranform_options.items())
356+
config = dict(list(self.app_configs['transforms'][sf].items()) + list(global_tranform_options.items()))
357357
transformers[sf] = self._load_transformer(config)
358358
return transformers
359359

@@ -451,7 +451,7 @@ def get_index(self, request):
451451
'''
452452
Just so there's something at /.
453453
'''
454-
f = file(path.join(self.www_dp, 'index.txt'))
454+
f = open(path.join(self.www_dp, 'index.txt'), 'rb')
455455
r = Response(f, content_type='text/plain')
456456
if self.enable_caching:
457457
r.add_etag()
@@ -460,7 +460,7 @@ def get_index(self, request):
460460

461461
def get_favicon(self, request):
462462
f = path.join(self.www_dp, 'icons', 'loris-icon.png')
463-
r = Response(file(f), content_type='image/x-icon')
463+
r = Response(open(f, 'rb'), content_type='image/x-icon')
464464
if self.enable_caching:
465465
r.add_etag()
466466
r.make_conditional(request)
@@ -470,9 +470,9 @@ def get_info(self, request, ident, base_uri):
470470
try:
471471
info, last_mod = self._get_info(ident,request,base_uri)
472472
except ResolverException as re:
473-
return NotFoundResponse(re.message)
473+
return NotFoundResponse(str(re))
474474
except ImageInfoException as ie:
475-
return ServerSideErrorResponse(ie.message)
475+
return ServerSideErrorResponse(str(ie))
476476
except IOError as e:
477477
msg = '%s \n(This is likely a permissions problem)' % e
478478
return ServerSideErrorResponse(msg)
@@ -586,7 +586,7 @@ def get_img(self, request, ident, region, size, rotation, quality, target_fmt, b
586586
# ... still cheaper than always resolving as likely to be cached
587587
info = self._get_info(ident, request, base_uri)[0]
588588
except ResolverException as re:
589-
return NotFoundResponse(re.message)
589+
return NotFoundResponse(str(re))
590590

591591
if self.authorizer and self.authorizer.is_protected(info):
592592
authed = self.authorizer.is_authorized(info, request)
@@ -615,7 +615,7 @@ def get_img(self, request, ident, region, size, rotation, quality, target_fmt, b
615615
r.status_code = 200
616616
r.last_modified = img_last_mod
617617
r.headers['Content-Length'] = path.getsize(fp)
618-
r.response = file(fp)
618+
r.response = open(fp, 'rb')
619619

620620
# hand the Image object its info
621621
info = self._get_info(ident, request, base_uri)[0]
@@ -651,11 +651,11 @@ def get_img(self, request, ident, region, size, rotation, quality, target_fmt, b
651651
fp = self._make_image(image_request, info.src_img_fp, info.src_format)
652652

653653
except ResolverException as re:
654-
return NotFoundResponse(re.message)
654+
return NotFoundResponse(str(re))
655655
except TransformException as te:
656656
return ServerSideErrorResponse(te)
657657
except (RequestException, SyntaxException) as e:
658-
return BadRequestResponse(e.message)
658+
return BadRequestResponse(str(e))
659659
except (ImageException,ImageInfoException) as ie:
660660
# 500s!
661661
# ImageException is only raised in when ImageRequest.info
@@ -679,7 +679,7 @@ def get_img(self, request, ident, region, size, rotation, quality, target_fmt, b
679679
r.last_modified = datetime.utcfromtimestamp(path.getctime(fp))
680680
r.headers['Content-Length'] = path.getsize(fp)
681681
self._set_canonical_link(request, image_request, r)
682-
r.response = file(fp)
682+
r.response = open(fp, 'rb')
683683

684684
if not self.enable_caching:
685685
r.call_on_close(lambda: unlink(fp))

0 commit comments

Comments
 (0)