Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.

Commit cec17ae

Browse files
committed
Bug fix: csv parsing
1 parent d8982c1 commit cec17ae

2 files changed

Lines changed: 91 additions & 49 deletions

File tree

runestone/spreadsheet/spreadsheet.py

Lines changed: 85 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,35 @@
1313
# You should have received a copy of the GNU General Public License
1414
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
#
16-
__author__ = 'bmiller'
16+
__author__ = "bmiller"
1717

18-
import re, os
18+
import csv
19+
import io
20+
import os
21+
import re
1922
from docutils import nodes
2023
from docutils.parsers.rst import directives
21-
from runestone.common.runestonedirective import RunestoneNode, RunestoneIdDirective, get_node_line
24+
from runestone.common.runestonedirective import (
25+
RunestoneNode,
26+
RunestoneIdDirective,
27+
get_node_line,
28+
)
2229
from runestone.server.componentdb import addQuestionToDB, addHTMLToDB
2330

2431
#
2532
# setup is called when extensions are loaded. This registers the new directive and
2633
# logs any js or css files that should be loaded for this extension.
2734
#
2835
def setup(app):
29-
app.add_directive('spreadsheet',SpreadSheet)
36+
app.add_directive("spreadsheet", SpreadSheet)
3037

31-
app.add_autoversioned_javascript('spreadsheet.js')
32-
app.add_javascript('jexcel.js')
33-
app.add_javascript('japp.js')
38+
app.add_autoversioned_javascript("spreadsheet.js")
39+
app.add_javascript("jexcel.js")
40+
app.add_javascript("japp.js")
3441

35-
app.add_autoversioned_stylesheet('spreadsheet.css')
36-
app.add_stylesheet('jexcel.css')
37-
app.add_stylesheet('japp.css')
42+
app.add_autoversioned_stylesheet("spreadsheet.css")
43+
app.add_stylesheet("jexcel.css")
44+
app.add_stylesheet("japp.css")
3845

3946
app.add_node(SpreadSheetNode, html=(visit_ss_node, depart_ss_node))
4047

@@ -54,6 +61,7 @@ def __init__(self, content, **kwargs):
5461
super(SpreadSheetNode, self).__init__(**kwargs)
5562
self.ss_options = content
5663

