Skip to content

Commit 5897cbb

Browse files
committed
Merge in non-conflicting changes from ba67e397f063
including workaround for: https://bugs.python.org/issue27777 1) cgi/client.py: override cgi.FieldStorage's make_file so that file is always created in binary/byte mode. This means that json (and xml) are bytes not strings. 2) rest.py: try harder to find dicttoxml in roundup directory or on sys.path. This just worked under python 2 but python 3 only searches sys.path by default and does not search relative like python 2. 3) test/rest_common.py: workaround for issue27777 Also removed an unneeded case insensitive dict implementation.
2 parents d4f6b84 + 2759f04 commit 5897cbb

File tree

3 files changed

+74
-24
lines changed

3 files changed

+74
-24
lines changed

roundup/cgi/client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,19 @@ def __init__(self, instance, request, env, form=None, translator=None):
375375
logger.debug("Setting CONTENT_LENGTH to 0 for method: %s",
376376
self.env['REQUEST_METHOD'])
377377

378+
# cgi.FieldStorage must save all data as
379+
# binary/bytes. This is needed for handling json and xml
380+
# data blobs under python 3. Under python 2, str and binary
381+
# are interchangable, not so under 3.
382+
def make_file(self):
383+
''' work around https://bugs.python.org/issue27777 '''
384+
import tempfile
385+
return tempfile.TemporaryFile("wb+")
386+
387+
saved_make_file = cgi.FieldStorage.make_file
388+
cgi.FieldStorage.make_file = make_file
378389
self.form = cgi.FieldStorage(fp=request.rfile, environ=env)
390+
cgi.FieldStorage.make_file = saved_make_file
379391
# In some case (e.g. content-type application/xml), cgi
380392
# will not parse anything. Fake a list property in this case
381393
if self.form.list is None:

roundup/rest.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@
2020
import re
2121

2222
try:
23-
from dicttoxml import dicttoxml
23+
# if dicttoxml installed in roundup directory, use it
24+
from .dicttoxml import dicttoxml
2425
except ImportError:
25-
dicttoxml = None
26+
try:
27+
# else look in sys.path
28+
from dicttoxml import dicttoxml
29+
except ImportError:
30+
# else not supported
31+
dicttoxml = None
2632

2733
from roundup import hyperdb
2834
from roundup import date
2935
from roundup import actions
30-
from roundup.anypy.strings import bs2b, s2b
36+
from roundup.anypy.strings import bs2b, b2s
3137
from roundup.exceptions import *
3238
from roundup.cgi.exceptions import *
3339

@@ -1346,12 +1352,12 @@ def dispatch(self, method, uri, input):
13461352
"Access-Control-Allow-Methods",
13471353
"HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
13481354
)
1349-
13501355
# Is there an input.value with format json data?
13511356
# If so turn it into an object that emulates enough
13521357
# of the FieldStorge methods/props to allow a response.
13531358
content_type_header = headers.get('Content-Type', None)
1354-
if type(input.value) == str and content_type_header:
1359+
# python2 is str type, python3 is bytes
1360+
if type(input.value) in ( str, bytes ) and content_type_header:
13551361
parsed_content_type_header = content_type_header
13561362
# the structure of a content-type header
13571363
# is complex: mime-type; options(charset ...)
@@ -1366,7 +1372,7 @@ def dispatch(self, method, uri, input):
13661372
# for example.
13671373
if content_type_header.lower() == "application/json":
13681374
try:
1369-
input = SimulateFieldStorageFromJson(input.value)
1375+
input = SimulateFieldStorageFromJson(b2s(input.value))
13701376
except ValueError as msg:
13711377
output = self.error_obj(400, msg)
13721378

@@ -1404,7 +1410,7 @@ def dispatch(self, method, uri, input):
14041410

14051411
# Make output json end in a newline to
14061412
# separate from following text in logs etc..
1407-
return s2b(output + "\n")
1413+
return bs2b(output + "\n")
14081414

14091415

14101416
class RoundupJSONEncoder(json.JSONEncoder):

test/rest_common.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def tearDown(self):
8585
def get_header (self, header, not_found=None):
8686
try:
8787
return self.headers[header.lower()]
88-
except (AttributeError, KeyError):
88+
except (AttributeError, KeyError, TypeError):
8989
return not_found
9090

9191
def testGet(self):
@@ -352,7 +352,7 @@ def testEtagProcessing(self):
352352
353353
Run over header only, etag in form only, both,
354354
each one broke and no etag. Use the put command
355-
to triger the etag checking code.
355+
to trigger the etag checking code.
356356
'''
357357
for mode in ('header', 'etag', 'both',
358358
'brokenheader', 'brokenetag', 'none'):
@@ -399,14 +399,27 @@ def testEtagProcessing(self):
399399
else:
400400
self.assertEqual(self.dummy_client.response_code, 412)
401401

402+
def make_file(self, arg=None):
403+
''' work around https://bugs.python.org/issue27777 '''
404+
import tempfile
405+
return tempfile.TemporaryFile("wb+")
406+
402407
def testDispatch(self):
403408
"""
404409
run changes through rest dispatch(). This also tests
405410
sending json payload through code as dispatch is the
406411
code that changes json payload into something we can
407412
process.
408413
"""
409-
# Set joe's 'realname' using json data.
414+
415+
# Override the make_file so it is always set to binary
416+
# read mode. This is needed so we can send a json
417+
# body.
418+
saved_make_file = cgi.FieldStorage.make_file
419+
cgi.FieldStorage.make_file = self.make_file
420+
421+
# TEST #1
422+
# PUT: joe's 'realname' using json data.
410423
# simulate: /rest/data/user/<id>/realname
411424
# use etag in header
412425
etag = calculate_etag(self.db.user.getnode(self.joeid))
@@ -417,6 +430,7 @@ def testDispatch(self):
417430
}
418431
headers={"accept": "application/json",
419432
"content-type": env['CONTENT_TYPE'],
433+
"content-length": env['CONTENT_LENGTH'],
420434
"etag": etag
421435
}
422436
self.headers=headers
@@ -439,24 +453,29 @@ def testDispatch(self):
439453
'Joe Doe 1')
440454
del(self.headers)
441455

