Exemple #1
0
def report(message, line, *args, colno=None, **kwargs):
    message = full_stop(message)
    culprits = get_culprit()
    if culprits:
        kwargs['source'] = culprits[0]
    if line:
        kwargs['culprit'] = get_culprit(line.lineno)
        if colno is not None:
            # build codicil that shows both the line and the preceding line
            if line.prev_line is not None:
                codicil = [
                    f'{line.prev_line.lineno:>4} «{line.prev_line.text}»'
                ]
            else:
                codicil = []
            codicil += [
                f'{line.lineno:>4} «{line.text}»',
                '      ' + (colno * ' ') + '▲',
            ]
            kwargs['codicil'] = '\n'.join(codicil)
            kwargs['colno'] = colno
        else:
            kwargs['codicil'] = f'{line.lineno:>4} «{line.text}»'
        kwargs['line'] = line.text
        kwargs['lineno'] = line.lineno
        if line.prev_line:
            kwargs['prev_line'] = line.prev_line.text
    else:
        kwargs['culprit'] = culprits  # pragma: no cover
    raise NestedTextError(template=message, *args, **kwargs)
Exemple #2
0
    def from_text(cls, text):
        """
        Read a protocol from the given text.

        See `Protocol.parse()` for more information.  This function adds some 
        additional error handling and sanity checking.
        """
        io = cls()

        try:
            io.protocol = Protocol.parse(text)

        except ParseError as err:
            if not cls.all_errors:
                warn(
                    "the protocol could not be properly rendered due to error(s):"
                )
            err.report(informant=warn)

            io.protocol = err.content
            io.errors = 1

        else:
            io.protocol.set_current_date()

            if not io.protocol.steps:
                warn("protocol is empty.", culprit=inform.get_culprit())

        return io
Exemple #3
0
def _check_version_control(path):
    """
    Raise a warning if the given path has changes that haven't been committed.
    """
    from subprocess import run

    # Check that the file is in a repository.
    p1 = run(shlex.split('git rev-parse --show-toplevel'),
             cwd=path.parent,
             capture_output=True,
             text=True)
    if p1.returncode != 0:
        raise VersionControlWarning(f"not in a git repository!",
                                    culprit=get_culprit() or path)

    git_dir = Path(p1.stdout.strip()).resolve()
    git_relpath = path.relative_to(git_dir)

    # Check that the file is being tracked.
    p2 = run(
        ['git', 'log', '-n1', '--pretty=format:%H', '--', git_relpath],
        cwd=git_dir,
        capture_output=True,
    )
    if p2.returncode != 0:
        raise VersionControlWarning(f"not committed",
                                    culprit=get_culprit() or path)

    # Check that the file doesn't have any uncommitted changes.
    p3 = run(
        shlex.split(
            'git ls-files --modified --deleted --others --exclude-standard'),
        cwd=git_dir,
        capture_output=True,
        text=True,
    )
    if str(git_relpath) in p3.stdout:
        raise VersionControlWarning(f"uncommitted changes",
                                    culprit=get_culprit() or path)
Exemple #4
0
 def check_footnotes(footnotes):
     """
     Complain if there are any footnotes that don't refer to anything.
     """
     for i, line in enumerate(lines, start=1):
         for match in re.finditer(cls.FOOTNOTE_REGEX, line):
             refs = parse_range(match.group(1))
             unknown_refs = set(refs) - set(footnotes)
             if unknown_refs:
                 raise ParseError(
                         template=f"unknown {plural(unknown_refs):footnote/s} [{format_range(unknown_refs)}]",
                         culprit=inform.get_culprit(i),
                         unknown_refs=unknown_refs,
                 )
