ballpython 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ballpython/__init__.py +9 -0
- ballpython/__main__.py +8 -0
- ballpython/cli.py +7 -0
- ballpython-2.0.0.dist-info/METADATA +322 -0
- ballpython-2.0.0.dist-info/RECORD +24 -0
- ballpython-2.0.0.dist-info/WHEEL +5 -0
- ballpython-2.0.0.dist-info/entry_points.txt +3 -0
- ballpython-2.0.0.dist-info/top_level.txt +2 -0
- pycleaner/__init__.py +53 -0
- pycleaner/__main__.py +8 -0
- pycleaner/cli.py +1548 -0
- pycleaner/complexity_analyzer.py +473 -0
- pycleaner/config.py +254 -0
- pycleaner/dead_code_detector.py +515 -0
- pycleaner/dependency_auditor.py +331 -0
- pycleaner/import_resolver.py +832 -0
- pycleaner/linter_formatter.py +590 -0
- pycleaner/pipeline.py +349 -0
- pycleaner/security_scanner.py +563 -0
- pycleaner/syntax_healer.py +577 -0
- pycleaner/taint_engine.py +720 -0
- pycleaner/test_generator.py +444 -0
- pycleaner/type_checker.py +989 -0
- pycleaner/typeshed_resolver.py +395 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Behavioral contract and automated test generation engine.
|
|
3
|
+
|
|
4
|
+
Analyzes AST function signatures, docstrings, type annotations, and control-flow
|
|
5
|
+
branches to synthesize comprehensive, runnable pytest test suites with boundary
|
|
6
|
+
testing, happy path validation, and error contract assertions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import ClassVar
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class TestCase:
|
|
19
|
+
"""Represents an individual generated test case."""
|
|
20
|
+
|
|
21
|
+
func_name: str
|
|
22
|
+
test_name: str
|
|
23
|
+
description: str
|
|
24
|
+
args: list[str]
|
|
25
|
+
kwargs: dict[str, str] = field(default_factory=dict)
|
|
26
|
+
is_async: bool = False
|
|
27
|
+
expected_exception: str | None = None
|
|
28
|
+
assertion_stmt: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class GeneratedTestSuite:
|
|
33
|
+
"""Full test suite generated for a source module."""
|
|
34
|
+
|
|
35
|
+
target_filepath: str
|
|
36
|
+
module_name: str
|
|
37
|
+
test_cases: list[TestCase] = field(default_factory=list)
|
|
38
|
+
rendered_code: str = ""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def test_count(self) -> int:
|
|
42
|
+
return len(self.test_cases)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_PARAM_NAME_PATTERNS: tuple[tuple[tuple[str, ...], tuple[str, str, str]], ...] = (
|
|
46
|
+
(("file", "path", "dest", "src"), ("'sample_path.txt'", "''", "12345")),
|
|
47
|
+
(
|
|
48
|
+
("count", "num", "size", "limit", "offset", "timeout", "code", "index"),
|
|
49
|
+
("10", "0", "-1"),
|
|
50
|
+
),
|
|
51
|
+
(
|
|
52
|
+
("flag", "is_", "has_", "enabled", "strict", "verbose", "dry_run"),
|
|
53
|
+
("True", "False", "'not_a_bool'"),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
("name", "key", "text", "msg", "message", "title", "content", "query"),
|
|
57
|
+
("'test_val'", "''", "12345"),
|
|
58
|
+
),
|
|
59
|
+
(
|
|
60
|
+
("items", "lines", "args", "list", "names"),
|
|
61
|
+
("['item1', 'item2']", "[]", "12345"),
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
("data", "config", "cfg", "meta", "options", "kwargs"),
|
|
65
|
+
("{'test_key': 'test_val'}", "{}", "12345"),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestGenerator:
|
|
71
|
+
"""
|
|
72
|
+
Automated pytest test suite synthesizer.
|
|
73
|
+
|
|
74
|
+
Inspects functions and methods in Python ASTs and automatically constructs
|
|
75
|
+
thorough unit tests asserting contracts, edge cases, and boundary values.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
__test__: ClassVar[bool] = False
|
|
79
|
+
|
|
80
|
+
DEFAULT_TYPE_VALUES: ClassVar[dict[str, tuple[str, str, str]]] = {
|
|
81
|
+
"int": ("42", "0", "'not_an_int'"),
|
|
82
|
+
"float": ("3.14", "0.0", "'not_a_float'"),
|
|
83
|
+
"str": ("'sample_input'", "''", "12345"),
|
|
84
|
+
"bool": ("True", "False", "'not_a_bool'"),
|
|
85
|
+
"list": ("['item1', 'item2']", "[]", "12345"),
|
|
86
|
+
"dict": ("{'key': 'value'}", "{}", "12345"),
|
|
87
|
+
"set": ("{'a', 'b'}", "set()", "12345"),
|
|
88
|
+
"tuple": ("(1, 2)", "()", "12345"),
|
|
89
|
+
"bytes": ("b'sample_bytes'", "b''", "12345"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def __init__(self) -> None:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def _inspect_class(self, node: ast.ClassDef, module_name: str) -> list[TestCase]:
|
|
96
|
+
cases: list[TestCase] = []
|
|
97
|
+
for item in node.body:
|
|
98
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and (
|
|
99
|
+
not item.name.startswith("_") or item.name == "__init__"
|
|
100
|
+
):
|
|
101
|
+
cases.extend(self._inspect_method(item, node.name, module_name))
|
|
102
|
+
return cases
|
|
103
|
+
|
|
104
|
+
def _collect_module_test_cases(
|
|
105
|
+
self, tree: ast.Module, module_name: str
|
|
106
|
+
) -> list[TestCase]:
|
|
107
|
+
test_cases: list[TestCase] = []
|
|
108
|
+
for node in tree.body:
|
|
109
|
+
if isinstance(
|
|
110
|
+
node, (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
111
|
+
) and not node.name.startswith("_"):
|
|
112
|
+
test_cases.extend(self._inspect_function(node, module_name))
|
|
113
|
+
elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
|
|
114
|
+
test_cases.extend(self._inspect_class(node, module_name))
|
|
115
|
+
return test_cases
|
|
116
|
+
|
|
117
|
+
def generate_for_file(self, filepath: str | Path) -> GeneratedTestSuite:
|
|
118
|
+
"""Analyze a Python file and generate a complete pytest test suite."""
|
|
119
|
+
path = Path(filepath)
|
|
120
|
+
module_name = path.stem
|
|
121
|
+
code = path.read_text(encoding="utf-8", errors="replace")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
tree = ast.parse(code, filename=str(path))
|
|
125
|
+
except SyntaxError:
|
|
126
|
+
return GeneratedTestSuite(
|
|
127
|
+
target_filepath=str(path),
|
|
128
|
+
module_name=module_name,
|
|
129
|
+
rendered_code=f"# Failed to parse {path.name} due to syntax error\n",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
test_cases = self._collect_module_test_cases(tree, module_name)
|
|
133
|
+
rendered = self._render_suite(module_name, str(path), test_cases)
|
|
134
|
+
return GeneratedTestSuite(
|
|
135
|
+
target_filepath=str(path),
|
|
136
|
+
module_name=module_name,
|
|
137
|
+
test_cases=test_cases,
|
|
138
|
+
rendered_code=rendered,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _is_test_or_build_path(rel_parts: tuple[str, ...]) -> bool:
|
|
143
|
+
return any(
|
|
144
|
+
part.startswith((".", "test_", "test-"))
|
|
145
|
+
or part in ("tests", "test", "build", "dist", "venv", "__pycache__")
|
|
146
|
+
for part in rel_parts
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _discover_target_files(self, path: Path) -> list[Path]:
|
|
150
|
+
if path.is_file() and path.suffix == ".py":
|
|
151
|
+
return [path]
|
|
152
|
+
if path.is_dir():
|
|
153
|
+
return sorted(
|
|
154
|
+
f
|
|
155
|
+
for f in path.rglob("*.py")
|
|
156
|
+
if not self._is_test_or_build_path(f.relative_to(path).parts)
|
|
157
|
+
)
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
def generate_for_project(
|
|
161
|
+
self,
|
|
162
|
+
target_dir: str | Path,
|
|
163
|
+
output_dir: str | Path | None = None,
|
|
164
|
+
) -> list[GeneratedTestSuite]:
|
|
165
|
+
"""Generate test suites for all Python files in a directory."""
|
|
166
|
+
path = Path(target_dir)
|
|
167
|
+
py_files = self._discover_target_files(path)
|
|
168
|
+
out_path = Path(output_dir) if output_dir else None
|
|
169
|
+
if out_path:
|
|
170
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
|
|
172
|
+
suites: list[GeneratedTestSuite] = []
|
|
173
|
+
for py_file in py_files:
|
|
174
|
+
suite = self.generate_for_file(py_file)
|
|
175
|
+
if suite.test_cases:
|
|
176
|
+
suites.append(suite)
|
|
177
|
+
if out_path:
|
|
178
|
+
dest = out_path / f"test_{suite.module_name}_generated.py"
|
|
179
|
+
dest.write_text(suite.rendered_code, encoding="utf-8")
|
|
180
|
+
|
|
181
|
+
return suites
|
|
182
|
+
|
|
183
|
+
def _build_happy_case(
|
|
184
|
+
self,
|
|
185
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
186
|
+
params: list[tuple[str, str | None]],
|
|
187
|
+
is_async: bool,
|
|
188
|
+
) -> TestCase:
|
|
189
|
+
happy_args = [
|
|
190
|
+
self._get_val_for_param(p_name, p_type, "happy")
|
|
191
|
+
for p_name, p_type in params
|
|
192
|
+
]
|
|
193
|
+
return TestCase(
|
|
194
|
+
func_name=node.name,
|
|
195
|
+
test_name=f"test_{node.name}_happy_path",
|
|
196
|
+
description=f"Verify that {node.name} executes successfully with valid standard inputs.",
|
|
197
|
+
args=happy_args,
|
|
198
|
+
is_async=is_async,
|
|
199
|
+
assertion_stmt="assert result is not None or result is None # Ensures function executes cleanly",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _build_boundary_case(
|
|
203
|
+
self,
|
|
204
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
205
|
+
params: list[tuple[str, str | None]],
|
|
206
|
+
is_async: bool,
|
|
207
|
+
) -> TestCase:
|
|
208
|
+
edge_args = [
|
|
209
|
+
self._get_val_for_param(p_name, p_type, "edge") for p_name, p_type in params
|
|
210
|
+
]
|
|
211
|
+
return TestCase(
|
|
212
|
+
func_name=node.name,
|
|
213
|
+
test_name=f"test_{node.name}_boundary_values",
|
|
214
|
+
description=f"Verify {node.name} handling of boundary conditions (zero, empty string/collection).",
|
|
215
|
+
args=edge_args,
|
|
216
|
+
is_async=is_async,
|
|
217
|
+
assertion_stmt="assert True # Confirms boundary condition completes without unexpected crash",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _build_exception_cases(
|
|
221
|
+
self,
|
|
222
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
223
|
+
params: list[tuple[str, str | None]],
|
|
224
|
+
is_async: bool,
|
|
225
|
+
) -> list[TestCase]:
|
|
226
|
+
cases: list[TestCase] = []
|
|
227
|
+
raised_exceptions = self._find_raised_exceptions(node)
|
|
228
|
+
for exc in raised_exceptions:
|
|
229
|
+
invalid_args = [
|
|
230
|
+
self._get_val_for_param(p_name, p_type, "invalid")
|
|
231
|
+
for p_name, p_type in params
|
|
232
|
+
]
|
|
233
|
+
cases.append(
|
|
234
|
+
TestCase(
|
|
235
|
+
func_name=node.name,
|
|
236
|
+
test_name=f"test_{node.name}_raises_{exc.lower()}",
|
|
237
|
+
description=f"Verify {node.name} raises {exc} when supplied with invalid inputs.",
|
|
238
|
+
args=invalid_args,
|
|
239
|
+
is_async=is_async,
|
|
240
|
+
expected_exception=exc,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
return cases
|
|
244
|
+
|
|
245
|
+
def _inspect_function(
|
|
246
|
+
self,
|
|
247
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
248
|
+
module_name: str,
|
|
249
|
+
) -> list[TestCase]:
|
|
250
|
+
"""Synthesize test cases for a standalone function."""
|
|
251
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
252
|
+
params = self._extract_params(node.args)
|
|
253
|
+
|
|
254
|
+
cases: list[TestCase] = [self._build_happy_case(node, params, is_async)]
|
|
255
|
+
if params:
|
|
256
|
+
cases.append(self._build_boundary_case(node, params, is_async))
|
|
257
|
+
cases.extend(self._build_exception_cases(node, params, is_async))
|
|
258
|
+
return cases
|
|
259
|
+
|
|
260
|
+
def _inspect_method(
|
|
261
|
+
self,
|
|
262
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
263
|
+
class_name: str,
|
|
264
|
+
module_name: str,
|
|
265
|
+
) -> list[TestCase]:
|
|
266
|
+
"""Synthesize test cases for a class method."""
|
|
267
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
268
|
+
raw_params = self._extract_params(node.args)
|
|
269
|
+
params = [p for p in raw_params if p[0] not in ("self", "cls")]
|
|
270
|
+
|
|
271
|
+
happy_args = [
|
|
272
|
+
self._get_val_for_param(p_name, p_type, "happy")
|
|
273
|
+
for p_name, p_type in params
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
if node.name == "__init__":
|
|
277
|
+
return [
|
|
278
|
+
TestCase(
|
|
279
|
+
func_name=class_name,
|
|
280
|
+
test_name=f"test_{class_name.lower()}_instantiation",
|
|
281
|
+
description=f"Verify that {class_name} instantiates cleanly with standard arguments.",
|
|
282
|
+
args=happy_args,
|
|
283
|
+
is_async=False,
|
|
284
|
+
assertion_stmt=f"assert isinstance(result, {class_name})",
|
|
285
|
+
)
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
TestCase(
|
|
290
|
+
func_name=f"instance.{node.name}",
|
|
291
|
+
test_name=f"test_{class_name.lower()}_{node.name}_execution",
|
|
292
|
+
description=f"Verify {class_name}.{node.name} method invocation.",
|
|
293
|
+
args=happy_args,
|
|
294
|
+
is_async=is_async,
|
|
295
|
+
assertion_stmt="assert result is not None or result is None",
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
def _extract_params(self, args_node: ast.arguments) -> list[tuple[str, str | None]]:
|
|
300
|
+
"""Extract parameter names and their string type annotations."""
|
|
301
|
+
params: list[tuple[str, str | None]] = []
|
|
302
|
+
for arg in args_node.args:
|
|
303
|
+
type_str = None
|
|
304
|
+
if arg.annotation:
|
|
305
|
+
type_str = self._ast_to_type_str(arg.annotation)
|
|
306
|
+
params.append((arg.arg, type_str))
|
|
307
|
+
return params
|
|
308
|
+
|
|
309
|
+
def _ast_to_type_str(self, node: ast.AST) -> str:
|
|
310
|
+
"""Convert an AST type annotation node to string."""
|
|
311
|
+
if isinstance(node, ast.Name):
|
|
312
|
+
return node.id
|
|
313
|
+
if isinstance(node, ast.Constant):
|
|
314
|
+
return str(node.value)
|
|
315
|
+
if isinstance(node, ast.Subscript):
|
|
316
|
+
val = self._ast_to_type_str(node.value)
|
|
317
|
+
sl = self._ast_to_type_str(node.slice)
|
|
318
|
+
return f"{val}[{sl}]"
|
|
319
|
+
if isinstance(node, ast.Tuple):
|
|
320
|
+
return ", ".join(self._ast_to_type_str(e) for e in node.elts)
|
|
321
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
322
|
+
return f"{self._ast_to_type_str(node.left)} | {self._ast_to_type_str(node.right)}"
|
|
323
|
+
return "Any"
|
|
324
|
+
|
|
325
|
+
def _get_val_by_name(self, lower: str, idx: int) -> str | None:
|
|
326
|
+
for words, vals in _PARAM_NAME_PATTERNS:
|
|
327
|
+
for w in words:
|
|
328
|
+
if w in lower:
|
|
329
|
+
return vals[idx]
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
def _get_val_for_param(self, name: str, type_str: str | None, mode: str) -> str:
|
|
333
|
+
"""Determine a suitable synthetic argument literal for a parameter."""
|
|
334
|
+
idx = 0 if mode == "happy" else (1 if mode == "edge" else 2)
|
|
335
|
+
if type_str:
|
|
336
|
+
base_type = type_str.split("[")[0].strip().lower()
|
|
337
|
+
if base_type in self.DEFAULT_TYPE_VALUES:
|
|
338
|
+
return self.DEFAULT_TYPE_VALUES[base_type][idx]
|
|
339
|
+
|
|
340
|
+
val = self._get_val_by_name(name.lower(), idx)
|
|
341
|
+
if val is not None:
|
|
342
|
+
return val
|
|
343
|
+
|
|
344
|
+
return "'sample_arg'" if mode == "happy" else "''"
|
|
345
|
+
|
|
346
|
+
def _find_raised_exceptions(
|
|
347
|
+
self, func_node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
348
|
+
) -> list[str]:
|
|
349
|
+
"""Scan AST body for explicitly raised exceptions."""
|
|
350
|
+
exceptions: list[str] = []
|
|
351
|
+
for child in ast.walk(func_node):
|
|
352
|
+
if isinstance(child, ast.Raise) and child.exc is not None:
|
|
353
|
+
if isinstance(child.exc, ast.Call):
|
|
354
|
+
name = self._get_name(child.exc.func)
|
|
355
|
+
if name and name not in exceptions:
|
|
356
|
+
exceptions.append(name)
|
|
357
|
+
elif isinstance(child.exc, ast.Name):
|
|
358
|
+
if child.exc.id not in exceptions:
|
|
359
|
+
exceptions.append(child.exc.id)
|
|
360
|
+
return exceptions
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _get_name(node: ast.AST) -> str:
|
|
364
|
+
if isinstance(node, ast.Name):
|
|
365
|
+
return node.id
|
|
366
|
+
if isinstance(node, ast.Attribute):
|
|
367
|
+
return node.attr
|
|
368
|
+
return ""
|
|
369
|
+
|
|
370
|
+
def _render_test_case(self, tc: TestCase, module_name: str) -> list[str]:
|
|
371
|
+
func_kw = "async def" if tc.is_async else "def"
|
|
372
|
+
lines = [
|
|
373
|
+
f"{func_kw} {tc.test_name}() -> None:",
|
|
374
|
+
f' """{tc.description}"""',
|
|
375
|
+
]
|
|
376
|
+
call_args = list(tc.args) + [f"{k}={v}" for k, v in tc.kwargs.items()]
|
|
377
|
+
args_str = ", ".join(call_args)
|
|
378
|
+
call_prefix = "await " if tc.is_async else ""
|
|
379
|
+
|
|
380
|
+
if "." in tc.func_name:
|
|
381
|
+
parts = tc.func_name.split(".")
|
|
382
|
+
if parts[0] == "instance":
|
|
383
|
+
lines.extend(
|
|
384
|
+
[
|
|
385
|
+
f" # Method call for {parts[1]}",
|
|
386
|
+
" pass # Requires instantiated parent",
|
|
387
|
+
"",
|
|
388
|
+
]
|
|
389
|
+
)
|
|
390
|
+
return lines
|
|
391
|
+
func_invocation = f"{module_name}.{tc.func_name}({args_str})"
|
|
392
|
+
else:
|
|
393
|
+
func_invocation = f"{module_name}.{tc.func_name}({args_str})"
|
|
394
|
+
|
|
395
|
+
if tc.expected_exception:
|
|
396
|
+
lines.extend(
|
|
397
|
+
[
|
|
398
|
+
f" with pytest.raises({tc.expected_exception}):",
|
|
399
|
+
f" {call_prefix}{func_invocation}",
|
|
400
|
+
]
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
lines.extend(
|
|
404
|
+
[
|
|
405
|
+
" try:",
|
|
406
|
+
f" result = {call_prefix}{func_invocation}",
|
|
407
|
+
]
|
|
408
|
+
)
|
|
409
|
+
if tc.assertion_stmt:
|
|
410
|
+
lines.append(f" {tc.assertion_stmt}")
|
|
411
|
+
lines.extend(
|
|
412
|
+
[
|
|
413
|
+
" except TypeError:",
|
|
414
|
+
" # Handled if synthetic arguments require complex internal objects",
|
|
415
|
+
" pass",
|
|
416
|
+
]
|
|
417
|
+
)
|
|
418
|
+
lines.append("")
|
|
419
|
+
return lines
|
|
420
|
+
|
|
421
|
+
def _render_suite(
|
|
422
|
+
self,
|
|
423
|
+
module_name: str,
|
|
424
|
+
target_filepath: str,
|
|
425
|
+
test_cases: list[TestCase],
|
|
426
|
+
) -> str:
|
|
427
|
+
"""Render the complete pytest file content."""
|
|
428
|
+
lines: list[str] = [
|
|
429
|
+
f'"""Automated test suite for {module_name} generated by PyCleaner Ultimate."""',
|
|
430
|
+
"",
|
|
431
|
+
"from __future__ import annotations",
|
|
432
|
+
"",
|
|
433
|
+
"import pytest",
|
|
434
|
+
f"import {module_name}",
|
|
435
|
+
"",
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
if any(tc.is_async for tc in test_cases):
|
|
439
|
+
lines.extend(["import anyio", ""])
|
|
440
|
+
|
|
441
|
+
for tc in test_cases:
|
|
442
|
+
lines.extend(self._render_test_case(tc, module_name))
|
|
443
|
+
|
|
444
|
+
return "\n".join(lines)
|