hassle.generate_tests

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

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):
100def generate_test_files(package_path: Pathier, tests_dir: Pathier = None):
101    """Generate test files for all .py files in 'src'
102    directory of 'package_path'."""
103    pyfiles = [
104        file
105        for file in (package_path / "src").rglob("*.py")
106        if file.name != "__init__.py"
107    ]
108    for pyfile in pyfiles:
109        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):
112def main(args: argparse.Namespace = None):
113    if not args:
114        args = get_args()
115    args.paths = [Pathier(path).resolve() for path in args.paths]
116    if args.tests_dir:
117        args.tests_dir = Pathier(args.tests_dir).resolve()
118    for path in args.paths:
119        if path.is_dir():
120            generate_test_files(path, args.tests_dir)
121        elif path.is_file():
122            write_placeholders(
123                path.parent, path, get_function_names(path), args.tests_dir
124            )