removed obsolete issues (many of them fixed with AE)
[docutils.git] / sandbox / dpriest / csvtable / csvtable.py
blob7b69a9d54a65749f64eb6b16211cf5bc6c2ead8b
1 # Author: David Priest & David Goodger
2 # Contact: priest@sfu.ca
3 # Revision: $Revision$
4 # Date: $Date$
5 # Copyright: This module has been placed in the public domain.
7 """
8 Directive for CSV (comma-separated values) Tables.
9 """
11 import csv
12 import os.path
13 import operator
14 from docutils import nodes, statemachine, utils
15 from docutils.utils import SystemMessagePropagation
16 from docutils.transforms import references
17 from docutils.parsers.rst import directives
19 try:
20 import urllib2
21 except ImportError:
22 urllib2 = None
24 try:
25 True
26 except NameError: # Python 2.2 & 2.1 compatibility
27 True = not 0
28 False = not 1
31 class DocutilsDialect(csv.Dialect):
33 delimiter = ','
34 quotechar = '"'
35 doublequote = True
36 skipinitialspace = True
37 lineterminator = '\n'
38 quoting = csv.QUOTE_MINIMAL
40 def __init__(self, options):
41 if options.has_key('delim'):
42 self.delimiter = str(options['delim'])
43 if options.has_key('quote'):
44 self.quotechar = str(options['quote'])
45 if options.has_key('escape'):
46 self.doublequote = False
47 self.escapechar = str(options['escape'])
48 csv.Dialect.__init__(self)
51 class HeaderDialect(csv.Dialect):
53 """CSV dialect to use for the "header" option data."""
55 delimiter = ','
56 quotechar = '"'
57 escapechar = '\\'
58 doublequote = False
59 skipinitialspace = True
60 lineterminator = '\n'
61 quoting = csv.QUOTE_MINIMAL
64 def csv_table(name, arguments, options, content, lineno,
65 content_offset, block_text, state, state_machine):
67 title, messages = make_title(arguments, state, lineno)
68 try:
69 csv_data, source = get_csv_data(
70 name, options, content, lineno, block_text, state, state_machine)
71 table_head, max_header_cols = process_header_option(
72 options, state_machine, lineno)
73 rows, max_cols = parse_csv_data_into_rows(
74 csv_data, DocutilsDialect(options), source, options)
75 max_cols = max(max_cols, max_header_cols)
76 header_rows = options.get('header-rows', 0) # default 0
77 table_head.extend(rows[:header_rows])
78 table_body = rows[header_rows:]
79 if not table_body:
80 error = state_machine.reporter.error(
81 '"%s" directive requires table body content.' % name,
82 nodes.literal_block(block_text, block_text), line=lineno)
83 return [error]
84 col_widths = get_col_widths(
85 max_cols, name, options, lineno, block_text, state_machine)
86 extend_short_rows_with_empty_cells(max_cols, (table_head, table_body))
87 except SystemMessagePropagation, detail:
88 return [detail.args[0]]
89 except csv.Error, detail:
90 error = state_machine.reporter.error(
91 'Error with CSV data in "%s" directive:\n%s' % (name, detail),
92 nodes.literal_block(block_text, block_text), line=lineno)
93 return [error]
94 table = (col_widths, table_head, table_body)
95 table_node = state.build_table(table, content_offset)
96 if options.has_key('class'):
97 table_node.set_class(options['class'])
98 if title:
99 table_node.insert(0, title)
100 return [table_node] + messages
102 def make_title(arguments, state, lineno):
103 if arguments:
104 title_text = arguments[0]
105 text_nodes, messages = state.inline_text(title_text, lineno)
106 title = nodes.title(title_text, '', *text_nodes)
107 else:
108 title = None
109 messages = []
110 return title, messages
112 def get_csv_data(name, options, content, lineno, block_text,
113 state, state_machine):
114 if content: # CSV data is from directive content
115 if options.has_key('file') or options.has_key('url'):
116 error = state_machine.reporter.error(
117 '"%s" directive may not both specify an external file and '
118 'have content.' % name,
119 nodes.literal_block(block_text, block_text), line=lineno)
120 raise SystemMessagePropagation(error)
121 source = content.source(0)
122 csv_data = content
123 elif options.has_key('file'): # CSV data is from an external file
124 if options.has_key('url'):
125 error = state_machine.reporter.error(
126 'The "file" and "url" options may not be simultaneously '
127 'specified for the "%s" directive.' % name,
128 nodes.literal_block(block_text, block_text), line=lineno)
129 raise SystemMessagePropagation(error)
130 source_dir = os.path.dirname(
131 os.path.abspath(state.document.current_source))
132 source = os.path.normpath(os.path.join(source_dir, options['file']))
133 source = utils.relative_path(None, source)
134 try:
135 csv_file = open(source, 'rb')
136 try:
137 csv_data = csv_file.read().splitlines()
138 finally:
139 csv_file.close()
140 except IOError, error:
141 severe = state_machine.reporter.severe(
142 'Problems with "%s" directive path:\n%s.' % (name, error),
143 nodes.literal_block(block_text, block_text), line=lineno)
144 raise SystemMessagePropagation(severe)
145 elif options.has_key('url'): # CSV data is from a URL
146 if not urllib2:
147 severe = state_machine.reporter.severe(
148 'Problems with the "%s" directive and its "url" option: '
149 'unable to access the required functionality (from the '
150 '"urllib2" module).' % name,
151 nodes.literal_block(block_text, block_text), line=lineno)
152 raise SystemMessagePropagation(severe)
153 source = options['url']
154 try:
155 csv_data = urllib2.urlopen(source).read().splitlines()
156 except (urllib2.URLError, IOError, OSError, ValueError), error:
157 severe = state_machine.reporter.severe(
158 'Problems with "%s" directive URL "%s":\n%s.'
159 % (name, options['url'], error),
160 nodes.literal_block(block_text, block_text), line=lineno)
161 raise SystemMessagePropagation(severe)
162 else:
163 error = state_machine.reporter.warning(
164 'The "%s" directive requires content; none supplied.' % (name),
165 nodes.literal_block(block_text, block_text), line=lineno)
166 raise SystemMessagePropagation(error)
167 return csv_data, source
169 def process_header_option(options, state_machine, lineno):
170 source = state_machine.get_source(lineno - 1)
171 table_head = []
172 max_header_cols = 0
173 if options.has_key('header'): # separate table header in option
174 rows, max_header_cols = parse_csv_data_into_rows(
175 options['header'].split('\n'), HeaderDialect(), source, options)
176 table_head.extend(rows)
177 return table_head, max_header_cols
179 def parse_csv_data_into_rows(csv_data, dialect, source, options):
180 csv_reader = csv.reader(csv_data, dialect=dialect)
181 rows = []
182 max_cols = 0
183 for row in csv_reader:
184 row_data = []
185 for cell in row:
186 cell_data = (0, 0, 0, statemachine.StringList(cell.splitlines(),
187 source=source))
188 row_data.append(cell_data)
189 rows.append(row_data)
190 max_cols = max(max_cols, len(row))
191 return rows, max_cols
193 def get_col_widths(max_cols, name, options, lineno, block_text,
194 state_machine):
195 if options.has_key('widths'):
196 col_widths = options['widths']
197 if len(col_widths) != max_cols:
198 error = state_machine.reporter.error(
199 '"%s" widths do not match the number of columns in table (%s).'
200 % (name, max_cols),
201 nodes.literal_block(block_text, block_text), line=lineno)
202 raise SystemMessagePropagation(error)
203 else:
204 col_widths = [100 / max_cols] * max_cols
205 return col_widths
207 def extend_short_rows_with_empty_cells(columns, parts):
208 for part in parts:
209 for row in part:
210 if len(row) < columns:
211 row.extend([(0, 0, 0, [])] * (columns - len(row)))
213 def single_char_or_unicode(argument):
214 char = directives.unicode_code(argument)
215 if len(char) > 1:
216 raise ValueError('%r invalid; must be a single character or '
217 'a Unicode code' % char)
218 return char
220 def single_char_or_whitespace_or_unicode(argument):
221 if argument == 'tab':
222 char = '\t'
223 elif argument == 'space':
224 char = ' '
225 else:
226 char = single_char_or_unicode(argument)
227 return char
229 def positive_int(argument):
230 value = int(argument)
231 if value < 1:
232 raise ValueError('negative or zero value; must be positive')
233 return value
235 def positive_int_list(argument):
236 if ',' in argument:
237 entries = argument.split(',')
238 else:
239 entries = argument.split()
240 return [positive_int(entry) for entry in entries]
242 csv_table.arguments = (0, 1, 1)
243 csv_table.options = {'header-rows': directives.nonnegative_int,
244 'header': directives.unchanged,
245 'widths': positive_int_list,
246 'file': directives.path,
247 'url': directives.path,
248 'class': directives.class_option,
249 # field delimiter char
250 'delim': single_char_or_whitespace_or_unicode,
251 # text field quote/unquote char:
252 'quote': single_char_or_unicode,
253 # char used to escape delim & quote as-needed:
254 'escape': single_char_or_unicode,}
255 csv_table.content = 1
257 directives.register_directive('csvtable', csv_table)