Module snippet.snippet

Text snippet extractor.

Expand source code
#
# Copyright (C) 2020 Arm Mbed. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Text snippet extractor."""
from typing import List, Optional

from snippet import exceptions
from snippet.config import Config


class Example:
    """An example."""

    def __init__(self, path: str, line_num: int, example_name: str, line: str) -> None:
        """Initialiser."""
        self._key = (path, line_num, example_name)
        self._strip = len(line) - len(line.lstrip())
        self._text: List[str] = list()
        self._cloaking = False

    def add_line(self, line: str) -> None:
        """Adds a line."""
        if self._cloaking:
            return
        self._text.append(line)

    def cloak(self, line_num: int) -> None:
        """Starts cloaking."""
        if self._cloaking:
            raise exceptions.CloakMismatch(f"Already cloaked at {self.debug_id} ({line_num})")
        self._cloaking = True

    def uncloak(self, line_num: int) -> None:
        """Stops cloaking."""
        if not self._cloaking:
            raise exceptions.CloakMismatch(f"Already uncloaked at {self.debug_id} ({line_num})")
        self._cloaking = False

    @property
    def is_cloaking(self) -> bool:
        """States whether it's in cloaking mode."""
        return self._cloaking

    @property
    def is_empty(self) -> bool:
        """States whether the example is empty or not."""
        return len(self._text) == 0

    @property
    def text(self) -> List[str]:
        """Gets example text."""
        return self._text

    @property
    def strip_number(self) -> int:
        """Gets the example strip number."""
        return self._strip

    @property
    def key(self) -> tuple:
        """Gets the example key."""
        return self._key

    @property
    def debug_id(self) -> str:
        """Gets some debug information about the example."""
        return str(self.key)


class Examples:
    """All the examples in a file."""

    def __init__(self) -> None:
        """Initialiser."""
        self._examples: List[Example] = list()
        self._current_example: Optional[Example] = None

    def set_current(self, example: Example, line_num: int) -> None:
        """Sets current example."""
        if self._current_example:
            raise exceptions.StartEndMismatch(f"Already capturing at {self._current_example.debug_id} ({line_num})")
        self._current_example = example

    def store_current(self, line_num: int) -> None:
        """Stores current example."""
        if not self._current_example:
            raise exceptions.StartEndMismatch(f"Not yet capturing at {line_num}")
        if self._current_example.is_cloaking:
            raise exceptions.CloakMismatch(
                f"End of example reached whilst still cloaked {self._current_example.debug_id} ({line_num})"
            )
        if not self._current_example.is_empty:
            self._examples.append(self._current_example)
        self._current_example = None

    def cloak(self, line_num: int) -> None:
        """Start cloaking."""
        if self._current_example:
            self._current_example.cloak(line_num)

    def uncloak(self, line_num: int) -> None:
        """Stops cloaking."""
        if self._current_example:
            self._current_example.uncloak(line_num)

    def end(self, line_num: int) -> None:
        """Ends."""
        if self._current_example:
            raise exceptions.StartEndMismatch(
                f"EOF reached whilst still capturing {self._current_example.debug_id} ({line_num})"
            )

    def add_line(self, line: str) -> None:
        """Adds a line."""
        if self._current_example:
            self._current_example.add_line(line)

    def validate_dedent(self, line: str, line_num: int) -> None:
        """Validates dedent."""
        if not self._current_example:
            return
        if any(line[: self._current_example.strip_number].lstrip()):
            raise exceptions.ValidationFailure(
                f"Unexpected dedent whilst capturing {self._current_example.debug_id} ({line_num})"
            )

    def validate_line(self, fail_on_contains: List[str], line: str, line_num: int) -> None:
        """Validates line."""
        for trigger in fail_on_contains:
            if trigger in line:
                debug_info = self._current_example.debug_id if self._current_example else ""
                raise exceptions.ValidationFailure(f"Unexpected phrase {repr(trigger)} at {debug_info} ({line_num})")

    def clean_line(self, line: str) -> str:
        """Cleans a line."""
        if not self._current_example:
            return line
        start = self._current_example.strip_number
        return line[start:].rstrip()

    @property
    def all(self) -> list:
        """Gets all the examples."""
        return self._examples


