From f8812f895ea6f9d7badc1d3fddc14c3c0f221e75 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 20:05:41 +0200 Subject: [PATCH 01/16] Update cli with Rich --- def_form/cli/cli.py | 78 ------ def_form/cli/commands/__init__.py | 0 def_form/cli/commands/check.py | 42 ++++ def_form/cli/commands/format.py | 41 ++++ def_form/cli/commands/options.py | 70 ++++++ def_form/cli/console.py | 27 +++ def_form/cli/context.py | 16 ++ def_form/cli/errors/__init__.py | 10 + def_form/cli/main.py | 38 ++- def_form/cli/ui/__init__.py | 7 + def_form/cli/ui/base.py | 34 +++ def_form/cli/ui/rich.py | 223 +++++++++++++++++ def_form/formatters/def_formatter/checker.py | 5 +- def_form/formatters/def_formatter/manager.py | 240 ++++++++++--------- pyproject.toml | 1 + uv.lock | 36 +++ 16 files changed, 673 insertions(+), 195 deletions(-) delete mode 100644 def_form/cli/cli.py create mode 100644 def_form/cli/commands/__init__.py create mode 100644 def_form/cli/commands/check.py create mode 100644 def_form/cli/commands/format.py create mode 100644 def_form/cli/commands/options.py create mode 100644 def_form/cli/console.py create mode 100644 def_form/cli/context.py create mode 100644 def_form/cli/errors/__init__.py create mode 100644 def_form/cli/ui/__init__.py create mode 100644 def_form/cli/ui/base.py create mode 100644 def_form/cli/ui/rich.py diff --git a/def_form/cli/cli.py b/def_form/cli/cli.py deleted file mode 100644 index 72a96b8..0000000 --- a/def_form/cli/cli.py +++ /dev/null @@ -1,78 +0,0 @@ -import sys - -import click - -from def_form.exceptions.base import BaseDefFormException -from def_form.formatters import DefManager - - -@click.command() -@click.argument('path', type=str, default='src') -@click.option('--max-def-length', type=int, default=None, help='max length of your function definition') -@click.option('--max-inline-args', type=int, default=None, help='max number of inline arguments') -@click.option('--indent-size', type=int, default=None, help='indent size in spaces (default: 4)') -@click.option('--config', type=str, default=None, help='path to pyproject.toml') -@click.option('--exclude', multiple=True, help='paths to exclude from formatting') -@click.option('--show-skipped', is_flag=True, help='show skipped files/directories') -def format( # noqa: PLR0913 - path: str, - max_def_length: int | None, - max_inline_args: int | None, - indent_size: int | None, - config: str | None, - exclude: tuple[str, ...], - show_skipped: bool, -) -> None: - click.echo('Start formatting your code') - try: - DefManager( - path=path, - excluded=exclude, - max_def_length=max_def_length, - max_inline_args=max_inline_args, - indent_size=indent_size, - config=config, - show_skipped=show_skipped, - ).format() - except Exception as e: - click.echo(f'Something went wrong: {e}', err=True) - sys.exit(1) - else: - click.echo('Formatted!') - - -@click.command() -@click.argument('path', type=str, default='src') -@click.option('--max-def-length', type=int, default=None, help='max length of your function definition') -@click.option('--max-inline-args', type=int, default=None, help='max number of inline arguments') -@click.option('--indent-size', type=int, default=None, help='indent size in spaces (default: 4)') -@click.option('--config', type=str, default=None, help='path to pyproject.toml') -@click.option('--exclude', multiple=True, help='paths to exclude from checking') -@click.option('--show-skipped', is_flag=True, help='show skipped files/directories') -def check( # noqa: PLR0913 - path: str, - max_def_length: int | None, - max_inline_args: int | None, - indent_size: int | None, - config: str | None, - exclude: tuple[str, ...], - show_skipped: bool, -) -> None: - click.echo('Start checking your code') - try: - DefManager( - path=path, - excluded=exclude, - max_def_length=max_def_length, - max_inline_args=max_inline_args, - indent_size=indent_size, - config=config, - show_skipped=show_skipped, - ).check() - except BaseDefFormException: - sys.exit(1) - except Exception as e: - click.echo(f'Something went wrong: {e}', err=True) - sys.exit(1) - else: - click.echo('All checks passed!') diff --git a/def_form/cli/commands/__init__.py b/def_form/cli/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/def_form/cli/commands/check.py b/def_form/cli/commands/check.py new file mode 100644 index 0000000..37664d3 --- /dev/null +++ b/def_form/cli/commands/check.py @@ -0,0 +1,42 @@ +import click + +from def_form.cli.commands.options import common_options +from def_form.cli.console import Console +from def_form.cli.context import context +from def_form.cli.errors import CheckFailedError +from def_form.cli.ui.rich import RichUI +from def_form.exceptions.base import BaseDefFormException +from def_form.formatters import DefManager + + +@click.command() +@common_options +def check( # noqa: PLR0913 + path: str, + max_def_length: int | None, + max_inline_args: int | None, + indent_size: int | None, + config: str | None, + exclude: tuple[str, ...], + show_skipped: bool, +) -> None: + console = Console(context=context) + console.info(f"Checking [bold]{path}[/bold]") + + try: + DefManager( + path=path, + excluded=exclude, + max_def_length=max_def_length, + max_inline_args=max_inline_args, + indent_size=indent_size, + config=config, + show_skipped=show_skipped, + ui=RichUI(context=context), + ).check() + except BaseDefFormException: + raise CheckFailedError("Code style violations found") + except Exception as exc: + raise CheckFailedError(str(exc)) from exc + + console.success("All checks passed") diff --git a/def_form/cli/commands/format.py b/def_form/cli/commands/format.py new file mode 100644 index 0000000..94d3807 --- /dev/null +++ b/def_form/cli/commands/format.py @@ -0,0 +1,41 @@ +import click + +from def_form.cli.commands.options import common_options +from def_form.cli.console import Console +from def_form.cli.context import context +from def_form.cli.errors import FormatterFailedError +from def_form.cli.ui.rich import RichUI +from def_form.formatters import DefManager + + +@click.command() +@common_options +def format( # noqa: PLR0913 + path: str, + max_def_length: int | None, + max_inline_args: int | None, + indent_size: int | None, + config: str | None, + exclude: tuple[str, ...], + show_skipped: bool, +) -> None: + context.show_skipped = show_skipped + console = Console(context=context) + console.info(f"Formatting [bold]{path}[/bold]") + console.debug("Initializing formatter") + + try: + DefManager( + path=path, + excluded=exclude, + max_def_length=max_def_length, + max_inline_args=max_inline_args, + indent_size=indent_size, + config=config, + show_skipped=show_skipped, + ui=RichUI(context=context), + ).format() + except Exception as exc: + raise FormatterFailedError(str(exc)) from exc + + console.success("Formatting completed") diff --git a/def_form/cli/commands/options.py b/def_form/cli/commands/options.py new file mode 100644 index 0000000..7888621 --- /dev/null +++ b/def_form/cli/commands/options.py @@ -0,0 +1,70 @@ +import click + + +def path_option(func): + return click.argument('path', type=click.Path(exists=True), default='.')(func) + + +def max_def_length_option(func): + return click.option( + '--max-def-length', + type=int, + default=None, + help='Maximum length of function definition' + )(func) + + +def max_inline_args_option(func): + return click.option( + '--max-inline-args', + type=int, + default=None, + help='Maximum number of inline arguments' + )(func) + + +def indent_size_option(func): + return click.option( + '--indent-size', + type=int, + default=None, + help='Indent size in spaces (default: 4)' + )(func) + + +def config_option(func): + return click.option( + '--config', + type=click.Path(exists=True, dir_okay=False), + default=None, + help='Path to pyproject.toml configuration file' + )(func) + + +def exclude_option(func): + return click.option( + '--exclude', + multiple=True, + type=click.Path(), + help='Paths to exclude from processing' + )(func) + + +def show_skipped_option(func): + return click.option( + '--show-skipped', + is_flag=True, + default=False, + help='Show skipped files and directories' + )(func) + + +def common_options(func): + func = path_option(func) + func = max_def_length_option(func) + func = max_inline_args_option(func) + func = indent_size_option(func) + func = exclude_option(func) + func = show_skipped_option(func) + func = config_option(func) + return func \ No newline at end of file diff --git a/def_form/cli/console.py b/def_form/cli/console.py new file mode 100644 index 0000000..d7950b8 --- /dev/null +++ b/def_form/cli/console.py @@ -0,0 +1,27 @@ +from rich.console import Console as RichConsole + +from def_form.cli.context import CLIContext + + +class Console(RichConsole): + def __init__(self, context: CLIContext, *args, **kwargs) -> None: + self.context = context + super().__init__(*args, **kwargs) + + def info(self, message: str) -> None: + if self.context.should_output: + self.print(message) + + def success(self, message: str) -> None: + if self.context.should_output: + self.print(f"[green]{message}[/green]") + + def warning(self, message: str) -> None: + self.print(f"[yellow]{message}[/yellow]") + + def error(self, message: str) -> None: + self.print(f"[red]{message}[/red]") + + def debug(self, message: str) -> None: + if self.context.verbose: + self.print(f"[dim]{message}[/dim]") diff --git a/def_form/cli/context.py b/def_form/cli/context.py new file mode 100644 index 0000000..59847a6 --- /dev/null +++ b/def_form/cli/context.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + + +@dataclass +class CLIContext: + verbose: bool = False + quiet: bool = False + show_skipped: bool = False + config_path: str | None = None + + @property + def should_output(self) -> bool: + return not self.quiet + + +context: CLIContext = CLIContext() diff --git a/def_form/cli/errors/__init__.py b/def_form/cli/errors/__init__.py new file mode 100644 index 0000000..52af20d --- /dev/null +++ b/def_form/cli/errors/__init__.py @@ -0,0 +1,10 @@ +class CLIError(Exception): + pass + + +class FormatterFailedError(CLIError): + pass + + +class CheckFailedError(CLIError): + pass diff --git a/def_form/cli/main.py b/def_form/cli/main.py index 04dbcdc..8298335 100644 --- a/def_form/cli/main.py +++ b/def_form/cli/main.py @@ -1,16 +1,38 @@ import click +import sys +from def_form.cli.console import Console +from def_form.cli.context import context +from def_form.cli.errors import CLIError +from def_form.cli.commands.check import check +from def_form.cli.commands.format import format -from def_form.cli.cli import check -from def_form.cli.cli import format +console = Console(context=context) +@click.group() +@click.option('--verbose', is_flag=True, help='Enable verbose output') +@click.option('--quiet', is_flag=True, help='Disable all output') +def cli(verbose: bool, quiet: bool) -> None: + console.context.verbose = verbose + console.context.quiet = quiet -@click.group(name='def-form') -def main() -> None: - click.help_option() +cli.add_command(check) +cli.add_command(format) + + +def main() -> None: + try: + cli() + except CLIError as exc: + console.error(str(exc)) + sys.exit(1) + except KeyboardInterrupt: + console.error("Operation cancelled by user") + sys.exit(130) + except Exception as exc: + console.error(f"Unexpected error: {exc}") + sys.exit(1) -main.add_command(format) -main.add_command(check) if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/def_form/cli/ui/__init__.py b/def_form/cli/ui/__init__.py new file mode 100644 index 0000000..ccc4c7f --- /dev/null +++ b/def_form/cli/ui/__init__.py @@ -0,0 +1,7 @@ +from def_form.cli.ui.base import BaseUI +from def_form.cli.ui.rich import RichUI + +__all__ = [ + 'BaseUI', + 'RichUI', +] \ No newline at end of file diff --git a/def_form/cli/ui/base.py b/def_form/cli/ui/base.py new file mode 100644 index 0000000..a0942a1 --- /dev/null +++ b/def_form/cli/ui/base.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any +from def_form.exceptions.base import BaseDefFormException + + +class BaseUI(ABC): + @abstractmethod + def show_config_info(self, **config: Any) -> None: + raise NotImplemented + + @abstractmethod + def start(self, total: int | None) -> None: + raise NotImplemented + + @abstractmethod + def processing(self, path: Path) -> None: + raise NotImplemented + + @abstractmethod + def skipped(self, path: Path) -> None: + raise NotImplemented + + @abstractmethod + def finish(self, processed: int, issues: list[BaseDefFormException]) -> None: + raise NotImplemented + + @abstractmethod + def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> None: + raise NotImplemented + + @abstractmethod + def show_summary(self, processed: int, issues: list[BaseDefFormException]) -> None: + raise NotImplemented diff --git a/def_form/cli/ui/rich.py b/def_form/cli/ui/rich.py new file mode 100644 index 0000000..d41e7ae --- /dev/null +++ b/def_form/cli/ui/rich.py @@ -0,0 +1,223 @@ +from pathlib import Path +from typing import Any + +from rich import box +from rich.console import Group +from rich.live import Live +from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn +from rich.table import Table +from rich.text import Text + +from def_form.cli.console import Console +from def_form.cli.context import CLIContext +from def_form.cli.ui.base import BaseUI +from def_form.exceptions.base import BaseDefFormException + + +class RichUI(BaseUI): + def __init__(self, context: CLIContext) -> None: + self.context = context + self.console = Console(context=context) + self.progress: Progress | None = None + self.task_id: int | None = None + self.current_file: Path | None = None + + def _convert_to_string(self, value: Any) -> str: + if isinstance(value, Path): + return str(value.relative_to(Path.cwd())) + + if isinstance(value, bool): + return 'Yes' if value else 'No' + + return str(value) + + def show_config_info(self, **config: Any) -> None: + if not self.context.should_output: + return + + if not config: + return + + config_table = Table( + title="[yellow]Configuration[/yellow]", + box=box.HORIZONTALS, + show_header=False, + border_style="dim" + ) + config_table.add_column(style="dim") + config_table.add_column(style="cyan") + + for key, value in config.items(): + if value is None or value == "": + continue + + display_key = key.replace('_', ' ').title() + + if key == 'config_path': + config_table.add_row(f"{display_key}:", f"[bold yellow]{value}[/bold yellow]") + else: + + if isinstance(value, (list, set, tuple)): + if not value: + continue + + items = [self._convert_to_string(item) for item in value] + + if len(items) <= 3: + display_value = ", ".join(items) + else: + preview = ", ".join(items[:3]) + display_value = f"{preview} (+{len(items) - 3} more)" + else: + display_value = self._convert_to_string(value) + + config_table.add_row(f"{display_key}:", display_value) + + self.console.print() + self.console.print(config_table) + self.console.print() + + def start(self, total: int | None) -> None: + if not self.context.should_output: + return + + self._issues = [] + + self.progress = Progress( + SpinnerColumn(), + BarColumn(bar_width=None, complete_style="blue", finished_style="green"), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + TextColumn("•"), + TimeElapsedColumn(), + TextColumn("•"), + TextColumn("{task.completed}/{task.total}"), + console=self.console, + expand=True + ) + + self._progress_display = Group( + Text("", style="dim"), + self.progress + ) + + self._live = Live(self._progress_display, console=self.console, refresh_per_second=10) + self._live.start() + + self.task_id = self.progress.add_task( + "Processing files...", + total=total, + ) + + def processing(self, path: Path) -> None: + if not self.context.should_output: + return + + self.current_file = path + + file_text = Text(str(path), style="dim") + self._progress_display.renderables[0] = file_text + + self.progress.update( + self.task_id, + advance=1, + ) + + self._live.refresh() + + def skipped(self, path: Path) -> None: + if not self.context.should_output: + return + + if not self.context.show_skipped: + return + + self.console.print(f"[yellow]SKIPPED[/yellow] {path}") + + def issue(self, issue: BaseDefFormException) -> None: + pass + + def finish(self, processed: int, issues: list[BaseDefFormException]) -> None: + if not self.context.should_output: + return + + if self._live: + self._live.stop() + self._live = None + + if self.progress: + self.progress.stop() + self.progress = None + + if issues: + self.show_issues(processed, issues) + + def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> None: + if not self.context.should_output: + return + + unique_files = set(issue.path.split(':')[0] for issue in issues) + + if issues: + self.console.print("\n") + + self.console.print(f"[bold yellow]Found {len(issues)} errors in {len(unique_files)} files[/bold yellow]") + self.console.print() + + issues_by_def = {} + for issue in issues: + if issue.path not in issues_by_def: + issues_by_def[issue.path] = [] + issues_by_def[issue.path].append(issue) + + for i, (def_path, def_issues) in enumerate(sorted(issues_by_def.items())): + if i > 0: + self.console.print() + + + self.console.print( + f"[bold cyan link=file://{def_path}]{def_path}[/bold cyan link=file://{def_path}]") + + for j, issue in enumerate(def_issues): + + bullet = "•" + + line_info = "" + if ":" in def_path: + line_info = "" + elif hasattr(issue, 'line') and issue.line: + line_info = f":{issue.line}" + + self.console.print(f" [red]{bullet}[/red] [white]{issue.message}[/white]{line_info}") + + self.console.print() + + self.show_summary(processed, issues) + + def show_summary(self, processed: int, issues: list[BaseDefFormException]) -> None: + if not self.context.should_output: + return + + unique_files = set(issue.path.split(':')[0] for issue in issues) + + summary = Table( + title="[yellow]Summary[/yellow]", + box=box.HORIZONTALS, + show_header=False, + border_style="dim" + ) + summary.add_column(style="bold") + summary.add_column() + + summary.add_row("Files processed:", f"[cyan]{processed}[/cyan]") + summary.add_row("Files with issues:", f"[yellow]{len(unique_files)}[/yellow]") + summary.add_row("Total errors:", f"[red]{len(issues)}[/red]") + + if processed > 0: + success_rate = (processed - len(unique_files)) / processed * 100 + summary.add_row("Success rate:", + f"[{'green' if success_rate > 90 else 'yellow' if success_rate > 70 else 'red'}]" + f"{success_rate:.1f}%" + f"[/{'green' if success_rate > 90 else 'yellow' if success_rate > 70 else 'red'}]") + + self.console.print(summary) + self.console.print() diff --git a/def_form/formatters/def_formatter/checker.py b/def_form/formatters/def_formatter/checker.py index 6f4a248..c2251ec 100644 --- a/def_form/formatters/def_formatter/checker.py +++ b/def_form/formatters/def_formatter/checker.py @@ -13,7 +13,10 @@ def __init__( indent_size: int | None, ): super().__init__( - filepath=filepath, max_def_length=max_def_length, max_inline_args=max_inline_args, indent_size=indent_size + filepath=filepath, + max_def_length=max_def_length, + max_inline_args=max_inline_args, + indent_size=indent_size, ) def leave_FunctionDef(self, original_node: FunctionDef) -> None: diff --git a/def_form/formatters/def_formatter/manager.py b/def_form/formatters/def_formatter/manager.py index fa6bb3e..fd9598b 100644 --- a/def_form/formatters/def_formatter/manager.py +++ b/def_form/formatters/def_formatter/manager.py @@ -1,15 +1,15 @@ import os -import tomli from collections.abc import Generator from pathlib import Path -import click +import tomli import libcst as cst from def_form.exceptions.base import BaseDefFormException from def_form.formatters.def_formatter.checker import DefChecker from def_form.formatters.def_formatter.formatter import DefFormatter from def_form.utils.find_pyproject import find_pyproject_toml +from def_form.cli.ui import BaseUI class DefManager: @@ -24,13 +24,19 @@ def __init__( # noqa: PLR0913 indent_size: int | None = None, config: str | None = None, show_skipped: bool = False, - ): + ui: BaseUI | None = None, + ) -> None: + self.config: str = config or find_pyproject_toml() self.path = Path(path).resolve() - self.show_skipped = show_skipped + self.ui = ui + self.issues: list[BaseDefFormException] = [] + self.formatter_class = formatter + self.checker_class = checker + self._init_config( - config=config, + config=self.config, max_def_length=max_def_length, max_inline_args=max_inline_args, indent_size=indent_size, @@ -38,12 +44,22 @@ def __init__( # noqa: PLR0913 self._init_exclusions(excluded or ()) - self.formatter_class = formatter - self.checker_class = checker + self.ui.show_config_info( + config_path=self.config, + max_inline_args=self.max_inline_args, + max_def_length=self.max_def_length, + indent_size=f'{self.indent_size} spaces', + show_skipped=show_skipped, + excluded=self.excluded, + ) + + # --------------------------------------------------------------------- # + # Configuration + # --------------------------------------------------------------------- # def _init_config( self, - config: str | None, + config: str, max_def_length: int | None, max_inline_args: int | None, indent_size: int | None, @@ -52,98 +68,103 @@ def _init_config( self.max_inline_args = max_inline_args self.indent_size = indent_size - if not config: - config = find_pyproject_toml() - - config_excluded: list[str] = [] - - if config: - try: - with Path.open(Path(config), 'rb') as f: - click.secho(f'Using config: {config}', fg='yellow') - config_data = tomli.load(f) - - config_def = config_data.get('tool', {}).get('def-form', {}) - - self.max_def_length = config_def.get( - 'max_def_length', - self.max_def_length, - ) - self.max_inline_args = config_def.get( - 'max_inline_args', - self.max_inline_args, - ) - - self.indent_size = config_def.get( - 'indent_size', - self.indent_size, - ) + self._config_excluded: list[str] = [] - config_excluded = config_def.get('exclude', []) - except (FileNotFoundError, tomli.TOMLDecodeError) as e: - click.secho(f'Error loading config {config}: {e}', fg='red') - self.is_config_found = False + if not config: + return - self._config_excluded = config_excluded + try: + with Path(config).open("rb") as f: + config_data = tomli.load(f) + + config_def = config_data.get("tool", {}).get("def-form", {}) + + self.max_def_length = config_def.get( + "max_def_length", + self.max_def_length, + ) + self.max_inline_args = config_def.get( + "max_inline_args", + self.max_inline_args, + ) + self.indent_size = config_def.get( + "indent_size", + self.indent_size, + ) + self._config_excluded = config_def.get("exclude", []) + + except (FileNotFoundError, tomli.TOMLDecodeError): + self._config_excluded = [] + + # --------------------------------------------------------------------- # + # Exclusions + # --------------------------------------------------------------------- # def _init_exclusions(self, cli_excluded: tuple[str, ...]) -> None: self.excluded: set[Path] = set() for p in (*cli_excluded, *self._config_excluded): try: - excluded_path = Path(p).resolve() - self.excluded.add(excluded_path) + self.excluded.add(Path(p).resolve()) except Exception: - click.secho(f'Warning: invalid excluded path: {p}', fg='yellow') + continue + + def _is_excluded(self, path: Path) -> bool: + for excluded in self.excluded: + try: + path.relative_to(excluded) + return True + except ValueError: + pass + + if excluded.name in path.parts: + return True + + return False - def _iter_py_files(self) -> Generator[str, None, None]: + # --------------------------------------------------------------------- # + # File iteration + # --------------------------------------------------------------------- # + + def _iter_py_files(self) -> Generator[Path, None, None]: if self.path.is_file(): - if self.path.suffix != '.py': + if self.path.suffix != ".py": return if self._is_excluded(self.path): - if self.show_skipped: - click.secho(f'SKIPPED {self.path}', fg='yellow') + self.ui.skipped(self.path) return - click.secho(f'Processing: {self.path}', fg='green') - yield str(self.path) + yield self.path return for root, dirs, files in os.walk(self.path): root_path = Path(root) - dirs[:] = [d for d in dirs if not self._is_excluded(root_path / d)] + dirs[:] = [ + d for d in dirs if not self._is_excluded(root_path / d) + ] for filename in files: - if not filename.endswith('.py'): + if not filename.endswith(".py"): continue file_path = root_path / filename if self._is_excluded(file_path): - if self.show_skipped: - click.secho(f'SKIPPED {file_path}', fg='yellow') + self.ui.skipped(file_path) continue - click.secho(f'Processing: {file_path}', fg='green') - yield str(file_path) - - def _is_excluded(self, path: Path) -> bool: - for excluded in self.excluded: - try: - path.relative_to(excluded) - return True - except ValueError: - pass - - if excluded.name in path.parts: - return True + yield file_path - return False + # --------------------------------------------------------------------- # + # Processing + # --------------------------------------------------------------------- # def _create_processor( - self, processor_class: type[DefFormatter] | type[DefChecker], filepath: str + self, + processor_class: type[DefFormatter] | type[DefChecker], + filepath: str, ) -> DefFormatter | DefChecker: return processor_class( filepath=filepath, @@ -154,79 +175,82 @@ def _create_processor( def _process_file( self, - filepath: str, + filepath: Path, processor_class: type[DefFormatter] | type[DefChecker], ) -> tuple[cst.Module | None, list[BaseDefFormException]]: try: - with Path.open(Path(filepath), encoding='utf-8') as f: - code = f.read() - except (OSError, UnicodeDecodeError) as e: - click.secho(f'Error reading {filepath}: {e}', fg='red') + code = filepath.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): return None, [] try: tree = cst.parse_module(code) wrapper = cst.metadata.MetadataWrapper(tree) - processor = self._create_processor(processor_class, filepath) + processor = self._create_processor(processor_class, str(filepath)) if issubclass(processor_class, DefFormatter): new_tree = wrapper.visit(processor) return new_tree, processor.issues + wrapper.visit(processor) return None, processor.issues - except cst.ParserSyntaxError as e: - click.secho(f'Syntax error in {filepath}: {e}', fg='red') + except cst.ParserSyntaxError: return None, [] - except Exception as e: - click.secho(f'Unexpected error processing {filepath}: {e}', fg='red') + except Exception: return None, [] - def format(self, write_to: str | None = None) -> None: - processed_count = 0 + # --------------------------------------------------------------------- # + # Public API + # --------------------------------------------------------------------- # + + def format(self) -> None: self.issues.clear() + files = list(self._iter_py_files()) + + if self.ui: + self.ui.start(total=len(files)) - for filepath in self._iter_py_files(): - processed_count += 1 - new_tree, file_issues = self._process_file(filepath, self.formatter_class) + for path in files: + if self.ui: + self.ui.processing(path) + + new_tree, file_issues = self._process_file( + path, + self.formatter_class, + ) self.issues.extend(file_issues) if new_tree is not None: try: - with Path.open(Path(write_to or filepath), 'w', encoding='utf-8') as f: - f.write(new_tree.code) - except OSError as e: - click.secho(f'Error writing {filepath}: {e}', fg='red') + path.write_text(new_tree.code, encoding="utf-8") + except OSError: + continue - self._echo_summary('format', processed_count) + if self.ui: + self.ui.finish(len(files), self.issues) def check(self) -> None: - processed_count = 0 self.issues.clear() + files = list(self._iter_py_files()) - for filepath in self._iter_py_files(): - processed_count += 1 - _, file_issues = self._process_file(filepath, self.checker_class) - self.issues.extend(file_issues) + if self.ui: + self.ui.start(total=len(files)) - self._echo_summary('check', processed_count) + for path in files: + if self.ui: + self.ui.processing(path) - if self.issues: - raise BaseDefFormException + _, file_issues = self._process_file( + path, + self.checker_class, + ) + + self.issues.extend(file_issues) - def _echo_summary(self, mode: str, processed_count: int) -> None: - click.echo('\n') + if self.ui: + self.ui.finish(len(files), self.issues) if self.issues: - click.secho('Issues:', fg='yellow', bold=True) - for i, issue in enumerate(self.issues, 1): - click.secho(f'{issue.path}', color=True) - click.secho(f' {issue.message}', fg='white') - if i != len(self.issues): - click.echo('') - - click.echo('') - click.secho(f'{mode.capitalize()} Summary:', fg='cyan', bold=True) - click.echo(f'Processed files: {processed_count}') - click.echo(f'Issues found: {len(self.issues)}') + raise BaseDefFormException diff --git a/pyproject.toml b/pyproject.toml index 2b6e829..f4638db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ classifiers = [ dependencies = [ "click>=8.2.1", "libcst>=1.8.2", + "rich>=14.3.1", "tomli>=2.4.0", ] diff --git a/uv.lock b/uv.lock index a96900f..e42ece8 100644 --- a/uv.lock +++ b/uv.lock @@ -139,6 +139,7 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "libcst" }, + { name = "rich" }, { name = "tomli" }, ] @@ -154,6 +155,7 @@ dev = [ requires-dist = [ { name = "click", specifier = ">=8.2.1" }, { name = "libcst", specifier = ">=1.8.2" }, + { name = "rich", specifier = ">=14.3.1" }, { name = "tomli", specifier = ">=2.4.0" }, ] @@ -327,6 +329,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mypy" version = "1.19.1" @@ -538,6 +561,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" }, ] +[[package]] +name = "rich" +version = "14.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, +] + [[package]] name = "ruff" version = "0.11.13" From 71d0364fd6c33107284dc1c50369e4b68be41e75 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 23:51:42 +0200 Subject: [PATCH 02/16] Update CLI --- def_form/cli/commands/check.py | 16 ++-- def_form/cli/commands/format.py | 14 ++-- def_form/cli/commands/options.py | 58 +++++---------- def_form/cli/console.py | 27 ------- def_form/cli/console/__init__.py | 9 +++ def_form/cli/console/base.py | 26 +++++++ def_form/cli/console/null.py | 18 +++++ def_form/cli/console/rich.py | 21 ++++++ def_form/cli/main.py | 11 +-- def_form/cli/ui/__init__.py | 6 +- def_form/cli/ui/base.py | 21 ++++-- def_form/cli/ui/null.py | 28 +++++++ def_form/cli/ui/rich.py | 123 ++++++++++++++----------------- 13 files changed, 213 insertions(+), 165 deletions(-) delete mode 100644 def_form/cli/console.py create mode 100644 def_form/cli/console/__init__.py create mode 100644 def_form/cli/console/base.py create mode 100644 def_form/cli/console/null.py create mode 100644 def_form/cli/console/rich.py create mode 100644 def_form/cli/ui/null.py diff --git a/def_form/cli/commands/check.py b/def_form/cli/commands/check.py index 37664d3..ec7e399 100644 --- a/def_form/cli/commands/check.py +++ b/def_form/cli/commands/check.py @@ -1,12 +1,12 @@ import click from def_form.cli.commands.options import common_options -from def_form.cli.console import Console +from def_form.cli.console import RichConsole from def_form.cli.context import context from def_form.cli.errors import CheckFailedError from def_form.cli.ui.rich import RichUI from def_form.exceptions.base import BaseDefFormException -from def_form.formatters import DefManager +from def_form.core import DefManager @click.command() @@ -20,8 +20,8 @@ def check( # noqa: PLR0913 exclude: tuple[str, ...], show_skipped: bool, ) -> None: - console = Console(context=context) - console.info(f"Checking [bold]{path}[/bold]") + console = RichConsole(context=context) + console.info(f'Checking [bold]{path}[/bold]') try: DefManager( @@ -32,11 +32,11 @@ def check( # noqa: PLR0913 indent_size=indent_size, config=config, show_skipped=show_skipped, - ui=RichUI(context=context), + ui=RichUI(console=console), ).check() - except BaseDefFormException: - raise CheckFailedError("Code style violations found") + except BaseDefFormException as exc: + raise CheckFailedError('Code style violations found') from exc except Exception as exc: raise CheckFailedError(str(exc)) from exc - console.success("All checks passed") + console.success('All checks passed') diff --git a/def_form/cli/commands/format.py b/def_form/cli/commands/format.py index 94d3807..f592d37 100644 --- a/def_form/cli/commands/format.py +++ b/def_form/cli/commands/format.py @@ -1,11 +1,11 @@ import click from def_form.cli.commands.options import common_options -from def_form.cli.console import Console +from def_form.cli.console import RichConsole from def_form.cli.context import context from def_form.cli.errors import FormatterFailedError from def_form.cli.ui.rich import RichUI -from def_form.formatters import DefManager +from def_form.core import DefManager @click.command() @@ -20,9 +20,9 @@ def format( # noqa: PLR0913 show_skipped: bool, ) -> None: context.show_skipped = show_skipped - console = Console(context=context) - console.info(f"Formatting [bold]{path}[/bold]") - console.debug("Initializing formatter") + console = RichConsole(context=context) + console.info(f'Formatting [bold]{path}[/bold]') + console.debug('Initializing formatter') try: DefManager( @@ -33,9 +33,9 @@ def format( # noqa: PLR0913 indent_size=indent_size, config=config, show_skipped=show_skipped, - ui=RichUI(context=context), + ui=RichUI(console=console), ).format() except Exception as exc: raise FormatterFailedError(str(exc)) from exc - console.success("Formatting completed") + console.success('Formatting completed') diff --git a/def_form/cli/commands/options.py b/def_form/cli/commands/options.py index 7888621..e9b75c3 100644 --- a/def_form/cli/commands/options.py +++ b/def_form/cli/commands/options.py @@ -1,70 +1,46 @@ +from collections.abc import Callable + import click -def path_option(func): +def path_option(func: Callable) -> Callable: return click.argument('path', type=click.Path(exists=True), default='.')(func) -def max_def_length_option(func): - return click.option( - '--max-def-length', - type=int, - default=None, - help='Maximum length of function definition' - )(func) +def max_def_length_option(func: Callable) -> Callable: + return click.option('--max-def-length', type=int, default=None, help='Maximum length of function definition')(func) -def max_inline_args_option(func): - return click.option( - '--max-inline-args', - type=int, - default=None, - help='Maximum number of inline arguments' - )(func) +def max_inline_args_option(func: Callable) -> Callable: + return click.option('--max-inline-args', type=int, default=None, help='Maximum number of inline arguments')(func) -def indent_size_option(func): - return click.option( - '--indent-size', - type=int, - default=None, - help='Indent size in spaces (default: 4)' - )(func) +def indent_size_option(func: Callable) -> Callable: + return click.option('--indent-size', type=int, default=None, help='Indent size in spaces (default: 4)')(func) -def config_option(func): +def config_option(func: Callable) -> Callable: return click.option( '--config', type=click.Path(exists=True, dir_okay=False), default=None, - help='Path to pyproject.toml configuration file' + help='Path to pyproject.toml configuration file', )(func) -def exclude_option(func): - return click.option( - '--exclude', - multiple=True, - type=click.Path(), - help='Paths to exclude from processing' - )(func) +def exclude_option(func: Callable) -> Callable: + return click.option('--exclude', multiple=True, type=click.Path(), help='Paths to exclude from processing')(func) -def show_skipped_option(func): - return click.option( - '--show-skipped', - is_flag=True, - default=False, - help='Show skipped files and directories' - )(func) +def show_skipped_option(func: Callable) -> Callable: + return click.option('--show-skipped', is_flag=True, default=False, help='Show skipped files and directories')(func) -def common_options(func): +def common_options(func: Callable) -> Callable: func = path_option(func) func = max_def_length_option(func) func = max_inline_args_option(func) func = indent_size_option(func) func = exclude_option(func) func = show_skipped_option(func) - func = config_option(func) - return func \ No newline at end of file + return config_option(func) diff --git a/def_form/cli/console.py b/def_form/cli/console.py deleted file mode 100644 index d7950b8..0000000 --- a/def_form/cli/console.py +++ /dev/null @@ -1,27 +0,0 @@ -from rich.console import Console as RichConsole - -from def_form.cli.context import CLIContext - - -class Console(RichConsole): - def __init__(self, context: CLIContext, *args, **kwargs) -> None: - self.context = context - super().__init__(*args, **kwargs) - - def info(self, message: str) -> None: - if self.context.should_output: - self.print(message) - - def success(self, message: str) -> None: - if self.context.should_output: - self.print(f"[green]{message}[/green]") - - def warning(self, message: str) -> None: - self.print(f"[yellow]{message}[/yellow]") - - def error(self, message: str) -> None: - self.print(f"[red]{message}[/red]") - - def debug(self, message: str) -> None: - if self.context.verbose: - self.print(f"[dim]{message}[/dim]") diff --git a/def_form/cli/console/__init__.py b/def_form/cli/console/__init__.py new file mode 100644 index 0000000..48bf42e --- /dev/null +++ b/def_form/cli/console/__init__.py @@ -0,0 +1,9 @@ +from def_form.cli.console.base import BaseConsole +from def_form.cli.console.rich import RichConsole +from def_form.cli.console.null import NullConsole + +__all__ = [ + 'BaseConsole', + 'NullConsole', + 'RichConsole', +] diff --git a/def_form/cli/console/base.py b/def_form/cli/console/base.py new file mode 100644 index 0000000..9db0f48 --- /dev/null +++ b/def_form/cli/console/base.py @@ -0,0 +1,26 @@ +from typing import Any + +from rich.console import Console + +from def_form.cli.context import CLIContext + + +class BaseConsole(Console): + def __init__(self, context: CLIContext, *args: Any, **kwargs: Any) -> None: + self.context = context + super().__init__(*args, **kwargs) + + def info(self, message: str) -> None: + raise NotImplementedError + + def success(self, message: str) -> None: + raise NotImplementedError + + def warning(self, message: str) -> None: + raise NotImplementedError + + def error(self, message: str) -> None: + raise NotImplementedError + + def debug(self, message: str) -> None: + raise NotImplementedError diff --git a/def_form/cli/console/null.py b/def_form/cli/console/null.py new file mode 100644 index 0000000..41aec38 --- /dev/null +++ b/def_form/cli/console/null.py @@ -0,0 +1,18 @@ +from def_form.cli.console.base import BaseConsole + + +class NullConsole(BaseConsole): + def info(self, message: str) -> None: + return + + def success(self, message: str) -> None: + return + + def warning(self, message: str) -> None: + return + + def error(self, message: str) -> None: + return + + def debug(self, message: str) -> None: + return diff --git a/def_form/cli/console/rich.py b/def_form/cli/console/rich.py new file mode 100644 index 0000000..d30efbc --- /dev/null +++ b/def_form/cli/console/rich.py @@ -0,0 +1,21 @@ +from def_form.cli.console.base import BaseConsole + + +class RichConsole(BaseConsole): + def info(self, message: str) -> None: + if self.context.should_output: + self.print(message) + + def success(self, message: str) -> None: + if self.context.should_output: + self.print(f'[green]{message}[/green]') + + def warning(self, message: str) -> None: + self.print(f'[yellow]{message}[/yellow]') + + def error(self, message: str) -> None: + self.print(f'[red]{message}[/red]') + + def debug(self, message: str) -> None: + if self.context.verbose: + self.print(f'[dim]{message}[/dim]') diff --git a/def_form/cli/main.py b/def_form/cli/main.py index 8298335..851b68a 100644 --- a/def_form/cli/main.py +++ b/def_form/cli/main.py @@ -1,12 +1,13 @@ import click import sys -from def_form.cli.console import Console +from def_form.cli.console import RichConsole from def_form.cli.context import context from def_form.cli.errors import CLIError from def_form.cli.commands.check import check from def_form.cli.commands.format import format -console = Console(context=context) +console = RichConsole(context=context) + @click.group() @click.option('--verbose', is_flag=True, help='Enable verbose output') @@ -27,12 +28,12 @@ def main() -> None: console.error(str(exc)) sys.exit(1) except KeyboardInterrupt: - console.error("Operation cancelled by user") + console.error('Operation cancelled by user') sys.exit(130) except Exception as exc: - console.error(f"Unexpected error: {exc}") + console.error(f'Unexpected error: {exc}') sys.exit(1) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/def_form/cli/ui/__init__.py b/def_form/cli/ui/__init__.py index ccc4c7f..514ad55 100644 --- a/def_form/cli/ui/__init__.py +++ b/def_form/cli/ui/__init__.py @@ -1,7 +1,5 @@ from def_form.cli.ui.base import BaseUI +from def_form.cli.ui.null import NullUI from def_form.cli.ui.rich import RichUI -__all__ = [ - 'BaseUI', - 'RichUI', -] \ No newline at end of file +__all__ = ['BaseUI', 'NullUI', 'RichUI'] diff --git a/def_form/cli/ui/base.py b/def_form/cli/ui/base.py index a0942a1..d62ca11 100644 --- a/def_form/cli/ui/base.py +++ b/def_form/cli/ui/base.py @@ -1,34 +1,41 @@ from abc import ABC, abstractmethod from pathlib import Path from typing import Any + +from def_form.cli.console import BaseConsole +from def_form.cli.context import CLIContext from def_form.exceptions.base import BaseDefFormException class BaseUI(ABC): + def __init__(self, console: BaseConsole) -> None: + self.console: BaseConsole = console + self.context: CLIContext = console.context + @abstractmethod def show_config_info(self, **config: Any) -> None: - raise NotImplemented + raise NotImplementedError @abstractmethod def start(self, total: int | None) -> None: - raise NotImplemented + raise NotImplementedError @abstractmethod def processing(self, path: Path) -> None: - raise NotImplemented + raise NotImplementedError @abstractmethod def skipped(self, path: Path) -> None: - raise NotImplemented + raise NotImplementedError @abstractmethod def finish(self, processed: int, issues: list[BaseDefFormException]) -> None: - raise NotImplemented + raise NotImplementedError @abstractmethod def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> None: - raise NotImplemented + raise NotImplementedError @abstractmethod def show_summary(self, processed: int, issues: list[BaseDefFormException]) -> None: - raise NotImplemented + raise NotImplementedError diff --git a/def_form/cli/ui/null.py b/def_form/cli/ui/null.py new file mode 100644 index 0000000..ba3db3d --- /dev/null +++ b/def_form/cli/ui/null.py @@ -0,0 +1,28 @@ +from pathlib import Path +from typing import Any + +from def_form.cli.ui import BaseUI +from def_form.exceptions.base import BaseDefFormException + + +class NullUI(BaseUI): + def show_config_info(self, **config: Any) -> None: + return + + def start(self, total: int | None) -> None: + return + + def processing(self, path: Path) -> None: + return + + def skipped(self, path: Path) -> None: + return + + def finish(self, processed: int, issues: list[BaseDefFormException]) -> None: + return + + def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> None: + return + + def show_summary(self, processed: int, issues: list[BaseDefFormException]) -> None: + return diff --git a/def_form/cli/ui/rich.py b/def_form/cli/ui/rich.py index d41e7ae..c21f098 100644 --- a/def_form/cli/ui/rich.py +++ b/def_form/cli/ui/rich.py @@ -1,24 +1,27 @@ +# ruff: noqa: PLR2004 +from collections import defaultdict from pathlib import Path from typing import Any from rich import box from rich.console import Group from rich.live import Live -from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn +from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, TaskID from rich.table import Table from rich.text import Text -from def_form.cli.console import Console -from def_form.cli.context import CLIContext +from def_form.cli.console import BaseConsole from def_form.cli.ui.base import BaseUI from def_form.exceptions.base import BaseDefFormException class RichUI(BaseUI): - def __init__(self, context: CLIContext) -> None: - self.context = context - self.console = Console(context=context) + def __init__(self, console: BaseConsole) -> None: + super().__init__( + console=console, + ) self.progress: Progress | None = None + self._live: Live | None = None self.task_id: int | None = None self.current_file: Path | None = None @@ -39,39 +42,35 @@ def show_config_info(self, **config: Any) -> None: return config_table = Table( - title="[yellow]Configuration[/yellow]", - box=box.HORIZONTALS, - show_header=False, - border_style="dim" + title='[yellow]Configuration[/yellow]', box=box.HORIZONTALS, show_header=False, border_style='dim' ) - config_table.add_column(style="dim") - config_table.add_column(style="cyan") + config_table.add_column(style='dim') + config_table.add_column(style='cyan') for key, value in config.items(): - if value is None or value == "": + if value is None or value == '': continue display_key = key.replace('_', ' ').title() if key == 'config_path': - config_table.add_row(f"{display_key}:", f"[bold yellow]{value}[/bold yellow]") + config_table.add_row(f'{display_key}:', f'[bold yellow]{value}[/bold yellow]') else: - - if isinstance(value, (list, set, tuple)): + if isinstance(value, list | set | tuple): if not value: continue items = [self._convert_to_string(item) for item in value] if len(items) <= 3: - display_value = ", ".join(items) + display_value = ', '.join(items) else: - preview = ", ".join(items[:3]) - display_value = f"{preview} (+{len(items) - 3} more)" + preview = ', '.join(items[:3]) + display_value = f'{preview} (+{len(items) - 3} more)' else: display_value = self._convert_to_string(value) - config_table.add_row(f"{display_key}:", display_value) + config_table.add_row(f'{display_key}:', display_value) self.console.print() self.console.print(config_table) @@ -81,30 +80,25 @@ def start(self, total: int | None) -> None: if not self.context.should_output: return - self._issues = [] - self.progress = Progress( SpinnerColumn(), - BarColumn(bar_width=None, complete_style="blue", finished_style="green"), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TextColumn("•"), + BarColumn(bar_width=None, complete_style='blue', finished_style='green'), + TextColumn('[progress.percentage]{task.percentage:>3.0f}%'), + TextColumn('•'), TimeElapsedColumn(), - TextColumn("•"), - TextColumn("{task.completed}/{task.total}"), + TextColumn('•'), + TextColumn('{task.completed}/{task.total}'), console=self.console, - expand=True + expand=True, ) - self._progress_display = Group( - Text("", style="dim"), - self.progress - ) + self._progress_display = Group(Text('', style='dim'), self.progress) self._live = Live(self._progress_display, console=self.console, refresh_per_second=10) self._live.start() self.task_id = self.progress.add_task( - "Processing files...", + 'Processing files...', total=total, ) @@ -112,13 +106,16 @@ def processing(self, path: Path) -> None: if not self.context.should_output: return + if not (self.progress and self._live and self.task_id): + return + self.current_file = path - file_text = Text(str(path), style="dim") + file_text = Text(str(path), style='dim') self._progress_display.renderables[0] = file_text self.progress.update( - self.task_id, + TaskID(self.task_id), advance=1, ) @@ -131,7 +128,7 @@ def skipped(self, path: Path) -> None: if not self.context.show_skipped: return - self.console.print(f"[yellow]SKIPPED[/yellow] {path}") + self.console.print(f'[yellow]SKIPPED[/yellow] {path}') def issue(self, issue: BaseDefFormException) -> None: pass @@ -155,15 +152,15 @@ def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> Non if not self.context.should_output: return - unique_files = set(issue.path.split(':')[0] for issue in issues) + unique_files = {issue.path.split(':')[0] for issue in issues} if issues: - self.console.print("\n") + self.console.print('\n') - self.console.print(f"[bold yellow]Found {len(issues)} errors in {len(unique_files)} files[/bold yellow]") + self.console.print(f'[bold yellow]Found {len(issues)} errors in {len(unique_files)} files[/bold yellow]') self.console.print() - issues_by_def = {} + issues_by_def: dict[str, list[BaseDefFormException]] = defaultdict(list) for issue in issues: if issue.path not in issues_by_def: issues_by_def[issue.path] = [] @@ -173,21 +170,18 @@ def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> Non if i > 0: self.console.print() + self.console.print(f'[bold cyan link=file://{def_path}]{def_path}[/bold cyan link=file://{def_path}]') - self.console.print( - f"[bold cyan link=file://{def_path}]{def_path}[/bold cyan link=file://{def_path}]") - - for j, issue in enumerate(def_issues): + for _j, issue in enumerate(def_issues): + bullet = '•' - bullet = "•" - - line_info = "" - if ":" in def_path: - line_info = "" + line_info = '' + if ':' in def_path: + line_info = '' elif hasattr(issue, 'line') and issue.line: - line_info = f":{issue.line}" + line_info = f':{issue.line}' - self.console.print(f" [red]{bullet}[/red] [white]{issue.message}[/white]{line_info}") + self.console.print(f' [red]{bullet}[/red] [white]{issue.message}[/white]{line_info}') self.console.print() @@ -197,27 +191,24 @@ def show_summary(self, processed: int, issues: list[BaseDefFormException]) -> No if not self.context.should_output: return - unique_files = set(issue.path.split(':')[0] for issue in issues) + unique_files = {issue.path.split(':')[0] for issue in issues} - summary = Table( - title="[yellow]Summary[/yellow]", - box=box.HORIZONTALS, - show_header=False, - border_style="dim" - ) - summary.add_column(style="bold") + summary = Table(title='[yellow]Summary[/yellow]', box=box.HORIZONTALS, show_header=False, border_style='dim') + summary.add_column(style='bold') summary.add_column() - summary.add_row("Files processed:", f"[cyan]{processed}[/cyan]") - summary.add_row("Files with issues:", f"[yellow]{len(unique_files)}[/yellow]") - summary.add_row("Total errors:", f"[red]{len(issues)}[/red]") + summary.add_row('Files processed:', f'[cyan]{processed}[/cyan]') + summary.add_row('Files with issues:', f'[yellow]{len(unique_files)}[/yellow]') + summary.add_row('Total errors:', f'[red]{len(issues)}[/red]') if processed > 0: success_rate = (processed - len(unique_files)) / processed * 100 - summary.add_row("Success rate:", - f"[{'green' if success_rate > 90 else 'yellow' if success_rate > 70 else 'red'}]" - f"{success_rate:.1f}%" - f"[/{'green' if success_rate > 90 else 'yellow' if success_rate > 70 else 'red'}]") + summary.add_row( + 'Success rate:', + f'[{"green" if success_rate > 90 else "yellow" if success_rate > 70 else "red"}]' + f'{success_rate:.1f}%' + f'[/{"green" if success_rate > 90 else "yellow" if success_rate > 70 else "red"}]', + ) self.console.print(summary) self.console.print() From 6fbf6f486bb37d14aef2ddfe82770eadb758d2d8 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 23:54:30 +0200 Subject: [PATCH 03/16] Update core structure --- def_form/core/__init__.py | 5 + .../def_formatter => core}/base.py | 72 +++------ .../def_formatter => core}/checker.py | 2 +- def_form/core/formatter.py | 38 +++++ .../def_formatter => core}/manager.py | 73 +++++---- .../def_formatter => core}/models.py | 0 def_form/core/node_builder.py | 139 ++++++++++++++++ def_form/core/params.py | 17 ++ def_form/core/rules/__init__.py | 44 ++++++ def_form/core/rules/base.py | 11 ++ def_form/core/rules/context.py | 14 ++ def_form/core/rules/max_def_length.py | 18 +++ def_form/core/rules/max_inline_args.py | 18 +++ .../core/rules/multiline_params_indent.py | 16 ++ def_form/exceptions/base.py | 4 +- def_form/exceptions/def_formatter.py | 6 + def_form/formatters/__init__.py | 5 - def_form/formatters/def_formatter/__init__.py | 5 - .../formatters/def_formatter/formatter.py | 149 ------------------ 19 files changed, 386 insertions(+), 250 deletions(-) create mode 100644 def_form/core/__init__.py rename def_form/{formatters/def_formatter => core}/base.py (69%) rename def_form/{formatters/def_formatter => core}/checker.py (91%) create mode 100644 def_form/core/formatter.py rename def_form/{formatters/def_formatter => core}/manager.py (80%) rename def_form/{formatters/def_formatter => core}/models.py (100%) create mode 100644 def_form/core/node_builder.py create mode 100644 def_form/core/params.py create mode 100644 def_form/core/rules/__init__.py create mode 100644 def_form/core/rules/base.py create mode 100644 def_form/core/rules/context.py create mode 100644 def_form/core/rules/max_def_length.py create mode 100644 def_form/core/rules/max_inline_args.py create mode 100644 def_form/core/rules/multiline_params_indent.py delete mode 100644 def_form/formatters/__init__.py delete mode 100644 def_form/formatters/def_formatter/__init__.py delete mode 100644 def_form/formatters/def_formatter/formatter.py diff --git a/def_form/core/__init__.py b/def_form/core/__init__.py new file mode 100644 index 0000000..72e5374 --- /dev/null +++ b/def_form/core/__init__.py @@ -0,0 +1,5 @@ +from def_form.core.manager import DefManager + +__all__ = [ + 'DefManager', +] diff --git a/def_form/formatters/def_formatter/base.py b/def_form/core/base.py similarity index 69% rename from def_form/formatters/def_formatter/base.py rename to def_form/core/base.py index 7b2f6ba..4af973f 100644 --- a/def_form/formatters/def_formatter/base.py +++ b/def_form/core/base.py @@ -2,7 +2,8 @@ from pathlib import Path from typing import cast -from libcst import FunctionDef, Comma +from libcst import Comma +from libcst import FunctionDef from libcst import MetadataDependent from libcst import Module from libcst import Param @@ -12,10 +13,10 @@ from libcst.metadata import PositionProvider from def_form.exceptions.base import BaseDefFormException -from def_form.exceptions.def_formatter import DefStringTooLongException -from def_form.exceptions.def_formatter import InvalidMultilineParamsIndentException -from def_form.exceptions.def_formatter import TooManyInlineArgumentsException -from def_form.formatters.def_formatter.models import FunctionAnalysis +from def_form.core.models import FunctionAnalysis +from def_form.core.params import get_params_list +from def_form.core.rules import run_rules +from def_form.core.rules.context import RuleContext class DefBase(MetadataDependent): @@ -35,27 +36,6 @@ def __init__( self.indent_size = indent_size if indent_size is not None else 4 self.issues: list[BaseDefFormException] = [] - def _check_issues(self, line_length: int, line_no: int, arg_count: int) -> list[BaseDefFormException]: - issues: list[BaseDefFormException] = [] - - if self.max_def_length and line_length > self.max_def_length: - issues.append( - DefStringTooLongException( - path=f'{self.filepath}:{line_no}', - message=f'Function definition too long ({line_length} > {self.max_def_length})', - ) - ) - - if self.max_inline_args and arg_count > self.max_inline_args: - issues.append( - TooManyInlineArgumentsException( - path=f'{self.filepath}:{line_no}', - message=f'Too many inline args ({arg_count} > {self.max_inline_args})', - ) - ) - - return issues - def is_single_line_function(self, node: FunctionDef) -> bool: func_code = Module([]).code_for_node(node).strip() lines = func_code.split('\n') @@ -87,20 +67,6 @@ def has_skip_comment(self, node: FunctionDef) -> bool: return False - def _get_params_list(self, node: FunctionDef) -> list[Param]: - params: list[Param] = [] - params.extend(node.params.params) - - if isinstance(node.params.star_arg, Param): - params.append(node.params.star_arg) - - params.extend(node.params.kwonly_params) - - if isinstance(node.params.star_kwarg, Param): - params.append(node.params.star_kwarg) - - return params - def has_correct_multiline_params_format(self, node: FunctionDef) -> bool: # noqa: PLR0911, PLR0912 ws = node.whitespace_before_params if not isinstance(ws, ParenthesizedWhitespace): @@ -116,7 +82,7 @@ def has_correct_multiline_params_format(self, node: FunctionDef) -> bool: # noq if ws.last_line.value != expected_indent: return False - all_params = self._get_params_list(node) + all_params = get_params_list(node) if not all_params: return True @@ -195,18 +161,18 @@ def analyze_function(self, node: FunctionDef) -> FunctionAnalysis: pos = self.get_metadata(PositionProvider, node) line_no = pos.start.line - issues: list[BaseDefFormException] = [] - - if self.is_single_line_function(node): - issues = self._check_issues(line_length, line_no, arg_count) - elif not self.has_correct_multiline_params_format(node): - issues.append( - InvalidMultilineParamsIndentException( - path=f'{self.filepath}:{line_no}', - message=f'Invalid multiline function parameters indentation (expected {self.indent_size} spaces)', - ) - ) - + context = RuleContext( + filepath=self.filepath, + line_no=line_no, + line_length=line_length, + arg_count=arg_count, + is_single_line=self.is_single_line_function(node), + has_correct_multiline_format=self.has_correct_multiline_params_format(node), + indent_size=self.indent_size, + max_def_length=self.max_def_length, + max_inline_args=self.max_inline_args, + ) + issues = run_rules(context) has_issues = bool(issues) return FunctionAnalysis( diff --git a/def_form/formatters/def_formatter/checker.py b/def_form/core/checker.py similarity index 91% rename from def_form/formatters/def_formatter/checker.py rename to def_form/core/checker.py index c2251ec..cc75244 100644 --- a/def_form/formatters/def_formatter/checker.py +++ b/def_form/core/checker.py @@ -1,7 +1,7 @@ from libcst import CSTVisitor from libcst import FunctionDef -from def_form.formatters.def_formatter.base import DefBase +from def_form.core.base import DefBase class DefChecker(DefBase, CSTVisitor): diff --git a/def_form/core/formatter.py b/def_form/core/formatter.py new file mode 100644 index 0000000..4b1baac --- /dev/null +++ b/def_form/core/formatter.py @@ -0,0 +1,38 @@ +from libcst import CSTTransformer +from libcst import FunctionDef + +from def_form.core.base import DefBase +from def_form.core.node_builder import build_parameters + + +class DefFormatter(DefBase, CSTTransformer): + def __init__( + self, + filepath: str, + max_def_length: int | None, + max_inline_args: int | None, + indent_size: int | None = None, + ): + super().__init__( + filepath=filepath, + max_def_length=max_def_length, + max_inline_args=max_inline_args, + indent_size=indent_size, + ) + + def leave_FunctionDef(self, original_node: FunctionDef, updated_node: FunctionDef) -> FunctionDef: + analysis = self.analyze_function(original_node) + if not analysis.should_process: + return updated_node + if analysis.issues: + self.issues.extend(analysis.issues) + is_single_line = self.is_single_line_function(original_node) + params, whitespace_before_params = build_parameters( + updated_node, + is_single_line=is_single_line, + indent_size=self.indent_size, + ) + return updated_node.with_changes( + params=params, + whitespace_before_params=whitespace_before_params, + ) diff --git a/def_form/formatters/def_formatter/manager.py b/def_form/core/manager.py similarity index 80% rename from def_form/formatters/def_formatter/manager.py rename to def_form/core/manager.py index fd9598b..d68d37d 100644 --- a/def_form/formatters/def_formatter/manager.py +++ b/def_form/core/manager.py @@ -5,17 +5,19 @@ import tomli import libcst as cst +from def_form.cli.ui import BaseUI from def_form.exceptions.base import BaseDefFormException -from def_form.formatters.def_formatter.checker import DefChecker -from def_form.formatters.def_formatter.formatter import DefFormatter +from def_form.exceptions.def_formatter import CheckCommandFoundAnIssue +from def_form.core.checker import DefChecker +from def_form.core.formatter import DefFormatter from def_form.utils.find_pyproject import find_pyproject_toml -from def_form.cli.ui import BaseUI class DefManager: def __init__( # noqa: PLR0913 self, path: str, + ui: BaseUI, excluded: tuple[str, ...] | None = None, formatter: type[DefFormatter] = DefFormatter, checker: type[DefChecker] = DefChecker, @@ -24,9 +26,8 @@ def __init__( # noqa: PLR0913 indent_size: int | None = None, config: str | None = None, show_skipped: bool = False, - ui: BaseUI | None = None, ) -> None: - self.config: str = config or find_pyproject_toml() + self.config: str | None = config or find_pyproject_toml() self.path = Path(path).resolve() self.ui = ui @@ -59,7 +60,7 @@ def __init__( # noqa: PLR0913 def _init_config( self, - config: str, + config: str | None, max_def_length: int | None, max_inline_args: int | None, indent_size: int | None, @@ -74,24 +75,24 @@ def _init_config( return try: - with Path(config).open("rb") as f: + with Path(config).open('rb') as f: config_data = tomli.load(f) - config_def = config_data.get("tool", {}).get("def-form", {}) + config_def = config_data.get('tool', {}).get('def-form', {}) self.max_def_length = config_def.get( - "max_def_length", + 'max_def_length', self.max_def_length, ) self.max_inline_args = config_def.get( - "max_inline_args", + 'max_inline_args', self.max_inline_args, ) self.indent_size = config_def.get( - "indent_size", + 'indent_size', self.indent_size, ) - self._config_excluded = config_def.get("exclude", []) + self._config_excluded = config_def.get('exclude', []) except (FileNotFoundError, tomli.TOMLDecodeError): self._config_excluded = [] @@ -128,7 +129,7 @@ def _is_excluded(self, path: Path) -> bool: def _iter_py_files(self) -> Generator[Path, None, None]: if self.path.is_file(): - if self.path.suffix != ".py": + if self.path.suffix != '.py': return if self._is_excluded(self.path): @@ -141,12 +142,10 @@ def _iter_py_files(self) -> Generator[Path, None, None]: for root, dirs, files in os.walk(self.path): root_path = Path(root) - dirs[:] = [ - d for d in dirs if not self._is_excluded(root_path / d) - ] + dirs[:] = [d for d in dirs if not self._is_excluded(root_path / d)] for filename in files: - if not filename.endswith(".py"): + if not filename.endswith('.py'): continue file_path = root_path / filename @@ -179,7 +178,7 @@ def _process_file( processor_class: type[DefFormatter] | type[DefChecker], ) -> tuple[cst.Module | None, list[BaseDefFormException]]: try: - code = filepath.read_text(encoding="utf-8") + code = filepath.read_text(encoding='utf-8') except (OSError, UnicodeDecodeError): return None, [] @@ -200,6 +199,16 @@ def _process_file( except Exception: return None, [] + def _write( + self, + dest: str | Path, + module: str, + ) -> None: + try: + Path(dest).write_text(module, encoding='utf-8') + except OSError: + self.ui.console.error(f'Exception occurred while writing to {dest}') + # --------------------------------------------------------------------- # # Public API # --------------------------------------------------------------------- # @@ -208,12 +217,10 @@ def format(self) -> None: self.issues.clear() files = list(self._iter_py_files()) - if self.ui: - self.ui.start(total=len(files)) + self.ui.start(total=len(files)) for path in files: - if self.ui: - self.ui.processing(path) + self.ui.processing(path) new_tree, file_issues = self._process_file( path, @@ -223,24 +230,21 @@ def format(self) -> None: self.issues.extend(file_issues) if new_tree is not None: - try: - path.write_text(new_tree.code, encoding="utf-8") - except OSError: - continue + self._write( + dest=path, + module=new_tree.code, + ) - if self.ui: - self.ui.finish(len(files), self.issues) + self.ui.finish(len(files), self.issues) def check(self) -> None: self.issues.clear() files = list(self._iter_py_files()) - if self.ui: - self.ui.start(total=len(files)) + self.ui.start(total=len(files)) for path in files: - if self.ui: - self.ui.processing(path) + self.ui.processing(path) _, file_issues = self._process_file( path, @@ -249,8 +253,7 @@ def check(self) -> None: self.issues.extend(file_issues) - if self.ui: - self.ui.finish(len(files), self.issues) + self.ui.finish(len(files), self.issues) if self.issues: - raise BaseDefFormException + raise CheckCommandFoundAnIssue(str(self.path), 'check command did found an issue') diff --git a/def_form/formatters/def_formatter/models.py b/def_form/core/models.py similarity index 100% rename from def_form/formatters/def_formatter/models.py rename to def_form/core/models.py diff --git a/def_form/core/node_builder.py b/def_form/core/node_builder.py new file mode 100644 index 0000000..e37d76a --- /dev/null +++ b/def_form/core/node_builder.py @@ -0,0 +1,139 @@ +from typing import Any + +from libcst import Comma, MaybeSentinel +from libcst import Comment +from libcst import FunctionDef +from libcst import Param +from libcst import Parameters +from libcst import ParenthesizedWhitespace +from libcst import SimpleWhitespace +from libcst import TrailingWhitespace + +from def_form.core.params import get_params_list + + +def _is_valid_param(param: Any) -> bool: + return isinstance(param, Param) + + +def _extract_comment_from_whitespace(ws: Any) -> TrailingWhitespace | None: + if isinstance(ws, TrailingWhitespace): + return ws + if isinstance(ws, ParenthesizedWhitespace) and isinstance(ws.first_line, TrailingWhitespace): + return ws.first_line + if isinstance(ws, SimpleWhitespace) and '#' in ws.value: + parts = ws.value.split('#', 1) + spaces = parts[0].rstrip() + comment_text = '#' + parts[1].rstrip() + return TrailingWhitespace( + whitespace=SimpleWhitespace(spaces + ' ' if spaces else ''), + comment=Comment(comment_text), + ) + return None + + +def _extract_comment_from_param(param: Param) -> TrailingWhitespace | None: + if isinstance(param.comma, Comma) and param.comma.whitespace_after: + comment = _extract_comment_from_whitespace(param.comma.whitespace_after) + if comment: + return comment + if param.whitespace_after_param: + return _extract_comment_from_whitespace(param.whitespace_after_param) + return None + + +def _create_whitespace( + indent_size: int, + comment: TrailingWhitespace | None, + last_line: SimpleWhitespace, +) -> ParenthesizedWhitespace: + if comment: + return ParenthesizedWhitespace(first_line=comment, empty_lines=[], indent=True, last_line=last_line) + return ParenthesizedWhitespace(last_line=last_line, indent=True) + + +def _create_formatted_param_for_single_line( + indent_size: int, + param: Param, + is_last: bool = False, +) -> Param: + indent = ' ' * indent_size + comment = _extract_comment_from_param(param) + last_line = SimpleWhitespace('' if is_last else indent) + new_ws = _create_whitespace(indent_size, comment, last_line) + need_comma = param.comma is not None if is_last else True + new_comma = Comma(whitespace_after=new_ws) if need_comma else None + return param.with_changes(comma=new_comma, whitespace_after_param=SimpleWhitespace('')) + + +def _create_formatted_param_for_multi_line( + indent_size: int, + param: Param, + is_last: bool = False, +) -> Param: + indent = ' ' * indent_size + last_line = SimpleWhitespace('' if is_last else indent) + original_ws = param.comma.whitespace_after if isinstance(param.comma, Comma) else None + + if isinstance(original_ws, ParenthesizedWhitespace): + comment = original_ws.first_line if isinstance(original_ws.first_line, TrailingWhitespace) else None + new_ws = _create_whitespace(indent_size, comment, last_line) + elif isinstance(original_ws, TrailingWhitespace): + new_ws = _create_whitespace(indent_size, original_ws, last_line) + else: + new_ws = _create_whitespace(indent_size, None, last_line) + + need_comma = param.comma is not None if is_last else True + new_comma = Comma(whitespace_after=new_ws) if need_comma else None + return param.with_changes(comma=new_comma, whitespace_after_param=SimpleWhitespace('')) + + +def _restore_param_groups( + formatted_params: list[Param], + node: FunctionDef, +) -> tuple[list[Param], list[Param], Param | None, Param | None]: + param_count = len(node.params.params) + kwonly_count = len(node.params.kwonly_params) + new_params = formatted_params[:param_count] + remaining = formatted_params[param_count:] + + new_star_arg = None + if _is_valid_param(node.params.star_arg) and remaining: + new_star_arg = remaining[0] + remaining = remaining[1:] + + new_kwonly_params = remaining[:kwonly_count] if kwonly_count else [] + remaining = remaining[kwonly_count:] + + new_star_kwarg = None + if _is_valid_param(node.params.star_kwarg) and remaining: + new_star_kwarg = remaining[0] + + return new_params, new_kwonly_params, new_star_arg, new_star_kwarg + + +def build_parameters( + node: FunctionDef, + is_single_line: bool, + indent_size: int, +) -> tuple[Parameters, ParenthesizedWhitespace]: + all_params = get_params_list(node) + total_params = len(all_params) + if is_single_line: + format_param = lambda p, last: _create_formatted_param_for_single_line(indent_size, p, last) + else: + format_param = lambda p, last: _create_formatted_param_for_multi_line(indent_size, p, last) + formatted_params = [format_param(param, i == total_params - 1) for i, param in enumerate(all_params)] + new_params, new_kwonly_params, new_star_arg, new_star_kwarg = _restore_param_groups(formatted_params, node) + params = Parameters( + params=new_params, + posonly_params=node.params.posonly_params, + kwonly_params=new_kwonly_params, + star_arg=new_star_arg if new_star_arg is not None else MaybeSentinel.DEFAULT, + star_kwarg=new_star_kwarg, + ) + whitespace_before_params = ParenthesizedWhitespace( + last_line=SimpleWhitespace(' ' * indent_size), + indent=True, + ) + return params, whitespace_before_params diff --git a/def_form/core/params.py b/def_form/core/params.py new file mode 100644 index 0000000..c846ef3 --- /dev/null +++ b/def_form/core/params.py @@ -0,0 +1,17 @@ +from libcst import FunctionDef +from libcst import Param + + +def get_params_list(node: FunctionDef) -> list[Param]: + params: list[Param] = [] + params.extend(node.params.params) + + if isinstance(node.params.star_arg, Param): + params.append(node.params.star_arg) + + params.extend(node.params.kwonly_params) + + if isinstance(node.params.star_kwarg, Param): + params.append(node.params.star_kwarg) + + return params diff --git a/def_form/core/rules/__init__.py b/def_form/core/rules/__init__.py new file mode 100644 index 0000000..5a8af26 --- /dev/null +++ b/def_form/core/rules/__init__.py @@ -0,0 +1,44 @@ +"""Rules layer: rule classes and runner. + +The analyzer builds RuleContext and runs rules; it does not contain +any "what is a violation" logic — that lives in rule classes. +""" + +from def_form.exceptions.base import BaseDefFormException + +from def_form.core.rules.base import Rule +from def_form.core.rules.context import RuleContext +from def_form.core.rules.max_def_length import RuleMaxDefLength +from def_form.core.rules.max_inline_args import RuleMaxInlineArgs +from def_form.core.rules.multiline_params_indent import ( + RuleMultilineParamsIndent, +) + +# Default rule set used when none is passed +DEFAULT_RULES: tuple[Rule, ...] = ( + RuleMaxDefLength(), + RuleMaxInlineArgs(), + RuleMultilineParamsIndent(), +) + + +def run_rules( + context: RuleContext, + rules: tuple[Rule, ...] | list[Rule] | None = None, +) -> list[BaseDefFormException]: + """Run all rules on the context and return combined issues.""" + rule_list = rules if rules is not None else list(DEFAULT_RULES) + issues: list[BaseDefFormException] = [] + for rule in rule_list: + issues.extend(rule.check(context)) + return issues + + +__all__ = [ + 'DEFAULT_RULES', + 'Rule', + 'RuleMaxDefLength', + 'RuleMaxInlineArgs', + 'RuleMultilineParamsIndent', + 'run_rules', +] diff --git a/def_form/core/rules/base.py b/def_form/core/rules/base.py new file mode 100644 index 0000000..9bc32a4 --- /dev/null +++ b/def_form/core/rules/base.py @@ -0,0 +1,11 @@ +from abc import ABC +from abc import abstractmethod + +from def_form.exceptions.base import BaseDefFormException +from def_form.core.rules.context import RuleContext + + +class Rule(ABC): + @abstractmethod + def check(self, context: RuleContext) -> list[BaseDefFormException]: + raise NotImplementedError diff --git a/def_form/core/rules/context.py b/def_form/core/rules/context.py new file mode 100644 index 0000000..9c486d6 --- /dev/null +++ b/def_form/core/rules/context.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class RuleContext: + filepath: str + line_no: int + line_length: int + arg_count: int + is_single_line: bool + has_correct_multiline_format: bool + indent_size: int + max_def_length: int | None + max_inline_args: int | None diff --git a/def_form/core/rules/max_def_length.py b/def_form/core/rules/max_def_length.py new file mode 100644 index 0000000..22a1d16 --- /dev/null +++ b/def_form/core/rules/max_def_length.py @@ -0,0 +1,18 @@ +from def_form.exceptions.base import BaseDefFormException +from def_form.exceptions.def_formatter import DefStringTooLongException +from def_form.core.rules.base import Rule +from def_form.core.rules.context import RuleContext + + +class RuleMaxDefLength(Rule): + def check(self, context: RuleContext) -> list[BaseDefFormException]: + if not context.is_single_line: + return [] + if not context.max_def_length or context.line_length <= context.max_def_length: + return [] + return [ + DefStringTooLongException( + path=f'{context.filepath}:{context.line_no}', + message=f'Function definition too long ({context.line_length} > {context.max_def_length})', + ) + ] diff --git a/def_form/core/rules/max_inline_args.py b/def_form/core/rules/max_inline_args.py new file mode 100644 index 0000000..25de0c2 --- /dev/null +++ b/def_form/core/rules/max_inline_args.py @@ -0,0 +1,18 @@ +from def_form.exceptions.base import BaseDefFormException +from def_form.exceptions.def_formatter import TooManyInlineArgumentsException +from def_form.core.rules.base import Rule +from def_form.core.rules.context import RuleContext + + +class RuleMaxInlineArgs(Rule): + def check(self, context: RuleContext) -> list[BaseDefFormException]: + if not context.is_single_line: + return [] + if not context.max_inline_args or context.arg_count <= context.max_inline_args: + return [] + return [ + TooManyInlineArgumentsException( + path=f'{context.filepath}:{context.line_no}', + message=f'Too many inline args ({context.arg_count} > {context.max_inline_args})', + ) + ] diff --git a/def_form/core/rules/multiline_params_indent.py b/def_form/core/rules/multiline_params_indent.py new file mode 100644 index 0000000..bce5470 --- /dev/null +++ b/def_form/core/rules/multiline_params_indent.py @@ -0,0 +1,16 @@ +from def_form.exceptions.base import BaseDefFormException +from def_form.exceptions.def_formatter import InvalidMultilineParamsIndentException +from def_form.core.rules.base import Rule +from def_form.core.rules.context import RuleContext + + +class RuleMultilineParamsIndent(Rule): + def check(self, context: RuleContext) -> list[BaseDefFormException]: + if context.is_single_line or context.has_correct_multiline_format: + return [] + return [ + InvalidMultilineParamsIndentException( + path=f'{context.filepath}:{context.line_no}', + message=f'Invalid multiline function parameters indentation (expected {context.indent_size} spaces)', + ) + ] diff --git a/def_form/exceptions/base.py b/def_form/exceptions/base.py index 86c4143..0bd77ec 100644 --- a/def_form/exceptions/base.py +++ b/def_form/exceptions/base.py @@ -3,6 +3,6 @@ @dataclass class BaseDefFormException(Exception): - path: str | None = None - message: str | None = None + path: str + message: str description: str | None = None diff --git a/def_form/exceptions/def_formatter.py b/def_form/exceptions/def_formatter.py index 3b62b58..4c1b200 100644 --- a/def_form/exceptions/def_formatter.py +++ b/def_form/exceptions/def_formatter.py @@ -29,3 +29,9 @@ class InvalidMultilineParamsIndentException(BaseDefFormException): path: str message: str = 'Invalid multiline params indentation' description: str | None = None + +@dataclass +class CheckCommandFoundAnIssue(BaseDefFormException): + path: str + message: str = 'check command did found an issue' + description: str | None = None \ No newline at end of file diff --git a/def_form/formatters/__init__.py b/def_form/formatters/__init__.py deleted file mode 100644 index 6722c49..0000000 --- a/def_form/formatters/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from def_form.formatters.def_formatter import DefManager - -__all__ = [ - 'DefManager', -] diff --git a/def_form/formatters/def_formatter/__init__.py b/def_form/formatters/def_formatter/__init__.py deleted file mode 100644 index caed506..0000000 --- a/def_form/formatters/def_formatter/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from def_form.formatters.def_formatter.manager import DefManager - -__all__ = [ - 'DefManager', -] diff --git a/def_form/formatters/def_formatter/formatter.py b/def_form/formatters/def_formatter/formatter.py deleted file mode 100644 index bf0dd55..0000000 --- a/def_form/formatters/def_formatter/formatter.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import Any, cast - -from libcst import Comma -from libcst import Comment -from libcst import CSTTransformer -from libcst import FunctionDef -from libcst import Param -from libcst import Parameters -from libcst import ParenthesizedWhitespace -from libcst import SimpleWhitespace -from libcst import TrailingWhitespace - -from def_form.formatters.def_formatter.base import DefBase - - -class DefFormatter(DefBase, CSTTransformer): - def __init__( - self, - filepath: str, - max_def_length: int | None, - max_inline_args: int | None, - indent_size: int | None = None, - ): - super().__init__(filepath, max_def_length, max_inline_args) - self.indent_size = indent_size if indent_size is not None else 4 - - def _is_valid_param(self, param: Any) -> bool: - return isinstance(param, Param) - - def _extract_comment_from_whitespace(self, ws: Any) -> TrailingWhitespace | None: - if isinstance(ws, TrailingWhitespace): - return ws - if isinstance(ws, ParenthesizedWhitespace) and isinstance(ws.first_line, TrailingWhitespace): - return ws.first_line - if isinstance(ws, SimpleWhitespace) and '#' in ws.value: - parts = ws.value.split('#', 1) - spaces = parts[0].rstrip() - comment_text = '#' + parts[1].rstrip() - return TrailingWhitespace( - whitespace=SimpleWhitespace(spaces + ' ' if spaces else ''), comment=Comment(comment_text) - ) - return None - - def _extract_comment_from_param(self, param: Param) -> TrailingWhitespace | None: - if isinstance(param.comma, Comma) and param.comma.whitespace_after: - comment = self._extract_comment_from_whitespace(param.comma.whitespace_after) - if comment: - return comment - if param.whitespace_after_param: - return self._extract_comment_from_whitespace(param.whitespace_after_param) - return None - - def _create_whitespace( - self, comment: TrailingWhitespace | None, last_line: SimpleWhitespace - ) -> ParenthesizedWhitespace: - if comment: - return ParenthesizedWhitespace(first_line=comment, empty_lines=[], indent=True, last_line=last_line) - return ParenthesizedWhitespace(last_line=last_line, indent=True) - - def _create_formatted_param_for_single_line(self, param: Param, is_last: bool = False) -> Param: - indent = ' ' * self.indent_size - comment = self._extract_comment_from_param(param) - last_line = SimpleWhitespace('' if is_last else indent) - new_ws = self._create_whitespace(comment, last_line) - need_comma = param.comma is not None if is_last else True - new_comma = Comma(whitespace_after=new_ws) if need_comma else None - return param.with_changes(comma=new_comma, whitespace_after_param=SimpleWhitespace('')) - - def _create_formatted_param_for_multi_line(self, param: Param, is_last: bool = False) -> Param: - indent = ' ' * self.indent_size - last_line = SimpleWhitespace('' if is_last else indent) - original_ws = param.comma.whitespace_after if isinstance(param.comma, Comma) else None - - if isinstance(original_ws, ParenthesizedWhitespace): - comment = original_ws.first_line if isinstance(original_ws.first_line, TrailingWhitespace) else None - new_ws = self._create_whitespace(comment, last_line) - elif isinstance(original_ws, TrailingWhitespace): - new_ws = self._create_whitespace(original_ws, last_line) - else: - new_ws = self._create_whitespace(None, last_line) - - need_comma = param.comma is not None if is_last else True - new_comma = Comma(whitespace_after=new_ws) if need_comma else None - return param.with_changes(comma=new_comma, whitespace_after_param=SimpleWhitespace('')) - - def _collect_all_params(self, node: FunctionDef) -> list[Param]: - all_params: list[Param] = list(node.params.params) - - if self._is_valid_param(node.params.star_arg): - all_params.append(cast(Param, node.params.star_arg)) - - all_params.extend(node.params.kwonly_params) - - if self._is_valid_param(node.params.star_kwarg): - all_params.append(cast(Param, node.params.star_kwarg)) - - return all_params - - def _restore_param_groups(self, formatted_params: list[Param], node: FunctionDef) -> tuple: - param_count = len(node.params.params) - kwonly_count = len(node.params.kwonly_params) - new_params = formatted_params[:param_count] - remaining = formatted_params[param_count:] - - new_star_arg = None - if self._is_valid_param(node.params.star_arg) and remaining: - new_star_arg = remaining[0] - remaining = remaining[1:] - - new_kwonly_params = remaining[:kwonly_count] if kwonly_count else [] - remaining = remaining[kwonly_count:] - - new_star_kwarg = None - if self._is_valid_param(node.params.star_kwarg) and remaining: - new_star_kwarg = remaining[0] - - return new_params, new_kwonly_params, new_star_arg, new_star_kwarg - - def _process_parameters(self, node: FunctionDef, is_single_line: bool) -> FunctionDef: - all_params = self._collect_all_params(node) - total_params = len(all_params) - format_func = ( - self._create_formatted_param_for_single_line - if is_single_line - else self._create_formatted_param_for_multi_line - ) - formatted_params = [format_func(param, i == total_params - 1) for i, param in enumerate(all_params)] - new_params, new_kwonly_params, new_star_arg, new_star_kwarg = self._restore_param_groups(formatted_params, node) - - return node.with_changes( - params=Parameters( - params=new_params, - posonly_params=node.params.posonly_params, - kwonly_params=new_kwonly_params, - star_arg=new_star_arg, - star_kwarg=new_star_kwarg, - ), - whitespace_before_params=ParenthesizedWhitespace( - last_line=SimpleWhitespace(' ' * self.indent_size), indent=True - ), - ) - - def leave_FunctionDef(self, original_node: FunctionDef, updated_node: FunctionDef) -> FunctionDef: - analysis = self.analyze_function(original_node) - if not analysis.should_process: - return updated_node - if analysis.issues: - self.issues.extend(analysis.issues) - return self._process_parameters(updated_node, self.is_single_line_function(original_node)) From c49e63e86da767845502c3b611f3bdf83fae7a45 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 23:55:16 +0200 Subject: [PATCH 04/16] Update tests --- tests/cases/__init__.py | 0 tests/cases/async_def/__init__.py | 0 tests/cases/async_def/source.py | 2 + tests/cases/async_def_with_args/__init__.py | 0 tests/cases/async_def_with_args/expected.py | 6 + .../async_def_with_args/expected_issues.json | 1 + tests/cases/async_def_with_args/source.py | 2 + .../async_multiline_bad_comment/__init__.py | 0 .../async_multiline_bad_comment/expected.py | 6 + .../expected_issues.json | 1 + .../async_multiline_bad_comment/source.py | 3 + .../class_async_def_with_args/__init__.py | 0 .../class_async_def_with_args/expected.py | 8 + .../expected_issues.json | 1 + .../cases/class_async_def_with_args/source.py | 3 + .../cases/class_def_with_kw_args/__init__.py | 0 .../cases/class_def_with_kw_args/expected.py | 10 + .../expected_issues.json | 1 + tests/cases/class_def_with_kw_args/source.py | 3 + .../class_def_with_kw_only_args/__init__.py | 0 .../class_def_with_kw_only_args/expected.py | 9 + .../expected_issues.json | 1 + .../class_def_with_kw_only_args/source.py | 3 + tests/cases/class_method/__init__.py | 0 tests/cases/class_method/expected.py | 3 + tests/cases/class_method/source.py | 3 + .../__init__.py | 0 .../expected.py | 9 + .../expected_issues.json | 1 + .../source.py | 6 + .../cases/class_method_long_name/__init__.py | 0 .../cases/class_method_long_name/expected.py | 5 + .../expected_issues.json | 1 + tests/cases/class_method_long_name/source.py | 3 + .../class_method_multiline_bad/__init__.py | 0 .../class_method_multiline_bad/expected.py | 8 + .../expected_issues.json | 1 + .../class_method_multiline_bad/source.py | 4 + tests/cases/class_method_skipped/__init__.py | 0 tests/cases/class_method_skipped/source.py | 3 + .../cases/class_method_with_args/__init__.py | 0 .../cases/class_method_with_args/expected.py | 7 + .../expected_issues.json | 1 + tests/cases/class_method_with_args/source.py | 3 + .../__init__.py | 0 .../expected.py | 11 + .../expected_issues.json | 1 + .../source.py | 6 + tests/cases/class_static_method/__init__.py | 0 tests/cases/class_static_method/expected.py | 4 + tests/cases/class_static_method/source.py | 4 + .../cases/decorated_multiline_bad/__init__.py | 0 .../cases/decorated_multiline_bad/expected.py | 16 + .../expected_issues.json | 1 + tests/cases/decorated_multiline_bad/source.py | 13 + tests/cases/def_kw_args_first/__init__.py | 0 tests/cases/def_kw_args_first/expected.py | 7 + .../def_kw_args_first/expected_issues.json | 1 + tests/cases/def_kw_args_first/source.py | 2 + tests/cases/def_too_long/__init__.py | 0 tests/cases/def_too_long/expected.py | 4 + tests/cases/def_too_long/expected_issues.json | 1 + tests/cases/def_too_long/source.py | 2 + tests/cases/def_with_decorator/__init__.py | 0 tests/cases/def_with_decorator/expected.py | 12 + tests/cases/def_with_decorator/source.py | 12 + .../def_with_decorator_and_args/__init__.py | 0 .../def_with_decorator_and_args/expected.py | 16 + .../expected_issues.json | 1 + .../def_with_decorator_and_args/source.py | 12 + tests/cases/kw_args/__init__.py | 0 tests/cases/kw_args/expected.py | 7 + tests/cases/kw_args/expected_issues.json | 1 + tests/cases/kw_args/source.py | 2 + tests/cases/long_typehint_no_args/__init__.py | 0 tests/cases/long_typehint_no_args/source.py | 5 + .../cases/long_typehint_with_args/__init__.py | 0 .../cases/long_typehint_with_args/expected.py | 7 + .../expected_issues.json | 1 + tests/cases/long_typehint_with_args/source.py | 5 + tests/cases/multiline_bad_indent/__init__.py | 0 tests/cases/multiline_bad_indent/expected.py | 5 + .../multiline_bad_indent/expected_issues.json | 1 + tests/cases/multiline_bad_indent/source.py | 5 + .../no_issues_already_formatted/__init__.py | 0 .../no_issues_already_formatted/expected.py | 6 + .../no_issues_already_formatted/source.py | 6 + tests/cases/no_issues_empty_def/__init__.py | 0 tests/cases/no_issues_empty_def/source.py | 2 + .../single_line_comment_first/__init__.py | 0 .../single_line_comment_first/expected.py | 6 + .../expected_issues.json | 1 + .../cases/single_line_comment_first/source.py | 3 + .../single_line_comment_last/__init__.py | 0 .../single_line_comment_last/expected.py | 6 + .../expected_issues.json | 1 + .../cases/single_line_comment_last/source.py | 2 + .../single_line_comment_middle/__init__.py | 0 .../single_line_comment_middle/expected.py | 6 + .../expected_issues.json | 1 + .../single_line_comment_middle/source.py | 3 + .../__init__.py | 0 .../expected.py | 8 + .../expected_issues.json | 1 + .../source.py | 5 + .../single_line_needs_format/__init__.py | 0 .../single_line_needs_format/expected.py | 6 + .../expected_issues.json | 1 + .../cases/single_line_needs_format/source.py | 2 + .../single_line_ok_no_format/__init__.py | 0 .../single_line_ok_no_format/expected.py | 2 + .../cases/single_line_ok_no_format/source.py | 2 + .../__init__.py | 0 .../expected.py | 2 + .../single_line_return_type_comment/source.py | 2 + .../single_line_with_all_features/__init__.py | 0 .../single_line_with_all_features/expected.py | 8 + .../expected_issues.json | 1 + .../single_line_with_all_features/source.py | 6 + tests/cases/skip_comment/__init__.py | 0 tests/cases/skip_comment/source.py | 3 + .../cases/skipped_with_decorator/__init__.py | 0 tests/cases/skipped_with_decorator/source.py | 12 + .../skipped_with_right_comment/__init__.py | 0 .../skipped_with_right_comment/source.py | 2 + .../__init__.py | 0 .../expected.py | 7 + .../expected_issues.json | 1 + .../source.py | 2 + tests/cases/too_many_inline_args/__init__.py | 0 tests/cases/too_many_inline_args/expected.py | 7 + .../too_many_inline_args/expected_issues.json | 1 + tests/cases/too_many_inline_args/source.py | 2 + .../__init__.py | 0 .../expected.py | 6 + .../expected_issues.json | 1 + .../source.py | 2 + .../truly_class_method_kw_only/__init__.py | 0 .../truly_class_method_kw_only/expected.py | 9 + .../expected_issues.json | 1 + .../truly_class_method_kw_only/source.py | 3 + .../__init__.py | 0 .../expected.py | 8 + .../expected_issues.json | 1 + .../truly_class_method_single_line/source.py | 3 + .../__init__.py | 0 .../expected.py | 8 + .../expected_issues.json | 1 + .../truly_single_line_all_features/source.py | 2 + .../truly_single_line_decorated/__init__.py | 0 .../truly_single_line_decorated/expected.py | 16 + .../expected_issues.json | 1 + .../truly_single_line_decorated/source.py | 12 + .../truly_single_line_kw_only/__init__.py | 0 .../truly_single_line_kw_only/expected.py | 8 + .../expected_issues.json | 1 + .../cases/truly_single_line_kw_only/source.py | 2 + .../__init__.py | 0 .../expected.py | 6 + .../expected_issues.json | 1 + .../source.py | 2 + .../__init__.py | 0 .../expected.py | 6 + .../expected_issues.json | 1 + .../truly_single_line_with_comment/source.py | 2 + .../__init__.py | 0 .../expected.py | 2 + .../source.py | 2 + tests/cases/two_issues_same_line/__init__.py | 0 tests/cases/two_issues_same_line/expected.py | 6 + .../two_issues_same_line/expected_issues.json | 4 + tests/cases/two_issues_same_line/source.py | 2 + tests/cases/with_comments/__init__.py | 0 tests/cases/with_comments/expected.py | 6 + .../cases/with_comments/expected_issues.json | 1 + tests/cases/with_comments/source.py | 6 + tests/conftest.py | 73 +-- tests/constants.py | 11 - tests/helpers.py | 54 ++ tests/mock_data/__init__.py | 68 --- tests/mock_data/example.py | 277 ---------- tests/mock_data/expected.py | 492 ------------------ tests/test_checker/test_checker.py | 28 +- tests/test_formatter/test_formatter.py | 41 +- 184 files changed, 680 insertions(+), 905 deletions(-) create mode 100644 tests/cases/__init__.py create mode 100644 tests/cases/async_def/__init__.py create mode 100644 tests/cases/async_def/source.py create mode 100644 tests/cases/async_def_with_args/__init__.py create mode 100644 tests/cases/async_def_with_args/expected.py create mode 100644 tests/cases/async_def_with_args/expected_issues.json create mode 100644 tests/cases/async_def_with_args/source.py create mode 100644 tests/cases/async_multiline_bad_comment/__init__.py create mode 100644 tests/cases/async_multiline_bad_comment/expected.py create mode 100644 tests/cases/async_multiline_bad_comment/expected_issues.json create mode 100644 tests/cases/async_multiline_bad_comment/source.py create mode 100644 tests/cases/class_async_def_with_args/__init__.py create mode 100644 tests/cases/class_async_def_with_args/expected.py create mode 100644 tests/cases/class_async_def_with_args/expected_issues.json create mode 100644 tests/cases/class_async_def_with_args/source.py create mode 100644 tests/cases/class_def_with_kw_args/__init__.py create mode 100644 tests/cases/class_def_with_kw_args/expected.py create mode 100644 tests/cases/class_def_with_kw_args/expected_issues.json create mode 100644 tests/cases/class_def_with_kw_args/source.py create mode 100644 tests/cases/class_def_with_kw_only_args/__init__.py create mode 100644 tests/cases/class_def_with_kw_only_args/expected.py create mode 100644 tests/cases/class_def_with_kw_only_args/expected_issues.json create mode 100644 tests/cases/class_def_with_kw_only_args/source.py create mode 100644 tests/cases/class_method/__init__.py create mode 100644 tests/cases/class_method/expected.py create mode 100644 tests/cases/class_method/source.py create mode 100644 tests/cases/class_method_kw_only_with_comments/__init__.py create mode 100644 tests/cases/class_method_kw_only_with_comments/expected.py create mode 100644 tests/cases/class_method_kw_only_with_comments/expected_issues.json create mode 100644 tests/cases/class_method_kw_only_with_comments/source.py create mode 100644 tests/cases/class_method_long_name/__init__.py create mode 100644 tests/cases/class_method_long_name/expected.py create mode 100644 tests/cases/class_method_long_name/expected_issues.json create mode 100644 tests/cases/class_method_long_name/source.py create mode 100644 tests/cases/class_method_multiline_bad/__init__.py create mode 100644 tests/cases/class_method_multiline_bad/expected.py create mode 100644 tests/cases/class_method_multiline_bad/expected_issues.json create mode 100644 tests/cases/class_method_multiline_bad/source.py create mode 100644 tests/cases/class_method_skipped/__init__.py create mode 100644 tests/cases/class_method_skipped/source.py create mode 100644 tests/cases/class_method_with_args/__init__.py create mode 100644 tests/cases/class_method_with_args/expected.py create mode 100644 tests/cases/class_method_with_args/expected_issues.json create mode 100644 tests/cases/class_method_with_args/source.py create mode 100644 tests/cases/class_method_with_args_with_typehints/__init__.py create mode 100644 tests/cases/class_method_with_args_with_typehints/expected.py create mode 100644 tests/cases/class_method_with_args_with_typehints/expected_issues.json create mode 100644 tests/cases/class_method_with_args_with_typehints/source.py create mode 100644 tests/cases/class_static_method/__init__.py create mode 100644 tests/cases/class_static_method/expected.py create mode 100644 tests/cases/class_static_method/source.py create mode 100644 tests/cases/decorated_multiline_bad/__init__.py create mode 100644 tests/cases/decorated_multiline_bad/expected.py create mode 100644 tests/cases/decorated_multiline_bad/expected_issues.json create mode 100644 tests/cases/decorated_multiline_bad/source.py create mode 100644 tests/cases/def_kw_args_first/__init__.py create mode 100644 tests/cases/def_kw_args_first/expected.py create mode 100644 tests/cases/def_kw_args_first/expected_issues.json create mode 100644 tests/cases/def_kw_args_first/source.py create mode 100644 tests/cases/def_too_long/__init__.py create mode 100644 tests/cases/def_too_long/expected.py create mode 100644 tests/cases/def_too_long/expected_issues.json create mode 100644 tests/cases/def_too_long/source.py create mode 100644 tests/cases/def_with_decorator/__init__.py create mode 100644 tests/cases/def_with_decorator/expected.py create mode 100644 tests/cases/def_with_decorator/source.py create mode 100644 tests/cases/def_with_decorator_and_args/__init__.py create mode 100644 tests/cases/def_with_decorator_and_args/expected.py create mode 100644 tests/cases/def_with_decorator_and_args/expected_issues.json create mode 100644 tests/cases/def_with_decorator_and_args/source.py create mode 100644 tests/cases/kw_args/__init__.py create mode 100644 tests/cases/kw_args/expected.py create mode 100644 tests/cases/kw_args/expected_issues.json create mode 100644 tests/cases/kw_args/source.py create mode 100644 tests/cases/long_typehint_no_args/__init__.py create mode 100644 tests/cases/long_typehint_no_args/source.py create mode 100644 tests/cases/long_typehint_with_args/__init__.py create mode 100644 tests/cases/long_typehint_with_args/expected.py create mode 100644 tests/cases/long_typehint_with_args/expected_issues.json create mode 100644 tests/cases/long_typehint_with_args/source.py create mode 100644 tests/cases/multiline_bad_indent/__init__.py create mode 100644 tests/cases/multiline_bad_indent/expected.py create mode 100644 tests/cases/multiline_bad_indent/expected_issues.json create mode 100644 tests/cases/multiline_bad_indent/source.py create mode 100644 tests/cases/no_issues_already_formatted/__init__.py create mode 100644 tests/cases/no_issues_already_formatted/expected.py create mode 100644 tests/cases/no_issues_already_formatted/source.py create mode 100644 tests/cases/no_issues_empty_def/__init__.py create mode 100644 tests/cases/no_issues_empty_def/source.py create mode 100644 tests/cases/single_line_comment_first/__init__.py create mode 100644 tests/cases/single_line_comment_first/expected.py create mode 100644 tests/cases/single_line_comment_first/expected_issues.json create mode 100644 tests/cases/single_line_comment_first/source.py create mode 100644 tests/cases/single_line_comment_last/__init__.py create mode 100644 tests/cases/single_line_comment_last/expected.py create mode 100644 tests/cases/single_line_comment_last/expected_issues.json create mode 100644 tests/cases/single_line_comment_last/source.py create mode 100644 tests/cases/single_line_comment_middle/__init__.py create mode 100644 tests/cases/single_line_comment_middle/expected.py create mode 100644 tests/cases/single_line_comment_middle/expected_issues.json create mode 100644 tests/cases/single_line_comment_middle/source.py create mode 100644 tests/cases/single_line_kw_only_with_comments/__init__.py create mode 100644 tests/cases/single_line_kw_only_with_comments/expected.py create mode 100644 tests/cases/single_line_kw_only_with_comments/expected_issues.json create mode 100644 tests/cases/single_line_kw_only_with_comments/source.py create mode 100644 tests/cases/single_line_needs_format/__init__.py create mode 100644 tests/cases/single_line_needs_format/expected.py create mode 100644 tests/cases/single_line_needs_format/expected_issues.json create mode 100644 tests/cases/single_line_needs_format/source.py create mode 100644 tests/cases/single_line_ok_no_format/__init__.py create mode 100644 tests/cases/single_line_ok_no_format/expected.py create mode 100644 tests/cases/single_line_ok_no_format/source.py create mode 100644 tests/cases/single_line_return_type_comment/__init__.py create mode 100644 tests/cases/single_line_return_type_comment/expected.py create mode 100644 tests/cases/single_line_return_type_comment/source.py create mode 100644 tests/cases/single_line_with_all_features/__init__.py create mode 100644 tests/cases/single_line_with_all_features/expected.py create mode 100644 tests/cases/single_line_with_all_features/expected_issues.json create mode 100644 tests/cases/single_line_with_all_features/source.py create mode 100644 tests/cases/skip_comment/__init__.py create mode 100644 tests/cases/skip_comment/source.py create mode 100644 tests/cases/skipped_with_decorator/__init__.py create mode 100644 tests/cases/skipped_with_decorator/source.py create mode 100644 tests/cases/skipped_with_right_comment/__init__.py create mode 100644 tests/cases/skipped_with_right_comment/source.py create mode 100644 tests/cases/to_much_inline_args_with_typehints/__init__.py create mode 100644 tests/cases/to_much_inline_args_with_typehints/expected.py create mode 100644 tests/cases/to_much_inline_args_with_typehints/expected_issues.json create mode 100644 tests/cases/to_much_inline_args_with_typehints/source.py create mode 100644 tests/cases/too_many_inline_args/__init__.py create mode 100644 tests/cases/too_many_inline_args/expected.py create mode 100644 tests/cases/too_many_inline_args/expected_issues.json create mode 100644 tests/cases/too_many_inline_args/source.py create mode 100644 tests/cases/truly_async_single_line_with_comment/__init__.py create mode 100644 tests/cases/truly_async_single_line_with_comment/expected.py create mode 100644 tests/cases/truly_async_single_line_with_comment/expected_issues.json create mode 100644 tests/cases/truly_async_single_line_with_comment/source.py create mode 100644 tests/cases/truly_class_method_kw_only/__init__.py create mode 100644 tests/cases/truly_class_method_kw_only/expected.py create mode 100644 tests/cases/truly_class_method_kw_only/expected_issues.json create mode 100644 tests/cases/truly_class_method_kw_only/source.py create mode 100644 tests/cases/truly_class_method_single_line/__init__.py create mode 100644 tests/cases/truly_class_method_single_line/expected.py create mode 100644 tests/cases/truly_class_method_single_line/expected_issues.json create mode 100644 tests/cases/truly_class_method_single_line/source.py create mode 100644 tests/cases/truly_single_line_all_features/__init__.py create mode 100644 tests/cases/truly_single_line_all_features/expected.py create mode 100644 tests/cases/truly_single_line_all_features/expected_issues.json create mode 100644 tests/cases/truly_single_line_all_features/source.py create mode 100644 tests/cases/truly_single_line_decorated/__init__.py create mode 100644 tests/cases/truly_single_line_decorated/expected.py create mode 100644 tests/cases/truly_single_line_decorated/expected_issues.json create mode 100644 tests/cases/truly_single_line_decorated/source.py create mode 100644 tests/cases/truly_single_line_kw_only/__init__.py create mode 100644 tests/cases/truly_single_line_kw_only/expected.py create mode 100644 tests/cases/truly_single_line_kw_only/expected_issues.json create mode 100644 tests/cases/truly_single_line_kw_only/source.py create mode 100644 tests/cases/truly_single_line_with_args_comment/__init__.py create mode 100644 tests/cases/truly_single_line_with_args_comment/expected.py create mode 100644 tests/cases/truly_single_line_with_args_comment/expected_issues.json create mode 100644 tests/cases/truly_single_line_with_args_comment/source.py create mode 100644 tests/cases/truly_single_line_with_comment/__init__.py create mode 100644 tests/cases/truly_single_line_with_comment/expected.py create mode 100644 tests/cases/truly_single_line_with_comment/expected_issues.json create mode 100644 tests/cases/truly_single_line_with_comment/source.py create mode 100644 tests/cases/truly_single_line_with_return_type/__init__.py create mode 100644 tests/cases/truly_single_line_with_return_type/expected.py create mode 100644 tests/cases/truly_single_line_with_return_type/source.py create mode 100644 tests/cases/two_issues_same_line/__init__.py create mode 100644 tests/cases/two_issues_same_line/expected.py create mode 100644 tests/cases/two_issues_same_line/expected_issues.json create mode 100644 tests/cases/two_issues_same_line/source.py create mode 100644 tests/cases/with_comments/__init__.py create mode 100644 tests/cases/with_comments/expected.py create mode 100644 tests/cases/with_comments/expected_issues.json create mode 100644 tests/cases/with_comments/source.py delete mode 100644 tests/constants.py create mode 100644 tests/helpers.py delete mode 100644 tests/mock_data/__init__.py delete mode 100644 tests/mock_data/example.py delete mode 100644 tests/mock_data/expected.py diff --git a/tests/cases/__init__.py b/tests/cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/async_def/__init__.py b/tests/cases/async_def/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/async_def/source.py b/tests/cases/async_def/source.py new file mode 100644 index 0000000..415e2f9 --- /dev/null +++ b/tests/cases/async_def/source.py @@ -0,0 +1,2 @@ +async def async_def(): + return diff --git a/tests/cases/async_def_with_args/__init__.py b/tests/cases/async_def_with_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/async_def_with_args/expected.py b/tests/cases/async_def_with_args/expected.py new file mode 100644 index 0000000..11cb779 --- /dev/null +++ b/tests/cases/async_def_with_args/expected.py @@ -0,0 +1,6 @@ +async def async_def_with_args( + a, + b, + c, +): + return diff --git a/tests/cases/async_def_with_args/expected_issues.json b/tests/cases/async_def_with_args/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/async_def_with_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/async_def_with_args/source.py b/tests/cases/async_def_with_args/source.py new file mode 100644 index 0000000..241c959 --- /dev/null +++ b/tests/cases/async_def_with_args/source.py @@ -0,0 +1,2 @@ +async def async_def_with_args(a, b, c): + return diff --git a/tests/cases/async_multiline_bad_comment/__init__.py b/tests/cases/async_multiline_bad_comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/async_multiline_bad_comment/expected.py b/tests/cases/async_multiline_bad_comment/expected.py new file mode 100644 index 0000000..08b3daa --- /dev/null +++ b/tests/cases/async_multiline_bad_comment/expected.py @@ -0,0 +1,6 @@ +async def async_single_line_with_comment( + a, # async with comment + b, + c, +): + return diff --git a/tests/cases/async_multiline_bad_comment/expected_issues.json b/tests/cases/async_multiline_bad_comment/expected_issues.json new file mode 100644 index 0000000..fa9621c --- /dev/null +++ b/tests/cases/async_multiline_bad_comment/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/async_multiline_bad_comment/source.py b/tests/cases/async_multiline_bad_comment/source.py new file mode 100644 index 0000000..a13d222 --- /dev/null +++ b/tests/cases/async_multiline_bad_comment/source.py @@ -0,0 +1,3 @@ +async def async_single_line_with_comment(a, # async with comment + b, c): + return diff --git a/tests/cases/class_async_def_with_args/__init__.py b/tests/cases/class_async_def_with_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_async_def_with_args/expected.py b/tests/cases/class_async_def_with_args/expected.py new file mode 100644 index 0000000..771400f --- /dev/null +++ b/tests/cases/class_async_def_with_args/expected.py @@ -0,0 +1,8 @@ +class ClassWithFunctions: + async def async_def_with_args( + self, + a, + b, + c, + ): + return diff --git a/tests/cases/class_async_def_with_args/expected_issues.json b/tests/cases/class_async_def_with_args/expected_issues.json new file mode 100644 index 0000000..44ec0e2 --- /dev/null +++ b/tests/cases/class_async_def_with_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/class_async_def_with_args/source.py b/tests/cases/class_async_def_with_args/source.py new file mode 100644 index 0000000..0494510 --- /dev/null +++ b/tests/cases/class_async_def_with_args/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + async def async_def_with_args(self, a, b, c): + return diff --git a/tests/cases/class_def_with_kw_args/__init__.py b/tests/cases/class_def_with_kw_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_def_with_kw_args/expected.py b/tests/cases/class_def_with_kw_args/expected.py new file mode 100644 index 0000000..4ab1e83 --- /dev/null +++ b/tests/cases/class_def_with_kw_args/expected.py @@ -0,0 +1,10 @@ +class ClassWithFunctions: + def def_with_kw_args( + self, + a, + b, + c, + *args, + **kwargs, + ): + return diff --git a/tests/cases/class_def_with_kw_args/expected_issues.json b/tests/cases/class_def_with_kw_args/expected_issues.json new file mode 100644 index 0000000..44ec0e2 --- /dev/null +++ b/tests/cases/class_def_with_kw_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/class_def_with_kw_args/source.py b/tests/cases/class_def_with_kw_args/source.py new file mode 100644 index 0000000..379ee44 --- /dev/null +++ b/tests/cases/class_def_with_kw_args/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def def_with_kw_args(self, a, b, c, *args, **kwargs): + return diff --git a/tests/cases/class_def_with_kw_only_args/__init__.py b/tests/cases/class_def_with_kw_only_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_def_with_kw_only_args/expected.py b/tests/cases/class_def_with_kw_only_args/expected.py new file mode 100644 index 0000000..20c9fae --- /dev/null +++ b/tests/cases/class_def_with_kw_only_args/expected.py @@ -0,0 +1,9 @@ +class ClassWithFunctions: + def def_with_kw_only_args( + self, + *args, + b: str, + s: str, + **kwargs, + ): + return diff --git a/tests/cases/class_def_with_kw_only_args/expected_issues.json b/tests/cases/class_def_with_kw_only_args/expected_issues.json new file mode 100644 index 0000000..44ec0e2 --- /dev/null +++ b/tests/cases/class_def_with_kw_only_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/class_def_with_kw_only_args/source.py b/tests/cases/class_def_with_kw_only_args/source.py new file mode 100644 index 0000000..3c2f79b --- /dev/null +++ b/tests/cases/class_def_with_kw_only_args/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def def_with_kw_only_args(self, *args, b: str, s: str, **kwargs): + return diff --git a/tests/cases/class_method/__init__.py b/tests/cases/class_method/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_method/expected.py b/tests/cases/class_method/expected.py new file mode 100644 index 0000000..caf6e76 --- /dev/null +++ b/tests/cases/class_method/expected.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def class_method(self): + return diff --git a/tests/cases/class_method/source.py b/tests/cases/class_method/source.py new file mode 100644 index 0000000..caf6e76 --- /dev/null +++ b/tests/cases/class_method/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def class_method(self): + return diff --git a/tests/cases/class_method_kw_only_with_comments/__init__.py b/tests/cases/class_method_kw_only_with_comments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_method_kw_only_with_comments/expected.py b/tests/cases/class_method_kw_only_with_comments/expected.py new file mode 100644 index 0000000..88c4489 --- /dev/null +++ b/tests/cases/class_method_kw_only_with_comments/expected.py @@ -0,0 +1,9 @@ +class ClassWithFunctions: + def class_method_kw_only_with_comments( + self, + *args, # varargs in class + b: str, # keyword-only in class + s: str, # another keyword-only + **kwargs, + ): # kwargs in class + return diff --git a/tests/cases/class_method_kw_only_with_comments/expected_issues.json b/tests/cases/class_method_kw_only_with_comments/expected_issues.json new file mode 100644 index 0000000..fec236f --- /dev/null +++ b/tests/cases/class_method_kw_only_with_comments/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/class_method_kw_only_with_comments/source.py b/tests/cases/class_method_kw_only_with_comments/source.py new file mode 100644 index 0000000..2a78228 --- /dev/null +++ b/tests/cases/class_method_kw_only_with_comments/source.py @@ -0,0 +1,6 @@ +class ClassWithFunctions: + def class_method_kw_only_with_comments(self, *args, # varargs in class + b: str, # keyword-only in class + s: str, # another keyword-only + **kwargs): # kwargs in class + return diff --git a/tests/cases/class_method_long_name/__init__.py b/tests/cases/class_method_long_name/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_method_long_name/expected.py b/tests/cases/class_method_long_name/expected.py new file mode 100644 index 0000000..8c92e3b --- /dev/null +++ b/tests/cases/class_method_long_name/expected.py @@ -0,0 +1,5 @@ +class ClassWithFunctions: + def class_method_with_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name( + self, + ): + return diff --git a/tests/cases/class_method_long_name/expected_issues.json b/tests/cases/class_method_long_name/expected_issues.json new file mode 100644 index 0000000..940735b --- /dev/null +++ b/tests/cases/class_method_long_name/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "DefStringTooLongException"}] diff --git a/tests/cases/class_method_long_name/source.py b/tests/cases/class_method_long_name/source.py new file mode 100644 index 0000000..bd47826 --- /dev/null +++ b/tests/cases/class_method_long_name/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def class_method_with_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name(self): + return diff --git a/tests/cases/class_method_multiline_bad/__init__.py b/tests/cases/class_method_multiline_bad/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_method_multiline_bad/expected.py b/tests/cases/class_method_multiline_bad/expected.py new file mode 100644 index 0000000..6e9aee8 --- /dev/null +++ b/tests/cases/class_method_multiline_bad/expected.py @@ -0,0 +1,8 @@ +class ClassWithFunctions: + def class_method_single_line_with_comment( + self, + a, # class method comment + b, + c, + ): + return diff --git a/tests/cases/class_method_multiline_bad/expected_issues.json b/tests/cases/class_method_multiline_bad/expected_issues.json new file mode 100644 index 0000000..fec236f --- /dev/null +++ b/tests/cases/class_method_multiline_bad/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/class_method_multiline_bad/source.py b/tests/cases/class_method_multiline_bad/source.py new file mode 100644 index 0000000..8940b3b --- /dev/null +++ b/tests/cases/class_method_multiline_bad/source.py @@ -0,0 +1,4 @@ +class ClassWithFunctions: + def class_method_single_line_with_comment(self, a, # class method comment + b, c): + return diff --git a/tests/cases/class_method_skipped/__init__.py b/tests/cases/class_method_skipped/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_method_skipped/source.py b/tests/cases/class_method_skipped/source.py new file mode 100644 index 0000000..981a4a5 --- /dev/null +++ b/tests/cases/class_method_skipped/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def skipped_with_right_comment(self, a, b, c): # def-form: skip + return diff --git a/tests/cases/class_method_with_args/__init__.py b/tests/cases/class_method_with_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_method_with_args/expected.py b/tests/cases/class_method_with_args/expected.py new file mode 100644 index 0000000..98bcab3 --- /dev/null +++ b/tests/cases/class_method_with_args/expected.py @@ -0,0 +1,7 @@ +class ClassWithFunctions: + def class_method_with_args( + self, + a, + b, + ): + return diff --git a/tests/cases/class_method_with_args/expected_issues.json b/tests/cases/class_method_with_args/expected_issues.json new file mode 100644 index 0000000..44ec0e2 --- /dev/null +++ b/tests/cases/class_method_with_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/class_method_with_args/source.py b/tests/cases/class_method_with_args/source.py new file mode 100644 index 0000000..08d90a9 --- /dev/null +++ b/tests/cases/class_method_with_args/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def class_method_with_args(self, a, b): + return diff --git a/tests/cases/class_method_with_args_with_typehints/__init__.py b/tests/cases/class_method_with_args_with_typehints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_method_with_args_with_typehints/expected.py b/tests/cases/class_method_with_args_with_typehints/expected.py new file mode 100644 index 0000000..b3c7ecf --- /dev/null +++ b/tests/cases/class_method_with_args_with_typehints/expected.py @@ -0,0 +1,11 @@ +from typing import Optional + + +class ClassWithFunctions: + def class_method_with_args_with_typehints( + self, + a: Optional[int], + b: int, + c, + ): + return diff --git a/tests/cases/class_method_with_args_with_typehints/expected_issues.json b/tests/cases/class_method_with_args_with_typehints/expected_issues.json new file mode 100644 index 0000000..3360185 --- /dev/null +++ b/tests/cases/class_method_with_args_with_typehints/expected_issues.json @@ -0,0 +1 @@ +[{"line": 5, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/class_method_with_args_with_typehints/source.py b/tests/cases/class_method_with_args_with_typehints/source.py new file mode 100644 index 0000000..328cf4d --- /dev/null +++ b/tests/cases/class_method_with_args_with_typehints/source.py @@ -0,0 +1,6 @@ +from typing import Optional + + +class ClassWithFunctions: + def class_method_with_args_with_typehints(self, a: Optional[int], b: int, c): + return diff --git a/tests/cases/class_static_method/__init__.py b/tests/cases/class_static_method/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/class_static_method/expected.py b/tests/cases/class_static_method/expected.py new file mode 100644 index 0000000..6753512 --- /dev/null +++ b/tests/cases/class_static_method/expected.py @@ -0,0 +1,4 @@ +class ClassWithFunctions: + @staticmethod + def static_method(): + return diff --git a/tests/cases/class_static_method/source.py b/tests/cases/class_static_method/source.py new file mode 100644 index 0000000..6753512 --- /dev/null +++ b/tests/cases/class_static_method/source.py @@ -0,0 +1,4 @@ +class ClassWithFunctions: + @staticmethod + def static_method(): + return diff --git a/tests/cases/decorated_multiline_bad/__init__.py b/tests/cases/decorated_multiline_bad/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/decorated_multiline_bad/expected.py b/tests/cases/decorated_multiline_bad/expected.py new file mode 100644 index 0000000..4b355a8 --- /dev/null +++ b/tests/cases/decorated_multiline_bad/expected.py @@ -0,0 +1,16 @@ +from typing import Callable, Optional + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def single_line_decorated_with_comment( + a, # decorated with comment + b, + c: Optional[str], +): + return diff --git a/tests/cases/decorated_multiline_bad/expected_issues.json b/tests/cases/decorated_multiline_bad/expected_issues.json new file mode 100644 index 0000000..1aadd0d --- /dev/null +++ b/tests/cases/decorated_multiline_bad/expected_issues.json @@ -0,0 +1 @@ +[{"line": 11, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/decorated_multiline_bad/source.py b/tests/cases/decorated_multiline_bad/source.py new file mode 100644 index 0000000..87ea85d --- /dev/null +++ b/tests/cases/decorated_multiline_bad/source.py @@ -0,0 +1,13 @@ +from typing import Callable, Optional + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def single_line_decorated_with_comment(a, # decorated with comment + b, c: Optional[str]): + return diff --git a/tests/cases/def_kw_args_first/__init__.py b/tests/cases/def_kw_args_first/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/def_kw_args_first/expected.py b/tests/cases/def_kw_args_first/expected.py new file mode 100644 index 0000000..4060bb6 --- /dev/null +++ b/tests/cases/def_kw_args_first/expected.py @@ -0,0 +1,7 @@ +def def_kw_args_first( + a, + *args, + b, + **kwargs, +): + return diff --git a/tests/cases/def_kw_args_first/expected_issues.json b/tests/cases/def_kw_args_first/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/def_kw_args_first/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/def_kw_args_first/source.py b/tests/cases/def_kw_args_first/source.py new file mode 100644 index 0000000..4b83ab7 --- /dev/null +++ b/tests/cases/def_kw_args_first/source.py @@ -0,0 +1,2 @@ +def def_kw_args_first(a, *args, b, **kwargs): + return diff --git a/tests/cases/def_too_long/__init__.py b/tests/cases/def_too_long/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/def_too_long/expected.py b/tests/cases/def_too_long/expected.py new file mode 100644 index 0000000..d407cbe --- /dev/null +++ b/tests/cases/def_too_long/expected.py @@ -0,0 +1,4 @@ +def loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_def_name( + a, +): + return diff --git a/tests/cases/def_too_long/expected_issues.json b/tests/cases/def_too_long/expected_issues.json new file mode 100644 index 0000000..ab60a89 --- /dev/null +++ b/tests/cases/def_too_long/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "DefStringTooLongException"}] diff --git a/tests/cases/def_too_long/source.py b/tests/cases/def_too_long/source.py new file mode 100644 index 0000000..fbbce42 --- /dev/null +++ b/tests/cases/def_too_long/source.py @@ -0,0 +1,2 @@ +def loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_def_name(a): + return diff --git a/tests/cases/def_with_decorator/__init__.py b/tests/cases/def_with_decorator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/def_with_decorator/expected.py b/tests/cases/def_with_decorator/expected.py new file mode 100644 index 0000000..33c928e --- /dev/null +++ b/tests/cases/def_with_decorator/expected.py @@ -0,0 +1,12 @@ +from typing import Callable + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def def_with_decorator(): + return diff --git a/tests/cases/def_with_decorator/source.py b/tests/cases/def_with_decorator/source.py new file mode 100644 index 0000000..33c928e --- /dev/null +++ b/tests/cases/def_with_decorator/source.py @@ -0,0 +1,12 @@ +from typing import Callable + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def def_with_decorator(): + return diff --git a/tests/cases/def_with_decorator_and_args/__init__.py b/tests/cases/def_with_decorator_and_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/def_with_decorator_and_args/expected.py b/tests/cases/def_with_decorator_and_args/expected.py new file mode 100644 index 0000000..b801399 --- /dev/null +++ b/tests/cases/def_with_decorator_and_args/expected.py @@ -0,0 +1,16 @@ +from typing import Callable, Optional + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def def_with_decorator_and_args( + a, + b, + c: Optional[str], +): + return diff --git a/tests/cases/def_with_decorator_and_args/expected_issues.json b/tests/cases/def_with_decorator_and_args/expected_issues.json new file mode 100644 index 0000000..9c8b7a3 --- /dev/null +++ b/tests/cases/def_with_decorator_and_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 11, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/def_with_decorator_and_args/source.py b/tests/cases/def_with_decorator_and_args/source.py new file mode 100644 index 0000000..cfcdc03 --- /dev/null +++ b/tests/cases/def_with_decorator_and_args/source.py @@ -0,0 +1,12 @@ +from typing import Callable, Optional + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def def_with_decorator_and_args(a, b, c: Optional[str]): + return diff --git a/tests/cases/kw_args/__init__.py b/tests/cases/kw_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/kw_args/expected.py b/tests/cases/kw_args/expected.py new file mode 100644 index 0000000..d26259f --- /dev/null +++ b/tests/cases/kw_args/expected.py @@ -0,0 +1,7 @@ +def kw_args( + def_, + with_, + *args, + **kwargs, +): + return diff --git a/tests/cases/kw_args/expected_issues.json b/tests/cases/kw_args/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/kw_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/kw_args/source.py b/tests/cases/kw_args/source.py new file mode 100644 index 0000000..42a3345 --- /dev/null +++ b/tests/cases/kw_args/source.py @@ -0,0 +1,2 @@ +def kw_args(def_, with_, *args, **kwargs): + return diff --git a/tests/cases/long_typehint_no_args/__init__.py b/tests/cases/long_typehint_no_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/long_typehint_no_args/source.py b/tests/cases/long_typehint_no_args/source.py new file mode 100644 index 0000000..5264356 --- /dev/null +++ b/tests/cases/long_typehint_no_args/source.py @@ -0,0 +1,5 @@ +from typing import Optional, Union + + +def long_typehint_without_args() -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: + return diff --git a/tests/cases/long_typehint_with_args/__init__.py b/tests/cases/long_typehint_with_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/long_typehint_with_args/expected.py b/tests/cases/long_typehint_with_args/expected.py new file mode 100644 index 0000000..28abb93 --- /dev/null +++ b/tests/cases/long_typehint_with_args/expected.py @@ -0,0 +1,7 @@ +from typing import Optional, Union + + +def long_typehint_with_args( + a, +) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: + return diff --git a/tests/cases/long_typehint_with_args/expected_issues.json b/tests/cases/long_typehint_with_args/expected_issues.json new file mode 100644 index 0000000..fd1c0ff --- /dev/null +++ b/tests/cases/long_typehint_with_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 4, "type": "DefStringTooLongException"}] diff --git a/tests/cases/long_typehint_with_args/source.py b/tests/cases/long_typehint_with_args/source.py new file mode 100644 index 0000000..260af8e --- /dev/null +++ b/tests/cases/long_typehint_with_args/source.py @@ -0,0 +1,5 @@ +from typing import Optional, Union + + +def long_typehint_with_args(a) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: + return diff --git a/tests/cases/multiline_bad_indent/__init__.py b/tests/cases/multiline_bad_indent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/multiline_bad_indent/expected.py b/tests/cases/multiline_bad_indent/expected.py new file mode 100644 index 0000000..e50c061 --- /dev/null +++ b/tests/cases/multiline_bad_indent/expected.py @@ -0,0 +1,5 @@ +def wrong_formatted_def( + a, + b: int | None = None, +): + return diff --git a/tests/cases/multiline_bad_indent/expected_issues.json b/tests/cases/multiline_bad_indent/expected_issues.json new file mode 100644 index 0000000..fa9621c --- /dev/null +++ b/tests/cases/multiline_bad_indent/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/multiline_bad_indent/source.py b/tests/cases/multiline_bad_indent/source.py new file mode 100644 index 0000000..77a87e3 --- /dev/null +++ b/tests/cases/multiline_bad_indent/source.py @@ -0,0 +1,5 @@ +def wrong_formatted_def( + a, + b: int | None = None, +): + return diff --git a/tests/cases/no_issues_already_formatted/__init__.py b/tests/cases/no_issues_already_formatted/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/no_issues_already_formatted/expected.py b/tests/cases/no_issues_already_formatted/expected.py new file mode 100644 index 0000000..dccc6d8 --- /dev/null +++ b/tests/cases/no_issues_already_formatted/expected.py @@ -0,0 +1,6 @@ +def formatted_def( + a, + b: int | None = None, + c: str = "", +): + return diff --git a/tests/cases/no_issues_already_formatted/source.py b/tests/cases/no_issues_already_formatted/source.py new file mode 100644 index 0000000..dccc6d8 --- /dev/null +++ b/tests/cases/no_issues_already_formatted/source.py @@ -0,0 +1,6 @@ +def formatted_def( + a, + b: int | None = None, + c: str = "", +): + return diff --git a/tests/cases/no_issues_empty_def/__init__.py b/tests/cases/no_issues_empty_def/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/no_issues_empty_def/source.py b/tests/cases/no_issues_empty_def/source.py new file mode 100644 index 0000000..3698a47 --- /dev/null +++ b/tests/cases/no_issues_empty_def/source.py @@ -0,0 +1,2 @@ +def clear_def(): + return diff --git a/tests/cases/single_line_comment_first/__init__.py b/tests/cases/single_line_comment_first/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_comment_first/expected.py b/tests/cases/single_line_comment_first/expected.py new file mode 100644 index 0000000..d08ef51 --- /dev/null +++ b/tests/cases/single_line_comment_first/expected.py @@ -0,0 +1,6 @@ +def single_line_with_comment_first( + a, # first param comment + b, + c, +): + return diff --git a/tests/cases/single_line_comment_first/expected_issues.json b/tests/cases/single_line_comment_first/expected_issues.json new file mode 100644 index 0000000..fa9621c --- /dev/null +++ b/tests/cases/single_line_comment_first/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/single_line_comment_first/source.py b/tests/cases/single_line_comment_first/source.py new file mode 100644 index 0000000..80dd9ff --- /dev/null +++ b/tests/cases/single_line_comment_first/source.py @@ -0,0 +1,3 @@ +def single_line_with_comment_first(a, # first param comment + b, c): + return diff --git a/tests/cases/single_line_comment_last/__init__.py b/tests/cases/single_line_comment_last/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_comment_last/expected.py b/tests/cases/single_line_comment_last/expected.py new file mode 100644 index 0000000..7382201 --- /dev/null +++ b/tests/cases/single_line_comment_last/expected.py @@ -0,0 +1,6 @@ +def single_line_with_comment_last( + a, + b, + c, +): # end comment + return diff --git a/tests/cases/single_line_comment_last/expected_issues.json b/tests/cases/single_line_comment_last/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/single_line_comment_last/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/single_line_comment_last/source.py b/tests/cases/single_line_comment_last/source.py new file mode 100644 index 0000000..4beb87a --- /dev/null +++ b/tests/cases/single_line_comment_last/source.py @@ -0,0 +1,2 @@ +def single_line_with_comment_last(a, b, c): # end comment + return diff --git a/tests/cases/single_line_comment_middle/__init__.py b/tests/cases/single_line_comment_middle/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_comment_middle/expected.py b/tests/cases/single_line_comment_middle/expected.py new file mode 100644 index 0000000..4e0c557 --- /dev/null +++ b/tests/cases/single_line_comment_middle/expected.py @@ -0,0 +1,6 @@ +def single_line_with_comment_middle( + a, + b, # middle param comment + c, +): + return diff --git a/tests/cases/single_line_comment_middle/expected_issues.json b/tests/cases/single_line_comment_middle/expected_issues.json new file mode 100644 index 0000000..fa9621c --- /dev/null +++ b/tests/cases/single_line_comment_middle/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/single_line_comment_middle/source.py b/tests/cases/single_line_comment_middle/source.py new file mode 100644 index 0000000..3e53c6e --- /dev/null +++ b/tests/cases/single_line_comment_middle/source.py @@ -0,0 +1,3 @@ +def single_line_with_comment_middle(a, b, # middle param comment + c): + return diff --git a/tests/cases/single_line_kw_only_with_comments/__init__.py b/tests/cases/single_line_kw_only_with_comments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_kw_only_with_comments/expected.py b/tests/cases/single_line_kw_only_with_comments/expected.py new file mode 100644 index 0000000..756267e --- /dev/null +++ b/tests/cases/single_line_kw_only_with_comments/expected.py @@ -0,0 +1,8 @@ +def single_line_kw_only_with_comments( + a, + *args, # varargs + b: str, # keyword-only + c: int = 10, # keyword-only with default + **kwargs, +): # kwargs + return diff --git a/tests/cases/single_line_kw_only_with_comments/expected_issues.json b/tests/cases/single_line_kw_only_with_comments/expected_issues.json new file mode 100644 index 0000000..fa9621c --- /dev/null +++ b/tests/cases/single_line_kw_only_with_comments/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/single_line_kw_only_with_comments/source.py b/tests/cases/single_line_kw_only_with_comments/source.py new file mode 100644 index 0000000..c7c39af --- /dev/null +++ b/tests/cases/single_line_kw_only_with_comments/source.py @@ -0,0 +1,5 @@ +def single_line_kw_only_with_comments(a, *args, # varargs + b: str, # keyword-only + c: int = 10, # keyword-only with default + **kwargs): # kwargs + return diff --git a/tests/cases/single_line_needs_format/__init__.py b/tests/cases/single_line_needs_format/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_needs_format/expected.py b/tests/cases/single_line_needs_format/expected.py new file mode 100644 index 0000000..cfb4147 --- /dev/null +++ b/tests/cases/single_line_needs_format/expected.py @@ -0,0 +1,6 @@ +def single_line_with_comment( + a, + b, + c, +): # first + return diff --git a/tests/cases/single_line_needs_format/expected_issues.json b/tests/cases/single_line_needs_format/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/single_line_needs_format/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/single_line_needs_format/source.py b/tests/cases/single_line_needs_format/source.py new file mode 100644 index 0000000..cea48e8 --- /dev/null +++ b/tests/cases/single_line_needs_format/source.py @@ -0,0 +1,2 @@ +def single_line_with_comment(a, b, c): # first + return diff --git a/tests/cases/single_line_ok_no_format/__init__.py b/tests/cases/single_line_ok_no_format/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_ok_no_format/expected.py b/tests/cases/single_line_ok_no_format/expected.py new file mode 100644 index 0000000..51f1aee --- /dev/null +++ b/tests/cases/single_line_ok_no_format/expected.py @@ -0,0 +1,2 @@ +def ok_two_args(a, b): + return diff --git a/tests/cases/single_line_ok_no_format/source.py b/tests/cases/single_line_ok_no_format/source.py new file mode 100644 index 0000000..51f1aee --- /dev/null +++ b/tests/cases/single_line_ok_no_format/source.py @@ -0,0 +1,2 @@ +def ok_two_args(a, b): + return diff --git a/tests/cases/single_line_return_type_comment/__init__.py b/tests/cases/single_line_return_type_comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_return_type_comment/expected.py b/tests/cases/single_line_return_type_comment/expected.py new file mode 100644 index 0000000..419fc8f --- /dev/null +++ b/tests/cases/single_line_return_type_comment/expected.py @@ -0,0 +1,2 @@ +def single_line_with_return_type_comment(a: int, b: str) -> bool: # return type comment + return True diff --git a/tests/cases/single_line_return_type_comment/source.py b/tests/cases/single_line_return_type_comment/source.py new file mode 100644 index 0000000..419fc8f --- /dev/null +++ b/tests/cases/single_line_return_type_comment/source.py @@ -0,0 +1,2 @@ +def single_line_with_return_type_comment(a: int, b: str) -> bool: # return type comment + return True diff --git a/tests/cases/single_line_with_all_features/__init__.py b/tests/cases/single_line_with_all_features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/single_line_with_all_features/expected.py b/tests/cases/single_line_with_all_features/expected.py new file mode 100644 index 0000000..c177bf8 --- /dev/null +++ b/tests/cases/single_line_with_all_features/expected.py @@ -0,0 +1,8 @@ +def single_line_with_all_features( + a: int, # typed with comment + b: str = "default", # default with comment + *args, # varargs with comment + c: int = 10, # keyword-only with comment + **kwargs, +) -> bool: # kwargs and return type + return True diff --git a/tests/cases/single_line_with_all_features/expected_issues.json b/tests/cases/single_line_with_all_features/expected_issues.json new file mode 100644 index 0000000..fa9621c --- /dev/null +++ b/tests/cases/single_line_with_all_features/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/single_line_with_all_features/source.py b/tests/cases/single_line_with_all_features/source.py new file mode 100644 index 0000000..d7dbf7b --- /dev/null +++ b/tests/cases/single_line_with_all_features/source.py @@ -0,0 +1,6 @@ +def single_line_with_all_features(a: int, # typed with comment + b: str = "default", # default with comment + *args, # varargs with comment + c: int = 10, # keyword-only with comment + **kwargs) -> bool: # kwargs and return type + return True diff --git a/tests/cases/skip_comment/__init__.py b/tests/cases/skip_comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/skip_comment/source.py b/tests/cases/skip_comment/source.py new file mode 100644 index 0000000..944287f --- /dev/null +++ b/tests/cases/skip_comment/source.py @@ -0,0 +1,3 @@ +# def-form: skip +def skipped_with_up_comment(a, b, c): + return diff --git a/tests/cases/skipped_with_decorator/__init__.py b/tests/cases/skipped_with_decorator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/skipped_with_decorator/source.py b/tests/cases/skipped_with_decorator/source.py new file mode 100644 index 0000000..9d904bf --- /dev/null +++ b/tests/cases/skipped_with_decorator/source.py @@ -0,0 +1,12 @@ +from typing import Callable + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def skipped_with_up_comment_and_decorator(a, b, c): # def-form: skip + return diff --git a/tests/cases/skipped_with_right_comment/__init__.py b/tests/cases/skipped_with_right_comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/skipped_with_right_comment/source.py b/tests/cases/skipped_with_right_comment/source.py new file mode 100644 index 0000000..ee316be --- /dev/null +++ b/tests/cases/skipped_with_right_comment/source.py @@ -0,0 +1,2 @@ +def skipped_with_right_comment(a, b, c): # def-form: skip + return diff --git a/tests/cases/to_much_inline_args_with_typehints/__init__.py b/tests/cases/to_much_inline_args_with_typehints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/to_much_inline_args_with_typehints/expected.py b/tests/cases/to_much_inline_args_with_typehints/expected.py new file mode 100644 index 0000000..3ef3b8b --- /dev/null +++ b/tests/cases/to_much_inline_args_with_typehints/expected.py @@ -0,0 +1,7 @@ +def to_much_inline_args_with_typehints( + to: str, + much, + inline: int, + arguments, +): + return diff --git a/tests/cases/to_much_inline_args_with_typehints/expected_issues.json b/tests/cases/to_much_inline_args_with_typehints/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/to_much_inline_args_with_typehints/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/to_much_inline_args_with_typehints/source.py b/tests/cases/to_much_inline_args_with_typehints/source.py new file mode 100644 index 0000000..dbd7993 --- /dev/null +++ b/tests/cases/to_much_inline_args_with_typehints/source.py @@ -0,0 +1,2 @@ +def to_much_inline_args_with_typehints(to: str, much, inline: int, arguments): + return diff --git a/tests/cases/too_many_inline_args/__init__.py b/tests/cases/too_many_inline_args/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/too_many_inline_args/expected.py b/tests/cases/too_many_inline_args/expected.py new file mode 100644 index 0000000..be36c0e --- /dev/null +++ b/tests/cases/too_many_inline_args/expected.py @@ -0,0 +1,7 @@ +def to_much_inline_args( + to, + much, + inline, + arguments, +): + return diff --git a/tests/cases/too_many_inline_args/expected_issues.json b/tests/cases/too_many_inline_args/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/too_many_inline_args/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/too_many_inline_args/source.py b/tests/cases/too_many_inline_args/source.py new file mode 100644 index 0000000..1918d7f --- /dev/null +++ b/tests/cases/too_many_inline_args/source.py @@ -0,0 +1,2 @@ +def to_much_inline_args(to, much, inline, arguments): + return diff --git a/tests/cases/truly_async_single_line_with_comment/__init__.py b/tests/cases/truly_async_single_line_with_comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_async_single_line_with_comment/expected.py b/tests/cases/truly_async_single_line_with_comment/expected.py new file mode 100644 index 0000000..21064c9 --- /dev/null +++ b/tests/cases/truly_async_single_line_with_comment/expected.py @@ -0,0 +1,6 @@ +async def truly_async_single_line_with_comment( + a, + b, + c, +): # async single line + return diff --git a/tests/cases/truly_async_single_line_with_comment/expected_issues.json b/tests/cases/truly_async_single_line_with_comment/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/truly_async_single_line_with_comment/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_async_single_line_with_comment/source.py b/tests/cases/truly_async_single_line_with_comment/source.py new file mode 100644 index 0000000..d3923f5 --- /dev/null +++ b/tests/cases/truly_async_single_line_with_comment/source.py @@ -0,0 +1,2 @@ +async def truly_async_single_line_with_comment(a, b, c): # async single line + return diff --git a/tests/cases/truly_class_method_kw_only/__init__.py b/tests/cases/truly_class_method_kw_only/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_class_method_kw_only/expected.py b/tests/cases/truly_class_method_kw_only/expected.py new file mode 100644 index 0000000..ccee8b0 --- /dev/null +++ b/tests/cases/truly_class_method_kw_only/expected.py @@ -0,0 +1,9 @@ +class ClassWithFunctions: + def truly_class_method_kw_only( + self, + *args, + b: str, + s: str, + **kwargs, + ): # kw-only in class single line + return diff --git a/tests/cases/truly_class_method_kw_only/expected_issues.json b/tests/cases/truly_class_method_kw_only/expected_issues.json new file mode 100644 index 0000000..6c4f368 --- /dev/null +++ b/tests/cases/truly_class_method_kw_only/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "DefStringTooLongException"}, {"line": 2, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_class_method_kw_only/source.py b/tests/cases/truly_class_method_kw_only/source.py new file mode 100644 index 0000000..7d1116b --- /dev/null +++ b/tests/cases/truly_class_method_kw_only/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def truly_class_method_kw_only(self, *args, b: str, s: str, **kwargs): # kw-only in class single line + return diff --git a/tests/cases/truly_class_method_single_line/__init__.py b/tests/cases/truly_class_method_single_line/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_class_method_single_line/expected.py b/tests/cases/truly_class_method_single_line/expected.py new file mode 100644 index 0000000..42c66dd --- /dev/null +++ b/tests/cases/truly_class_method_single_line/expected.py @@ -0,0 +1,8 @@ +class ClassWithFunctions: + def truly_class_method_single_line( + self, + a, + b, + c, + ): # class method single line + return diff --git a/tests/cases/truly_class_method_single_line/expected_issues.json b/tests/cases/truly_class_method_single_line/expected_issues.json new file mode 100644 index 0000000..44ec0e2 --- /dev/null +++ b/tests/cases/truly_class_method_single_line/expected_issues.json @@ -0,0 +1 @@ +[{"line": 2, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_class_method_single_line/source.py b/tests/cases/truly_class_method_single_line/source.py new file mode 100644 index 0000000..bd5af3d --- /dev/null +++ b/tests/cases/truly_class_method_single_line/source.py @@ -0,0 +1,3 @@ +class ClassWithFunctions: + def truly_class_method_single_line(self, a, b, c): # class method single line + return diff --git a/tests/cases/truly_single_line_all_features/__init__.py b/tests/cases/truly_single_line_all_features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_single_line_all_features/expected.py b/tests/cases/truly_single_line_all_features/expected.py new file mode 100644 index 0000000..48e0e04 --- /dev/null +++ b/tests/cases/truly_single_line_all_features/expected.py @@ -0,0 +1,8 @@ +def truly_single_line_all_features( + a: int, + b: str = "default", + *args, + c: int = 10, + **kwargs, +) -> bool: # all features in one line + return True diff --git a/tests/cases/truly_single_line_all_features/expected_issues.json b/tests/cases/truly_single_line_all_features/expected_issues.json new file mode 100644 index 0000000..7f132d8 --- /dev/null +++ b/tests/cases/truly_single_line_all_features/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "DefStringTooLongException"}, {"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_single_line_all_features/source.py b/tests/cases/truly_single_line_all_features/source.py new file mode 100644 index 0000000..29a9031 --- /dev/null +++ b/tests/cases/truly_single_line_all_features/source.py @@ -0,0 +1,2 @@ +def truly_single_line_all_features(a: int, b: str = "default", *args, c: int = 10, **kwargs) -> bool: # all features in one line + return True diff --git a/tests/cases/truly_single_line_decorated/__init__.py b/tests/cases/truly_single_line_decorated/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_single_line_decorated/expected.py b/tests/cases/truly_single_line_decorated/expected.py new file mode 100644 index 0000000..d02e4f9 --- /dev/null +++ b/tests/cases/truly_single_line_decorated/expected.py @@ -0,0 +1,16 @@ +from typing import Callable, Optional + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def truly_single_line_decorated( + a, + b, + c: Optional[str], +): # decorated single line + return diff --git a/tests/cases/truly_single_line_decorated/expected_issues.json b/tests/cases/truly_single_line_decorated/expected_issues.json new file mode 100644 index 0000000..9c8b7a3 --- /dev/null +++ b/tests/cases/truly_single_line_decorated/expected_issues.json @@ -0,0 +1 @@ +[{"line": 11, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_single_line_decorated/source.py b/tests/cases/truly_single_line_decorated/source.py new file mode 100644 index 0000000..b1e52a0 --- /dev/null +++ b/tests/cases/truly_single_line_decorated/source.py @@ -0,0 +1,12 @@ +from typing import Callable, Optional + + +def example_of_decorator(f: Callable) -> Callable: + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@example_of_decorator +def truly_single_line_decorated(a, b, c: Optional[str]): # decorated single line + return diff --git a/tests/cases/truly_single_line_kw_only/__init__.py b/tests/cases/truly_single_line_kw_only/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_single_line_kw_only/expected.py b/tests/cases/truly_single_line_kw_only/expected.py new file mode 100644 index 0000000..65a696c --- /dev/null +++ b/tests/cases/truly_single_line_kw_only/expected.py @@ -0,0 +1,8 @@ +def truly_single_line_kw_only( + a, + *args, + b: str, + c: int = 10, + **kwargs, +): # kw-only in one line + return diff --git a/tests/cases/truly_single_line_kw_only/expected_issues.json b/tests/cases/truly_single_line_kw_only/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/truly_single_line_kw_only/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_single_line_kw_only/source.py b/tests/cases/truly_single_line_kw_only/source.py new file mode 100644 index 0000000..1c1c9d6 --- /dev/null +++ b/tests/cases/truly_single_line_kw_only/source.py @@ -0,0 +1,2 @@ +def truly_single_line_kw_only(a, *args, b: str, c: int = 10, **kwargs): # kw-only in one line + return diff --git a/tests/cases/truly_single_line_with_args_comment/__init__.py b/tests/cases/truly_single_line_with_args_comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_single_line_with_args_comment/expected.py b/tests/cases/truly_single_line_with_args_comment/expected.py new file mode 100644 index 0000000..e90ad8f --- /dev/null +++ b/tests/cases/truly_single_line_with_args_comment/expected.py @@ -0,0 +1,6 @@ +def truly_single_line_with_args_comment( + a, + *args, + **kwargs, +): # args and kwargs + return diff --git a/tests/cases/truly_single_line_with_args_comment/expected_issues.json b/tests/cases/truly_single_line_with_args_comment/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/truly_single_line_with_args_comment/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_single_line_with_args_comment/source.py b/tests/cases/truly_single_line_with_args_comment/source.py new file mode 100644 index 0000000..e5d726f --- /dev/null +++ b/tests/cases/truly_single_line_with_args_comment/source.py @@ -0,0 +1,2 @@ +def truly_single_line_with_args_comment(a, *args, **kwargs): # args and kwargs + return diff --git a/tests/cases/truly_single_line_with_comment/__init__.py b/tests/cases/truly_single_line_with_comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_single_line_with_comment/expected.py b/tests/cases/truly_single_line_with_comment/expected.py new file mode 100644 index 0000000..6b163d3 --- /dev/null +++ b/tests/cases/truly_single_line_with_comment/expected.py @@ -0,0 +1,6 @@ +def truly_single_line_with_comment( + a, + b, + c, +): # truly single line + return diff --git a/tests/cases/truly_single_line_with_comment/expected_issues.json b/tests/cases/truly_single_line_with_comment/expected_issues.json new file mode 100644 index 0000000..375ebee --- /dev/null +++ b/tests/cases/truly_single_line_with_comment/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "TooManyInlineArgumentsException"}] diff --git a/tests/cases/truly_single_line_with_comment/source.py b/tests/cases/truly_single_line_with_comment/source.py new file mode 100644 index 0000000..35bbec7 --- /dev/null +++ b/tests/cases/truly_single_line_with_comment/source.py @@ -0,0 +1,2 @@ +def truly_single_line_with_comment(a, b, c): # truly single line + return diff --git a/tests/cases/truly_single_line_with_return_type/__init__.py b/tests/cases/truly_single_line_with_return_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/truly_single_line_with_return_type/expected.py b/tests/cases/truly_single_line_with_return_type/expected.py new file mode 100644 index 0000000..3550e06 --- /dev/null +++ b/tests/cases/truly_single_line_with_return_type/expected.py @@ -0,0 +1,2 @@ +def truly_single_line_with_return_type(a: int, b: str) -> bool: # return type in one line + return True diff --git a/tests/cases/truly_single_line_with_return_type/source.py b/tests/cases/truly_single_line_with_return_type/source.py new file mode 100644 index 0000000..3550e06 --- /dev/null +++ b/tests/cases/truly_single_line_with_return_type/source.py @@ -0,0 +1,2 @@ +def truly_single_line_with_return_type(a: int, b: str) -> bool: # return type in one line + return True diff --git a/tests/cases/two_issues_same_line/__init__.py b/tests/cases/two_issues_same_line/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/two_issues_same_line/expected.py b/tests/cases/two_issues_same_line/expected.py new file mode 100644 index 0000000..5987131 --- /dev/null +++ b/tests/cases/two_issues_same_line/expected.py @@ -0,0 +1,6 @@ +def truly_single_line_with_type_and_comment( + a: int, + b: str, + c: int | None = None, +): # all in one line + return diff --git a/tests/cases/two_issues_same_line/expected_issues.json b/tests/cases/two_issues_same_line/expected_issues.json new file mode 100644 index 0000000..4b65c72 --- /dev/null +++ b/tests/cases/two_issues_same_line/expected_issues.json @@ -0,0 +1,4 @@ +[ + {"line": 1, "type": "DefStringTooLongException"}, + {"line": 1, "type": "TooManyInlineArgumentsException"} +] diff --git a/tests/cases/two_issues_same_line/source.py b/tests/cases/two_issues_same_line/source.py new file mode 100644 index 0000000..f885196 --- /dev/null +++ b/tests/cases/two_issues_same_line/source.py @@ -0,0 +1,2 @@ +def truly_single_line_with_type_and_comment(a: int, b: str, c: int | None = None): # all in one line + return diff --git a/tests/cases/with_comments/__init__.py b/tests/cases/with_comments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cases/with_comments/expected.py b/tests/cases/with_comments/expected.py new file mode 100644 index 0000000..f06d037 --- /dev/null +++ b/tests/cases/with_comments/expected.py @@ -0,0 +1,6 @@ +def with_comments( + a, # this is an argument + b: int | None = None, + c: str = "", +): + return diff --git a/tests/cases/with_comments/expected_issues.json b/tests/cases/with_comments/expected_issues.json new file mode 100644 index 0000000..fa9621c --- /dev/null +++ b/tests/cases/with_comments/expected_issues.json @@ -0,0 +1 @@ +[{"line": 1, "type": "InvalidMultilineParamsIndentException"}] diff --git a/tests/cases/with_comments/source.py b/tests/cases/with_comments/source.py new file mode 100644 index 0000000..276b19e --- /dev/null +++ b/tests/cases/with_comments/source.py @@ -0,0 +1,6 @@ +def with_comments( + a, # this is an argument + b: int | None = None, + c: str = "", +): + return diff --git a/tests/conftest.py b/tests/conftest.py index 468cacb..50d05e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,45 +1,58 @@ -from collections.abc import Callable - import pytest -from def_form.formatters.def_formatter import DefManager -from tests.constants import EXAMPLE_PATH -from tests.constants import EXPECTED_PATH -from tests.constants import FORMATTED_PATH -from tests.constants import MAX_DEF_LENGTH -from tests.constants import MAX_INLINE_ARGS +from def_form.cli.console import NullConsole +from def_form.cli.context import context +from def_form.cli.ui import NullUI +from def_form.core import DefManager + +from tests.helpers import discover_cases +from tests.helpers import load_expected_content +from tests.helpers import load_expected_issues +from tests.helpers import MAX_DEF_LENGTH +from tests.helpers import MAX_INLINE_ARGS + + +_cases = discover_cases() +CASE_IDS = [name for name, _ in _cases] +CASE_DIR_BY_ID = {name: path for name, path in _cases} + + +def _manager_kwargs(path: str) -> dict: + return { + 'config': '/nonexistent', + 'excluded': (), + 'max_def_length': MAX_DEF_LENGTH, + 'max_inline_args': MAX_INLINE_ARGS, + 'path': path, + 'ui': NullUI(console=NullConsole(context=context)), + } @pytest.fixture -def get_def_manager() -> DefManager: - return DefManager( - excluded=(), - max_def_length=MAX_DEF_LENGTH, - max_inline_args=MAX_INLINE_ARGS, - path=EXAMPLE_PATH, - ) +def case_id(request: pytest.FixtureRequest) -> str: + return request.param @pytest.fixture -def get_correct_def_manager() -> DefManager: - return DefManager( - excluded=(), - max_def_length=MAX_DEF_LENGTH, - max_inline_args=MAX_INLINE_ARGS, - path=EXPECTED_PATH, - ) +def case_dir(case_id: str): + return CASE_DIR_BY_ID[case_id] @pytest.fixture -def get_expected() -> str: - with open(EXPECTED_PATH, encoding='utf-8') as f: - return f.read() +def case_source_path(case_dir) -> str: + return str(case_dir / 'source.py') @pytest.fixture -def get_formatted() -> Callable[[], str]: - def read() -> str: - with open(FORMATTED_PATH, encoding='utf-8') as f: - return f.read() +def case_expected_issues(case_dir): + return load_expected_issues(case_dir) - return read + +@pytest.fixture +def case_expected_content(case_dir) -> str | None: + return load_expected_content(case_dir) + + +@pytest.fixture +def case_manager(case_source_path: str) -> DefManager: + return DefManager(**_manager_kwargs(case_source_path)) diff --git a/tests/constants.py b/tests/constants.py deleted file mode 100644 index 5acff22..0000000 --- a/tests/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path - -MAX_DEF_LENGTH = 100 -MAX_INLINE_ARGS = 2 -EXPECTED_TOTAL_ISSUES = 62 - -src_path = Path(__file__).resolve().parent - -EXAMPLE_PATH: str = str(src_path / 'mock_data/example.py') -EXPECTED_PATH: str = str(src_path / 'mock_data/expected.py') -FORMATTED_PATH: str = str(src_path / 'mock_data/formatted.py') diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..4d9faf1 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,54 @@ +import json +from pathlib import Path + +from def_form.exceptions.base import BaseDefFormException + + +MAX_DEF_LENGTH = 100 +MAX_INLINE_ARGS = 2 + + +def get_case_dir() -> Path: + return Path(__file__).resolve().parent / 'cases' + + +def discover_cases() -> list[tuple[str, Path]]: + cases_dir = get_case_dir() + if not cases_dir.is_dir(): + return [] + result: list[tuple[str, Path]] = [] + for path in sorted(cases_dir.iterdir()): + if path.is_dir() and (path / 'source.py').is_file(): + result.append((path.name, path)) + return result + + +def normalize_issues(issues: list[BaseDefFormException]) -> list[tuple[int, str]]: + out: list[tuple[int, str]] = [] + for exc in issues: + path_str = getattr(exc, 'path', '') + if ':' in path_str: + line_str = path_str.rsplit(':', 1)[-1] + try: + line_no = int(line_str) + except ValueError: + line_no = 0 + else: + line_no = 0 + out.append((line_no, type(exc).__name__)) + return sorted(out) + + +def load_expected_issues(case_dir: Path) -> list[tuple[int, str]]: + path = case_dir / 'expected_issues.json' + if not path.is_file(): + return [] + data = json.loads(path.read_text(encoding='utf-8')) + return [(_item['line'], _item['type']) for _item in data] + + +def load_expected_content(case_dir: Path) -> str | None: + path = case_dir / 'expected.py' + if not path.is_file(): + return None + return path.read_text(encoding='utf-8') diff --git a/tests/mock_data/__init__.py b/tests/mock_data/__init__.py deleted file mode 100644 index 7ab28e7..0000000 --- a/tests/mock_data/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -from def_form.exceptions.def_formatter import DefStringTooLongException, InvalidMultilineParamsIndentException -from def_form.exceptions.def_formatter import TooManyInlineArgumentsException -from tests.constants import EXAMPLE_PATH - -EXPECTED_ISSUES: list[DefStringTooLongException | TooManyInlineArgumentsException | InvalidMultilineParamsIndentException] = [ - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:19", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:25", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:32", message="Too many inline args (4 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:35", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:39", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:43", message="Too many inline args (3 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:46", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:51", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:55", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:59", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:64", message="Too many inline args (3 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:67", message="Too many inline args (3 > 2)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:70", message="Function definition too long (104 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:70", message="Too many inline args (3 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:73", message="Too many inline args (3 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:79", message="Too many inline args (3 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:82", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:86", message="Too many inline args (3 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:89", message="Too many inline args (4 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:92", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:96", message="Function definition too long (142 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:96", message="Too many inline args (3 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:99", message="Too many inline args (4 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:102", message="Too many inline args (4 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:105", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:111", message="Too many inline args (5 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:119", message="Too many inline args (3 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:123", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:128", message="Too many inline args (3 > 2)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:149", message="Function definition too long (163 > 100)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:152", message="Function definition too long (126 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:160", message="Too many inline args (3 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:163", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:167", message="Too many inline args (4 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:170", message="Too many inline args (4 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:173", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:177", message="Function definition too long (108 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:177", message="Too many inline args (4 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:183", message="Too many inline args (4 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:186", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:190", message="Too many inline args (4 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:194", message="Too many inline args (4 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:198", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:203", message="Too many inline args (4 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:218", message="Too many inline args (4 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:225", message="Too many inline args (6 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:228", message="Too many inline args (6 > 2)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:231", message="Too many inline args (5 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:234", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:240", message="Function definition too long (102 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:240", message="Too many inline args (5 > 2)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:243", message="Function definition too long (174 > 100)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:246", message="Function definition too long (199 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:246", message="Too many inline args (4 > 2)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:249", message="Function definition too long (127 > 100)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:252", message="Function definition too long (138 > 100)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:255", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:260", message="Function definition too long (118 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:260", message="Too many inline args (4 > 2)", description=None), - InvalidMultilineParamsIndentException(path=f"{EXAMPLE_PATH}:269", message="Invalid multiline function parameters indentation (expected 4 spaces)", description=None), - DefStringTooLongException(path=f"{EXAMPLE_PATH}:276", message="Function definition too long (129 > 100)", description=None), - TooManyInlineArgumentsException(path=f"{EXAMPLE_PATH}:276", message="Too many inline args (5 > 2)", description=None), -] \ No newline at end of file diff --git a/tests/mock_data/example.py b/tests/mock_data/example.py deleted file mode 100644 index 5a5e25e..0000000 --- a/tests/mock_data/example.py +++ /dev/null @@ -1,277 +0,0 @@ -from typing import Optional, Union, Callable - - -def example_of_decorator(f: Callable) -> Callable: - def wrapper(*args, **kwargs): - return f(*args, **kwargs) - return wrapper - -def clear_def(): - return - -def formatted_def( - a, - b: Optional[Union[int, str]] = None, - c: int | str | None = None, -): - return - -def wrong_formatted_def( - a, - b: Optional[Union[int, str]] = None, -): - return - -def with_comments( - a, # this is an argument - b: Optional[Union[int, str]] = None, - c: int | str | None = None, -): - return - -def to_much_inline_args(to, much, inline, arguments): - return - -def single_line_with_comment_first(a, # first param comment - b, c): - return - -def single_line_with_comment_middle(a, b, # middle param comment - c): - return - -def single_line_with_comment_last(a, b, c): # end comment - return - -def single_line_with_multiple_comments(a, # first comment - b, # second comment - c): # third comment - return - -def single_line_with_comment_and_default(a, # param with default - b: int = 42, c: str = "test"): - return - -def single_line_with_comment_and_typehint(a: int, # typed param - b: str, c: Optional[int] = None): - return - -def single_line_with_comment_and_args(a, # regular param - *args, # varargs comment - **kwargs): # keyword args comment - return - -def truly_single_line_with_comment(a, b, c): # truly single line - return - -def truly_single_line_with_comment_first(a, b, c): # comment at end of signature - return - -def truly_single_line_with_type_and_comment(a: int, b: str, c: Optional[int] = None): # all in one line - return - -def truly_single_line_with_args_comment(a, *args, **kwargs): # args and kwargs - return - -async def async_def(): - return - -async def async_def_with_args(a, b, c): - return - -async def async_single_line_with_comment(a, # async with comment - b, c): - return - -async def truly_async_single_line_with_comment(a, b, c): # async single line - return - -def to_much_inline_args_with_typehints(to: str, much, inline: int, arguments): - return - -def single_line_with_comment_and_complex_type(a: Optional[Union[int, str]], # complex type - b: dict[str, int] = {}, c: list[str] = []): - return - -def truly_single_line_with_complex_type(a: Optional[Union[int, str]], b: dict[str, int] = {}, c: list[str] = []): # complex types in one line - return - -def kw_args(def_, with_, *args, **kwargs): - return - -def def_kw_args_first(a, *args, b, **kwargs): - return - -def single_line_kw_only_with_comments(a, *args, # varargs - b: str, # keyword-only - c: int = 10, # keyword-only with default - **kwargs): # kwargs - return - -def truly_single_line_kw_only(a, *args, b: str, c: int = 10, **kwargs): # kw-only in one line - return - -@example_of_decorator -def def_with_decorator(): - return - -@example_of_decorator -def def_with_decorator_and_args(a, b, c: Optional[str]): - return - -@example_of_decorator -def single_line_decorated_with_comment(a, # decorated with comment - b, c: Optional[str]): - return - -@example_of_decorator -def truly_single_line_decorated(a, b, c: Optional[str]): # decorated single line - return - -def skipped_with_right_comment(a, b, c): # def-form: skip - return - -# def-form: skip -def skipped_with_up_comment(a, b, c): - return - -@example_of_decorator -def skipped_with_right_comment_and_decorator(a, b, c): # def-form: skip - return - -@example_of_decorator -def skipped_with_up_comment_and_decorator(a, b, c): # def-form: skip - return - -def long_typehint_without_args() -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - -def long_typehint_with_args(a) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - -def loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_def_name(a): - return - - -class ClassWithFunctions: - def class_method(self): - return - - def class_method_with_args(self, a, b): - return - - def class_method_single_line_with_comment(self, a, # class method comment - b, c): - return - - def truly_class_method_single_line(self, a, b, c): # class method single line - return - - def class_method_with_args_with_typehints(self, a: Optional[int], b: int, c): - return - - def class_method_single_line_with_typehint_comment(self, a: int, # typed comment - b: str, c: Optional[int] = None): - return - - def truly_class_method_with_types(self, a: int, b: str, c: Optional[int] = None): # class method with types - return - - async def async_def(self): - return - - async def async_def_with_args(self, a, b, c): - return - - async def async_class_method_single_line(self, a, # async class method - b, c): - return - - async def truly_async_class_method(self, a, b, c): # async class method single line - return - - @example_of_decorator - async def async_def_with_args_and_decorator(self, a, b, c): - return - - @example_of_decorator - async def async_class_method_with_comment(self, a, # decorated async - b, c): - return - - @example_of_decorator - async def truly_async_decorated_class_method(self, a, b, c): # decorated async class method - return - - def skipped_with_right_comment(self, a, b, c): # def-form: skip - return - - # def-form: skip - def skipped_with_up_comment(self, a, b, c): - return - - @example_of_decorator - def def_with_decorator(self): - return - - @example_of_decorator - def def_with_decorator_and_args(self, a, b, c): - return - - @staticmethod - def static_method(): - return - - def def_with_kw_args(self, a, b, c, *args, **kwargs): - return - - def def_with_kw_args_first(self, *args, a, b, c, **kwargs): - return - - def def_with_kw_only_args(self, *args, b: str, s: str, **kwargs): - return - - def class_method_kw_only_with_comments(self, *args, # varargs in class - b: str, # keyword-only in class - s: str, # another keyword-only - **kwargs): # kwargs in class - return - - def truly_class_method_kw_only(self, *args, b: str, s: str, **kwargs): # kw-only in class single line - return - - def class_method_with_long_typehint(self) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - - def class_method_with_long_typehint_with_args(self, a, b: int, c) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - - def class_method_with_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name(self): - return - - def class_method_with_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name_and_arg(self, a): - return - - def class_method_single_line_with_defaults(self, a: int = 1, # default value - b: str = "test", # another default - c: Optional[int] = None): # optional default - return - - def truly_class_method_with_defaults(self, a: int = 1, b: str = "test", c: Optional[int] = None): # defaults in class - return - -def single_line_with_return_type_comment(a: int, b: str) -> bool: # return type comment - return True - -def truly_single_line_with_return_type(a: int, b: str) -> bool: # return type in one line - return True - -def single_line_with_all_features(a: int, # typed with comment - b: str = "default", # default with comment - *args, # varargs with comment - c: int = 10, # keyword-only with comment - **kwargs) -> bool: # kwargs and return type - return True - -def truly_single_line_all_features(a: int, b: str = "default", *args, c: int = 10, **kwargs) -> bool: # all features in one line - return True diff --git a/tests/mock_data/expected.py b/tests/mock_data/expected.py deleted file mode 100644 index febc558..0000000 --- a/tests/mock_data/expected.py +++ /dev/null @@ -1,492 +0,0 @@ -from typing import Optional, Union, Callable - - -def example_of_decorator(f: Callable) -> Callable: - def wrapper(*args, **kwargs): - return f(*args, **kwargs) - return wrapper - -def clear_def(): - return - -def formatted_def( - a, - b: Optional[Union[int, str]] = None, - c: int | str | None = None, -): - return - -def wrong_formatted_def( - a, - b: Optional[Union[int, str]] = None, -): - return - -def with_comments( - a, # this is an argument - b: Optional[Union[int, str]] = None, - c: int | str | None = None, -): - return - -def to_much_inline_args( - to, - much, - inline, - arguments, -): - return - -def single_line_with_comment_first( - a, # first param comment - b, - c, -): - return - -def single_line_with_comment_middle( - a, - b, # middle param comment - c, -): - return - -def single_line_with_comment_last( - a, - b, - c, -): # end comment - return - -def single_line_with_multiple_comments( - a, # first comment - b, # second comment - c, -): # third comment - return - -def single_line_with_comment_and_default( - a, # param with default - b: int = 42, - c: str = "test", -): - return - -def single_line_with_comment_and_typehint( - a: int, # typed param - b: str, - c: Optional[int] = None, -): - return - -def single_line_with_comment_and_args( - a, # regular param - *args, # varargs comment - **kwargs, -): # keyword args comment - return - -def truly_single_line_with_comment( - a, - b, - c, -): # truly single line - return - -def truly_single_line_with_comment_first( - a, - b, - c, -): # comment at end of signature - return - -def truly_single_line_with_type_and_comment( - a: int, - b: str, - c: Optional[int] = None, -): # all in one line - return - -def truly_single_line_with_args_comment( - a, - *args, - **kwargs, -): # args and kwargs - return - -async def async_def(): - return - -async def async_def_with_args( - a, - b, - c, -): - return - -async def async_single_line_with_comment( - a, # async with comment - b, - c, -): - return - -async def truly_async_single_line_with_comment( - a, - b, - c, -): # async single line - return - -def to_much_inline_args_with_typehints( - to: str, - much, - inline: int, - arguments, -): - return - -def single_line_with_comment_and_complex_type( - a: Optional[Union[int, str]], # complex type - b: dict[str, int] = {}, - c: list[str] = [], -): - return - -def truly_single_line_with_complex_type( - a: Optional[Union[int, str]], - b: dict[str, int] = {}, - c: list[str] = [], -): # complex types in one line - return - -def kw_args( - def_, - with_, - *args, - **kwargs, -): - return - -def def_kw_args_first( - a, - *args, - b, - **kwargs, -): - return - -def single_line_kw_only_with_comments( - a, - *args, # varargs - b: str, # keyword-only - c: int = 10, # keyword-only with default - **kwargs, -): # kwargs - return - -def truly_single_line_kw_only( - a, - *args, - b: str, - c: int = 10, - **kwargs, -): # kw-only in one line - return - -@example_of_decorator -def def_with_decorator(): - return - -@example_of_decorator -def def_with_decorator_and_args( - a, - b, - c: Optional[str], -): - return - -@example_of_decorator -def single_line_decorated_with_comment( - a, # decorated with comment - b, - c: Optional[str], -): - return - -@example_of_decorator -def truly_single_line_decorated( - a, - b, - c: Optional[str], -): # decorated single line - return - -def skipped_with_right_comment(a, b, c): # def-form: skip - return - -# def-form: skip -def skipped_with_up_comment(a, b, c): - return - -@example_of_decorator -def skipped_with_right_comment_and_decorator(a, b, c): # def-form: skip - return - -@example_of_decorator -def skipped_with_up_comment_and_decorator(a, b, c): # def-form: skip - return - -def long_typehint_without_args() -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - -def long_typehint_with_args( - a, -) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - -def loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_def_name( - a, -): - return - - -class ClassWithFunctions: - def class_method(self): - return - - def class_method_with_args( - self, - a, - b, - ): - return - - def class_method_single_line_with_comment( - self, - a, # class method comment - b, - c, - ): - return - - def truly_class_method_single_line( - self, - a, - b, - c, - ): # class method single line - return - - def class_method_with_args_with_typehints( - self, - a: Optional[int], - b: int, - c, - ): - return - - def class_method_single_line_with_typehint_comment( - self, - a: int, # typed comment - b: str, - c: Optional[int] = None, - ): - return - - def truly_class_method_with_types( - self, - a: int, - b: str, - c: Optional[int] = None, - ): # class method with types - return - - async def async_def(self): - return - - async def async_def_with_args( - self, - a, - b, - c, - ): - return - - async def async_class_method_single_line( - self, - a, # async class method - b, - c, - ): - return - - async def truly_async_class_method( - self, - a, - b, - c, - ): # async class method single line - return - - @example_of_decorator - async def async_def_with_args_and_decorator( - self, - a, - b, - c, - ): - return - - @example_of_decorator - async def async_class_method_with_comment( - self, - a, # decorated async - b, - c, - ): - return - - @example_of_decorator - async def truly_async_decorated_class_method( - self, - a, - b, - c, - ): # decorated async class method - return - - def skipped_with_right_comment(self, a, b, c): # def-form: skip - return - - # def-form: skip - def skipped_with_up_comment(self, a, b, c): - return - - @example_of_decorator - def def_with_decorator(self): - return - - @example_of_decorator - def def_with_decorator_and_args( - self, - a, - b, - c, - ): - return - - @staticmethod - def static_method(): - return - - def def_with_kw_args( - self, - a, - b, - c, - *args, - **kwargs, - ): - return - - def def_with_kw_args_first( - self, - *args, - a, - b, - c, - **kwargs, - ): - return - - def def_with_kw_only_args( - self, - *args, - b: str, - s: str, - **kwargs, - ): - return - - def class_method_kw_only_with_comments( - self, - *args, # varargs in class - b: str, # keyword-only in class - s: str, # another keyword-only - **kwargs, - ): # kwargs in class - return - - def truly_class_method_kw_only( - self, - *args, - b: str, - s: str, - **kwargs, - ): # kw-only in class single line - return - - def class_method_with_long_typehint( - self, - ) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - - def class_method_with_long_typehint_with_args( - self, - a, - b: int, - c, - ) -> Optional[Union[int, str, list, dict[str, str], tuple[str], None, dict[str | int, list[str] | tuple[str, str, int, int] | None]]]: - return - - def class_method_with_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name( - self, - ): - return - - def class_method_with_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_name_and_arg( - self, - a, - ): - return - - def class_method_single_line_with_defaults( - self, - a: int = 1, # default value - b: str = "test", # another default - c: Optional[int] = None, - ): # optional default - return - - def truly_class_method_with_defaults( - self, - a: int = 1, - b: str = "test", - c: Optional[int] = None, - ): # defaults in class - return - -def single_line_with_return_type_comment(a: int, b: str) -> bool: # return type comment - return True - -def truly_single_line_with_return_type(a: int, b: str) -> bool: # return type in one line - return True - -def single_line_with_all_features( - a: int, # typed with comment - b: str = "default", # default with comment - *args, # varargs with comment - c: int = 10, # keyword-only with comment - **kwargs, -) -> bool: # kwargs and return type - return True - -def truly_single_line_all_features( - a: int, - b: str = "default", - *args, - c: int = 10, - **kwargs, -) -> bool: # all features in one line - return True diff --git a/tests/test_checker/test_checker.py b/tests/test_checker/test_checker.py index e4d3d72..0122af4 100644 --- a/tests/test_checker/test_checker.py +++ b/tests/test_checker/test_checker.py @@ -1,19 +1,23 @@ import pytest from def_form.exceptions.base import BaseDefFormException -from def_form.formatters.def_formatter import DefManager -from tests.constants import EXPECTED_TOTAL_ISSUES -from tests.mock_data import EXPECTED_ISSUES +from def_form.exceptions.def_formatter import CheckCommandFoundAnIssue +from tests.helpers import normalize_issues +from tests.conftest import CASE_IDS -def test_failed_check(get_def_manager: DefManager): - with pytest.raises(BaseDefFormException): - get_def_manager.check() - assert get_def_manager.issues == EXPECTED_ISSUES - assert len(get_def_manager.issues) == EXPECTED_TOTAL_ISSUES +@pytest.mark.parametrize('case_id', CASE_IDS, indirect=True) +def test_check_case( + case_id: str, + case_manager, + case_expected_issues: list[tuple[int, str]], +): + if case_expected_issues: + with pytest.raises(CheckCommandFoundAnIssue): + case_manager.check() + else: + case_manager.check() - -def test_successful_check(get_correct_def_manager: DefManager): - get_correct_def_manager.check() - assert len(get_correct_def_manager.issues) == 0 + got = normalize_issues(case_manager.issues) + assert got == case_expected_issues diff --git a/tests/test_formatter/test_formatter.py b/tests/test_formatter/test_formatter.py index 150dc06..b01eb73 100644 --- a/tests/test_formatter/test_formatter.py +++ b/tests/test_formatter/test_formatter.py @@ -1,21 +1,32 @@ -import os -from collections.abc import Callable +from pathlib import Path +from unittest.mock import patch -from def_form.formatters.def_formatter import DefManager -from tests.constants import EXPECTED_TOTAL_ISSUES -from tests.constants import FORMATTED_PATH -from tests.mock_data import EXPECTED_ISSUES +import pytest +from def_form.core import DefManager +from tests.helpers import normalize_issues +from tests.conftest import CASE_IDS -def test_successful_format(get_def_manager: DefManager, get_expected: str, get_formatted: Callable[[], str]): - get_def_manager.format(write_to=FORMATTED_PATH) - assert len(get_def_manager.issues) == EXPECTED_TOTAL_ISSUES - assert get_def_manager.issues == EXPECTED_ISSUES - assert get_expected == get_formatted() - os.remove(FORMATTED_PATH) +@pytest.mark.parametrize('case_id', CASE_IDS, indirect=True) +def test_format_case( + case_id: str, + case_dir: Path, + case_manager: DefManager, + case_expected_issues: list[tuple[int, str]], + case_expected_content: str | None, +): + def capture_write(dest, module: str): + pass + with patch.object(case_manager, '_write', side_effect=capture_write) as mocked_write: + case_manager.format() -def test_no_need_format(get_correct_def_manager: DefManager): - get_correct_def_manager.format() - assert len(get_correct_def_manager.issues) == 0 + got_issues = normalize_issues(case_manager.issues) + assert got_issues == case_expected_issues, ( + f'case_id={case_id}: expected issues {case_expected_issues}, got {got_issues}' + ) + + if case_expected_content is not None: + mocked_write.assert_called_once() + assert mocked_write.call_args[1]['module'].strip() == case_expected_content.strip() From d46ea4b8e47e8a480518a12ec086add7df40ab64 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 23:55:31 +0200 Subject: [PATCH 05/16] Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a9031e8..0caf6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +*.lcov # Translations *.mo From 78d660c0c98fd6c318cded27dc192160cb30e854 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 23:56:41 +0200 Subject: [PATCH 06/16] Update pyproject and Justfile --- Justfile | 3 +++ pyproject.toml | 7 +++---- uv.lock | 18 +++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Justfile b/Justfile index c6b9ffd..e6dfce8 100755 --- a/Justfile +++ b/Justfile @@ -1,6 +1,9 @@ SOURCE_PATH := "def_form" TESTS_PATH := "tests" +default: + @just --list + upgrade: uv lock --upgrade diff --git a/pyproject.toml b/pyproject.toml index f4638db..37d72d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ indent_size = 4 exclude = [ ".venv/", "build/", + "tests/cases/" ] [tool.mypy] @@ -79,8 +80,7 @@ warn_unused_ignores = true no_implicit_reexport = true exclude = [ ".venv/", - "tests/mock_data/example.py", - "tests/mock_data/expected.py", + "tests/cases/", ] [tool.ruff] @@ -88,8 +88,7 @@ target-version = "py310" line-length = 120 exclude = [ ".venv/", - "tests/mock_data/example.py", - "tests/mock_data/expected.py", + "tests/cases/", ] lint.flake8-tidy-imports.ban-relative-imports = "all" lint.mccabe.max-complexity = 20 diff --git a/uv.lock b/uv.lock index e42ece8..457e66e 100644 --- a/uv.lock +++ b/uv.lock @@ -407,20 +407,20 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pathspec" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -563,15 +563,15 @@ wheels = [ [[package]] name = "rich" -version = "14.3.1" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] From 61ccc190ae6cbbb80695f5abff4714dff82af817 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 23:56:59 +0200 Subject: [PATCH 07/16] Update README --- README.md | 16 ++++++++++------ assets/logo-transparent.png | Bin 0 -> 118779 bytes assets/logo.png | Bin 0 -> 110108 bytes 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 assets/logo-transparent.png create mode 100644 assets/logo.png diff --git a/README.md b/README.md index 67e46ee..9624a4b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ -# def-form +
+ def-form logo -Python function definition formatter +

Python function definition formatter

+ + [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + [![PyPI](https://img.shields.io/pypi/v/def-form.svg)](https://pypi.python.org/pypi/def-form) + [![PyPI](https://img.shields.io/pypi/dm/def-form.svg)](https://pypi.python.org/pypi/def-form) + [![Coverage Status](https://coveralls.io/repos/github/TopNik073/def-form/badge.svg?branch=init)](https://coveralls.io/github/TopNik073/def-form?branch=init) + +
-[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![PyPI](https://img.shields.io/pypi/v/def-form.svg)](https://pypi.python.org/pypi/def-form) -[![PyPI](https://img.shields.io/pypi/dm/def-form.svg)](https://pypi.python.org/pypi/def-form) -[![Coverage Status](https://coveralls.io/repos/github/TopNik073/def-form/badge.svg?branch=init)](https://coveralls.io/github/TopNik073/def-form?branch=init) ## Overview `def-form` is a code formatting tool that focuses specifically on Python function definitions. It helps maintain consistent formatting of function signatures by automatically organizing arguments vertically when they exceed specified thresholds. diff --git a/assets/logo-transparent.png b/assets/logo-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..ee08cbd438cfdbe5bb671c2ff9ff8a07ad176fbb GIT binary patch literal 118779 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 literal 0 HcmV?d00001 diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9628b52d80fce271740f58f4da2c80d82d140343 GIT binary patch literal 110108 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 z
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 cNa!IM0gu%E&yLs2@$;4vlYf%^NbmLk1&7k2 Date: Sun, 1 Feb 2026 23:59:22 +0200 Subject: [PATCH 08/16] Format --- def_form/exceptions/def_formatter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/def_form/exceptions/def_formatter.py b/def_form/exceptions/def_formatter.py index 4c1b200..1d20423 100644 --- a/def_form/exceptions/def_formatter.py +++ b/def_form/exceptions/def_formatter.py @@ -30,8 +30,9 @@ class InvalidMultilineParamsIndentException(BaseDefFormException): message: str = 'Invalid multiline params indentation' description: str | None = None + @dataclass class CheckCommandFoundAnIssue(BaseDefFormException): path: str message: str = 'check command did found an issue' - description: str | None = None \ No newline at end of file + description: str | None = None From 70983a9be0846bb80712897362b90c8616da0fcb Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Sun, 1 Feb 2026 23:59:51 +0200 Subject: [PATCH 09/16] Bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37d72d8..163f78f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "def-form" -version = "0.1.0" +version = "0.2.0" description = "Formatter for functions" readme = "README.md" requires-python = ">=3.10" diff --git a/uv.lock b/uv.lock index 457e66e..3927bcc 100644 --- a/uv.lock +++ b/uv.lock @@ -134,7 +134,7 @@ toml = [ [[package]] name = "def-form" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "click" }, From 3837ecc8fe5b740a4c938dbec52b352720b3da7b Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Mon, 2 Feb 2026 00:35:27 +0200 Subject: [PATCH 10/16] Fix RichUI --- def_form/cli/ui/rich.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/def_form/cli/ui/rich.py b/def_form/cli/ui/rich.py index c21f098..e920307 100644 --- a/def_form/cli/ui/rich.py +++ b/def_form/cli/ui/rich.py @@ -106,7 +106,7 @@ def processing(self, path: Path) -> None: if not self.context.should_output: return - if not (self.progress and self._live and self.task_id): + if not (self.progress and self._live and self.task_id is not None): return self.current_file = path From d6a7bfcbbfb7535323a177df6c56711f9965d571 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Mon, 2 Feb 2026 00:35:48 +0200 Subject: [PATCH 11/16] Update just tests command --- Justfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Justfile b/Justfile index e6dfce8..5351f45 100755 --- a/Justfile +++ b/Justfile @@ -23,5 +23,6 @@ tests: uv run pytest \ --cov=def_form \ --cov-report=lcov:tests.lcov \ + --cov-report=term \ tests/ From 4d5954161fe1ddbbaf71ef36b9262f797be5e395 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Mon, 2 Feb 2026 00:38:18 +0200 Subject: [PATCH 12/16] Add CLI tests --- tests/test_cli/__init__.py | 0 tests/test_cli/test_cli_invoke.py | 44 +++++ tests/test_cli/test_commands.py | 156 +++++++++++++++ tests/test_cli/test_console.py | 96 ++++++++++ tests/test_cli/test_main.py | 26 +++ tests/test_cli/test_ui.py | 303 ++++++++++++++++++++++++++++++ 6 files changed, 625 insertions(+) create mode 100644 tests/test_cli/__init__.py create mode 100644 tests/test_cli/test_cli_invoke.py create mode 100644 tests/test_cli/test_commands.py create mode 100644 tests/test_cli/test_console.py create mode 100644 tests/test_cli/test_main.py create mode 100644 tests/test_cli/test_ui.py diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cli/test_cli_invoke.py b/tests/test_cli/test_cli_invoke.py new file mode 100644 index 0000000..4b833f6 --- /dev/null +++ b/tests/test_cli/test_cli_invoke.py @@ -0,0 +1,44 @@ +from click.testing import CliRunner + +from def_form.cli.main import cli + + +runner = CliRunner() + + +def test_cli_help_exit_zero() -> None: + result = runner.invoke(cli, ['--help']) + assert result.exit_code == 0 + assert 'format' in result.output + assert 'check' in result.output + assert '--verbose' in result.output + assert '--quiet' in result.output + + +def test_format_help_exit_zero() -> None: + result = runner.invoke(cli, ['format', '--help']) + assert result.exit_code == 0 + assert 'path' in result.output.lower() + assert '--max-def-length' in result.output + assert '--max-inline-args' in result.output + assert '--indent-size' in result.output + assert '--config' in result.output + assert '--exclude' in result.output + assert '--show-skipped' in result.output + + +def test_check_help_exit_zero() -> None: + result = runner.invoke(cli, ['check', '--help']) + assert result.exit_code == 0 + assert 'path' in result.output.lower() + assert '--max-def-length' in result.output + assert '--max-inline-args' in result.output + assert '--config' in result.output + assert '--exclude' in result.output + + +def test_cli_verbose_quiet_flags() -> None: + result = runner.invoke(cli, ['--verbose', '--help']) + assert result.exit_code == 0 + result = runner.invoke(cli, ['--quiet', '--help']) + assert result.exit_code == 0 diff --git a/tests/test_cli/test_commands.py b/tests/test_cli/test_commands.py new file mode 100644 index 0000000..521a5f6 --- /dev/null +++ b/tests/test_cli/test_commands.py @@ -0,0 +1,156 @@ +from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch + +from click.testing import CliRunner + +from def_form.cli.main import cli + + +runner = CliRunner() + + +def test_format_default_path_is_current_dir() -> None: + with patch('def_form.cli.commands.format.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_manager_class.return_value = mock_instance + + result = runner.invoke(cli, ['format']) + + assert result.exit_code == 0 + call_kw = mock_manager_class.call_args[1] + assert call_kw['path'] == '.' + + +def test_format_invokes_def_manager_and_format(tmp_path: Path) -> None: + with patch('def_form.cli.commands.format.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_manager_class.return_value = mock_instance + + result = runner.invoke(cli, ['format', str(tmp_path)]) + + assert result.exit_code == 0 + mock_manager_class.assert_called_once() + call_kw = mock_manager_class.call_args[1] + assert call_kw['path'] == str(tmp_path) + assert call_kw['excluded'] == () + assert call_kw['max_def_length'] is None + assert call_kw['max_inline_args'] is None + assert call_kw['indent_size'] is None + assert call_kw['config'] is None + assert call_kw['show_skipped'] is False + assert 'ui' in call_kw + mock_instance.format.assert_called_once() + + +def test_format_passes_options_to_def_manager(tmp_path: Path) -> None: + with patch('def_form.cli.commands.format.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_manager_class.return_value = mock_instance + + result = runner.invoke( + cli, + [ + 'format', + str(tmp_path), + '--max-def-length', '88', + '--max-inline-args', '3', + '--indent-size', '2', + '--exclude', 'foo', + '--exclude', 'bar', + '--show-skipped', + ], + ) + + assert result.exit_code == 0 + call_kw = mock_manager_class.call_args[1] + assert call_kw['max_def_length'] == 88 + assert call_kw['max_inline_args'] == 3 + assert call_kw['indent_size'] == 2 + assert call_kw['excluded'] == ('foo', 'bar') + assert call_kw['show_skipped'] is True + + +def test_check_invokes_def_manager_and_check(tmp_path: Path) -> None: + with patch('def_form.cli.commands.check.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_manager_class.return_value = mock_instance + + result = runner.invoke(cli, ['check', str(tmp_path)]) + + assert result.exit_code == 0 + mock_manager_class.assert_called_once() + call_kw = mock_manager_class.call_args[1] + assert call_kw['path'] == str(tmp_path) + assert call_kw['excluded'] == () + mock_instance.check.assert_called_once() + + +def test_check_passes_options_to_def_manager(tmp_path: Path) -> None: + with patch('def_form.cli.commands.check.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_manager_class.return_value = mock_instance + + result = runner.invoke( + cli, + [ + 'check', + str(tmp_path), + '--max-def-length', '100', + '--max-inline-args', '2', + '--exclude', 'build', + ], + ) + + assert result.exit_code == 0 + call_kw = mock_manager_class.call_args[1] + assert call_kw['max_def_length'] == 100 + assert call_kw['max_inline_args'] == 2 + assert call_kw['excluded'] == ('build',) + + +def test_format_raises_cli_error_on_exception(tmp_path: Path) -> None: + with patch('def_form.cli.commands.format.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_instance.format.side_effect = RuntimeError('formatter broke') + mock_manager_class.return_value = mock_instance + + result = runner.invoke(cli, ['format', str(tmp_path)]) + + assert result.exit_code != 0 + assert result.exc_info is not None + from def_form.cli.errors import FormatterFailedError + assert isinstance(result.exc_info[1], FormatterFailedError) + + +def test_check_raises_cli_error_on_generic_exception(tmp_path: Path) -> None: + with patch('def_form.cli.commands.check.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_instance.check.side_effect = RuntimeError('something broke') + mock_manager_class.return_value = mock_instance + + result = runner.invoke(cli, ['check', str(tmp_path)]) + + assert result.exit_code != 0 + assert result.exc_info is not None + from def_form.cli.errors import CheckFailedError + assert isinstance(result.exc_info[1], CheckFailedError) + assert 'something broke' in str(result.exc_info[1]) + + +def test_check_raises_cli_error_on_base_def_form_exception(tmp_path: Path) -> None: + from def_form.exceptions.def_formatter import TooManyInlineArgumentsException + + with patch('def_form.cli.commands.check.DefManager') as mock_manager_class: + mock_instance = MagicMock() + mock_instance.check.side_effect = TooManyInlineArgumentsException( + path='file.py:1', message='too many args' + ) + mock_manager_class.return_value = mock_instance + + result = runner.invoke(cli, ['check', str(tmp_path)]) + + assert result.exit_code != 0 + assert result.exc_info is not None + from def_form.cli.errors import CheckFailedError + assert isinstance(result.exc_info[1], CheckFailedError) diff --git a/tests/test_cli/test_console.py b/tests/test_cli/test_console.py new file mode 100644 index 0000000..6fec563 --- /dev/null +++ b/tests/test_cli/test_console.py @@ -0,0 +1,96 @@ +from unittest.mock import MagicMock + +import pytest + +from def_form.cli.context import CLIContext +from def_form.cli.console.base import BaseConsole +from def_form.cli.console.null import NullConsole +from def_form.cli.console.rich import RichConsole + + +def test_base_console_info_raises_not_implemented() -> None: + ctx = CLIContext() + console = BaseConsole(context=ctx) + with pytest.raises(NotImplementedError): + console.info('x') + + +def test_base_console_success_raises_not_implemented() -> None: + ctx = CLIContext() + console = BaseConsole(context=ctx) + with pytest.raises(NotImplementedError): + console.success('x') + + +def test_base_console_warning_raises_not_implemented() -> None: + ctx = CLIContext() + console = BaseConsole(context=ctx) + with pytest.raises(NotImplementedError): + console.warning('x') + + +def test_base_console_error_raises_not_implemented() -> None: + ctx = CLIContext() + console = BaseConsole(context=ctx) + with pytest.raises(NotImplementedError): + console.error('x') + + +def test_base_console_debug_raises_not_implemented() -> None: + ctx = CLIContext() + console = BaseConsole(context=ctx) + with pytest.raises(NotImplementedError): + console.debug('x') + + +def test_null_console_all_methods_no_op() -> None: + ctx = CLIContext() + console = NullConsole(context=ctx) + console.info('x') + console.success('x') + console.warning('x') + console.error('x') + console.debug('x') + + +def test_rich_console_info_respects_should_output() -> None: + ctx = CLIContext() + ctx.quiet = True + console = RichConsole(context=ctx) + console.print = MagicMock() + console.info('hi') + console.print.assert_not_called() + ctx.quiet = False + console.info('hi') + console.print.assert_called_once_with('hi') + + +def test_rich_console_success_respects_should_output() -> None: + ctx = CLIContext() + console = RichConsole(context=ctx) + console.print = MagicMock() + console.success('ok') + console.print.assert_called_once() + assert '[green]' in str(console.print.call_args[0][0]) + + +def test_rich_console_warning_error_always_print() -> None: + ctx = CLIContext() + console = RichConsole(context=ctx) + console.print = MagicMock() + console.warning('w') + console.print.assert_called_once() + console.error('e') + assert console.print.call_count == 2 + + +def test_rich_console_debug_only_when_verbose() -> None: + ctx = CLIContext() + ctx.verbose = False + console = RichConsole(context=ctx) + console.print = MagicMock() + console.debug('d') + console.print.assert_not_called() + ctx.verbose = True + console.debug('d') + console.print.assert_called_once() diff --git a/tests/test_cli/test_main.py b/tests/test_cli/test_main.py new file mode 100644 index 0000000..e60a366 --- /dev/null +++ b/tests/test_cli/test_main.py @@ -0,0 +1,26 @@ +import pytest +from unittest.mock import patch + +from def_form.cli.main import main +from def_form.cli.errors import CLIError + + +def test_main_exits_1_on_cli_error() -> None: + with patch('def_form.cli.main.cli', side_effect=CLIError('test error')): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 + + +def test_main_exits_130_on_keyboard_interrupt() -> None: + with patch('def_form.cli.main.cli', side_effect=KeyboardInterrupt): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 130 + + +def test_main_exits_1_on_generic_exception() -> None: + with patch('def_form.cli.main.cli', side_effect=RuntimeError('unexpected')): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 diff --git a/tests/test_cli/test_ui.py b/tests/test_cli/test_ui.py new file mode 100644 index 0000000..2eabdc7 --- /dev/null +++ b/tests/test_cli/test_ui.py @@ -0,0 +1,303 @@ +from io import StringIO +from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest + +from def_form.cli.context import CLIContext +from def_form.cli.console.base import BaseConsole +from def_form.cli.console.rich import RichConsole +from def_form.cli.ui.base import BaseUI +from def_form.cli.ui.null import NullUI +from def_form.cli.ui.rich import RichUI +from def_form.exceptions.base import BaseDefFormException +from def_form.exceptions.def_formatter import TooManyInlineArgumentsException + + +def _make_base_ui_console() -> MagicMock: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + return console + + +def test_base_ui_abstract_methods_raise_not_implemented() -> None: + class StubUI(BaseUI): + def show_config_info(self, **kwargs: object) -> None: + super().show_config_info(**kwargs) + + def start(self, total: int | None) -> None: + super().start(total) + + def processing(self, path: Path) -> None: + super().processing(path) + + def skipped(self, path: Path) -> None: + super().skipped(path) + + def finish(self, processed: int, issues: list[BaseDefFormException]) -> None: + super().finish(processed, issues) + + def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> None: + super().show_issues(processed, issues) + + def show_summary(self, processed: int, issues: list[BaseDefFormException]) -> None: + super().show_summary(processed, issues) + + console = _make_base_ui_console() + ui = StubUI(console=console) + + with pytest.raises(NotImplementedError): + ui.show_config_info() + with pytest.raises(NotImplementedError): + ui.start(1) + with pytest.raises(NotImplementedError): + ui.processing(Path('x.py')) + with pytest.raises(NotImplementedError): + ui.skipped(Path('y')) + with pytest.raises(NotImplementedError): + ui.finish(1, []) + with pytest.raises(NotImplementedError): + ui.show_issues(1, []) + with pytest.raises(NotImplementedError): + ui.show_summary(1, []) + + +def test_null_ui_all_methods_no_op() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = NullUI(console=console) + ui.show_config_info() + ui.start(1) + ui.processing(Path('x.py')) + ui.skipped(Path('y')) + ui.finish(1, []) + ui.show_issues(1, []) + ui.show_summary(1, []) + + +def test_rich_ui_show_config_info_skips_when_quiet() -> None: + ctx = CLIContext() + ctx.quiet = True + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_config_info(config_path='x', max_def_length=100) + console.print.assert_not_called() + + +def test_rich_ui_show_config_info_skips_when_empty_config() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_config_info() + console.print.assert_not_called() + + +def test_rich_ui_show_config_info_prints_table_when_output_on() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_config_info(config_path='/cfg', max_def_length=100, max_inline_args=2) + assert console.print.call_count >= 1 + + +def test_rich_ui_start_skips_when_quiet() -> None: + ctx = CLIContext() + ctx.quiet = True + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.start(5) + assert ui.progress is None + + +def test_rich_ui_processing_skips_when_quiet() -> None: + ctx = CLIContext() + ctx.quiet = True + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.processing(Path('a.py')) + assert ui.current_file is None + + +def test_rich_ui_skipped_skips_when_show_skipped_false() -> None: + ctx = CLIContext() + ctx.show_skipped = False + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.skipped(Path('x')) + console.print.assert_not_called() + + +def test_rich_ui_finish_skips_when_quiet() -> None: + ctx = CLIContext() + ctx.quiet = True + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.finish(0, []) + + +def test_rich_ui_show_issues_skips_when_quiet() -> None: + ctx = CLIContext() + ctx.quiet = True + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_issues(1, [TooManyInlineArgumentsException(path='f:1', message='m')]) + console.print.assert_not_called() + + +def test_rich_ui_show_issues_prints_when_issues_and_output_on() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_issues(1, [TooManyInlineArgumentsException(path='f:1', message='msg')]) + assert console.print.call_count >= 1 + + +def test_rich_ui_show_summary_skips_when_quiet() -> None: + ctx = CLIContext() + ctx.quiet = True + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_summary(1, []) + console.print.assert_not_called() + + +def test_rich_ui_convert_to_string_path(tmp_path: Path) -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + with patch.object(Path, 'cwd', return_value=tmp_path): + s = ui._convert_to_string(tmp_path / 'sub' / 'file.py') + assert s == 'sub/file.py' or 'sub' in s + + +def test_rich_ui_convert_to_string_bool() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + assert ui._convert_to_string(True) == 'Yes' + assert ui._convert_to_string(False) == 'No' + + +def test_rich_ui_show_config_info_skips_none_and_empty_values() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_config_info(config_path='/x', max_def_length=None, indent_size='') + # config_path is shown; None and '' are skipped (cover lines 49-50) + assert console.print.call_count >= 1 + + +def test_rich_ui_show_config_info_config_path_bold_yellow() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_config_info(config_path='/project/pyproject.toml') + assert console.print.call_count >= 1 + + +def test_rich_ui_show_config_info_list_value_more_than_three() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_config_info(excluded=['a', 'b', 'c', 'd']) + assert console.print.call_count >= 1 + + +def test_rich_ui_start_creates_progress_when_output_on() -> None: + ctx = CLIContext() + console = RichConsole(context=ctx, file=StringIO()) + ui = RichUI(console=console) + ui.start(10) + assert ui.progress is not None + assert ui._live is not None + assert ui.task_id is not None + + +def test_rich_ui_processing_updates_display_when_started() -> None: + ctx = CLIContext() + console = RichConsole(context=ctx, file=StringIO()) + ui = RichUI(console=console) + ui.start(5) + ui.processing(Path('foo/bar.py')) + assert ui.current_file == Path('foo/bar.py') + assert ui._progress_display.renderables[0] is not None + + +def test_rich_ui_skipped_prints_when_show_skipped_true() -> None: + ctx = CLIContext() + ctx.show_skipped = True + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.skipped(Path('skipped.py')) + console.print.assert_called_once() + assert 'SKIPPED' in str(console.print.call_args[0][0]) + + +def test_rich_ui_issue_no_op() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.issue(TooManyInlineArgumentsException(path='f:1', message='m')) + + +def test_rich_ui_finish_stops_live_and_shows_issues_when_present() -> None: + ctx = CLIContext() + console = RichConsole(context=ctx, file=StringIO()) + ui = RichUI(console=console) + ui.start(2) + ui.finish(2, [TooManyInlineArgumentsException(path='f:1', message='err')]) + assert ui._live is None + assert ui.progress is None + + +def test_rich_ui_show_issues_line_info_when_colon_in_path() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_issues(1, [TooManyInlineArgumentsException(path='f.py:10', message='m')]) + assert console.print.call_count >= 1 + + +def test_rich_ui_show_issues_line_info_when_issue_has_line() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + exc = TooManyInlineArgumentsException(path='f.py', message='m') + exc.line = 5 + ui.show_issues(1, [exc]) + assert console.print.call_count >= 1 + out = str(console.print.call_args_list) + assert ':5' in out or '5' in out + + +def test_rich_ui_show_summary_success_rate_branches() -> None: + ctx = CLIContext() + console = MagicMock(spec=BaseConsole) + console.context = ctx + ui = RichUI(console=console) + ui.show_summary(100, [TooManyInlineArgumentsException(path='a.py:1', message='x')]) + assert console.print.call_count >= 1 + ui.show_summary(10, [TooManyInlineArgumentsException(path=f'f{i}.py:1', message='x') for i in range(10)]) + assert console.print.call_count >= 2 From d89b9215f9c1aad2a0223441cbc2d7c2ab818a40 Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Mon, 2 Feb 2026 00:38:29 +0200 Subject: [PATCH 13/16] Add core tests --- tests/test_core/__init__.py | 0 tests/test_core/test_base.py | 92 +++++++++++++++++ tests/test_core/test_manager.py | 152 +++++++++++++++++++++++++++++ tests/test_core/test_rules_base.py | 25 +++++ 4 files changed, 269 insertions(+) create mode 100644 tests/test_core/__init__.py create mode 100644 tests/test_core/test_base.py create mode 100644 tests/test_core/test_manager.py create mode 100644 tests/test_core/test_rules_base.py diff --git a/tests/test_core/__init__.py b/tests/test_core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_core/test_base.py b/tests/test_core/test_base.py new file mode 100644 index 0000000..5dbd79a --- /dev/null +++ b/tests/test_core/test_base.py @@ -0,0 +1,92 @@ +from pathlib import Path +from unittest.mock import patch + +import libcst as cst + +from def_form.core.base import DefBase +from def_form.core.checker import DefChecker + + +def _parse_and_get_function(code: str) -> cst.FunctionDef: + tree = cst.parse_module(code) + for node in tree.body: + if isinstance(node, cst.FunctionDef): + return node + raise AssertionError('No FunctionDef in code') + + +def test_is_single_line_function_returns_false_when_def_line_has_no_paren_colon() -> None: + code = 'def f(\n x,\n):\n pass\n' + node = _parse_and_get_function(code) + base = DefBase(filepath='x.py', max_def_length=None, max_inline_args=None) + result = base.is_single_line_function(node) + assert result is False + + +def test_has_skip_comment_returns_false_on_file_open_error(tmp_path: Path) -> None: + py_file = tmp_path / 'f.py' + py_file.write_text('def f(): pass\n', encoding='utf-8') + code = py_file.read_text() + tree = cst.parse_module(code) + wrapper = cst.metadata.MetadataWrapper(tree) + checker = DefChecker( + filepath=str(py_file), + max_def_length=None, + max_inline_args=None, + indent_size=4, + ) + + def open_side_effect(self: Path, *args: object, **kwargs: object) -> object: + if str(self) == str(py_file): + raise OSError('permission denied') + return Path.open(self, *args, **kwargs) + + with patch.object(Path, 'open', open_side_effect): + wrapper.visit(checker) + assert checker.issues == [] + + +def test_has_skip_comment_returns_false_on_unicode_decode_error(tmp_path: Path) -> None: + py_file = tmp_path / 'f.py' + py_file.write_text('def f(): pass\n', encoding='utf-8') + tree = cst.parse_module(py_file.read_text()) + wrapper = cst.metadata.MetadataWrapper(tree) + checker = DefChecker( + filepath=str(py_file), + max_def_length=None, + max_inline_args=None, + indent_size=4, + ) + + def open_side_effect(self: Path, *args: object, **kwargs: object) -> object: + if str(self) == str(py_file): + raise UnicodeDecodeError('utf-8', b'', 0, 1, 'invalid') + return Path.open(self, *args, **kwargs) + + with patch.object(Path, 'open', open_side_effect): + wrapper.visit(checker) + assert checker.issues == [] + + +def test_has_correct_multiline_params_format_false_when_not_parenthesized_whitespace() -> None: + code = 'def f(x): pass\n' + node = _parse_and_get_function(code) + base = DefBase(filepath='x.py', max_def_length=None, max_inline_args=None) + result = base.has_correct_multiline_params_format(node) + assert result is False + + +def test_count_arguments_includes_star_arg() -> None: + code = 'def f(a, *args): pass\n' + node = _parse_and_get_function(code) + base = DefBase(filepath='x.py', max_def_length=None, max_inline_args=None) + count = base._count_arguments(node) + assert count == 2 + + +def test_count_arguments_includes_star_kwarg() -> None: + code = 'def f(a, **kwargs): pass\n' + node = _parse_and_get_function(code) + base = DefBase(filepath='x.py', max_def_length=None, max_inline_args=None) + count = base._count_arguments(node) + assert count == 2 diff --git a/tests/test_core/test_manager.py b/tests/test_core/test_manager.py new file mode 100644 index 0000000..1956488 --- /dev/null +++ b/tests/test_core/test_manager.py @@ -0,0 +1,152 @@ +from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch + +from def_form.cli.context import CLIContext +from def_form.cli.console.null import NullConsole +from def_form.cli.ui.null import NullUI +from def_form.core.manager import DefManager + + +def _make_manager(path: str = '.', config: str | None = None, excluded: tuple[str, ...] = ()) -> DefManager: + ctx = CLIContext() + ui = NullUI(console=NullConsole(context=ctx)) + return DefManager(path=path, ui=ui, config=config, excluded=excluded or ()) + + +def test_init_config_with_no_config_keeps_defaults(tmp_path: Path) -> None: + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path), config=None) + assert m.max_def_length is None + assert m.max_inline_args is None + assert m.indent_size is None + assert m._config_excluded == [] + + +def test_init_config_loads_from_pyproject(tmp_path: Path) -> None: + pyproject = tmp_path / 'pyproject.toml' + pyproject.write_text( + '[tool.def-form]\n' + 'max_def_length = 88\n' + 'max_inline_args = 3\n' + 'indent_size = 2\n' + 'exclude = ["venv", "build"]\n', + encoding='utf-8', + ) + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path), config=str(pyproject)) + assert m.max_def_length == 88 + assert m.max_inline_args == 3 + assert m.indent_size == 2 + assert m._config_excluded == ['venv', 'build'] + + +def test_init_config_with_missing_file_sets_excluded_empty(tmp_path: Path) -> None: + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path), config='/nonexistent/pyproject.toml') + assert m._config_excluded == [] + + +def test_init_exclusions_adds_resolved_paths(tmp_path: Path) -> None: + sub = tmp_path / 'sub' + sub.mkdir(exist_ok=True) + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path), config=None, excluded=(str(sub),)) + assert len(m.excluded) >= 1 + + +def test_is_excluded_true_when_path_under_excluded(tmp_path: Path) -> None: + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path), config=None) + m.excluded = {tmp_path} + assert m._is_excluded(tmp_path / 'sub' / 'file.py') is True + + +def test_is_excluded_true_when_excluded_name_in_parts(tmp_path: Path) -> None: + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path), config=None) + other = tmp_path.parent / 'other_dir' + other.mkdir(exist_ok=True) + m.excluded = {other} + p = Path('/any/other_dir/file.py') + assert m._is_excluded(p) is True + + +def test_is_excluded_false_when_not_under_and_name_not_in_parts(tmp_path: Path) -> None: + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path), config=None) + m.excluded = {tmp_path / 'x'} + assert m._is_excluded(tmp_path / 'file.py') is False + + +def test_iter_py_files_file_not_py_returns_nothing(tmp_path: Path) -> None: + f = tmp_path / 'readme.txt' + f.write_text('x') + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(f)) + m.excluded = set() + assert list(m._iter_py_files()) == [] + + +def test_iter_py_files_file_excluded_calls_ui_skipped(tmp_path: Path) -> None: + py = tmp_path / 'f.py' + py.write_text('x = 1') + ui = MagicMock() + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = DefManager(path=str(py), ui=ui, config=None) + m.excluded = {tmp_path} + assert list(m._iter_py_files()) == [] + ui.skipped.assert_called_once() + + +def test_process_file_read_error_returns_empty(tmp_path: Path) -> None: + f = tmp_path / 'x.py' + f.write_text('def f(): pass') + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path)) + with patch.object(Path, 'read_text', side_effect=OSError): + tree, issues = m._process_file(f, m.checker_class) + assert tree is None + assert issues == [] + + +def test_process_file_syntax_error_returns_empty(tmp_path: Path) -> None: + import libcst as cst + + f = tmp_path / 'x.py' + f.write_text('x = 1') + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path)) + err = cst.ParserSyntaxError('syntax error', lines=(), raw_line=1, raw_column=0) + with patch('def_form.core.manager.cst.parse_module', side_effect=err): + tree, issues = m._process_file(f, m.checker_class) + assert tree is None + assert issues == [] + + +def test_process_file_generic_exception_returns_empty(tmp_path: Path) -> None: + f = tmp_path / 'x.py' + f.write_text('def f(): pass') + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = _make_manager(path=str(tmp_path)) + + def raise_in_visit(*args: object, **kwargs: object) -> None: + raise RuntimeError('visit failed') + + with patch('def_form.core.manager.cst.metadata.MetadataWrapper') as MockWrapper: + mock_wrapper = MagicMock() + MockWrapper.return_value = mock_wrapper + mock_wrapper.visit.side_effect = raise_in_visit + tree, issues = m._process_file(f, m.checker_class) + assert tree is None + assert issues == [] + + +def test_write_on_os_error_calls_ui_error(tmp_path: Path) -> None: + ui = MagicMock() + with patch('def_form.core.manager.find_pyproject_toml', return_value=None): + m = DefManager(path=str(tmp_path), ui=ui, config=None) + with patch.object(Path, 'write_text', side_effect=OSError('permission')): + m._write(tmp_path / 'out.py', 'code') + ui.console.error.assert_called_once() + assert 'Exception occurred' in str(ui.console.error.call_args[0][0]) diff --git a/tests/test_core/test_rules_base.py b/tests/test_core/test_rules_base.py new file mode 100644 index 0000000..0ea1ff1 --- /dev/null +++ b/tests/test_core/test_rules_base.py @@ -0,0 +1,25 @@ +import pytest + +from def_form.core.rules.base import Rule +from def_form.core.rules.context import RuleContext + + +def test_rule_check_raises_not_implemented() -> None: + class StubRule(Rule): + def check(self, context: RuleContext): + return super().check(context) + + rule = StubRule() + ctx = RuleContext( + filepath='x.py', + line_no=1, + line_length=50, + arg_count=2, + is_single_line=True, + has_correct_multiline_format=True, + indent_size=4, + max_def_length=None, + max_inline_args=None, + ) + with pytest.raises(NotImplementedError): + rule.check(ctx) From 276312e8d41847edd9e3aec46809a086a54a009c Mon Sep 17 00:00:00 2001 From: Nikita Borzov Date: Mon, 2 Feb 2026 00:38:40 +0200 Subject: [PATCH 14/16] Add utils tests --- tests/test_utils/__init__.py | 0 tests/test_utils/test_find_pyproject.py | 43 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/test_utils/__init__.py create mode 100644 tests/test_utils/test_find_pyproject.py diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_utils/test_find_pyproject.py b/tests/test_utils/test_find_pyproject.py new file mode 100644 index 0000000..97375da --- /dev/null +++ b/tests/test_utils/test_find_pyproject.py @@ -0,0 +1,43 @@ +from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch + +from def_form.utils.find_pyproject import find_pyproject_toml + + +def test_find_pyproject_returns_path_when_in_cwd(tmp_path: Path) -> None: + (tmp_path / 'pyproject.toml').write_text('[project]\nname = "x"') + with patch.object(Path, 'cwd', return_value=tmp_path): + result = find_pyproject_toml() + assert result == str(tmp_path / 'pyproject.toml') + + +def test_find_pyproject_returns_none_when_no_file_in_any_parent() -> None: + cwd = MagicMock(spec=Path) + cwd.__truediv__ = lambda self, other: cwd._child if other == 'pyproject.toml' else MagicMock() + cwd._child = MagicMock() + cwd._child.is_file.return_value = False + parent1 = MagicMock(spec=Path) + parent1.__truediv__ = lambda self, other: parent1._child if other == 'pyproject.toml' else MagicMock() + parent1._child = MagicMock() + parent1._child.is_file.return_value = False + cwd.parents = [parent1] + with patch.object(Path, 'cwd', return_value=cwd): + result = find_pyproject_toml() + assert result is None + + +def test_find_pyproject_returns_path_when_in_parent() -> None: + cwd = MagicMock(spec=Path) + child_cwd = MagicMock() + child_cwd.is_file.return_value = False + cwd.__truediv__ = lambda self, other: child_cwd if other == 'pyproject.toml' else MagicMock() + parent = MagicMock(spec=Path) + parent_file = MagicMock() + parent_file.is_file.return_value = True + parent_file.__str__ = lambda self: '/fake/parent/pyproject.toml' + parent.__truediv__ = lambda self, other: parent_file if other == 'pyproject.toml' else MagicMock() + cwd.parents = [parent] + with patch.object(Path, 'cwd', return_value=cwd): + result = find_pyproject_toml() + assert result == '/fake/parent/pyproject.toml' From aa87b69f5e44d6bd5c1c53fe7c4fb92240be3734 Mon Sep 17 00:00:00 2001 From: Nikita Borzov <152521469+TopNik073@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:41:00 +0200 Subject: [PATCH 15/16] Format --- def_form/core/rules/__init__.py | 8 -------- tests/test_cli/test_ui.py | 1 - 2 files changed, 9 deletions(-) diff --git a/def_form/core/rules/__init__.py b/def_form/core/rules/__init__.py index 5a8af26..2491017 100644 --- a/def_form/core/rules/__init__.py +++ b/def_form/core/rules/__init__.py @@ -1,9 +1,3 @@ -"""Rules layer: rule classes and runner. - -The analyzer builds RuleContext and runs rules; it does not contain -any "what is a violation" logic — that lives in rule classes. -""" - from def_form.exceptions.base import BaseDefFormException from def_form.core.rules.base import Rule @@ -14,7 +8,6 @@ RuleMultilineParamsIndent, ) -# Default rule set used when none is passed DEFAULT_RULES: tuple[Rule, ...] = ( RuleMaxDefLength(), RuleMaxInlineArgs(), @@ -26,7 +19,6 @@ def run_rules( context: RuleContext, rules: tuple[Rule, ...] | list[Rule] | None = None, ) -> list[BaseDefFormException]: - """Run all rules on the context and return combined issues.""" rule_list = rules if rules is not None else list(DEFAULT_RULES) issues: list[BaseDefFormException] = [] for rule in rule_list: diff --git a/tests/test_cli/test_ui.py b/tests/test_cli/test_ui.py index 2eabdc7..b5b90d1 100644 --- a/tests/test_cli/test_ui.py +++ b/tests/test_cli/test_ui.py @@ -199,7 +199,6 @@ def test_rich_ui_show_config_info_skips_none_and_empty_values() -> None: console.context = ctx ui = RichUI(console=console) ui.show_config_info(config_path='/x', max_def_length=None, indent_size='') - # config_path is shown; None and '' are skipped (cover lines 49-50) assert console.print.call_count >= 1 From 1b0131d74e3ce98523ee7562345e75631b20d2b4 Mon Sep 17 00:00:00 2001 From: Nikita Borzov <152521469+TopNik073@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:41:12 +0200 Subject: [PATCH 16/16] Update README --- README.md | 118 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 9624a4b..b44e2d3 100644 --- a/README.md +++ b/README.md @@ -51,38 +51,114 @@ def-form format my_module.py def-form check src/ ``` -### Command line options +### Example + +You can check your code with `check` command and see the result + +```text +(.venv) user@MacBook-Pro def-form % def-form check test_file.py +Checking test_file.py + + Configuration + ───────────────────────────────────────────────────────────────── + Config Path: /Users/user/Documents/def-form/pyproject.toml + Max Inline Args: 2 + Max Def Length: 100 + Indent Size: 4 spaces + Show Skipped: No + Excluded: .venv, tests/cases, build + ───────────────────────────────────────────────────────────────── + +Found 1 errors in 1 files + +/Users/user/Documents/def-form/test_file.py:19 + • Invalid multiline function parameters indentation (expected 4 spaces) + + Summary + ─────────────────────────── + Files processed: 1 + Files with issues: 1 + Total errors: 1 + Success rate: 0.0% + ─────────────────────────── + +Code style violations found +``` + +Or use `format` +```text +(.venv) user@MacBook-Pro def-form % def-form format test_file.py +Formatting test_file.py + + Configuration + ───────────────────────────────────────────────────────────────── + Config Path: /Users/user/Documents/def-form/pyproject.toml + Max Inline Args: 2 + Max Def Length: 100 + Indent Size: 4 spaces + Show Skipped: No + Excluded: build, .venv, tests/cases + ───────────────────────────────────────────────────────────────── + +Found 1 errors in 1 files + +/Users/user/Documents/def-form/test_file.py:19 + • Invalid multiline function parameters indentation (expected 4 spaces) + + Summary + ─────────────────────────── + Files processed: 1 + Files with issues: 1 + Total errors: 1 + Success rate: 0.0% + ─────────────────────────── + +Formatting completed +``` + +## Command line options + +There is global options ```text -def-form format [OPTIONS] [PATH] +Usage: def-form [OPTIONS] COMMAND [ARGS]... Options: - --max-def-length INTEGER Maximum length of function definition - --max-inline-args INTEGER Maximum number of inline arguments - --indent-size INTEGER indent size in spaces (default: 4) - --exclude TEXT Paths or files to exclude from checking/formatting - --show-skipped Show skipped files/directories - --config TEXT Path to pyproject.toml configuration file + --verbose Enable verbose output + --quiet Disable all output + --help Show this message and exit. + +Commands: + check + format ``` -### Configuration +And specific options for check/format + +```text +Usage: def-form format [OPTIONS] [PATH] + +Options: + --config FILE Path to pyproject.toml configuration file + --show-skipped Show skipped files and directories + --exclude PATH Paths to exclude from processing + --indent-size INTEGER Indent size in spaces (default: 4) + --max-inline-args INTEGER Maximum number of inline arguments + --max-def-length INTEGER Maximum length of function definition + --help Show this message and exit. +``` + +## Configuration Create a pyproject.toml file in your project root: ```toml [tool.def-form] -max_def_length = 100 -max_inline_args = 2 -indent_size = 4 -exclude = [ +max_def_length = 100 # Maximum allowed characters in a single-line function definition +max_inline_args = 2 # Maximum number of arguments allowed in inline format +indent_size = 4 # Indent for arguments in spaces +exclude = [ # Files or directories you want to exclude '.venv', 'migrations' ] -``` - -### Configuration options - -* max_def_length: Maximum allowed characters in a single-line function definition -* max_inline_args: Maximum number of arguments allowed in inline format -* indent_size: Indent for arguments in spaces -* exclude: Files or directories you want to exclude \ No newline at end of file +``` \ No newline at end of file