Change:
path = normalize_xpath(path) return len(tree.findall(path)) == count <ins> def stderr(*args): print(*args, file=sys.stderr) </ins> <del> def check(target, commands): cache = CachedFiles(target) for c in commands: </del> <ins> def print_err(lineno, context, err, message=None): global ERR_COUNT ERR_COUNT += 1 stderr("{}: {}".format(lineno, message or err)) if message and err: stderr("t{}".format(err)) if context: stderr("t{}".format(context)) ERR_COUNT = 0 def check_command(c, cache): try: cerr = "" </ins> if c.cmd == 'has' or c.cmd == 'matches': # string test regexp = (c.cmd == 'matches') if len(c.args) == 1 and not regexp: # @has <path> = file existence try: cache.get_file(c.args[0]) ret = True <del> except RuntimeError: </del> <ins> except FailedCheck as err: cerr = err.message </ins> ret = False elif len(c.args) == 2: # @has/matches <path> <pat> = string test <ins> cerr = "`PATTERN` did not match" </ins> ret = check_string(cache.get_file(c.args[0]), c.args[1], regexp) elif len(c.args) == 3: # @has/matches <path> <pat> <match> = XML tree test <ins> cerr = "`XPATH PATTERN` did not match" </ins> tree = cache.get_tree(c.args[0]) pat, sep, attr = c.args[1].partition('/@') if sep: # attribute <del> ret = check_tree_attr(cache.get_tree(c.args[0]), pat, attr, c.args[2], regexp) </del> <ins> tree = cache.get_tree(c.args[0]) ret = check_tree_attr(tree, pat, attr, c.args[2], regexp) </ins> else: # normalized text pat = c.args[1] if pat.endswith('/text()'): pat = pat[:-7] ret = check_tree_text(cache.get_tree(c.args[0]), pat, c.args[2], regexp) else: <del> raise RuntimeError('Invalid number of @{} arguments  at line {}'.format(c.cmd, c.lineno)) </del> <ins> raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd)) </ins> elif c.cmd == 'count': # count test if len(c.args) == 3: # @count <path> <pat> <count> = count test ret = check_tree_count(cache.get_tree(c.args[0]), c.args[1], int(c.args[2])) else: <del> raise RuntimeError('Invalid number of @{} arguments  at line {}'.format(c.cmd, c.lineno)) </del> <ins> raise InvalidCheck('Invalid number of @{} arguments'.format(c.cmd)) </ins> elif c.cmd == 'valid-html': <del> raise RuntimeError('Unimplemented @valid-html at line {}'.format(c.lineno)) </del> <ins> raise InvalidCheck('Unimplemented @valid-html') </ins> elif c.cmd == 'valid-links': <del> raise RuntimeError('Unimplemented @valid-links at line {}'.format(c.lineno)) </del> <ins> raise InvalidCheck('Unimplemented @valid-links') </ins> else: <del> raise RuntimeError('Unrecognized @{} at line {}'.format(c.cmd, c.lineno)) </del> <ins> raise InvalidCheck('Unrecognized @{}'.format(c.cmd)) </ins> if ret == c.negated: <del> raise RuntimeError('@{}{} check failed at line {}'.format('!' if c.negated else '', c.cmd, c.lineno)) </del> <ins> raise FailedCheck(cerr) except FailedCheck as err: message = '@{}{} check failed'.format('!' if c.negated else '', c.cmd) print_err(c.lineno, c.context, err.message, message) except InvalidCheck as err: print_err(c.lineno, c.context, err.message) def check(target, commands): cache = CachedFiles(target) for c in commands: check_command(c, cache) </ins> if __name__ == '__main__': <del> if len(sys.argv) < 3: print >>sys.stderr, 'Usage: {} <doc dir> <template>'.format(sys.argv[0]) </del> <ins> if len(sys.argv) != 3: stderr('Usage: {} <doc dir> <template>'.format(sys.argv[0])) raise SystemExit(1) check(sys.argv[1], get_commands(sys.argv[2])) if ERR_COUNT: stderr("nEncountered {} errors".format(ERR_COUNT)) </ins> raise SystemExit(1) <del> else: check(sys.argv[1], get_commands(sys.argv[2])) </del>

Discussion:
When a doc test fails, the console output looks like this: A traceback should only be shown if there was an error in the python script itself - in this case it should only show the matches error, something like this:
cc me. I'm surely willing to fix that as soon as I get enough spare time.
did you ever get a chance to look at this? (Do you know if the situation has changed?)
No, also I'm not aware of any update. Looks like that I've forgotten this at that time (as flt2dec was being matured); I have virtually no energy to contribute nowadays, so I probably cannot work on this right now.