def extract_snippets_from_text(config: Config, lines: list, path: str) -> dict:
    """Finds snippets in lines of text."""
    examples = Examples()
    line_index = 0
    for line_num, line in enumerate(lines):
        line_index = line_num
        if config.start_flag in line:
            # start capturing code from the next line
            examples.set_current(
                Example(path=path, line_num=line_num, example_name=line.rsplit(":")[-1].strip(), line=line), line_num
            )
            continue

        if config.end_flag in line:
            # stop capturing, and discard empty blocks
            examples.store_current(line_num)
            continue

        if config.uncloak_flag in line:
            examples.uncloak(line_num)
            continue

        if config.cloak_flag in line:
            examples.cloak(line_num)
            continue

        # whilst capturing, append code lines to the current block
        if config.fail_on_dedent:
            examples.validate_dedent(line, line_num)
        clean_line = examples.clean_line(line)
        if any(match in clean_line for match in config.drop_lines):
            continue
        for r_before, r_after in config.replacements.items():
            clean_line = clean_line.replace(r_before, r_after)
        examples.validate_line(config.fail_on_contains, clean_line, line_num)

        # add this line of code to the example block
        examples.add_line(clean_line)

    examples.end(line_index)
    return {example.key: example.text for example in examples.all}

Functions

def extract_snippets_from_text(config: Config, lines: list, path: str) -> dict

Finds snippets in lines of text.

Expand source code
def extract_snippets_from_text(config: Config, lines: list, path: str) -> dict:
    """Finds snippets in lines of text."""
    examples = Examples()
    line_index = 0
    for line_num, line in enumerate(lines):
        line_index = line_num
        if config.start_flag in line:
            # start capturing code from the next line
            examples.set_current(
                Example(path=path, line_num=line_num, example_name=line.rsplit(":")[-1].strip(), line=line), line_num
            )
            continue

        if config.end_flag in line:
            # stop capturing, and discard empty blocks
            examples.store_current(line_num)
            continue

        if config.uncloak_flag in line:
            examples.uncloak(line_num)
            continue

        if config.cloak_flag in line:
            examples.cloak(line_num)
            continue

        # whilst capturing, append code lines to the current block
        if config.fail_on_dedent:
            examples.validate_dedent(line, line_num)
        clean_line = examples.clean_line(line)
        if any(match in clean_line for match in config.drop_lines):
            continue
        for r_before, r_after in config.replacements.items():
            clean_line = clean_line.replace(r_before, r_after)
        examples.validate_line(config.fail_on_contains, clean_line, line_num)

        # add this line of code to the example block
        examples.add_line(clean_line)

    examples.end(line_index)
    return {example.key: example.text for example in examples.all}

Classes

class Example (path: str, line_num: int, example_name: str, line: str)

An example.

Initialiser.

Expand source code
class Example:
    """An example."""

    def __init__(self, path: str, line_num: int, example_name: str, line: str) -> None:
        """Initialiser."""
        self._key = (path, line_num, example_name)
        self._strip = len(line) - len(line.lstrip())
        self._text: List[str] = list()
        self._cloaking = False

    def add_line(self, line: str) -> None:
        """Adds a line."""
        if self._cloaking:
            return
        self._text.append(line)

    def cloak(self, line_num: int) -> None:
        """Starts cloaking."""
        if self._cloaking:
            raise exceptions.CloakMismatch(f"Already cloaked at {self.debug_id} ({line_num})")
        self._cloaking = True

    def uncloak(self, line_num: int) -> None:
        """Stops cloaking."""
        if not self._cloaking:
            raise exceptions.CloakMismatch(f"Already uncloaked at {self.debug_id} ({line_num})")
        self._cloaking = False

    @property
    def is_cloaking(self) -> bool:
        """States whether it's in cloaking mode."""
        return self._cloaking

    @property
    def is_empty(self) -> bool:
        """States whether the example is empty or not."""
        return len(self._text) == 0

    @property
    def text(self) -> List[str]:
        """Gets example text."""
        return self._text

    @property
    def strip_number(self) -> int:
        """Gets the example strip number."""
        return self._strip

    @property
    def key(self) -> tuple:
        """Gets the example key."""
        return self._key

    @property
    def debug_id(self) -> str:
        """Gets some debug information about the example."""
        return str(self.key)