456+
# TEST #2
442457
# Set joe's 'realname' using json data.
443458
# simulate: /rest/data/user/<id>/realname
444459
# use etag in payload
445460
etag = calculate_etag(self.db.user.getnode(self.joeid))
446461
body=s2b('{ "@etag": "%s", "data": "Joe Doe 2" }'%etag)
447462
env = { "CONTENT_TYPE": "application/json",
448463
"CONTENT_LENGTH": len(body),
449-
"REQUEST_METHOD": "PUT"
464+
"REQUEST_METHOD": "PUT",
450465
}
451-
headers={"accept": "application/json",
452-
"content-type": env['CONTENT_TYPE']
453-
}
454-
self.headers=headers
466+
self.headers=None # have FieldStorage get len from env.
455467
body_file=BytesIO(body) # FieldStorage needs a file
456468
form = cgi.FieldStorage(body_file,
457-
headers=headers,
469+
headers=None,
458470
environ=env)
459471
self.server.client.request.headers.get=self.get_header
472+
473+
headers={"accept": "application/json",
474+
"content-type": env['CONTENT_TYPE'],
475+
"etag": etag
476+
}
477+
self.headers=headers # set for dispatch
478+
460479
results = self.server.dispatch('PUT',
461480
"/rest/data/user/%s/realname"%self.joeid,
462481
form)
@@ -468,12 +487,13 @@ def testDispatch(self):
468487
'Joe Doe 2')
469488
del(self.headers)
470489

490+
# TEST #3
471491
# change Joe's realname via a normal web form
472492
# This generates a FieldStorage that looks like:
473493
# FieldStorage(None, None, [])
474494
# use etag from header
475495
#
476-
# also use a GET on the uri via the dispatch to get
496+
# Also use GET on the uri via the dispatch to retrieve
477497
# the results from the db.
478498
etag = calculate_etag(self.db.user.getnode(self.joeid))
479499
headers={"etag": etag,
@@ -499,12 +519,14 @@ def testDispatch(self):
499519
self.assertEqual(json_dict['data']['link'],
500520
"http://tracker.example/cgi-bin/"
501521
"roundup.cgi/bugs/rest/data/user/3/realname")
502-
self.assertEqual(json_dict['data']['type'], "<type 'str'>")
522+
self.assertIn(json_dict['data']['type'], ("<class 'str'>",
523+
"<type 'str'>"))
503524
self.assertEqual(json_dict['data']["id"], "3")
504525
del(self.headers)
505526

506527

507-
# PATCH joe's email address with json
528+
# TEST #4
529+
# PATCH: joe's email address with json
508530
# save address so we can use it later
509531
stored_results = self.server.get_element('user', self.joeid,
510532
self.empty_form)
@@ -517,7 +539,8 @@ def testDispatch(self):
517539
"REQUEST_METHOD": "PATCH"
518540
}
519541
headers={"accept": "application/json",
520-
"content-type": env['CONTENT_TYPE']
542+
"content-type": env['CONTENT_TYPE'],
543+
"content-length": len(body)
521544
}
522545
self.headers=headers
523546
body_file=BytesIO(body) # FieldStorage needs a file
@@ -535,7 +558,7 @@ def testDispatch(self):
535558
self.assertEqual(results['data']['attributes']['address'],
536559
537560

538-
# and set it back
561+
# and set it back reusing env and headers from last test
539562
etag = calculate_etag(self.db.user.getnode(self.joeid))
540563
body=s2b('{ "address": "%s", "@etag": "%s"}'%(
541564
stored_results['data']['attributes']['address'],
@@ -557,15 +580,21 @@ def testDispatch(self):
557580
558581
del(self.headers)
559582

560-
# POST to create new issue
583+
# TEST #5
584+
# POST: create new issue
585+
# no etag needed
586+
# FIXME at some point we probably want to implement
587+
# Post Once Only, so we need to add a Post Once Exactly
588+
# test and a resubmit as well.
589+
etag = "not needed"
561590
body=b'{ "title": "foo bar", "priority": "critical" }'
562-
563591
env = { "CONTENT_TYPE": "application/json",
564592
"CONTENT_LENGTH": len(body),
565593
"REQUEST_METHOD": "POST"
566594
}
567595
headers={"accept": "application/json",
568-
"content-type": env['CONTENT_TYPE']
596+
"content-type": env['CONTENT_TYPE'],
597+
"content-length": len(body)
569598
}
570599
self.headers=headers
571600
body_file=BytesIO(body) # FieldStorage needs a file
@@ -588,6 +617,9 @@ def testDispatch(self):
588617
'foo bar')
589618
del(self.headers)
590619

620+
# reset the make_file method in the class
621+
cgi.FieldStorage.make_file = saved_make_file
622+
591623
def testPut(self):
592624
"""
593625
Change joe's 'realname'

0 commit comments

Comments
 (0)