Skip to content

Commit cf81e1a

Browse files
committed
Improve htmldocck.py error messages
1 parent 8eee0ef commit cf81e1a

File tree

1 file changed

+86
-54
lines changed

1 file changed

+86
-54
lines changed

src/etc/htmldocck.py

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
105105
"""
106106

107+
from __future__ import print_function
107108
import sys
108109
import os.path
109110
import re
@@ -160,8 +161,13 @@ def close(self):
160161
HTMLParser.close(self)
161162
return self.__builder.close()
162163

163-
Command = namedtuple('Command', 'negated cmd args lineno')
164+
Command = namedtuple('Command', 'negated cmd args lineno context')
164165

166+
class FailedCheck(Exception):
167+
pass
168+
169+
class InvalidCheck(Exception):
170+
pass
165171

166172
def concat_multi_lines(f):
167173
"""returns a generator out of the file object, which
@@ -196,7 +202,7 @@ def concat_multi_lines(f):
196202
catenated = ''
197203

198204
if lastline is not None:
199-
raise RuntimeError('Trailing backslash in the end of file')
205+
print_err(lineno, line, 'Trailing backslash at the end of the file')
200206

201207
LINE_PATTERN = re.compile(r'''
202208
(?<=(?<!\S)@)(?P<negated>!?)
@@ -216,9 +222,10 @@ def get_commands(template):
216222
cmd = m.group('cmd')
217223
args = m.group('args')
218224
if args and not args[:1].isspace():
219-
raise RuntimeError('Invalid template syntax at line {}'.format(lineno+1))
225+
print_err(lineno, line, 'Invalid template syntax')
226+
continue
220227
args = shlex.split(args)
221-
yield Command(negated=negated, cmd=cmd, args=args, lineno=lineno+1)
228+
yield Command(negated=negated, cmd=cmd, args=args, lineno=lineno+1, context=line)
222229

223230

224231
def _flatten(node, acc):
@@ -242,8 +249,7 @@ def normalize_xpath(path):
242249
elif path.startswith('.//'):
243250
return path
244251
else:
245-
raise RuntimeError('Non-absolute XPath is not supported due to \
246-
the implementation issue.')
252+
raise InvalidCheck('Non-absolute XPath is not supported due to implementation issues')
247253

248254

249255
class CachedFiles(object):
@@ -259,41 +265,40 @@ def resolve_path(self, path):
259265
self.last_path = path
260266
return path
261267
elif self.last_path is None:
262-
raise RuntimeError('Tried to use the previous path in the first command')
268+
raise InvalidCheck('Tried to use the previous path in the first command')
263269
else:
264270
return self.last_path
265271

266272
def get_file(self, path):
267273
path = self.resolve_path(path)
268-
try:
274+
if path in self.files:
269275
return self.files[path]
270-
except KeyError:
271-
try:
272-
with open(os.path.join(self.root, path)) as f:
273-
data = f.read()
274-
except Exception as e:
275-
raise RuntimeError('Cannot open file {!r}: {}'.format(path, e))
276-
else:
277-
self.files[path] = data
278-
return data
276+
277+
abspath = os.path.join(self.root, path)
278+
if not(os.path.exists(abspath) and os.path.isfile(abspath)):
279+
raise FailedCheck('File does not exist {!r}'.format(path))
280+
281+
with open(abspath) as f:
282+
data = f.read()
283+
self.files[path] = data
284+
return data
279285

280286
def get_tree(self, path):
281287
path = self.resolve_path(path)
282-
try:
288+
if path in self.trees:
283289
return self.trees[path]
284-
except KeyError:
285-
try:
286-
f = open(os.path.join(self.root, path))
287-
except Exception as e:
288-
raise RuntimeError('Cannot open file {!r}: {}'.format(path, e))
290+
291+
abspath = os.path.join(self.root, path)
292+
if not(os.path.exists(abspath) and os.path.isfile(abspath)):
293+
raise FailedCheck('File does not exist {!r}'.format(path))
294+
295+
with open(abspath) as f:
289296
try:
290-
with f:
291-
tree = ET.parse(f, CustomHTMLParser())
297+
tree = ET.parse(f, CustomHTMLParser())
292298
except Exception as e:
293299
raise RuntimeError('Cannot parse an HTML file {!r}: {}'.format(path, e))
294-
else:
295-
self.trees[path] = tree
296-
return self.trees[path]
300+
self.trees[path] = tree
301+
return self.trees[path]
297302

298303

299304
def check_string(data, pat, regexp):
@@ -311,14 +316,14 @@ def check_tree_attr(tree, path, attr, pat, regexp):
311316
path = normalize_xpath(path)
312317
ret = False
313318
for e in tree.findall(path):
314-
try:
319+
if attr in e.attrib:
315320
value = e.attrib[attr]
316-
except KeyError:
317-
continue
318321
else:
319-
ret = check_string(value, pat, regexp)
320-
if ret:
321-
break
322+
continue
323+
324+
ret = check_string(value, pat, regexp)
325+
if ret:
326+
break
322327
return ret
323328

324329

@@ -341,57 +346,84 @@ def check_tree_count(tree, path, count):
341346
path = normalize_xpath(path)
342347
return len(tree.findall(path)) == count
343348

349+
def stderr(*args):
350+
print(*args, file=sys.stderr)
344351

345-
def check(target, commands):
346-
cache = CachedFiles(target)
347-
for c in commands:
352+
def print_err(lineno, context, err, message=None):
353+
global ERR_COUNT
354+
ERR_COUNT += 1
355+
stderr("{}: {}".format(lineno, message or err))
356+
if message and err:
357+
stderr("\t{}".format(err))
358+
359+
if context:
360+
stderr("\t{}".format(context))
361+
362+
ERR_COUNT = 0
363+
364+
def check_command(c, cache):
365+
try:
366+
cerr = ""
348367
if c.cmd == 'has' or c.cmd == 'matches': # string test
349368
regexp = (c.cmd == 'matches')
350369
if len(c.args) == 1 and not regexp: # @has <path> = file existence
351370
try:
352371
cache.get_file(c.args[0])
353372
ret = True
354-
except RuntimeError:
373+
except FailedCheck as err:
374+
cerr = err.message
355375
ret = False
356376
elif len(c.args) == 2: # @has/matches <path> <pat> = string test
377+
cerr = "`PATTERN` did not match"
357378
ret = check_string(cache.get_file(c.args[0]), c.args[1], regexp)
358379
elif len(c.args) == 3: # @has/matches <path> <pat> <match> = XML tree test
380+
cerr = "`XPATH PATTERN` did not match"
359381
tree = cache.get_tree(c.args[0])
360382
pat, sep, attr = c.args[1].partition('/@')
361383
if sep: # attribute
362-
ret = check_tree_attr(cache.get_tree(c.args[0]), pat, attr, c.args[2], regexp)
384+
tree = cache.get_tree(c.args[0])
385+
ret = check_tree_attr(tree, pat, attr, c.args[2], regexp)
363386
else: # normalized text
364387
pat = c.args[1]
365388
if pat.endswith('/text()'):
366389
pat = pat[:-7]
367390
ret = check_tree_text(cache.get_tree(c.args[0]), pat, c.args[2], regexp)
368391
else:
369-
raise RuntimeError('Invalid number of @{} arguments \
370-
at line {}'.format(c.cmd, c.lineno))
392+
raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd))
371393

372394
elif c.cmd == 'count': # count test
373395
if len(c.args) == 3: # @count <path> <pat> <count> = count test
374396
ret = check_tree_count(cache.get_tree(c.args[0]), c.args[1], int(c.args[2]))
375397
else:
376-
raise RuntimeError('Invalid number of @{} arguments \
377-
at line {}'.format(c.cmd, c.lineno))
378-
398+
raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd))
379399
elif c.cmd == 'valid-html':
380-
raise RuntimeError('Unimplemented @valid-html at line {}'.format(c.lineno))
400+
raise InvalidCheck('Unimplemented @valid-html')
381401

382402
elif c.cmd == 'valid-links':
383-
raise RuntimeError('Unimplemented @valid-links at line {}'.format(c.lineno))
384-
403+
raise InvalidCheck('Unimplemented @valid-links')
385404
else:
386-
raise RuntimeError('Unrecognized @{} at line {}'.format(c.cmd, c.lineno))
405+
raise InvalidCheck('Unrecognized @{}'.format(c.cmd))
387406

388407
if ret == c.negated:
389-
raise RuntimeError('@{}{} check failed at line {}'.format('!' if c.negated else '',
390-
c.cmd, c.lineno))
408+
raise FailedCheck(cerr)
409+
410+
except FailedCheck as err:
411+
message = '@{}{} check failed'.format('!' if c.negated else '', c.cmd)
412+
print_err(c.lineno, c.context, err.message, message)
413+
except InvalidCheck as err:
414+
print_err(c.lineno, c.context, err.message)
415+
416+
def check(target, commands):
417+
cache = CachedFiles(target)
418+
for c in commands:
419+
check_command(c, cache)
391420

392421
if __name__ == '__main__':
393-
if len(sys.argv) < 3:
394-
print >>sys.stderr, 'Usage: {} <doc dir> <template>'.format(sys.argv[0])
422+
if len(sys.argv) != 3:
423+
stderr('Usage: {} <doc dir> <template>'.format(sys.argv[0]))
424+
raise SystemExit(1)
425+
426+
check(sys.argv[1], get_commands(sys.argv[2]))
427+
if ERR_COUNT:
428+
stderr("\nEncountered {} errors".format(ERR_COUNT))
395429
raise SystemExit(1)
396-
else:
397-
check(sys.argv[1], get_commands(sys.argv[2]))

0 commit comments

Comments
 (0)