64+
5765
#
5866
# The spreadsheet class implements the directive.
5967
# When the directive is processed the run method is called.
@@ -71,81 +79,104 @@ class SpreadSheet(RunestoneIdDirective):
7179
A1,B1,C1,D1...
7280
A2,B2,C2,D2...
7381
"""
82+
7483
required_arguments = 1
7584
optional_arguments = 5
7685
has_content = True
7786
option_spec = RunestoneIdDirective.option_spec.copy()
78-
option_spec.update({
79-
'fromcsv': directives.unchanged,
80-
'colwidths': directives.unchanged,
81-
'coltitles': directives.unchanged,
82-
'mindimensions': directives.unchanged
83-
})
84-
87+
option_spec.update(
88+
{
89+
"fromcsv": directives.unchanged,
90+
"colwidths": directives.unchanged,
91+
"coltitles": directives.unchanged,
92+
"mindimensions": directives.unchanged,
93+
}
94+
)
8595

8696
def run(self):
8797
super(SpreadSheet, self).run()
8898
env = self.state.document.settings.env
8999

90-
self.options['divid'] = self.arguments[0].strip()
100+
self.options["divid"] = self.arguments[0].strip()
91101

92-
if '====' in self.content:
93-
idx = self.content.index('====')
94-
suffix = self.content[idx+1:]
95-
self.options['asserts'] = suffix
96-
self.options['autograde'] = 'data-autograde="true"'
102+
if "====" in self.content:
103+
idx = self.content.index("====")
104+
suffix = self.content[idx + 1 :]
105+
self.options["asserts"] = suffix
106+
self.options["autograde"] = 'data-autograde="true"'
97107
else:
98-
self.options['asserts'] = '""'
99-
self.options['autograde'] = ''
108+
self.options["asserts"] = '""'
109+
self.options["autograde"] = ""
100110

101-
if 'fromcsv' in self.options:
102-
self.content = self.body_from_csv(env, self.options['fromcsv'])
111+
if "fromcsv" in self.options:
112+
self.content = self.body_from_csv(env, self.options["fromcsv"])
103113
else:
104114
if self.content:
105115
self.content = self.body_to_csv(self.content)
106116
else:
107-
raise ValueError("You must specify either from csv or provide content in the body")
117+
raise ValueError(
118+
"You must specify either from csv or provide content in the body"
119+
)
108120

109-
self.options['data'] = self.content
121+
self.options["data"] = self.content
110122

111-
if 'coltitles' not in self.options:
112-
self.options['coltitles'] = ""
123+
if "coltitles" not in self.options:
124+
self.options["coltitles"] = ""
113125
else:
114-
self.options['coltitles'] = "data-coltitles=[{}]".format(self.options['coltitles'])
126+
self.options["coltitles"] = "data-coltitles=[{}]".format(
127+
",".join([x.strip() for x in self.options["coltitles"].split(",")])
128+
)
115129

116-
if 'mindimensions' not in self.options:
117-
self.options['mindimensions'] = ""
130+
if "mindimensions" not in self.options:
131+
self.options["mindimensions"] = ""
118132
else:
119-
self.options['mindimensions'] = "data-mindimensions=[{}]".format(self.options['mindimensions'])
133+
self.options["mindimensions"] = "data-mindimensions=[{}]".format(
134+
",".join([x.strip() for x in self.options["mindimensions"].split(",")])
135+
)
120136

121-
if 'colwidths' not in self.options:
122-
self.options['colwidths'] = ""
137+
if "colwidths" not in self.options:
138+
self.options["colwidths"] = ""
123139
else:
124-
self.options['colwidths'] = "data-colwidths=[{}]".format(self.options['colwidths'])
140+
self.options["colwidths"] = "data-colwidths=[{}]".format(
141+
",".join([x.strip() for x in self.options["colwidths"].split(",")])
142+
)
125143

126144
ssnode = SpreadSheetNode(self.options, rawsource=self.block_text)
127145
ssnode.source, ssnode.line = self.state_machine.get_source_and_line(self.lineno)
128-
self.add_name(ssnode) # make this divid available as a target for :ref:
146+
self.add_name(ssnode) # make this divid available as a target for :ref:
129147

130148
return [ssnode]
131149

150+
def body_to_csv(self, row_list):
151+
"""
152+
Use the csv reader and writer functionality to better parse the body.
132153
154+
1. Convert the contents to a StringIO object
155+
2. Then read and process using a csv reader
156+
3. Formulas with ,'s in them should be in double quotes, if there are "'s in the
157+
cell then they should be ""-ed
158+
"""
133159

134-
def body_to_csv(self, row_list):
135160
csvlist = []
161+
body_list = []
136162
for row in row_list:
137-
if re.match(r'^\s*====', row):
163+
if re.match(r"^\s*====", row):
138164
break
139-
items = row.split(',')
165+
body_list.append(row)
166+
body_file = io.StringIO("\n".join(body_list))
167+
body_reader = csv.reader(body_file)
168+
for row in body_reader:
140169
ilist = []
141-
for item in items:
170+
for item in row:
142171
item = item.strip()
172+
if item and item[0] == '"' and item[-1] == '"':
173+
item = item[1:-1]
143174
if is_float(item):
144175
ilist.append(as_int_or_float(item))
145176
elif item.startswith("="):
146-
ilist.append('{}'.format(item.upper()))
177+
ilist.append("{}".format(item.upper()))
147178
else:
148-
ilist.append('{}'.format(item))
179+
ilist.append("{}".format(item))
149180
csvlist.append(ilist)
150181
return csvlist
151182

@@ -155,18 +186,21 @@ def body_from_csv(self, env, csvfile):
155186
filename = os.path.join(env.srcdir, ffpath, csvfile)
156187

157188
print("\n\nPATH=", self.srcpath)
158-
with open(filename,'r') as csv:
189+
with open(filename, "r") as csv:
159190
content = csv.readlines()
160191

192+
content = [line[:-1] for line in content]
161193
return self.body_to_csv(content)
162194

195+
163196
def is_float(s):
164197
try:
165198
x = float(s)
166199
return True
167200
except:
168201
return False
169202

203+
170204
def as_int_or_float(s):
171205
try:
172206
x = int(s)
@@ -187,9 +221,11 @@ def as_int_or_float(s):
187221
</div>
188222
"""
189223

190-
def visit_ss_node(self,node):
224+
225+
def visit_ss_node(self, node):
191226
res = TEMPLATE.format(**node.ss_options)
192227
self.body.append(res)
193228

194-
def depart_ss_node(self,node):
195-
pass
229+
230+
def depart_ss_node(self, node):
231+
pass

runestone/spreadsheet/test/_sources/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ Cells in the spreadsheet that are graded are initially colored light blue. When
3737
====
3838
assert A151 == 150
3939

40+
41+
.. spreadsheet:: ss3
42+
:colwidths: 200,200
43+
44+
"&#61;CONCATENATE(""abc"", ""xyz"")","=CONCATENATE(""abc"", ""xyz"")"
45+
'=1+1, 2

0 commit comments

Comments
 (0)