hassle.generate_tests

  1import argparse
  2import os
  3import tokenize
  4
  5from pathier import Pathier
  6
  7root = Pathier(__file__).parent
  8
  9
 10def get_args() -> argparse.Namespace:
 11    parser = argparse.ArgumentParser()
 12
 13    parser.add_argument(
 14        "paths",
 15        type=str,
 16        default=".",
 17        nargs="*",
 18        help=""" The name of the package or project to generate tests for,
 19        assuming it's a subfolder of your current working directory.
 20        Can also be a full path to the package. If nothing is given,
 21        the current working directory will be used.
 22        Can also be individual files.""",
 23    )
 24
 25    parser.add_argument(
 26        "-t",
 27        "--tests_dir",
 28        type=str,
 29        default=None,
 30        help=""" A specific tests directory path to write tests to.
 31        When supplying individual files to paths arg, the default
 32        behavior is to create a 'tests' directory in the parent 
 33        directory of the specified file, resulting in multiple 
 34        'tests' directories being created if the files exist in
 35        subdirectories. Supply a path to this arg to override
 36        this behavior.""",
 37    )
 38
 39    args = parser.parse_args()
 40    return args
 41
 42
 43def get_function_names(filepath: Pathier) -> list[str]:
 44    """Returns a list of function names from a .py file."""
 45    with filepath.open("r") as file:
 46        tokens = list(tokenize.generate_tokens(file.readline))
 47    functions = []
 48    for i, token in enumerate(tokens):
 49        # If token.type is "name" and the preceeding token is "def"
 50        if (
 51            token.type == 1
 52            and tokens[i - 1].type == 1
 53            and tokens[i - 1].string == "def"
 54        ):
 55            functions.append(token.string)
 56    return functions
 57
 58
 59def write_placeholders(
 60    package_path: Pathier,
 61    pyfile: Pathier | str,
 62    functions: list[str],
 63    tests_dir: Pathier = None,
 64):
 65    """Write placeholder functions to the
 66    tests/test_{pyfile} file if they don't already exist.
 67    The placeholder functions use the naming convention
 68    test_{function_name}
 69
 70    :param package_path: Path to the package.
 71
 72    :param pyfile: Path to the pyfile to write placeholders for.
 73
 74    :param functions: List of functions to generate
 75    placehodlers for."""
 76    package_name = package_path.stem
 77    if not tests_dir:
 78        tests_dir = package_path / "tests"
 79    tests_dir.mkdir()
 80    pyfile = Pathier(pyfile)
 81    test_file = tests_dir / f"test_{pyfile.name}"
 82    # Makes sure not to overwrite previously written tests
 83    # or additional imports.
 84    if test_file.exists():
 85        content = test_file.read_text() + "\n\n"
 86    else:
 87        content = f"import pytest\nfrom {package_name} import {pyfile.stem}\n\n\n"
 88    for function in functions:
 89        test_function = f"def test_{function}"
 90        if test_function not in content and function != "__init__":
 91            content += f"{test_function}():\n    ...\n\n\n"
 92    test_file.write_text(content)
 93    os.system(f"black {tests_dir}")
 94    os.system(f"isort {tests_dir}")
 95
 96
 97def generate_test_files(package_path: Pathier, tests_dir: Pathier = None):
 98    """Generate test files for all .py files in 'src'
 99    directory of 'package_path'."""
