Skip to content
Stephen Moore edited this page Dec 26, 2024 · 3 revisions

For my own reference, here is a script that converts from noseOfYeti back to plain python/pytest

import argparse
import pathlib
import re

regexes = {
    "joins": re.compile(r"[- /]"),
    "repeated_underscore": re.compile(r"_{2,}"),
    "invalid_variable_name_start": re.compile(r"^[^a-zA-Z_]"),
    "invalid_variable_name_characters": re.compile(r"[^0-9a-zA-Z_]"),
    "describe_line": re.compile(
        r'^(?P<indent>\s*)describe "(?P<sentence>[^"]+)"(?P<args>.*):'
    ),
    "it_line": re.compile(
        r'^(?P<indent>\s*)(?P<async>async )?it "(?P<sentence>[^"]+)"(?P<args>.*):'
    ),
}


def acceptable(name, capitalize=False):
    """Convert a string into something that can be used as a valid python variable name"""
    # Convert space and dashes into underscores
    name = regexes["joins"].sub("_", name)

    # Remove invalid characters
    name = regexes["invalid_variable_name_characters"].sub("", name)

    # Remove leading characters until we find a letter or underscore
    name = regexes["invalid_variable_name_start"].sub("", name)

    # Clean up irregularities in underscores.
    name = regexes["repeated_underscore"].sub("_", name.strip("_"))

    if capitalize:
        # We don't use python's built in capitalize method here because it
        # turns all upper chars into lower chars if not at the start of
        # the string and we only want to change the first character.
        name_parts = []
        for word in name.split("_"):
            name_parts.append(word[0].upper())
            if len(word) > 1:
                name_parts.append(word[1:])
        name = "".join(name_parts)

    return name


def replace_describe(line: str) -> str:
    m = regexes["describe_line"].match(line)
    assert m is not None, breakpoint()
    groups = m.groupdict()
    args = ""
    if groups["args"]:
        args = f'({groups["args"]})'
    return f"{groups['indent']}class Test{acceptable(groups['sentence'], True)}{args}:"


def replace_it(line: str) -> str:
    m = regexes["it_line"].match(line)
    assert m is not None, breakpoint()
    # name =
    groups = m.groupdict()
    args = "(self)"
    if groups["args"]:
        args = f'(self{groups["args"]})'

    is_async = ""
    if groups["async"]:
        is_async = "async "

    return f"{groups['indent']}{is_async}def test_it_{acceptable(groups['sentence'], False)}{args}:"


def make_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser()
    parser.add_argument("--path", type=pathlib.Path, required=True)
    return parser


def main(argv: list[str] | None = None) -> None:
    args = make_parser().parse_args(argv)

    if not args.path.is_dir() or not args.path.exists():
        raise ValueError("Supply a path that exists")

    for root, dirs, files in args.path.walk():
        for filename in files:
            location = root / filename
            if location.suffix != ".py":
                continue

            content = location.read_text()
            if not content.strip():
                continue

            lines = content.split("\n")
            if lines[0].strip() == "# coding: spec":
                lines.pop(0)
                for i, line in list(enumerate(lines)):
                    if line.lstrip().startswith('describe "'):
                        lines[i] = replace_describe(line)

                    if line.lstrip().startswith('it "') or line.lstrip().startswith(
                        'async it "'
                    ):
                        lines[i] = replace_it(line)

            location.write_text("\n".join(lines))


if __name__ == "__main__":
    main()

It's fairly obvious to me that getting noseOfYeti support in Astral/Microsoft tooling will never happen.

Clone this wiki locally