Exemple #5
0
class Protocol:
    # Steps can either be strings or formatting objects.  Strings will be 
    # automatically wrapped; use `format.preformatted` to prevent this.

    BLANK_REGEX = r'^\s*$|^#.*'
    DATE_FORMAT = 'MMMM D, YYYY'
    COMMAND_REGEX = r'^[$] (.+)'
    STEP_REGEX = r'^(- |\s*\d+\. )(.+)'
    FOOTNOTE_HEADER_REGEX = r'Notes?:|Footnotes?:'
    FOOTNOTE_REGEX = r'\[(\d+(?:[-,]\d+)*)\]'
    FOOTNOTE_DEF_REGEX = fr'^\s*(\[(\d+)\] )(.+)'
    INDENT_OR_BLANK_REGEX = lambda n: fr'^({" "*n}|\s*$)(.*)$'

    def __init__(self, *, date=None, commands=None, steps=None, footnotes=None):
        self.date = date
        self.commands = commands or []
        self.steps = steps or []
        self.footnotes = footnotes or {}
        self.attachments = []

    def __repr__(self):
        return f'Protocol(date={self.date!r}, commands={self.commands!r}, steps={self.steps!r}, footnotes={self.footnotes!r})'

    def __bool__(self):
        return bool(self.steps)

    def __add__(self, other):
        return Protocol.merge(self, other)

    def __iadd__(self, other):
        self.append(other)
        return self

    def __getitem__(self, i):
        return self.steps[i]

    @classmethod
    def parse(cls, x):
        """
        Construct a protocol from a stream (i.e. an open file object), a 
        string, of a list of lines.

        No line-wrapping will be applied to the content parsed by this method.
        """
        from io import IOBase
        from collections.abc import Sequence

        if isinstance(x, IOBase):
            return cls._parse_stream(x)
        if isinstance(x, str):
            return cls._parse_str(x)
        if isinstance(x, Sequence):
            return cls._parse_lines(x)

        raise ParseError(f"expected stream or string or list-like, not {type(x)}")

    @classmethod
    def _parse_stream(cls, stream):
        return cls._parse_str(stream.read())

    @classmethod
    def _parse_str(cls, text):
        return cls._parse_lines(text.splitlines())

    @classmethod
    def _parse_lines(cls, lines):
        protocol = cls()

        class Transition:

            def __init__(self, next_parser, state=None, line_parsed=True):
                self.next_parser = next_parser
                self.next_state = state or {}
                self.line_parsed = line_parsed

        def parse_date(line, state):
            """
            If the first non-empty line contains a date, parse it.
            """
            if re.match(cls.BLANK_REGEX, line):
                return Transition(parse_date)

            try:
                protocol.date = arrow.get(line, cls.DATE_FORMAT)
                return Transition(parse_command)
            except arrow.ParserError:
                return Transition(parse_command, line_parsed=False)

        def parse_command(line, state):
            """
            Interpret each line beginning with '$ ' as a command.
            """
            if match := re.match(cls.COMMAND_REGEX, line):
                protocol.commands.append(match.group(1))
                return Transition(parse_command)

            if re.match(cls.BLANK_REGEX, line):
                return Transition(parse_command)

            return Transition(parse_new_step, line_parsed=False)

        def parse_new_step(line, state):
            if re.match(cls.FOOTNOTE_HEADER_REGEX, line):
                return Transition(parse_new_footnote)

            if match := re.match(cls.STEP_REGEX, line):
                state['indent'] = len(match.group(1))
                protocol.steps.append(match.group(2) + '\n')
                return Transition(parse_continued_step, state=state)

            raise ParseError(
                    template=truncate_error("expected a step (e.g. '- …' or '1. …'), not '{}'", line),
                    culprit=inform.get_culprit(),
            )
Exemple #6
0
            return Transition(parse_new_step, line_parsed=False)

        def parse_new_footnote(line, state):
            if match := re.match(cls.FOOTNOTE_DEF_REGEX, line):
                id = state['id'] = int(match.group(2))
                state['indent'] = len(match.group(1))
                protocol.footnotes[id] = match.group(3) + '\n'
                return Transition(parse_continued_footnote, state=state)

            if re.match(cls.BLANK_REGEX, line):
                return Transition(parse_new_footnote)

            raise ParseError(
                    template=truncate_error("expected a footnote (e.g. '[1] …'), not '{}'", line),
                    culprit=inform.get_culprit(),
            )

        def parse_continued_footnote(line, state):
            id = state['id']
            indent = state['indent']
            assert id in protocol.footnotes

            if match := re.match(cls.INDENT_OR_BLANK_REGEX(indent), line):
                protocol.footnotes[id] += match.group(2) + '\n'
                return Transition(parse_continued_footnote, state=state)

            return Transition(parse_new_footnote, line_parsed=False)


        def truncate_error(message, problem):
Exemple #7
0
def test_carbuncle():
    with messenger() as (msg, stdout, stderr, logfile):
        display('fuzzy', file=stdout)
        assert get_culprit() == ()
        assert get_culprit('x') == ('x',)
        assert get_culprit(('x', 'y')) == ('x', 'y')
        assert get_culprit(('x', 'y', 1)) == ('x', 'y', 1)
        with set_culprit('a'):
            assert get_culprit() == ('a',)
            assert get_culprit('x') == ('a', 'x')
            assert get_culprit(('x', 'y')) == ('a', 'x', 'y')
            with set_culprit('b'):
                assert get_culprit() == ('b',)
                assert get_culprit('x') == ('b', 'x')
                assert get_culprit(('x', 'y')) == ('b', 'x', 'y')
                with set_culprit('c'):
                    assert get_culprit() == ('c',)
                    assert get_culprit('x') == ('c', 'x')
                    assert get_culprit(('x', 'y')) == ('c', 'x', 'y')
        with add_culprit('a'):
            assert get_culprit() == ('a',)
            assert get_culprit('x') == ('a', 'x')
            assert get_culprit(('x', 'y')) == ('a', 'x', 'y')
            with add_culprit('b'):
                assert get_culprit() == ('a', 'b',)
                assert get_culprit('x') == ('a', 'b', 'x')
                assert get_culprit(('x', 'y')) == ('a', 'b', 'x', 'y')
                with add_culprit('c'):
                    assert get_culprit() == ('a', 'b', 'c',)
                    assert get_culprit('x') == ('a', 'b', 'c', 'x')
                    assert get_culprit(('x', 'y')) == ('a', 'b', 'c', 'x', 'y')
                    assert join_culprit(get_culprit((45, 99))) == 'a, b, c, 45, 99'