100    pyfiles = [
101        file
102        for file in (package_path / "src").rglob("*.py")
103        if file.name != "__init__.py"
104    ]
105    for pyfile in pyfiles:
106        write_placeholders(package_path, pyfile, get_function_names(pyfile), tests_dir)
107
108
109def main(args: argparse.Namespace = None):
110    if not args:
111        args = get_args()
112    args.paths = [Pathier(path).resolve() for path in args.paths]
113    if args.tests_dir:
114        args.tests_dir = Pathier(args.tests_dir).resolve()
115    for path in args.paths:
116        if path.is_dir():
117            generate_test_files(path, args.tests_dir)
118        elif path.is_file():
119            write_placeholders(
120                path.parent, path, get_function_names(path), args.tests_dir
121            )
122
123
124if __name__ == "__main__":
125    main(get_args())
def get_args() -> argparse.Namespace:
11def get_args() -> argparse.Namespace:
12    parser = argparse.ArgumentParser()
13
14    parser.add_argument(
15        "paths",
16        type=str,
17        default=".",
18        nargs="*",
19        help=""" The name of the package or project to generate tests for,
20        assuming it's a subfolder of your current working directory.
21        Can also be a full path to the package. If nothing is given,
22        the current working directory will be used.
23        Can also be individual files.""",
24    )
25
26    parser.add_argument(
27        "-t",
28        "--tests_dir",
29        type=str,
30        default=None,
31        help=""" A specific tests directory path to write tests to.
32        When supplying individual files to paths arg, the default
33        behavior is to create a 'tests' directory in the parent 
34        directory of the specified file, resulting in multiple 
35        'tests' directories being created if the files exist in
36        subdirectories. Supply a path to this arg to override
37        this behavior.""",
38    )
39
40    args = parser.parse_args()
41    return args
def get_function_names(filepath: pathier.pathier.Pathier) -> list[str]:
44def get_function_names(filepath: Pathier) -> list[str]:
45    """Returns a list of function names from a .py file."""
46    with filepath.open("r") as file:
47        tokens = list(tokenize.generate_tokens(file.readline))
48    functions = []
49    for i, token in enumerate(tokens):
50        # If token.type is "name" and the preceeding token is "def"
51        if (
52            token.type == 1
53            and tokens[i - 1].type == 1
54            and tokens[i - 1].string == "def"
55        ):
56            functions.append(token.string)
57    return functions

Returns a list of function names from a .py file.

def write_placeholders( package_path: pathier.pathier.Pathier, pyfile: pathier.pathier.Pathier | str, functions: list[str], tests_dir: pathier.pathier.Pathier = None):
60def write_placeholders(
61    package_path: Pathier,
62    pyfile: Pathier | str,
63    functions: list[str],
64    tests_dir: Pathier = None,
65):
66    """Write placeholder functions to the
67    tests/test_{pyfile} file if they don't already exist.
68    The placeholder functions use the naming convention
69    test_{function_name}
70
71    :param package_path: Path to the package.
72
73    :param pyfile: Path to the pyfile to write placeholders for.
74
75    :param functions: List of functions to generate
76    placehodlers for."""
77    package_name = package_path.stem
78    if not tests_dir:
79        tests_dir = package_path / "tests"
80    tests_dir.mkdir()
81    pyfile = Pathier(pyfile)
82    test_file = tests_dir / f"test_{pyfile.name}"
83    # Makes sure not to overwrite previously written tests
84    # or additional imports.
85    if test_file.exists():
86        content = test_file.read_text() + "\n\n"
87    else:
88        content = f"import pytest\nfrom {package_name} import {pyfile.stem}\n\n\n"
89    for function in functions:
90        test_function = f"def test_{function}"
91        if test_function not in content and function != "__init__":
92            content += f"{test_function}():\n    ...\n\n\n"
93    test_file.write_text(content)
94    os.system(f"black {tests_dir}")
95    os.system(f"isort {tests_dir}")

Write placeholder functions to the tests/test_{pyfile} file if they don't already exist. The placeholder functions use the naming convention test_{function_name}

Parameters
  • package_path: Path to the package.

  • pyfile: Path to the pyfile to write placeholders for.

  • functions: List of functions to generate placehodlers for.

def generate_test_files( package_path: pathier.pathier.Pathier, tests_dir: pathier.pathier.Pathier = None):
 98def generate_test_files(package_path: Pathier, tests_dir: Pathier = None):
 99    """Generate test files for all .py files in 'src'
100    directory of 'package_path'."""
101    pyfiles = [
102        file
103        for file in (package_path / "src").rglob("*.py")
104        if file.name != "__init__.py"
105    ]
106    for pyfile in pyfiles:
107        write_placeholders(package_path, pyfile, get_function_names(pyfile), tests_dir)

Generate test files for all .py files in 'src' directory of 'package_path'.

def main(args: argparse.Namespace = None):
110def main(args: argparse.Namespace = None):
111    if not args:
112        args = get_args()
113    args.paths = [Pathier(path).resolve() for path in args.paths]
114    if args.tests_dir:
115        args.tests_dir = Pathier(args.tests_dir).resolve()
116    for path in args.paths:
117        if path.is_dir():
118            generate_test_files(path, args.tests_dir)
119        elif path.is_file():
120            write_placeholders(
121                path.parent, path, get_function_names(path), args.tests_dir
122            )