Change:
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.

Discussion:
""" <ins> from __future__ import print_function </ins> import sys import os.path import re

HTMLParser.close(self) return self.__builder.close() <del> Command = namedtuple('Command', 'negated cmd args lineno') </del> <ins> Command = namedtuple('Command', 'negated cmd args lineno context') class FailedCheck(Exception): pass class InvalidCheck(Exception): pass </ins> def concat_multi_lines(f): """returns a generator out of the file object, which

catenated = '' if lastline is not None: <del> raise RuntimeError('Trailing backslash in the end of file') </del> <ins> print_err(lineno, line, 'Trailing backslash at the end of the file') </ins> LINE_PATTERN = re.compile(r''' (?<=(?<!S)@)(?P<negated>!?)

cmd = m.group('cmd') args = m.group('args') if args and not args[:1].isspace(): <del> raise RuntimeError('Invalid template syntax at line {}'.format(lineno+1)) </del> <ins> print_err(lineno, line, 'Invalid template syntax') continue </ins> args = shlex.split(args) <del> yield Command(negated=negated, cmd=cmd, args=args, lineno=lineno+1) </del> <ins> yield Command(negated=negated, cmd=cmd, args=args, lineno=lineno+1, context=line) </ins> def _flatten(node, acc):

elif path.startswith('.//'): return path else: <del> raise RuntimeError('Non-absolute XPath is not supported due to  the implementation issue.') </del> <ins> raise InvalidCheck('Non-absolute XPath is not supported due to implementation issues') </ins> class CachedFiles(object):

self.last_path = path return path elif self.last_path is None: <del> raise RuntimeError('Tried to use the previous path in the first command') </del> <ins> raise InvalidCheck('Tried to use the previous path in the first command') </ins> else: return self.last_path def get_file(self, path): path = self.resolve_path(path) <del> try: </del> <ins> if path in self.files: </ins> return self.files[path] <del> except KeyError: try: with open(os.path.join(self.root, path)) as f: data = f.read() except Exception as e: raise RuntimeError('Cannot open file {!r}: {}'.format(path, e)) else: self.files[path] = data return data </del> <ins> abspath = os.path.join(self.root, path) if not(os.path.exists(abspath) and os.path.isfile(abspath)): raise FailedCheck('File does not exist {!r}'.format(path)) with open(abspath) as f: data = f.read() self.files[path] = data return data </ins> def get_tree(self, path): path = self.resolve_path(path) <del> try: </del> <ins> if path in self.trees: </ins> return self.trees[path] <del> except KeyError: try: f = open(os.path.join(self.root, path)) except Exception as e: raise RuntimeError('Cannot open file {!r}: {}'.format(path, e)) </del> <ins> abspath = os.path.join(self.root, path) if not(os.path.exists(abspath) and os.path.isfile(abspath)): raise FailedCheck('File does not exist {!r}'.format(path)) with open(abspath) as f: </ins> try: <del> with f: tree = ET.parse(f, CustomHTMLParser()) </del> <ins> tree = ET.parse(f, CustomHTMLParser()) </ins> except Exception as e: raise RuntimeError('Cannot parse an HTML file {!r}: {}'.format(path, e)) <del> else: self.trees[path] = tree return self.trees[path] </del> <ins> self.trees[path] = tree return self.trees[path] </ins> def check_string(data, pat, regexp):

path = normalize_xpath(path) ret = False for e in tree.findall(path): <del> try: </del> <ins> if attr in e.attrib: </ins> value = e.attrib[attr] <del> except KeyError: continue </del> else: <del> ret = check_string(value, pat, regexp) if ret: break </del> <ins> continue ret = check_string(value, pat, regexp) if ret: break </ins> return ret

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>