diff --git a/.gitignore b/.gitignore index a023dcf..f91cc59 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode *.whl *.tar.gz -__pycache__ \ No newline at end of file +__pycache__ +.DS_Store \ No newline at end of file diff --git a/autopytest/autotest.py b/autopytest/autotest.py index fcd82be..b492d3f 100644 --- a/autopytest/autotest.py +++ b/autopytest/autotest.py @@ -3,13 +3,11 @@ import sys import time from pathlib import Path - from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer from .config import parse_pyproject_toml from .pytest_runner import PytestRunner - class Autotest(FileSystemEventHandler): def __init__(self, path:str): @@ -19,6 +17,12 @@ def __init__(self, path:str): self.config = parse_pyproject_toml(f"{path}/pyproject.toml") self.source_directories = self.config["source_directories"] self.test_directory = self.config["test_directory"] + self.pytest_args = self.config["pytest_args"] + self.exclude_paths = [Path(p) for p in self.config["exclude_paths"]] + + log_level = getattr(log, self.config["log_level"].upper(), log.INFO) + log_format = self.config["log_format"] + log.basicConfig(format=log_format, stream=sys.stdout, level=log_level) self.source_patterns = [] for directory in self.source_directories: @@ -27,7 +31,6 @@ def __init__(self, path:str): self.test_pattern = re.escape(self.test_directory) + r".+\.py$" - def start(self) -> None: self.observer.start() @@ -43,26 +46,27 @@ def start(self) -> None: self.observer.join() def on_modified(self, event: FileSystemEvent) -> None: + if Path(event.src_path) in self.exclude_paths: + return + for pattern in self.source_patterns: if re.search(pattern, event.src_path): path = Path(event.src_path) test_path_components = ["tests"] - for component in path.parts: - if re.search(r".py", component): - test_path_components.append(f"test_{component}") - else: - test_path_components.append(component) + for component in path.relative_to(Path.cwd()).parts: + if component != "app": + if re.search(r".py", component): + test_path_components.append(f"test_{component}") + else: + test_path_components.append(component) + test_path = "/".join(test_path_components) - if Path(test_path).exists() and PytestRunner.run(test_path) == 0: - PytestRunner.run(".") + if Path(test_path).exists() and PytestRunner.run(test_path, self.pytest_args) == 0: + PytestRunner.run(".", self.pytest_args) if ( re.search(self.test_pattern, event.src_path) - and PytestRunner.run(event.src_path) == 0 + and PytestRunner.run(event.src_path, self.pytest_args) == 0 ): - PytestRunner.run(".") - - - - + PytestRunner.run(".", self.pytest_args) diff --git a/autopytest/cli.py b/autopytest/cli.py index b2bde35..891ab49 100644 --- a/autopytest/cli.py +++ b/autopytest/cli.py @@ -1,7 +1,11 @@ from .autotest import Autotest import sys +from .commands import init_command def cli() -> None: + if len(sys.argv) > 1 and sys.argv[1] == "init": + init_command.init() + sys.exit(0) path = sys.argv[1] if len(sys.argv) > 1 else "." autotest = Autotest(path) autotest.start() diff --git a/autopytest/commands/init_command.py b/autopytest/commands/init_command.py new file mode 100644 index 0000000..cc6748d --- /dev/null +++ b/autopytest/commands/init_command.py @@ -0,0 +1,84 @@ +from pyfiglet import figlet_format +from termcolor import colored +import inquirer +from inquirer import Text, Path + +def ensure_directory_format(answers, current): + if not current.endswith('/'): + return current + '/' + return current + +def handle_pyproject_toml(user_config): + config_template = '''[tool.autopytest] +source_directories=[{source_directories}] +test_directory="{test_directory}" +pytest_args=[{pytest_args}] +exclude_paths=[{exclude_paths}] +log_format="%(message)s" +log_level="ERROR" +telemetry="{telemetry}" +''' + + config_str = config_template.format( + source_directories=','.join(f'"{dir}"' for dir in user_config['source_directories'].split(',')), + test_directory=user_config['test_directory'], + pytest_args=','.join(f'"{arg}"' for arg in user_config['pytest_args'].split(',')), + exclude_paths=','.join(f'"{path}"' for path in user_config['exclude_paths'].split(',')), + telemetry=user_config['telemetry'] + ) + + path = "pyproject.toml" + try: + with open(path, 'r') as file: + content = file.read() + if '[tool.autopytest]' in content: + start_index = content.index('[tool.autopytest]') + end_index = content.find('\n[', start_index) + end_index = len(content) if end_index == -1 else end_index + content = content[:start_index] + config_str + content[end_index:] + else: + content += '\\n' + config_str + with open(path, 'w') as file: + file.write(content) + return "Updated pyproject.toml with [tool.autopytest] configuration." + except FileNotFoundError: + with open(path, 'w') as file: + file.write(config_str) + return "Created pyproject.toml with [tool.autopytest] configuration." + +def updated_prompt_user_configuration(): + questions = [ + inquirer.Text('source_directories', + message="Enter source directories (comma separated, e.g., app,src)"), + + inquirer.Text('test_directory', + message="Enter the test directory (e.g., tests)"), + + inquirer.Text('pytest_args', + message="Enter pytest arguments (comma separated, e.g., --verbose,--cov)"), + + inquirer.Text('exclude_paths', + message="Enter paths to exclude (comma separated, e.g., app/__pycache__,build)"), + + inquirer.Confirm('telemetry', + message="Do you consent to sending anonymous telemetry data (related to crashes and test success/failure rates)?", + default=False), + ] + + return inquirer.prompt(questions) + +def init(): + print(colored(figlet_format('Autopytest Init', font='slant'), 'cyan')) + + user_config = updated_prompt_user_configuration() + + result_message = handle_pyproject_toml(user_config) + print(colored(result_message, 'green')) + + if user_config['telemetry']: + print(colored("Thank you! Anonymous telemetry data will be collected.", 'green')) + else: + print(colored("No telemetry data will be collected.", 'yellow')) + +if __name__ == "__main__": + init() diff --git a/autopytest/config.py b/autopytest/config.py index 179ca21..e201976 100644 --- a/autopytest/config.py +++ b/autopytest/config.py @@ -5,4 +5,10 @@ def parse_pyproject_toml(path: str) -> dict[str, Any]: with open(path, "rb") as f: pyproject_toml = tomllib.load(f) config: dict[str, Any] = pyproject_toml.get("tool", {}).get("autopytest", {}) + + config.setdefault("pytest_args", []) + config.setdefault("exclude_paths", []) + config.setdefault("log_format", "%(message)s") + config.setdefault("log_level", "INFO") + return config diff --git a/autopytest/pytest_runner.py b/autopytest/pytest_runner.py index 975c508..13d69c0 100644 --- a/autopytest/pytest_runner.py +++ b/autopytest/pytest_runner.py @@ -1,12 +1,12 @@ import subprocess -from typing import Final +from typing import Final, List class PytestRunner: - ARGS: Final[list] = [ + ARGS: Final[List[str]] = [ "/usr/bin/env", "pytest", ] @classmethod - def run(cls, path: str) -> int: - return subprocess.call([*cls.ARGS, path]) # noqa: S603 \ No newline at end of file + def run(cls, path: str, extra_args: List[str] = []) -> int: + return subprocess.call([*cls.ARGS, *extra_args, path]) # noqa: S603 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 72bdc3a..38672e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,10 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "watchdog==3.0.0", + "pytest", + "pyfiglet", + "termcolor", + "inquirer", ] [project.urls]