Instance variables

var debug_id : str

Gets some debug information about the example.

Expand source code
@property
def debug_id(self) -> str:
    """Gets some debug information about the example."""
    return str(self.key)
var is_cloaking : bool

States whether it's in cloaking mode.

Expand source code
@property
def is_cloaking(self) -> bool:
    """States whether it's in cloaking mode."""
    return self._cloaking
var is_empty : bool

States whether the example is empty or not.

Expand source code
@property
def is_empty(self) -> bool:
    """States whether the example is empty or not."""
    return len(self._text) == 0
var key : tuple

Gets the example key.

Expand source code
@property
def key(self) -> tuple:
    """Gets the example key."""
    return self._key
var strip_number : int

Gets the example strip number.

Expand source code
@property
def strip_number(self) -> int:
    """Gets the example strip number."""
    return self._strip
var text : List[str]

Gets example text.

Expand source code
@property
def text(self) -> List[str]:
    """Gets example text."""
    return self._text

Methods

def add_line(self, line: str) -> NoneType

Adds a line.

Expand source code
def add_line(self, line: str) -> None:
    """Adds a line."""
    if self._cloaking:
        return
    self._text.append(line)
def cloak(self, line_num: int) -> NoneType

Starts cloaking.

Expand source code
def cloak(self, line_num: int) -> None:
    """Starts cloaking."""
    if self._cloaking:
        raise exceptions.CloakMismatch(f"Already cloaked at {self.debug_id} ({line_num})")
    self._cloaking = True
def uncloak(self, line_num: int) -> NoneType

Stops cloaking.

Expand source code
def uncloak(self, line_num: int) -> None:
    """Stops cloaking."""
    if not self._cloaking:
        raise exceptions.CloakMismatch(f"Already uncloaked at {self.debug_id} ({line_num})")
    self._cloaking = False
class Examples

All the examples in a file.

Initialiser.

Expand source code
class Examples:
    """All the examples in a file."""

    def __init__(self) -> None:
        """Initialiser."""
        self._examples: List[Example] = list()
        self._current_example: Optional[Example] = None

    def set_current(self, example: Example, line_num: int) -> None:
        """Sets current example."""
        if self._current_example:
            raise exceptions.StartEndMismatch(f"Already capturing at {self._current_example.debug_id} ({line_num})")
        self._current_example = example

    def store_current(self, line_num: int) -> None:
        """Stores current example."""
        if not self._current_example:
            raise exceptions.StartEndMismatch(f"Not yet capturing at {line_num}")
        if self._current_example.is_cloaking:
            raise exceptions.CloakMismatch(
                f"End of example reached whilst still cloaked {self._current_example.debug_id} ({line_num})"
            )
        if not self._current_example.is_empty:
            self._examples.append(self._current_example)
        self._current_example = None

    def cloak(self, line_num: int) -> None:
        """Start cloaking."""
        if self._current_example:
            self._current_example.cloak(line_num)

    def uncloak(self, line_num: int) -> None:
        """Stops cloaking."""
        if self._current_example:
            self._current_example.uncloak(line_num)

    def end(self, line_num: int) -> None:
        """Ends."""
        if self._current_example:
            raise exceptions.StartEndMismatch(
                f"EOF reached whilst still capturing {self._current_example.debug_id} ({line_num})"
            )

    def add_line(self, line: str) -> None:
        """Adds a line."""
        if self._current_example:
            self._current_example.add_line(line)

    def validate_dedent(self, line: str, line_num: int) -> None:
        """Validates dedent."""
        if not self._current_example:
            return
        if any(line[: self._current_example.strip_number].lstrip()):
            raise exceptions.ValidationFailure(
                f"Unexpected dedent whilst capturing {self._current_example.debug_id} ({line_num})"
            )

    def validate_line(self, fail_on_contains: List[str], line: str, line_num: int) -> None:
        """Validates line."""
        for trigger in fail_on_contains:
            if trigger in line:
                debug_info = self._current_example.debug_id if self._current_example else ""
                raise exceptions.ValidationFailure(f"Unexpected phrase {repr(trigger)} at {debug_info} ({line_num})")

    def clean_line(self, line: str) -> str:
        """Cleans a line."""
        if not self._current_example:
            return line
        start = self._current_example.strip_number
        return line[start:].rstrip()

    @property
    def all(self) -> list:
        """Gets all the examples."""
        return self._examples

