У меня есть набор тестов (на основе unittest) для моего проекта Python.
Здесь у меня есть тестовые классы, мои методы тестирования и т. д.
В (некоторых из моих) тестов я вызываю функцию для инициализации сценариев тестов. Назовем эту функцию generate_scenario(...)
, и у нее есть куча параметров.
Мне было интересно, могу ли я написать дополнительный код Python, который мог бы найти все случаи вызова generate_scenario(...)
и с переданным параметром, чтобы я мог проверить, действительно ли генерируются все «возможные» сценарии.
В идеале мне нужен дополнительный тестовый модуль, чтобы проверить это.
интересно, можно ли написать код, чтобы найти все случаи вызова методаgener_scenario(...)
Да.
Обратитесь к документации по встроенным модулям «inspect» и «dis». Они очень помогают в работе объекты кода и исходные файлы.
Прилагаю демонстрацию.
Есть как минимум два способа решения вашего варианта использования:
простая обработка текста (прославленный grep
) и
изучение результатов анализа cPython.
from importlib import import_module
from inspect import getsource, isfunction, isgenerator
from pathlib import Path
from types import FunctionType, MethodType, ModuleType
from typing import Callable, Generator, Iterable, NamedTuple
from unittest import TestCase
from unittest.main import TestProgram
import dis
import io
import os
import re
import sys
def find_callable_functions(module: ModuleType | type) -> list[Callable]:
"""Finds callables within a module, including functions and classes."""
return [
obj
for obj in module.__dict__.values()
if callable(obj) and isinstance(obj, (FunctionType, MethodType, type))
]
# cf inspect.{isfunction, ismethod, isclass}
def find_callable_matches(
module: ModuleType | type, needle: str, verbose: bool = False
) -> Generator[Callable, None, None]:
for obj in module.__dict__.values():
if callable(obj) and isinstance(obj, (FunctionType, MethodType, type)):
if not isgenerator(obj) and isfunction(obj):
buf = io.StringIO()
dis.dis(obj, file=buf)
names = obj.__code__.co_names
if needle in buf.getvalue() and needle in names:
yield obj
if verbose:
print(getsource(obj))
# print(dis._disassemble_bytes(code, names=names))
# lines, start = findsource(obj)
# print("".join(lines[start : start + 5]), "\n")
# dis.disassemble(obj.__code__)
class Source(NamedTuple):
"""coordinates of a source code location"""
file: Path
line: int
src: list[str]
def find_functions_in(source_file: Path) -> Generator[Source, None, None]:
decorator = re.compile(r"^\s*@")
record_delimiter = re.compile(r"^(\s*def |if __name__ == .__main__.)")
record = Source(Path("/dev/null"), -1, []) # sentinel
with open(source_file) as fin:
for i, line in enumerate(fin):
if record_delimiter.match(line):
if record.line > 0:
yield record
record = Source(file=source_file.resolve(), line=i + 1, src=[])
if not decorator.match(line):
record.src.append(line)
if record.line > 0:
yield record
def find_functions_under(
paths: Iterable[Path], needle
) -> Generator[Source, None, None]:
for path in paths:
if path.is_file() and path.suffix == ".py":
for record in find_functions_in(path):
if needle in "".join(record.src):
yield record
# file = f"{record.file.relative_to(os.getcwd())}"
# m = import_module(file.replace("/", ".").removesuffix(".py"))
class FirstClass:
def __init__(self, x):
self.x = x
def generate_scenario(self, a, b, c):
self.x += a + b + c
def run_scenario(self):
self.generate_scenario(1, 2, 3)
print(self.x)
class SecondClass:
def __init__(self, y):
self.y = y
def generate_scenario(self, a, b, c):
self.y += a * b * c
def run_scenario(self):
print(self.y)
class UnrelatedClass:
def __init__(self):
self.z = None
class TestFindFunctions(TestCase):
def test_find_callable_functions(self) -> None:
self.assertEqual(
[TestProgram],
find_callable_functions(sys.modules["__main__"]),
)
self.assertEqual(
"<class '_frozen_importlib.FrozenImporter'>",
str(find_callable_functions(os)[0]),
)
self.assertEqual(os, import_module("os"))
self.assertEqual(
[
FirstClass.__init__,
FirstClass.generate_scenario,
FirstClass.run_scenario,
],
find_callable_functions(FirstClass),
)
def test_find_callable_matches(self) -> None:
self.assertEqual(
[FirstClass.run_scenario],
list(find_callable_matches(FirstClass, "generate_scenario")),
)
def test_find_functions(self) -> None:
source_records = list(find_functions_in(Path(__file__)))
self.assertEqual(15, len(source_records))
def test_find_functions_under(self, verbose: bool = False) -> None:
source_folder = Path(__file__).parent
glob = source_folder.glob("**/*.py")
records = list(find_functions_under(glob, "generate_scenario"))
self.assertEqual(6, len(records))
if verbose:
for record in records:
print(record[:2])
print("".join(record.src))
Насколько я знаю, проверка предназначена для «живых» объектов, а не для статического анализа. и хотя это может помочь, это очень низкий уровень. Мне было интересно, есть ли какая-нибудь библиотека с более простым в использовании интерфейсом.