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
diff --git a/Justfile b/Justfile
index c6b9ffd..5351f45 100755
--- a/Justfile
+++ b/Justfile
@@ -1,6 +1,9 @@
SOURCE_PATH := "def_form"
TESTS_PATH := "tests"
+default:
+ @just --list
+
upgrade:
uv lock --upgrade
@@ -20,5 +23,6 @@ tests:
uv run pytest \
--cov=def_form \
--cov-report=lcov:tests.lcov \
+ --cov-report=term \
tests/
diff --git a/README.md b/README.md
index 67e46ee..b44e2d3 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,15 @@
-# def-form
+
+

-Python function definition formatter
+
Python function definition formatter
+
+ [](https://github.com/astral-sh/ruff)
+ [](https://pypi.python.org/pypi/def-form)
+ [](https://pypi.python.org/pypi/def-form)
+ [](https://coveralls.io/github/TopNik073/def-form?branch=init)
+
+
-[](https://github.com/astral-sh/ruff)
-[](https://pypi.python.org/pypi/def-form)
-[](https://pypi.python.org/pypi/def-form)
-[](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.
@@ -47,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
+```
+
+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
+## 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
diff --git a/assets/logo-transparent.png b/assets/logo-transparent.png
new file mode 100644
index 0000000..ee08cbd
Binary files /dev/null and b/assets/logo-transparent.png differ
diff --git a/assets/logo.png b/assets/logo.png
new file mode 100644
index 0000000..9628b52
Binary files /dev/null and b/assets/logo.png differ
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..ec7e399
--- /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 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.core 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 = RichConsole(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(console=console),
+ ).check()
+ 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')
diff --git a/def_form/cli/commands/format.py b/def_form/cli/commands/format.py
new file mode 100644
index 0000000..f592d37
--- /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 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.core 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 = RichConsole(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(console=console),
+ ).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..e9b75c3
--- /dev/null
+++ b/def_form/cli/commands/options.py
@@ -0,0 +1,46 @@
+from collections.abc import Callable
+
+import click
+
+
+def path_option(func: Callable) -> Callable:
+ return click.argument('path', type=click.Path(exists=True), default='.')(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: Callable) -> Callable:
+ return click.option('--max-inline-args', type=int, default=None, help='Maximum number of inline arguments')(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: Callable) -> Callable:
+ 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: Callable) -> Callable:
+ return click.option('--exclude', multiple=True, type=click.Path(), help='Paths to exclude from processing')(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: 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)
+ return config_option(func)
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/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..851b68a 100644
--- a/def_form/cli/main.py
+++ b/def_form/cli/main.py
@@ -1,16 +1,39 @@
import click
+import sys
+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
-from def_form.cli.cli import check
-from def_form.cli.cli import format
+console = RichConsole(context=context)
-@click.group(name='def-form')
-def main() -> None:
- click.help_option()
+@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
+
+
+cli.add_command(check)
+cli.add_command(format)
-main.add_command(format)
-main.add_command(check)
+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)
+
if __name__ == '__main__':
main()
diff --git a/def_form/cli/ui/__init__.py b/def_form/cli/ui/__init__.py
new file mode 100644
index 0000000..514ad55
--- /dev/null
+++ b/def_form/cli/ui/__init__.py
@@ -0,0 +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', 'NullUI', 'RichUI']
diff --git a/def_form/cli/ui/base.py b/def_form/cli/ui/base.py
new file mode 100644
index 0000000..d62ca11
--- /dev/null
+++ b/def_form/cli/ui/base.py
@@ -0,0 +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 NotImplementedError
+
+ @abstractmethod
+ def start(self, total: int | None) -> None:
+ raise NotImplementedError
+
+ @abstractmethod
+ def processing(self, path: Path) -> None:
+ raise NotImplementedError
+
+ @abstractmethod
+ def skipped(self, path: Path) -> None:
+ raise NotImplementedError
+
+ @abstractmethod
+ def finish(self, processed: int, issues: list[BaseDefFormException]) -> None:
+ raise NotImplementedError
+
+ @abstractmethod
+ def show_issues(self, processed: int, issues: list[BaseDefFormException]) -> None:
+ raise NotImplementedError
+
+ @abstractmethod
+ def show_summary(self, processed: int, issues: list[BaseDefFormException]) -> None:
+ 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
new file mode 100644
index 0000000..e920307
--- /dev/null
+++ b/def_form/cli/ui/rich.py
@@ -0,0 +1,214 @@
+# 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, TaskID
+from rich.table import Table
+from rich.text import Text
+
+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, 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
+
+ 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.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
+
+ if not (self.progress and self._live and self.task_id is not None):
+ return
+
+ self.current_file = path
+
+ file_text = Text(str(path), style='dim')
+ self._progress_display.renderables[0] = file_text
+
+ self.progress.update(
+ TaskID(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 = {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: dict[str, list[BaseDefFormException]] = defaultdict(list)
+ 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 = {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/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 72%
rename from def_form/formatters/def_formatter/checker.py
rename to def_form/core/checker.py
index 6f4a248..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):
@@ -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/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/core/manager.py b/def_form/core/manager.py
new file mode 100644
index 0000000..d68d37d
--- /dev/null
+++ b/def_form/core/manager.py
@@ -0,0 +1,259 @@
+import os
+from collections.abc import Generator
+from pathlib import Path
+
+import tomli
+import libcst as cst
+
+from def_form.cli.ui import BaseUI
+from def_form.exceptions.base import BaseDefFormException
+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
+
+
+class DefManager:
+ def __init__( # noqa: PLR0913
+ self,
+ path: str,
+ ui: BaseUI,
+ excluded: tuple[str, ...] | None = None,
+ formatter: type[DefFormatter] = DefFormatter,
+ checker: type[DefChecker] = DefChecker,
+ max_def_length: int | None = None,
+ max_inline_args: int | None = None,
+ indent_size: int | None = None,
+ config: str | None = None,
+ show_skipped: bool = False,
+ ) -> None:
+ self.config: str | None = config or find_pyproject_toml()
+ self.path = Path(path).resolve()
+ self.ui = ui
+
+ self.issues: list[BaseDefFormException] = []
+
+ self.formatter_class = formatter
+ self.checker_class = checker
+
+ self._init_config(
+ config=self.config,
+ max_def_length=max_def_length,
+ max_inline_args=max_inline_args,
+ indent_size=indent_size,
+ )
+
+ self._init_exclusions(excluded or ())
+
+ 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,
+ max_def_length: int | None,
+ max_inline_args: int | None,
+ indent_size: int | None,
+ ) -> None:
+ self.max_def_length = max_def_length
+ self.max_inline_args = max_inline_args
+ self.indent_size = indent_size
+
+ self._config_excluded: list[str] = []
+
+ if not config:
+ return
+
+ 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:
+ self.excluded.add(Path(p).resolve())
+ except Exception:
+ 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
+
+ # --------------------------------------------------------------------- #
+ # File iteration
+ # --------------------------------------------------------------------- #
+
+ def _iter_py_files(self) -> Generator[Path, None, None]:
+ if self.path.is_file():
+ if self.path.suffix != '.py':
+ return
+
+ if self._is_excluded(self.path):
+ self.ui.skipped(self.path)
+ return
+
+ 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)]
+
+ for filename in files:
+ if not filename.endswith('.py'):
+ continue
+
+ file_path = root_path / filename
+
+ if self._is_excluded(file_path):
+ self.ui.skipped(file_path)
+ continue
+
+ yield file_path
+
+ # --------------------------------------------------------------------- #
+ # Processing
+ # --------------------------------------------------------------------- #
+
+ def _create_processor(
+ self,
+ processor_class: type[DefFormatter] | type[DefChecker],
+ filepath: str,
+ ) -> DefFormatter | DefChecker:
+ return processor_class(
+ filepath=filepath,
+ max_def_length=self.max_def_length,
+ max_inline_args=self.max_inline_args,
+ indent_size=self.indent_size,
+ )
+
+ def _process_file(
+ self,
+ filepath: Path,
+ processor_class: type[DefFormatter] | type[DefChecker],
+ ) -> tuple[cst.Module | None, list[BaseDefFormException]]:
+ try:
+ 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, 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:
+ return None, []
+ 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
+ # --------------------------------------------------------------------- #
+
+ def format(self) -> None:
+ self.issues.clear()
+ files = list(self._iter_py_files())
+
+ self.ui.start(total=len(files))
+
+ for path in files:
+ 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:
+ self._write(
+ dest=path,
+ module=new_tree.code,
+ )
+
+ self.ui.finish(len(files), self.issues)
+
+ def check(self) -> None:
+ self.issues.clear()
+ files = list(self._iter_py_files())
+
+ self.ui.start(total=len(files))
+
+ for path in files:
+ self.ui.processing(path)
+
+ _, file_issues = self._process_file(
+ path,
+ self.checker_class,
+ )
+
+ self.issues.extend(file_issues)
+
+ self.ui.finish(len(files), self.issues)
+
+ if self.issues:
+ 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..2491017
--- /dev/null
+++ b/def_form/core/rules/__init__.py
@@ -0,0 +1,36 @@
+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_RULES: tuple[Rule, ...] = (
+ RuleMaxDefLength(),
+ RuleMaxInlineArgs(),
+ RuleMultilineParamsIndent(),
+)
+
+
+def run_rules(
+ context: RuleContext,
+ rules: tuple[Rule, ...] | list[Rule] | None = None,
+) -> list[BaseDefFormException]:
+ 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..1d20423 100644
--- a/def_form/exceptions/def_formatter.py
+++ b/def_form/exceptions/def_formatter.py
@@ -29,3 +29,10 @@ 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
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))
diff --git a/def_form/formatters/def_formatter/manager.py b/def_form/formatters/def_formatter/manager.py
deleted file mode 100644
index fa6bb3e..0000000
--- a/def_form/formatters/def_formatter/manager.py
+++ /dev/null
@@ -1,232 +0,0 @@
-import os
-import tomli
-from collections.abc import Generator
-from pathlib import Path
-
-import click
-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
-
-
-class DefManager:
- def __init__( # noqa: PLR0913
- self,
- path: str,
- excluded: tuple[str, ...] | None = None,
- formatter: type[DefFormatter] = DefFormatter,
- checker: type[DefChecker] = DefChecker,
- max_def_length: int | None = None,
- max_inline_args: int | None = None,
- indent_size: int | None = None,
- config: str | None = None,
- show_skipped: bool = False,
- ):
- self.path = Path(path).resolve()
- self.show_skipped = show_skipped
- self.issues: list[BaseDefFormException] = []
-
- self._init_config(
- config=config,
- max_def_length=max_def_length,
- max_inline_args=max_inline_args,
- indent_size=indent_size,
- )
-
- self._init_exclusions(excluded or ())
-
- self.formatter_class = formatter
- self.checker_class = checker
-
- def _init_config(
- self,
- config: str | None,
- max_def_length: int | None,
- max_inline_args: int | None,
- indent_size: int | None,
- ) -> None:
- self.max_def_length = max_def_length
- 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,
- )
-
- 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
-
- self._config_excluded = config_excluded
-
- 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)
- except Exception:
- click.secho(f'Warning: invalid excluded path: {p}', fg='yellow')
-
- def _iter_py_files(self) -> Generator[str, None, None]:
- if self.path.is_file():
- if self.path.suffix != '.py':
- return
-
- if self._is_excluded(self.path):
- if self.show_skipped:
- click.secho(f'SKIPPED {self.path}', fg='yellow')
- return
-
- click.secho(f'Processing: {self.path}', fg='green')
- yield str(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)]
-
- for filename in files:
- 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')
- 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
-
- return False
-
- def _create_processor(
- self, processor_class: type[DefFormatter] | type[DefChecker], filepath: str
- ) -> DefFormatter | DefChecker:
- return processor_class(
- filepath=filepath,
- max_def_length=self.max_def_length,
- max_inline_args=self.max_inline_args,
- indent_size=self.indent_size,
- )
-
- def _process_file(
- self,
- filepath: str,
- 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')
- return None, []
-
- try:
- tree = cst.parse_module(code)
- wrapper = cst.metadata.MetadataWrapper(tree)
- processor = self._create_processor(processor_class, 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')
- return None, []
- except Exception as e:
- click.secho(f'Unexpected error processing {filepath}: {e}', fg='red')
- return None, []
-
- def format(self, write_to: str | None = None) -> None:
- processed_count = 0
- self.issues.clear()
-
- for filepath in self._iter_py_files():
- processed_count += 1
- new_tree, file_issues = self._process_file(filepath, 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')
-
- self._echo_summary('format', processed_count)
-
- def check(self) -> None:
- processed_count = 0
- self.issues.clear()
-
- for filepath in self._iter_py_files():
- processed_count += 1
- _, file_issues = self._process_file(filepath, self.checker_class)
- self.issues.extend(file_issues)
-
- self._echo_summary('check', processed_count)
-
- if self.issues:
- raise BaseDefFormException
-
- def _echo_summary(self, mode: str, processed_count: int) -> None:
- click.echo('\n')
-
- 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)}')
diff --git a/pyproject.toml b/pyproject.toml
index 2b6e829..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"
@@ -33,6 +33,7 @@ classifiers = [
dependencies = [
"click>=8.2.1",
"libcst>=1.8.2",
+ "rich>=14.3.1",
"tomli>=2.4.0",
]
@@ -63,6 +64,7 @@ indent_size = 4
exclude = [
".venv/",
"build/",
+ "tests/cases/"
]
[tool.mypy]
@@ -78,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]
@@ -87,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/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_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..b5b90d1
--- /dev/null
+++ b/tests/test_cli/test_ui.py
@@ -0,0 +1,302 @@
+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='')
+ 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
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)
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()
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'
diff --git a/uv.lock b/uv.lock
index a96900f..3927bcc 100644
--- a/uv.lock
+++ b/uv.lock
@@ -134,11 +134,12 @@ toml = [
[[package]]
name = "def-form"
-version = "0.1.0"
+version = "0.2.0"
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"
@@ -384,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]]
@@ -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.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+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/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]]
name = "ruff"
version = "0.11.13"