-
Notifications
You must be signed in to change notification settings - Fork 8
Home
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.