Instance variables

var all : list

Gets all the examples.

Expand source code
@property
def all(self) -> list:
    """Gets all the examples."""
    return self._examples

Methods

def add_line(self, line: str) -> NoneType

Adds a line.

Expand source code
def add_line(self, line: str) -> None:
    """Adds a line."""
    if self._current_example:
        self._current_example.add_line(line)
def clean_line(self, line: str) -> str

Cleans a line.

Expand source code
def clean_line(self, line: str) -> str:
    """Cleans a line."""
    if not self._current_example:
        return line
    start = self._current_example.strip_number
    return line[start:].rstrip()
def cloak(self, line_num: int) -> NoneType

Start cloaking.

Expand source code
def cloak(self, line_num: int) -> None:
    """Start cloaking."""
    if self._current_example:
        self._current_example.cloak(line_num)
def end(self, line_num: int) -> NoneType

Ends.

Expand source code
def end(self, line_num: int) -> None:
    """Ends."""
    if self._current_example:
        raise exceptions.StartEndMismatch(
            f"EOF reached whilst still capturing {self._current_example.debug_id} ({line_num})"
        )
def set_current(self, example: Example, line_num: int) -> NoneType

Sets current example.

Expand source code
def set_current(self, example: Example, line_num: int) -> None:
    """Sets current example."""
    if self._current_example:
        raise exceptions.StartEndMismatch(f"Already capturing at {self._current_example.debug_id} ({line_num})")
    self._current_example = example
def store_current(self, line_num: int) -> NoneType

Stores current example.

Expand source code
def store_current(self, line_num: int) -> None:
    """Stores current example."""
    if not self._current_example:
        raise exceptions.StartEndMismatch(f"Not yet capturing at {line_num}")
    if self._current_example.is_cloaking:
        raise exceptions.CloakMismatch(
            f"End of example reached whilst still cloaked {self._current_example.debug_id} ({line_num})"
        )
    if not self._current_example.is_empty:
        self._examples.append(self._current_example)
    self._current_example = None
def uncloak(self, line_num: int) -> NoneType

Stops cloaking.

Expand source code
def uncloak(self, line_num: int) -> None:
    """Stops cloaking."""
    if self._current_example:
        self._current_example.uncloak(line_num)
def validate_dedent(self, line: str, line_num: int) -> NoneType

Validates dedent.

Expand source code
def validate_dedent(self, line: str, line_num: int) -> None:
    """Validates dedent."""
    if not self._current_example:
        return
    if any(line[: self._current_example.strip_number].lstrip()):
        raise exceptions.ValidationFailure(
            f"Unexpected dedent whilst capturing {self._current_example.debug_id} ({line_num})"
        )
def validate_line(self, fail_on_contains: List[str], line: str, line_num: int) -> NoneType

Validates line.

Expand source code
def validate_line(self, fail_on_contains: List[str], line: str, line_num: int) -> None:
    """Validates line."""
    for trigger in fail_on_contains:
        if trigger in line:
            debug_info = self._current_example.debug_id if self._current_example else ""
            raise exceptions.ValidationFailure(f"Unexpected phrase {repr(trigger)} at {debug_info} ({line_num})")