From 784f7deef367b2ebe1d740a0d8b142b944ecd15a Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 00:19:03 +0500 Subject: [PATCH 01/44] WIP #3241 Add explain command --- poetry.lock | 2 +- pyproject.toml | 4 + tests/test_cli/test_explain.py | 63 ++++++++++++++++ wemake_python_styleguide/cli/__init__.py | 0 wemake_python_styleguide/cli/application.py | 18 +++++ wemake_python_styleguide/cli/cli_app.py | 33 ++++++++ .../cli/commands/__init__.py | 0 wemake_python_styleguide/cli/commands/base.py | 12 +++ .../cli/commands/explain/__init__.py | 0 .../cli/commands/explain/command.py | 22 ++++++ .../cli/commands/explain/message_formatter.py | 59 +++++++++++++++ .../cli/commands/explain/violation_loader.py | 75 +++++++++++++++++++ wemake_python_styleguide/cli/output.py | 41 ++++++++++ 13 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli/test_explain.py create mode 100644 wemake_python_styleguide/cli/__init__.py create mode 100644 wemake_python_styleguide/cli/application.py create mode 100644 wemake_python_styleguide/cli/cli_app.py create mode 100644 wemake_python_styleguide/cli/commands/__init__.py create mode 100644 wemake_python_styleguide/cli/commands/base.py create mode 100644 wemake_python_styleguide/cli/commands/explain/__init__.py create mode 100644 wemake_python_styleguide/cli/commands/explain/command.py create mode 100644 wemake_python_styleguide/cli/commands/explain/message_formatter.py create mode 100644 wemake_python_styleguide/cli/commands/explain/violation_loader.py create mode 100644 wemake_python_styleguide/cli/output.py diff --git a/poetry.lock b/poetry.lock index 0a113012e..21b11f2be 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1982,4 +1982,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "56601f1c4d4b37a8459ee90cb7898893d34d19ed7ed229a3689fdd9427469f3c" +content-hash = "fd92c3299a8884e3fb9086a89b360d69edde3de81527b65fadb7839497f5a535" diff --git a/pyproject.toml b/pyproject.toml index 7676d1477..b0f3073a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ python = "^3.10" flake8 = "^7.1" attrs = "*" pygments = "^2.4" +docutils = "^0.21.2" [tool.poetry.group.dev.dependencies] pytest = "^8.1" @@ -266,3 +267,6 @@ disallow_any_explicit = false module = "wemake_python_styleguide.compat.packaging" # We allow unused `ignore` comments, because we cannot sync it between versions: warn_unused_ignores = false + +[tool.poetry.scripts] +wps = "wemake_python_styleguide.cli.cli_app:main" diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py new file mode 100644 index 000000000..2c1dd29fd --- /dev/null +++ b/tests/test_cli/test_explain.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass + +import pytest + +from wemake_python_styleguide.cli.commands.explain import violation_loader, message_formatter +from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo +from wemake_python_styleguide.violations.best_practices import InitModuleHasLogicViolation +from wemake_python_styleguide.violations.naming import UpperCaseAttributeViolation +from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation + + +@pytest.mark.parametrize( + 'violation_params', + [ + (115, UpperCaseAttributeViolation), + (412, InitModuleHasLogicViolation), + (600, BuiltinSubclassViolation), + ] +) +def test_violation_getter(violation_params): + violation_code, expected_class = violation_params + violation = violation_loader.get_violation(violation_code) + assert violation.code is not None + assert violation.docstring == expected_class.__doc__ + + +@pytest.mark.parametrize( + 'test_params', + [ + ( + ' text\n text\n text', + 'text\ntext\ntext' + ), + ( + ' text\n\ttext\r\n text', + 'text\n text\ntext' + ), + ] +) +def test_indentation_removal(test_params): + input_text, expected = test_params + actual = message_formatter._remove_indentation(input_text) + assert actual == expected + + +violation = ViolationInfo( + identifier='Mock', + code=100, + docstring='docstring', + fully_qualified_id='mock.Mock', + section='mock', +) +violation_string = ( + 'WPS100 (Mock)\n' + 'docstring\n' + 'See at website: https://wemake-python-styleguide.readthedocs.io/en/' + 'latest/pages/usage/violations/mock.html#mock.Mock' +) + + +def test_formatter(): + formatted = message_formatter.format_violation(violation) + assert formatted == violation_string diff --git a/wemake_python_styleguide/cli/__init__.py b/wemake_python_styleguide/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py new file mode 100644 index 000000000..eba89e567 --- /dev/null +++ b/wemake_python_styleguide/cli/application.py @@ -0,0 +1,18 @@ +from abc import abstractmethod, ABC + +from wemake_python_styleguide.cli.commands.base import AbstractCommand +from wemake_python_styleguide.cli.commands.explain.command import ( + ExplainCommand +) +from wemake_python_styleguide.cli.output import Writable + + +class Application: + def __init__(self, writer: Writable): + self._writer = writer + + def run_explain(self, args) -> int: + return self._get_command(ExplainCommand).run(args) + + def _get_command(self, command_class: type) -> AbstractCommand: + return command_class(writer=self._writer) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py new file mode 100644 index 000000000..48188afeb --- /dev/null +++ b/wemake_python_styleguide/cli/cli_app.py @@ -0,0 +1,33 @@ +import argparse +import sys + +from wemake_python_styleguide.cli.application import Application +from wemake_python_styleguide.cli.output import BufferedStreamWriter + + +def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: + """Configures CLI arguments and subcommands""" + parser = argparse.ArgumentParser( + prog="wps", + description="WPS command line tool" + ) + sub_parsers = parser.add_subparsers(help="sub-command help") + + parser_explain = sub_parsers.add_parser("explain", help="Get violation description") + parser_explain.add_argument( + "violation_code", + help="Desired violation code" + ) + parser_explain.set_defaults(func=app.run_explain) + + return parser + + +def main(): + app = Application(BufferedStreamWriter(sys.stdout, sys.stderr)) + args = _configure_arg_parser(app).parse_args() + return args.func(args) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/wemake_python_styleguide/cli/commands/__init__.py b/wemake_python_styleguide/cli/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py new file mode 100644 index 000000000..83ce867c4 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/base.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + +from wemake_python_styleguide.cli.output import Writable + + +class AbstractCommand(ABC): + def __init__(self, writer: Writable): + self.writer = writer + + @abstractmethod + def run(self, args) -> int: + ... diff --git a/wemake_python_styleguide/cli/commands/explain/__init__.py b/wemake_python_styleguide/cli/commands/explain/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py new file mode 100644 index 000000000..a7ea09809 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -0,0 +1,22 @@ +from wemake_python_styleguide.cli.commands.base import AbstractCommand +from wemake_python_styleguide.cli.commands.explain import ( + message_formatter, + violation_loader, +) + + +def _format_violation_code(violation_str: str) -> int: + if violation_str.startswith("WPS"): + violation_str = violation_str[3:] + return int(violation_str) + + +class ExplainCommand(AbstractCommand): + def run(self, args): + code = _format_violation_code(args.violation_code) + violation = violation_loader.get_violation(code) + if violation is None: + self.writer.write_err("Violation not found") + return 1 + message = message_formatter.format_violation(violation) + self.writer.write_out(message) diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py new file mode 100644 index 000000000..d609855d4 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -0,0 +1,59 @@ +from typing import Final + +from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo + +_DOCS_URL: Final = ( + 'https://wemake-python-styleguide.readthedocs.io/en/latest/pages/' + 'usage/violations/{0}.html#{1}' +) + + +def _clean_text(text: str) -> str: + return text.replace("\r\n", "\n").replace("\r", "\n") + + +def _replace_tabs(text: str, tab_size: int = 4) -> str: + return text.replace("\t", " " * tab_size) + + +def _get_whitespace_prefix(line: str) -> int: + for char_index, char in enumerate(line): + if char != ' ': + return char_index + return len(line) + + +def _get_greatest_common_indent(text: str) -> int: + lines = text.split("\n") + if len(lines) == 0: + return 0 + greatest_common_indent = float("+inf") + for line in lines: + if len(line.strip()) == 0: + continue + greatest_common_indent = min( + greatest_common_indent, + _get_whitespace_prefix(line) + ) + if greatest_common_indent == float("+inf"): + greatest_common_indent = 0 + return greatest_common_indent + + +def _remove_indentation(text: str, tab_size: int = 4) -> str: + text = _replace_tabs(_clean_text(text), tab_size) + max_indent = _get_greatest_common_indent(text) + return "\n".join(line[max_indent:] for line in text.split("\n")) + + +def format_violation(violation: ViolationInfo) -> str: + cleaned_docstring = _remove_indentation(violation.docstring) + violation_url = _DOCS_URL.format( + violation.section, + violation.fully_qualified_id, + ) + return ( + f"WPS{violation.code} ({violation.identifier})\n" + f"{cleaned_docstring}\n" + f"See at website: {violation_url}" + ) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py new file mode 100644 index 000000000..f6a253c51 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -0,0 +1,75 @@ +import importlib +import inspect +from collections.abc import Collection, Mapping +from dataclasses import dataclass +from types import ModuleType +from typing import Final + +_VIOLATION_SUBMODULES: Final = ( + 'best_practices', + 'complexity', + 'consistency', + 'naming', + 'oop', + 'refactoring', + 'system', +) +_VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' + + +@dataclass +class ViolationInfo: + identifier: str + fully_qualified_id: str + code: int + docstring: str + section: str + + +def _is_a_violation(class_object) -> bool: + return hasattr(class_object, 'code') + + +def _get_violations_of_submodule(module: ModuleType) -> Collection: + return [ + class_ + for name, class_ in inspect.getmembers(module, inspect.isclass) + if _is_a_violation(class_) + ] + + +def _create_violation_info( + class_object, + submodule_name: str, + submodule_path: str +) -> ViolationInfo: + return ViolationInfo( + identifier=class_object.__name__, + fully_qualified_id=f'{submodule_path}.{class_object.__name__}', + code=class_object.code, + docstring=class_object.__doc__, + section=submodule_name, + ) + + +def _get_all_violations() -> Mapping[int, ViolationInfo]: + all_violations = {} + for submodule_name in _VIOLATION_SUBMODULES: + submodule_path = f'{_VIOLATION_MODULE_BASE}.{submodule_name}' + violations = _get_violations_of_submodule( + importlib.import_module(submodule_path) + ) + for violation in violations: + all_violations[violation.code] = _create_violation_info( + violation, + submodule_name, + submodule_path, + ) + return all_violations + + +def get_violation(code: int) -> ViolationInfo | None: + violations = _get_all_violations() + if code not in violations: + return None + return violations[code] diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py new file mode 100644 index 000000000..22e40ef4d --- /dev/null +++ b/wemake_python_styleguide/cli/output.py @@ -0,0 +1,41 @@ +import sys +from abc import abstractmethod +from typing import Protocol, Unpack, TextIO, AnyStr + + +class Writable(Protocol): + @abstractmethod + def write_out(self, *args: Unpack[AnyStr]) -> None: + ... + + @abstractmethod + def write_err(self, *args: Unpack[AnyStr]) -> None: + ... + + @abstractmethod + def flush(self) -> None: + ... + + +class BufferedStreamWriter(Writable): + def __init__( + self, + out_stream: TextIO, + err_stream: TextIO, + newline_sym: str = '\n' + ): + self._out = out_stream + self._err = err_stream + self._newline = newline_sym.encode() + + def write_out(self, *args: Unpack[AnyStr]) -> None: + self._out.buffer.write(' '.join(args).encode()) + self._out.buffer.write(self._newline) + + def write_err(self, *args: Unpack[AnyStr]) -> None: + self._err.buffer.write(' '.join(args).encode()) + self._err.buffer.write(self._newline) + + def flush(self) -> None: + self._out.flush() + self._err.flush() From 7ad0c7aa60e31a74f4428456e5d31440a08ee6c1 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 01:08:45 +0500 Subject: [PATCH 02/44] Tidy up so all checks could pass. Update CHANGELOG.md --- CHANGELOG.md | 5 +++ tests/test_cli/test_explain.py | 25 ++++++++---- wemake_python_styleguide/cli/__init__.py | 1 + wemake_python_styleguide/cli/application.py | 16 +++++--- wemake_python_styleguide/cli/cli_app.py | 24 +++++++----- .../cli/commands/__init__.py | 1 + wemake_python_styleguide/cli/commands/base.py | 8 +++- .../cli/commands/explain/__init__.py | 1 + .../cli/commands/explain/command.py | 31 ++++++++++++--- .../cli/commands/explain/message_formatter.py | 38 ++++++++++++++----- .../cli/commands/explain/violation_loader.py | 36 ++++++++++++++---- wemake_python_styleguide/cli/output.py | 27 ++++++++----- 12 files changed, 157 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e62f515de..8e670fff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ Semantic versioning in our case means: change the client facing API, change code conventions significantly, etc. +## 1.1.0 WIP +### Command line utility +This version introduces `wps` CLI tool. +- `wps explain ` command can be used to access WPS violation docs (same as on website) without internet access + ## 1.0.0 ### Ruff diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 2c1dd29fd..aaa44eb0e 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,11 +1,17 @@ -from dataclasses import dataclass +"""Test that wps explain command works fine.""" import pytest -from wemake_python_styleguide.cli.commands.explain import violation_loader, message_formatter -from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo -from wemake_python_styleguide.violations.best_practices import InitModuleHasLogicViolation -from wemake_python_styleguide.violations.naming import UpperCaseAttributeViolation +from wemake_python_styleguide.cli.commands.explain import ( + message_formatter, + violation_loader, +) +from wemake_python_styleguide.violations.best_practices import ( + InitModuleHasLogicViolation, +) +from wemake_python_styleguide.violations.naming import ( + UpperCaseAttributeViolation, +) from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation @@ -18,6 +24,7 @@ ] ) def test_violation_getter(violation_params): + """Test that violation loader can get violation by their codes.""" violation_code, expected_class = violation_params violation = violation_loader.get_violation(violation_code) assert violation.code is not None @@ -38,12 +45,13 @@ def test_violation_getter(violation_params): ] ) def test_indentation_removal(test_params): + """Test that indentation remover works in different conditions.""" input_text, expected = test_params - actual = message_formatter._remove_indentation(input_text) + actual = message_formatter._remove_indentation(input_text) # noqa: SLF001 assert actual == expected -violation = ViolationInfo( +violation_mock = violation_loader.ViolationInfo( identifier='Mock', code=100, docstring='docstring', @@ -59,5 +67,6 @@ def test_indentation_removal(test_params): def test_formatter(): - formatted = message_formatter.format_violation(violation) + """Test that formatter formats violations as expected.""" + formatted = message_formatter.format_violation(violation_mock) assert formatted == violation_string diff --git a/wemake_python_styleguide/cli/__init__.py b/wemake_python_styleguide/cli/__init__.py index e69de29bb..e08834af0 100644 --- a/wemake_python_styleguide/cli/__init__.py +++ b/wemake_python_styleguide/cli/__init__.py @@ -0,0 +1 @@ +"""Contains all files related to WPS CLI utility.""" diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index eba89e567..5a81b7524 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -1,18 +1,24 @@ -from abc import abstractmethod, ABC +"""Provides WPS CLI application class.""" from wemake_python_styleguide.cli.commands.base import AbstractCommand -from wemake_python_styleguide.cli.commands.explain.command import ( - ExplainCommand -) +from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand from wemake_python_styleguide.cli.output import Writable class Application: + """WPS CLI application class.""" + def __init__(self, writer: Writable): + """Create application.""" self._writer = writer def run_explain(self, args) -> int: + """Run explain command.""" return self._get_command(ExplainCommand).run(args) - def _get_command(self, command_class: type) -> AbstractCommand: + def _get_command( + self, + command_class: type[AbstractCommand], + ) -> AbstractCommand: + """Create command from its class and inject the selected writer.""" return command_class(writer=self._writer) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 48188afeb..01d438f6a 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -1,3 +1,5 @@ +"""Main CLI utility file.""" + import argparse import sys @@ -6,27 +8,31 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: - """Configures CLI arguments and subcommands""" + """Configures CLI arguments and subcommands.""" parser = argparse.ArgumentParser( - prog="wps", - description="WPS command line tool" + prog='wps', + description='WPS command line tool' ) - sub_parsers = parser.add_subparsers(help="sub-command help") + sub_parsers = parser.add_subparsers(help='sub-command help') - parser_explain = sub_parsers.add_parser("explain", help="Get violation description") + parser_explain = sub_parsers.add_parser( + 'explain', + help='Get violation description', + ) parser_explain.add_argument( - "violation_code", - help="Desired violation code" + 'violation_code', + help='Desired violation code', ) parser_explain.set_defaults(func=app.run_explain) return parser -def main(): +def main() -> int: + """Main function.""" app = Application(BufferedStreamWriter(sys.stdout, sys.stderr)) args = _configure_arg_parser(app).parse_args() - return args.func(args) + return int(args.func(args)) if __name__ == '__main__': diff --git a/wemake_python_styleguide/cli/commands/__init__.py b/wemake_python_styleguide/cli/commands/__init__.py index e69de29bb..e7f103bea 100644 --- a/wemake_python_styleguide/cli/commands/__init__.py +++ b/wemake_python_styleguide/cli/commands/__init__.py @@ -0,0 +1 @@ +"""Contains all files related to wps console commands.""" diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py index 83ce867c4..52d3fe454 100644 --- a/wemake_python_styleguide/cli/commands/base.py +++ b/wemake_python_styleguide/cli/commands/base.py @@ -1,12 +1,18 @@ +"""Contains files common for all wps commands.""" + from abc import ABC, abstractmethod from wemake_python_styleguide.cli.output import Writable class AbstractCommand(ABC): + """ABC for all commands.""" + def __init__(self, writer: Writable): + """Create a command and define its writer.""" self.writer = writer @abstractmethod def run(self, args) -> int: - ... + """Run the command.""" + raise NotImplementedError diff --git a/wemake_python_styleguide/cli/commands/explain/__init__.py b/wemake_python_styleguide/cli/commands/explain/__init__.py index e69de29bb..4774bf5fc 100644 --- a/wemake_python_styleguide/cli/commands/explain/__init__.py +++ b/wemake_python_styleguide/cli/commands/explain/__init__.py @@ -0,0 +1 @@ +"""Contains files related to wps explain command.""" diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index a7ea09809..5eb70b849 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -1,3 +1,5 @@ +"""Contains command implementation.""" + from wemake_python_styleguide.cli.commands.base import AbstractCommand from wemake_python_styleguide.cli.commands.explain import ( message_formatter, @@ -5,18 +7,35 @@ ) -def _format_violation_code(violation_str: str) -> int: - if violation_str.startswith("WPS"): - violation_str = violation_str[3:] +def _clean_violation_code(violation_str: str) -> int: + """ + Get int violation code from str violation code. + + Args: + violation_str: violation code expressed as string + WPS412, 412 - both acceptable + + Returns: + integer violation code + + Throws: + ValueError: violation str is not an integer (except WPS prefix). + """ + violation_str = violation_str.removeprefix('WPS') return int(violation_str) class ExplainCommand(AbstractCommand): - def run(self, args): - code = _format_violation_code(args.violation_code) + """Explain command impl.""" + + def run(self, args) -> int: + """Run command.""" + code = _clean_violation_code(args.violation_code) violation = violation_loader.get_violation(code) if violation is None: - self.writer.write_err("Violation not found") + self.writer.write_err('Violation not found') return 1 message = message_formatter.format_violation(violation) self.writer.write_out(message) + self.writer.flush() + return 0 diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index d609855d4..56f4c3405 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -1,6 +1,10 @@ +"""Provides tools for formatting explanations.""" + from typing import Final -from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo +from wemake_python_styleguide.cli.commands.explain.violation_loader import ( + ViolationInfo, +) _DOCS_URL: Final = ( 'https://wemake-python-styleguide.readthedocs.io/en/latest/pages/' @@ -9,14 +13,25 @@ def _clean_text(text: str) -> str: - return text.replace("\r\n", "\n").replace("\r", "\n") + """ + Cleans provided text. + + Args: + text: target text + + Returns: + text with normalized newlines (CRs and CRLFs transformed to LFs). + """ + return text.replace('\r\n', '\n').replace('\r', '\n') def _replace_tabs(text: str, tab_size: int = 4) -> str: - return text.replace("\t", " " * tab_size) + """Replace all tabs with defined amount of spaces.""" + return text.replace('\t', ' ' * tab_size) def _get_whitespace_prefix(line: str) -> int: + """Get length of whitespace prefix of string.""" for char_index, char in enumerate(line): if char != ' ': return char_index @@ -24,10 +39,11 @@ def _get_whitespace_prefix(line: str) -> int: def _get_greatest_common_indent(text: str) -> int: - lines = text.split("\n") + """Get the greatest common whitespace prefix length of all lines.""" + lines = text.split('\n') if len(lines) == 0: return 0 - greatest_common_indent = float("+inf") + greatest_common_indent = float('+inf') for line in lines: if len(line.strip()) == 0: continue @@ -35,25 +51,27 @@ def _get_greatest_common_indent(text: str) -> int: greatest_common_indent, _get_whitespace_prefix(line) ) - if greatest_common_indent == float("+inf"): + if isinstance(greatest_common_indent, float): greatest_common_indent = 0 return greatest_common_indent def _remove_indentation(text: str, tab_size: int = 4) -> str: + """Remove excessive indentation.""" text = _replace_tabs(_clean_text(text), tab_size) max_indent = _get_greatest_common_indent(text) - return "\n".join(line[max_indent:] for line in text.split("\n")) + return '\n'.join(line[max_indent:] for line in text.split('\n')) def format_violation(violation: ViolationInfo) -> str: + """Format violation information.""" cleaned_docstring = _remove_indentation(violation.docstring) violation_url = _DOCS_URL.format( violation.section, violation.fully_qualified_id, ) return ( - f"WPS{violation.code} ({violation.identifier})\n" - f"{cleaned_docstring}\n" - f"See at website: {violation_url}" + f'WPS{violation.code} ({violation.identifier})\n' + f'{cleaned_docstring}\n' + f'See at website: {violation_url}' ) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index f6a253c51..343161525 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -1,10 +1,13 @@ +"""Provides tools to extract violation info.""" + import importlib import inspect from collections.abc import Collection, Mapping -from dataclasses import dataclass from types import ModuleType from typing import Final +from wemake_python_styleguide.violations.base import BaseViolation + _VIOLATION_SUBMODULES: Final = ( 'best_practices', 'complexity', @@ -17,20 +20,34 @@ _VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' -@dataclass class ViolationInfo: - identifier: str - fully_qualified_id: str - code: int - docstring: str - section: str + """Contains violation info.""" + + def __init__( + self, + identifier: str, + fully_qualified_id: str, + code: int, + docstring: str, + section: str, + ): + """Create dataclass.""" + self.identifier = identifier + self.fully_qualified_id = fully_qualified_id + self.code = code + self.docstring = docstring + self.section = section def _is_a_violation(class_object) -> bool: + """Dumb check if class is a violation class.""" return hasattr(class_object, 'code') -def _get_violations_of_submodule(module: ModuleType) -> Collection: +def _get_violations_of_submodule( + module: ModuleType +) -> Collection[type[BaseViolation]]: + """Get all violation classes of defined module.""" return [ class_ for name, class_ in inspect.getmembers(module, inspect.isclass) @@ -43,6 +60,7 @@ def _create_violation_info( submodule_name: str, submodule_path: str ) -> ViolationInfo: + """Create violation info DTO from violation class and metadata.""" return ViolationInfo( identifier=class_object.__name__, fully_qualified_id=f'{submodule_path}.{class_object.__name__}', @@ -53,6 +71,7 @@ def _create_violation_info( def _get_all_violations() -> Mapping[int, ViolationInfo]: + """Get all violations inside all defined WPS violation modules.""" all_violations = {} for submodule_name in _VIOLATION_SUBMODULES: submodule_path = f'{_VIOLATION_MODULE_BASE}.{submodule_name}' @@ -69,6 +88,7 @@ def _get_all_violations() -> Mapping[int, ViolationInfo]: def get_violation(code: int) -> ViolationInfo | None: + """Get a violation by its integer code.""" violations = _get_all_violations() if code not in violations: return None diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py index 22e40ef4d..5a66a66b1 100644 --- a/wemake_python_styleguide/cli/output.py +++ b/wemake_python_styleguide/cli/output.py @@ -1,41 +1,50 @@ -import sys +"""Provides tool for outputting data.""" + from abc import abstractmethod -from typing import Protocol, Unpack, TextIO, AnyStr +from typing import Protocol, TextIO class Writable(Protocol): + """Interface for outputting text data.""" + @abstractmethod - def write_out(self, *args: Unpack[AnyStr]) -> None: - ... + def write_out(self, *args) -> None: + """Write usual text. Works as print.""" @abstractmethod - def write_err(self, *args: Unpack[AnyStr]) -> None: - ... + def write_err(self, *args) -> None: + """Write error text. Works as print.""" @abstractmethod def flush(self) -> None: - ... + """Flush all outputs.""" class BufferedStreamWriter(Writable): + """Writes to provided buffered text streams.""" + def __init__( self, out_stream: TextIO, err_stream: TextIO, newline_sym: str = '\n' ): + """Create stream writer.""" self._out = out_stream self._err = err_stream self._newline = newline_sym.encode() - def write_out(self, *args: Unpack[AnyStr]) -> None: + def write_out(self, *args) -> None: + """Write usual text. Works as print.""" self._out.buffer.write(' '.join(args).encode()) self._out.buffer.write(self._newline) - def write_err(self, *args: Unpack[AnyStr]) -> None: + def write_err(self, *args) -> None: + """Write error text. Works as print.""" self._err.buffer.write(' '.join(args).encode()) self._err.buffer.write(self._newline) def flush(self) -> None: + """Flush all outputs.""" self._out.flush() self._err.flush() From 7bff6aa63d6416c74e57d97c159265e204952a0a Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 01:23:29 +0500 Subject: [PATCH 03/44] Update docs --- docs/index.rst | 1 + docs/pages/usage/cli.rst | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 docs/pages/usage/cli.rst diff --git a/docs/index.rst b/docs/index.rst index 88f24121b..f52f705a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ pages/usage/configuration.rst pages/usage/violations/index.rst pages/usage/formatter.rst + pages/usage/cli.rst .. toctree:: diff --git a/docs/pages/usage/cli.rst b/docs/pages/usage/cli.rst new file mode 100644 index 000000000..1bd84bb42 --- /dev/null +++ b/docs/pages/usage/cli.rst @@ -0,0 +1,30 @@ +Command line tool +================= + +WPS v1.1.0 introduces new feature: a command-line utility called ``wps``. + +.. rubric:: ``wps explain`` + +This command can be used to get description of violation. +It will be the same description that is located on the website. + +Syntax: ``wps explain `` + +Examples: + +.. code:: + $ wps explain WPS115 + WPS115 (UpperCaseAttributeViolation) + + WPS115 - Require ``snake_case`` for naming class attributes. + ... + +.. code:: + $ wps explain 116 + WPS116 (ConsecutiveUnderscoresInNameViolation) + + WPS116 - Forbid using more than one consecutive underscore in variable names. + + + +.. versionadded:: 1.1.0 From 8c469bf7a26ccfc179df21427e20adeea0241159 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 01:33:23 +0500 Subject: [PATCH 04/44] Remove unused docutils --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b0f3073a4..1c64ee8e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,6 @@ python = "^3.10" flake8 = "^7.1" attrs = "*" pygments = "^2.4" -docutils = "^0.21.2" [tool.poetry.group.dev.dependencies] pytest = "^8.1" From daf71cc02efc4884196fecc6ed3e3687b8e20359 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 00:19:03 +0500 Subject: [PATCH 05/44] WIP #3241 Add explain command --- poetry.lock | 2 +- pyproject.toml | 4 + tests/test_cli/test_explain.py | 63 ++++++++++++++++ wemake_python_styleguide/cli/__init__.py | 0 wemake_python_styleguide/cli/application.py | 18 +++++ wemake_python_styleguide/cli/cli_app.py | 33 ++++++++ .../cli/commands/__init__.py | 0 wemake_python_styleguide/cli/commands/base.py | 12 +++ .../cli/commands/explain/__init__.py | 0 .../cli/commands/explain/command.py | 22 ++++++ .../cli/commands/explain/message_formatter.py | 59 +++++++++++++++ .../cli/commands/explain/violation_loader.py | 75 +++++++++++++++++++ wemake_python_styleguide/cli/output.py | 41 ++++++++++ 13 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli/test_explain.py create mode 100644 wemake_python_styleguide/cli/__init__.py create mode 100644 wemake_python_styleguide/cli/application.py create mode 100644 wemake_python_styleguide/cli/cli_app.py create mode 100644 wemake_python_styleguide/cli/commands/__init__.py create mode 100644 wemake_python_styleguide/cli/commands/base.py create mode 100644 wemake_python_styleguide/cli/commands/explain/__init__.py create mode 100644 wemake_python_styleguide/cli/commands/explain/command.py create mode 100644 wemake_python_styleguide/cli/commands/explain/message_formatter.py create mode 100644 wemake_python_styleguide/cli/commands/explain/violation_loader.py create mode 100644 wemake_python_styleguide/cli/output.py diff --git a/poetry.lock b/poetry.lock index 46b14837b..e2745e28a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2053,4 +2053,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "f76763dcb7d3944b719d05cf384a38c0e840ee2574ca66d133a9b9a7eb6ad624" +content-hash = "fd92c3299a8884e3fb9086a89b360d69edde3de81527b65fadb7839497f5a535" diff --git a/pyproject.toml b/pyproject.toml index c7c80c88d..648fdf576 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ python = "^3.10" flake8 = "^7.1" attrs = "*" pygments = "^2.4" +docutils = "^0.21.2" [tool.poetry.group.dev.dependencies] pytest = "^8.1" @@ -272,3 +273,6 @@ disallow_any_explicit = false module = "wemake_python_styleguide.compat.packaging" # We allow unused `ignore` comments, because we cannot sync it between versions: warn_unused_ignores = false + +[tool.poetry.scripts] +wps = "wemake_python_styleguide.cli.cli_app:main" diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py new file mode 100644 index 000000000..2c1dd29fd --- /dev/null +++ b/tests/test_cli/test_explain.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass + +import pytest + +from wemake_python_styleguide.cli.commands.explain import violation_loader, message_formatter +from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo +from wemake_python_styleguide.violations.best_practices import InitModuleHasLogicViolation +from wemake_python_styleguide.violations.naming import UpperCaseAttributeViolation +from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation + + +@pytest.mark.parametrize( + 'violation_params', + [ + (115, UpperCaseAttributeViolation), + (412, InitModuleHasLogicViolation), + (600, BuiltinSubclassViolation), + ] +) +def test_violation_getter(violation_params): + violation_code, expected_class = violation_params + violation = violation_loader.get_violation(violation_code) + assert violation.code is not None + assert violation.docstring == expected_class.__doc__ + + +@pytest.mark.parametrize( + 'test_params', + [ + ( + ' text\n text\n text', + 'text\ntext\ntext' + ), + ( + ' text\n\ttext\r\n text', + 'text\n text\ntext' + ), + ] +) +def test_indentation_removal(test_params): + input_text, expected = test_params + actual = message_formatter._remove_indentation(input_text) + assert actual == expected + + +violation = ViolationInfo( + identifier='Mock', + code=100, + docstring='docstring', + fully_qualified_id='mock.Mock', + section='mock', +) +violation_string = ( + 'WPS100 (Mock)\n' + 'docstring\n' + 'See at website: https://wemake-python-styleguide.readthedocs.io/en/' + 'latest/pages/usage/violations/mock.html#mock.Mock' +) + + +def test_formatter(): + formatted = message_formatter.format_violation(violation) + assert formatted == violation_string diff --git a/wemake_python_styleguide/cli/__init__.py b/wemake_python_styleguide/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py new file mode 100644 index 000000000..eba89e567 --- /dev/null +++ b/wemake_python_styleguide/cli/application.py @@ -0,0 +1,18 @@ +from abc import abstractmethod, ABC + +from wemake_python_styleguide.cli.commands.base import AbstractCommand +from wemake_python_styleguide.cli.commands.explain.command import ( + ExplainCommand +) +from wemake_python_styleguide.cli.output import Writable + + +class Application: + def __init__(self, writer: Writable): + self._writer = writer + + def run_explain(self, args) -> int: + return self._get_command(ExplainCommand).run(args) + + def _get_command(self, command_class: type) -> AbstractCommand: + return command_class(writer=self._writer) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py new file mode 100644 index 000000000..48188afeb --- /dev/null +++ b/wemake_python_styleguide/cli/cli_app.py @@ -0,0 +1,33 @@ +import argparse +import sys + +from wemake_python_styleguide.cli.application import Application +from wemake_python_styleguide.cli.output import BufferedStreamWriter + + +def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: + """Configures CLI arguments and subcommands""" + parser = argparse.ArgumentParser( + prog="wps", + description="WPS command line tool" + ) + sub_parsers = parser.add_subparsers(help="sub-command help") + + parser_explain = sub_parsers.add_parser("explain", help="Get violation description") + parser_explain.add_argument( + "violation_code", + help="Desired violation code" + ) + parser_explain.set_defaults(func=app.run_explain) + + return parser + + +def main(): + app = Application(BufferedStreamWriter(sys.stdout, sys.stderr)) + args = _configure_arg_parser(app).parse_args() + return args.func(args) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/wemake_python_styleguide/cli/commands/__init__.py b/wemake_python_styleguide/cli/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py new file mode 100644 index 000000000..83ce867c4 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/base.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + +from wemake_python_styleguide.cli.output import Writable + + +class AbstractCommand(ABC): + def __init__(self, writer: Writable): + self.writer = writer + + @abstractmethod + def run(self, args) -> int: + ... diff --git a/wemake_python_styleguide/cli/commands/explain/__init__.py b/wemake_python_styleguide/cli/commands/explain/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py new file mode 100644 index 000000000..a7ea09809 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -0,0 +1,22 @@ +from wemake_python_styleguide.cli.commands.base import AbstractCommand +from wemake_python_styleguide.cli.commands.explain import ( + message_formatter, + violation_loader, +) + + +def _format_violation_code(violation_str: str) -> int: + if violation_str.startswith("WPS"): + violation_str = violation_str[3:] + return int(violation_str) + + +class ExplainCommand(AbstractCommand): + def run(self, args): + code = _format_violation_code(args.violation_code) + violation = violation_loader.get_violation(code) + if violation is None: + self.writer.write_err("Violation not found") + return 1 + message = message_formatter.format_violation(violation) + self.writer.write_out(message) diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py new file mode 100644 index 000000000..d609855d4 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -0,0 +1,59 @@ +from typing import Final + +from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo + +_DOCS_URL: Final = ( + 'https://wemake-python-styleguide.readthedocs.io/en/latest/pages/' + 'usage/violations/{0}.html#{1}' +) + + +def _clean_text(text: str) -> str: + return text.replace("\r\n", "\n").replace("\r", "\n") + + +def _replace_tabs(text: str, tab_size: int = 4) -> str: + return text.replace("\t", " " * tab_size) + + +def _get_whitespace_prefix(line: str) -> int: + for char_index, char in enumerate(line): + if char != ' ': + return char_index + return len(line) + + +def _get_greatest_common_indent(text: str) -> int: + lines = text.split("\n") + if len(lines) == 0: + return 0 + greatest_common_indent = float("+inf") + for line in lines: + if len(line.strip()) == 0: + continue + greatest_common_indent = min( + greatest_common_indent, + _get_whitespace_prefix(line) + ) + if greatest_common_indent == float("+inf"): + greatest_common_indent = 0 + return greatest_common_indent + + +def _remove_indentation(text: str, tab_size: int = 4) -> str: + text = _replace_tabs(_clean_text(text), tab_size) + max_indent = _get_greatest_common_indent(text) + return "\n".join(line[max_indent:] for line in text.split("\n")) + + +def format_violation(violation: ViolationInfo) -> str: + cleaned_docstring = _remove_indentation(violation.docstring) + violation_url = _DOCS_URL.format( + violation.section, + violation.fully_qualified_id, + ) + return ( + f"WPS{violation.code} ({violation.identifier})\n" + f"{cleaned_docstring}\n" + f"See at website: {violation_url}" + ) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py new file mode 100644 index 000000000..f6a253c51 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -0,0 +1,75 @@ +import importlib +import inspect +from collections.abc import Collection, Mapping +from dataclasses import dataclass +from types import ModuleType +from typing import Final + +_VIOLATION_SUBMODULES: Final = ( + 'best_practices', + 'complexity', + 'consistency', + 'naming', + 'oop', + 'refactoring', + 'system', +) +_VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' + + +@dataclass +class ViolationInfo: + identifier: str + fully_qualified_id: str + code: int + docstring: str + section: str + + +def _is_a_violation(class_object) -> bool: + return hasattr(class_object, 'code') + + +def _get_violations_of_submodule(module: ModuleType) -> Collection: + return [ + class_ + for name, class_ in inspect.getmembers(module, inspect.isclass) + if _is_a_violation(class_) + ] + + +def _create_violation_info( + class_object, + submodule_name: str, + submodule_path: str +) -> ViolationInfo: + return ViolationInfo( + identifier=class_object.__name__, + fully_qualified_id=f'{submodule_path}.{class_object.__name__}', + code=class_object.code, + docstring=class_object.__doc__, + section=submodule_name, + ) + + +def _get_all_violations() -> Mapping[int, ViolationInfo]: + all_violations = {} + for submodule_name in _VIOLATION_SUBMODULES: + submodule_path = f'{_VIOLATION_MODULE_BASE}.{submodule_name}' + violations = _get_violations_of_submodule( + importlib.import_module(submodule_path) + ) + for violation in violations: + all_violations[violation.code] = _create_violation_info( + violation, + submodule_name, + submodule_path, + ) + return all_violations + + +def get_violation(code: int) -> ViolationInfo | None: + violations = _get_all_violations() + if code not in violations: + return None + return violations[code] diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py new file mode 100644 index 000000000..22e40ef4d --- /dev/null +++ b/wemake_python_styleguide/cli/output.py @@ -0,0 +1,41 @@ +import sys +from abc import abstractmethod +from typing import Protocol, Unpack, TextIO, AnyStr + + +class Writable(Protocol): + @abstractmethod + def write_out(self, *args: Unpack[AnyStr]) -> None: + ... + + @abstractmethod + def write_err(self, *args: Unpack[AnyStr]) -> None: + ... + + @abstractmethod + def flush(self) -> None: + ... + + +class BufferedStreamWriter(Writable): + def __init__( + self, + out_stream: TextIO, + err_stream: TextIO, + newline_sym: str = '\n' + ): + self._out = out_stream + self._err = err_stream + self._newline = newline_sym.encode() + + def write_out(self, *args: Unpack[AnyStr]) -> None: + self._out.buffer.write(' '.join(args).encode()) + self._out.buffer.write(self._newline) + + def write_err(self, *args: Unpack[AnyStr]) -> None: + self._err.buffer.write(' '.join(args).encode()) + self._err.buffer.write(self._newline) + + def flush(self) -> None: + self._out.flush() + self._err.flush() From b359f0ece0783f3708609fde7fd01230e9d1f4a8 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 01:08:45 +0500 Subject: [PATCH 06/44] Tidy up so all checks could pass. Update CHANGELOG.md --- CHANGELOG.md | 6 +++ tests/test_cli/test_explain.py | 25 ++++++++---- wemake_python_styleguide/cli/__init__.py | 1 + wemake_python_styleguide/cli/application.py | 16 +++++--- wemake_python_styleguide/cli/cli_app.py | 24 +++++++----- .../cli/commands/__init__.py | 1 + wemake_python_styleguide/cli/commands/base.py | 8 +++- .../cli/commands/explain/__init__.py | 1 + .../cli/commands/explain/command.py | 31 ++++++++++++--- .../cli/commands/explain/message_formatter.py | 38 ++++++++++++++----- .../cli/commands/explain/violation_loader.py | 36 ++++++++++++++---- wemake_python_styleguide/cli/output.py | 27 ++++++++----- 12 files changed, 158 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b81c97079..aee32f0a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ Semantic versioning in our case means: change the client facing API, change code conventions significantly, etc. +## 1.1.0 WIP +### Command line utility +This version introduces `wps` CLI tool. +- `wps explain ` command can be used to access WPS violation docs (same as on website) without internet access + + ## 1.0.1 WIP ### Bugfixes diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 2c1dd29fd..aaa44eb0e 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,11 +1,17 @@ -from dataclasses import dataclass +"""Test that wps explain command works fine.""" import pytest -from wemake_python_styleguide.cli.commands.explain import violation_loader, message_formatter -from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo -from wemake_python_styleguide.violations.best_practices import InitModuleHasLogicViolation -from wemake_python_styleguide.violations.naming import UpperCaseAttributeViolation +from wemake_python_styleguide.cli.commands.explain import ( + message_formatter, + violation_loader, +) +from wemake_python_styleguide.violations.best_practices import ( + InitModuleHasLogicViolation, +) +from wemake_python_styleguide.violations.naming import ( + UpperCaseAttributeViolation, +) from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation @@ -18,6 +24,7 @@ ] ) def test_violation_getter(violation_params): + """Test that violation loader can get violation by their codes.""" violation_code, expected_class = violation_params violation = violation_loader.get_violation(violation_code) assert violation.code is not None @@ -38,12 +45,13 @@ def test_violation_getter(violation_params): ] ) def test_indentation_removal(test_params): + """Test that indentation remover works in different conditions.""" input_text, expected = test_params - actual = message_formatter._remove_indentation(input_text) + actual = message_formatter._remove_indentation(input_text) # noqa: SLF001 assert actual == expected -violation = ViolationInfo( +violation_mock = violation_loader.ViolationInfo( identifier='Mock', code=100, docstring='docstring', @@ -59,5 +67,6 @@ def test_indentation_removal(test_params): def test_formatter(): - formatted = message_formatter.format_violation(violation) + """Test that formatter formats violations as expected.""" + formatted = message_formatter.format_violation(violation_mock) assert formatted == violation_string diff --git a/wemake_python_styleguide/cli/__init__.py b/wemake_python_styleguide/cli/__init__.py index e69de29bb..e08834af0 100644 --- a/wemake_python_styleguide/cli/__init__.py +++ b/wemake_python_styleguide/cli/__init__.py @@ -0,0 +1 @@ +"""Contains all files related to WPS CLI utility.""" diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index eba89e567..5a81b7524 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -1,18 +1,24 @@ -from abc import abstractmethod, ABC +"""Provides WPS CLI application class.""" from wemake_python_styleguide.cli.commands.base import AbstractCommand -from wemake_python_styleguide.cli.commands.explain.command import ( - ExplainCommand -) +from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand from wemake_python_styleguide.cli.output import Writable class Application: + """WPS CLI application class.""" + def __init__(self, writer: Writable): + """Create application.""" self._writer = writer def run_explain(self, args) -> int: + """Run explain command.""" return self._get_command(ExplainCommand).run(args) - def _get_command(self, command_class: type) -> AbstractCommand: + def _get_command( + self, + command_class: type[AbstractCommand], + ) -> AbstractCommand: + """Create command from its class and inject the selected writer.""" return command_class(writer=self._writer) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 48188afeb..01d438f6a 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -1,3 +1,5 @@ +"""Main CLI utility file.""" + import argparse import sys @@ -6,27 +8,31 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: - """Configures CLI arguments and subcommands""" + """Configures CLI arguments and subcommands.""" parser = argparse.ArgumentParser( - prog="wps", - description="WPS command line tool" + prog='wps', + description='WPS command line tool' ) - sub_parsers = parser.add_subparsers(help="sub-command help") + sub_parsers = parser.add_subparsers(help='sub-command help') - parser_explain = sub_parsers.add_parser("explain", help="Get violation description") + parser_explain = sub_parsers.add_parser( + 'explain', + help='Get violation description', + ) parser_explain.add_argument( - "violation_code", - help="Desired violation code" + 'violation_code', + help='Desired violation code', ) parser_explain.set_defaults(func=app.run_explain) return parser -def main(): +def main() -> int: + """Main function.""" app = Application(BufferedStreamWriter(sys.stdout, sys.stderr)) args = _configure_arg_parser(app).parse_args() - return args.func(args) + return int(args.func(args)) if __name__ == '__main__': diff --git a/wemake_python_styleguide/cli/commands/__init__.py b/wemake_python_styleguide/cli/commands/__init__.py index e69de29bb..e7f103bea 100644 --- a/wemake_python_styleguide/cli/commands/__init__.py +++ b/wemake_python_styleguide/cli/commands/__init__.py @@ -0,0 +1 @@ +"""Contains all files related to wps console commands.""" diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py index 83ce867c4..52d3fe454 100644 --- a/wemake_python_styleguide/cli/commands/base.py +++ b/wemake_python_styleguide/cli/commands/base.py @@ -1,12 +1,18 @@ +"""Contains files common for all wps commands.""" + from abc import ABC, abstractmethod from wemake_python_styleguide.cli.output import Writable class AbstractCommand(ABC): + """ABC for all commands.""" + def __init__(self, writer: Writable): + """Create a command and define its writer.""" self.writer = writer @abstractmethod def run(self, args) -> int: - ... + """Run the command.""" + raise NotImplementedError diff --git a/wemake_python_styleguide/cli/commands/explain/__init__.py b/wemake_python_styleguide/cli/commands/explain/__init__.py index e69de29bb..4774bf5fc 100644 --- a/wemake_python_styleguide/cli/commands/explain/__init__.py +++ b/wemake_python_styleguide/cli/commands/explain/__init__.py @@ -0,0 +1 @@ +"""Contains files related to wps explain command.""" diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index a7ea09809..5eb70b849 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -1,3 +1,5 @@ +"""Contains command implementation.""" + from wemake_python_styleguide.cli.commands.base import AbstractCommand from wemake_python_styleguide.cli.commands.explain import ( message_formatter, @@ -5,18 +7,35 @@ ) -def _format_violation_code(violation_str: str) -> int: - if violation_str.startswith("WPS"): - violation_str = violation_str[3:] +def _clean_violation_code(violation_str: str) -> int: + """ + Get int violation code from str violation code. + + Args: + violation_str: violation code expressed as string + WPS412, 412 - both acceptable + + Returns: + integer violation code + + Throws: + ValueError: violation str is not an integer (except WPS prefix). + """ + violation_str = violation_str.removeprefix('WPS') return int(violation_str) class ExplainCommand(AbstractCommand): - def run(self, args): - code = _format_violation_code(args.violation_code) + """Explain command impl.""" + + def run(self, args) -> int: + """Run command.""" + code = _clean_violation_code(args.violation_code) violation = violation_loader.get_violation(code) if violation is None: - self.writer.write_err("Violation not found") + self.writer.write_err('Violation not found') return 1 message = message_formatter.format_violation(violation) self.writer.write_out(message) + self.writer.flush() + return 0 diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index d609855d4..56f4c3405 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -1,6 +1,10 @@ +"""Provides tools for formatting explanations.""" + from typing import Final -from wemake_python_styleguide.cli.commands.explain.violation_loader import ViolationInfo +from wemake_python_styleguide.cli.commands.explain.violation_loader import ( + ViolationInfo, +) _DOCS_URL: Final = ( 'https://wemake-python-styleguide.readthedocs.io/en/latest/pages/' @@ -9,14 +13,25 @@ def _clean_text(text: str) -> str: - return text.replace("\r\n", "\n").replace("\r", "\n") + """ + Cleans provided text. + + Args: + text: target text + + Returns: + text with normalized newlines (CRs and CRLFs transformed to LFs). + """ + return text.replace('\r\n', '\n').replace('\r', '\n') def _replace_tabs(text: str, tab_size: int = 4) -> str: - return text.replace("\t", " " * tab_size) + """Replace all tabs with defined amount of spaces.""" + return text.replace('\t', ' ' * tab_size) def _get_whitespace_prefix(line: str) -> int: + """Get length of whitespace prefix of string.""" for char_index, char in enumerate(line): if char != ' ': return char_index @@ -24,10 +39,11 @@ def _get_whitespace_prefix(line: str) -> int: def _get_greatest_common_indent(text: str) -> int: - lines = text.split("\n") + """Get the greatest common whitespace prefix length of all lines.""" + lines = text.split('\n') if len(lines) == 0: return 0 - greatest_common_indent = float("+inf") + greatest_common_indent = float('+inf') for line in lines: if len(line.strip()) == 0: continue @@ -35,25 +51,27 @@ def _get_greatest_common_indent(text: str) -> int: greatest_common_indent, _get_whitespace_prefix(line) ) - if greatest_common_indent == float("+inf"): + if isinstance(greatest_common_indent, float): greatest_common_indent = 0 return greatest_common_indent def _remove_indentation(text: str, tab_size: int = 4) -> str: + """Remove excessive indentation.""" text = _replace_tabs(_clean_text(text), tab_size) max_indent = _get_greatest_common_indent(text) - return "\n".join(line[max_indent:] for line in text.split("\n")) + return '\n'.join(line[max_indent:] for line in text.split('\n')) def format_violation(violation: ViolationInfo) -> str: + """Format violation information.""" cleaned_docstring = _remove_indentation(violation.docstring) violation_url = _DOCS_URL.format( violation.section, violation.fully_qualified_id, ) return ( - f"WPS{violation.code} ({violation.identifier})\n" - f"{cleaned_docstring}\n" - f"See at website: {violation_url}" + f'WPS{violation.code} ({violation.identifier})\n' + f'{cleaned_docstring}\n' + f'See at website: {violation_url}' ) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index f6a253c51..343161525 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -1,10 +1,13 @@ +"""Provides tools to extract violation info.""" + import importlib import inspect from collections.abc import Collection, Mapping -from dataclasses import dataclass from types import ModuleType from typing import Final +from wemake_python_styleguide.violations.base import BaseViolation + _VIOLATION_SUBMODULES: Final = ( 'best_practices', 'complexity', @@ -17,20 +20,34 @@ _VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' -@dataclass class ViolationInfo: - identifier: str - fully_qualified_id: str - code: int - docstring: str - section: str + """Contains violation info.""" + + def __init__( + self, + identifier: str, + fully_qualified_id: str, + code: int, + docstring: str, + section: str, + ): + """Create dataclass.""" + self.identifier = identifier + self.fully_qualified_id = fully_qualified_id + self.code = code + self.docstring = docstring + self.section = section def _is_a_violation(class_object) -> bool: + """Dumb check if class is a violation class.""" return hasattr(class_object, 'code') -def _get_violations_of_submodule(module: ModuleType) -> Collection: +def _get_violations_of_submodule( + module: ModuleType +) -> Collection[type[BaseViolation]]: + """Get all violation classes of defined module.""" return [ class_ for name, class_ in inspect.getmembers(module, inspect.isclass) @@ -43,6 +60,7 @@ def _create_violation_info( submodule_name: str, submodule_path: str ) -> ViolationInfo: + """Create violation info DTO from violation class and metadata.""" return ViolationInfo( identifier=class_object.__name__, fully_qualified_id=f'{submodule_path}.{class_object.__name__}', @@ -53,6 +71,7 @@ def _create_violation_info( def _get_all_violations() -> Mapping[int, ViolationInfo]: + """Get all violations inside all defined WPS violation modules.""" all_violations = {} for submodule_name in _VIOLATION_SUBMODULES: submodule_path = f'{_VIOLATION_MODULE_BASE}.{submodule_name}' @@ -69,6 +88,7 @@ def _get_all_violations() -> Mapping[int, ViolationInfo]: def get_violation(code: int) -> ViolationInfo | None: + """Get a violation by its integer code.""" violations = _get_all_violations() if code not in violations: return None diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py index 22e40ef4d..5a66a66b1 100644 --- a/wemake_python_styleguide/cli/output.py +++ b/wemake_python_styleguide/cli/output.py @@ -1,41 +1,50 @@ -import sys +"""Provides tool for outputting data.""" + from abc import abstractmethod -from typing import Protocol, Unpack, TextIO, AnyStr +from typing import Protocol, TextIO class Writable(Protocol): + """Interface for outputting text data.""" + @abstractmethod - def write_out(self, *args: Unpack[AnyStr]) -> None: - ... + def write_out(self, *args) -> None: + """Write usual text. Works as print.""" @abstractmethod - def write_err(self, *args: Unpack[AnyStr]) -> None: - ... + def write_err(self, *args) -> None: + """Write error text. Works as print.""" @abstractmethod def flush(self) -> None: - ... + """Flush all outputs.""" class BufferedStreamWriter(Writable): + """Writes to provided buffered text streams.""" + def __init__( self, out_stream: TextIO, err_stream: TextIO, newline_sym: str = '\n' ): + """Create stream writer.""" self._out = out_stream self._err = err_stream self._newline = newline_sym.encode() - def write_out(self, *args: Unpack[AnyStr]) -> None: + def write_out(self, *args) -> None: + """Write usual text. Works as print.""" self._out.buffer.write(' '.join(args).encode()) self._out.buffer.write(self._newline) - def write_err(self, *args: Unpack[AnyStr]) -> None: + def write_err(self, *args) -> None: + """Write error text. Works as print.""" self._err.buffer.write(' '.join(args).encode()) self._err.buffer.write(self._newline) def flush(self) -> None: + """Flush all outputs.""" self._out.flush() self._err.flush() From a13e022a090478a17cb83bc986479628b75b9a4a Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 01:23:29 +0500 Subject: [PATCH 07/44] Update docs --- docs/index.rst | 1 + docs/pages/usage/cli.rst | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 docs/pages/usage/cli.rst diff --git a/docs/index.rst b/docs/index.rst index 88f24121b..f52f705a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ pages/usage/configuration.rst pages/usage/violations/index.rst pages/usage/formatter.rst + pages/usage/cli.rst .. toctree:: diff --git a/docs/pages/usage/cli.rst b/docs/pages/usage/cli.rst new file mode 100644 index 000000000..1bd84bb42 --- /dev/null +++ b/docs/pages/usage/cli.rst @@ -0,0 +1,30 @@ +Command line tool +================= + +WPS v1.1.0 introduces new feature: a command-line utility called ``wps``. + +.. rubric:: ``wps explain`` + +This command can be used to get description of violation. +It will be the same description that is located on the website. + +Syntax: ``wps explain `` + +Examples: + +.. code:: + $ wps explain WPS115 + WPS115 (UpperCaseAttributeViolation) + + WPS115 - Require ``snake_case`` for naming class attributes. + ... + +.. code:: + $ wps explain 116 + WPS116 (ConsecutiveUnderscoresInNameViolation) + + WPS116 - Forbid using more than one consecutive underscore in variable names. + + + +.. versionadded:: 1.1.0 From bdd8f41252709068911a35c6b445c91fd2b34b81 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 01:33:23 +0500 Subject: [PATCH 08/44] Remove unused docutils --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 648fdf576..aa3365898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,6 @@ python = "^3.10" flake8 = "^7.1" attrs = "*" pygments = "^2.4" -docutils = "^0.21.2" [tool.poetry.group.dev.dependencies] pytest = "^8.1" From fd9936522dd722362f6e7fc682c6edc42d23804a Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 13:08:15 +0500 Subject: [PATCH 09/44] Apply suggestions from code review --- docs/pages/usage/cli.rst | 10 +++++----- wemake_python_styleguide/cli/application.py | 4 +++- wemake_python_styleguide/cli/commands/base.py | 2 +- .../cli/commands/explain/command.py | 1 + .../cli/commands/explain/message_formatter.py | 13 ++++--------- .../cli/commands/explain/violation_loader.py | 5 +++-- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/pages/usage/cli.rst b/docs/pages/usage/cli.rst index 1bd84bb42..946660efb 100644 --- a/docs/pages/usage/cli.rst +++ b/docs/pages/usage/cli.rst @@ -1,7 +1,11 @@ Command line tool ================= -WPS v1.1.0 introduces new feature: a command-line utility called ``wps``. +.. versionadded:: 1.1.0 + +WPS has a command-line utility named ``wps`` + +Here are listed all the subcommands it has. .. rubric:: ``wps explain`` @@ -24,7 +28,3 @@ Examples: WPS116 (ConsecutiveUnderscoresInNameViolation) WPS116 - Forbid using more than one consecutive underscore in variable names. - - - -.. versionadded:: 1.1.0 diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index 5a81b7524..2572136a1 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -1,14 +1,16 @@ """Provides WPS CLI application class.""" +from typing import final from wemake_python_styleguide.cli.commands.base import AbstractCommand from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand from wemake_python_styleguide.cli.output import Writable +@final class Application: """WPS CLI application class.""" - def __init__(self, writer: Writable): + def __init__(self, writer: Writable) -> None: """Create application.""" self._writer = writer diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py index 52d3fe454..284fcd496 100644 --- a/wemake_python_styleguide/cli/commands/base.py +++ b/wemake_python_styleguide/cli/commands/base.py @@ -8,7 +8,7 @@ class AbstractCommand(ABC): """ABC for all commands.""" - def __init__(self, writer: Writable): + def __init__(self, writer: Writable) -> None: """Create a command and define its writer.""" self.writer = writer diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index 5eb70b849..2117a31d1 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -34,6 +34,7 @@ def run(self, args) -> int: violation = violation_loader.get_violation(code) if violation is None: self.writer.write_err('Violation not found') + self.writer.flush() return 1 message = message_formatter.format_violation(violation) self.writer.write_out(message) diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 56f4c3405..131488b23 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -2,14 +2,12 @@ from typing import Final +from wemake_python_styleguide import formatter from wemake_python_styleguide.cli.commands.explain.violation_loader import ( ViolationInfo, ) -_DOCS_URL: Final = ( - 'https://wemake-python-styleguide.readthedocs.io/en/latest/pages/' - 'usage/violations/{0}.html#{1}' -) +_DOCS_URL: Final = 'https://pyflak.es/{0}' def _clean_text(text: str) -> str: @@ -41,7 +39,7 @@ def _get_whitespace_prefix(line: str) -> int: def _get_greatest_common_indent(text: str) -> int: """Get the greatest common whitespace prefix length of all lines.""" lines = text.split('\n') - if len(lines) == 0: + if not lines: return 0 greatest_common_indent = float('+inf') for line in lines: @@ -66,10 +64,7 @@ def _remove_indentation(text: str, tab_size: int = 4) -> str: def format_violation(violation: ViolationInfo) -> str: """Format violation information.""" cleaned_docstring = _remove_indentation(violation.docstring) - violation_url = _DOCS_URL.format( - violation.section, - violation.fully_qualified_id, - ) + violation_url = _DOCS_URL.format(f"WPS{violation.code}") return ( f'WPS{violation.code} ({violation.identifier})\n' f'{cleaned_docstring}\n' diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 343161525..628647241 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -4,7 +4,7 @@ import inspect from collections.abc import Collection, Mapping from types import ModuleType -from typing import Final +from typing import Final, final from wemake_python_styleguide.violations.base import BaseViolation @@ -20,6 +20,7 @@ _VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' +@final class ViolationInfo: """Contains violation info.""" @@ -30,7 +31,7 @@ def __init__( code: int, docstring: str, section: str, - ): + ) -> None: """Create dataclass.""" self.identifier = identifier self.fully_qualified_id = fully_qualified_id From 9931697b4964dbb1252f39db961465ff2b161183 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 13:16:18 +0500 Subject: [PATCH 10/44] Ruff fixes --- poetry.lock | 46 +++++++++---------- .../cli/commands/explain/message_formatter.py | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/poetry.lock b/poetry.lock index e2745e28a..59ad157f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1464,13 +1464,13 @@ files = [ [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.0-py3-none-any.whl", hash = "sha256:4755e6e64d22161d5b61432c0600c923c5927214e7c956e31c23923c89251a9b"}, + {file = "pygments-2.19.0.tar.gz", hash = "sha256:afc4146269910d4bdfabcd27c24923137a74d562a23a320a41a55ad303e19783"}, ] [package.extras] @@ -1635,29 +1635,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.8.5" +version = "0.8.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88"}, - {file = "ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7"}, - {file = "ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd"}, - {file = "ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8"}, - {file = "ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5"}, - {file = "ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed"}, - {file = "ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47"}, - {file = "ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb"}, - {file = "ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317"}, + {file = "ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3"}, + {file = "ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1"}, + {file = "ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf"}, + {file = "ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a"}, + {file = "ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76"}, + {file = "ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764"}, + {file = "ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"}, + {file = "ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162"}, + {file = "ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5"}, ] [[package]] @@ -2053,4 +2053,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "fd92c3299a8884e3fb9086a89b360d69edde3de81527b65fadb7839497f5a535" +content-hash = "f76763dcb7d3944b719d05cf384a38c0e840ee2574ca66d133a9b9a7eb6ad624" diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 227ea1026..3b8784a3e 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -63,7 +63,7 @@ def _remove_indentation(text: str, tab_size: int = 4) -> str: def format_violation(violation: ViolationInfo) -> str: """Format violation information.""" cleaned_docstring = _remove_indentation(violation.docstring) - violation_url = _DOCS_URL.format(f"WPS{violation.code}") + violation_url = _DOCS_URL.format(f'WPS{violation.code}') return ( f'WPS{violation.code} ({violation.identifier})\n' f'{cleaned_docstring}\n' From b9074a1675c016ee52f61734d1907944c43c7ccf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 08:19:31 +0000 Subject: [PATCH 11/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/test_explain.py | 14 ++++---------- wemake_python_styleguide/cli/application.py | 1 + wemake_python_styleguide/cli/cli_app.py | 3 +-- .../cli/commands/explain/message_formatter.py | 3 +-- .../cli/commands/explain/violation_loader.py | 6 ++---- wemake_python_styleguide/cli/output.py | 5 +---- 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index aaa44eb0e..3fcc01294 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -21,7 +21,7 @@ (115, UpperCaseAttributeViolation), (412, InitModuleHasLogicViolation), (600, BuiltinSubclassViolation), - ] + ], ) def test_violation_getter(violation_params): """Test that violation loader can get violation by their codes.""" @@ -34,15 +34,9 @@ def test_violation_getter(violation_params): @pytest.mark.parametrize( 'test_params', [ - ( - ' text\n text\n text', - 'text\ntext\ntext' - ), - ( - ' text\n\ttext\r\n text', - 'text\n text\ntext' - ), - ] + (' text\n text\n text', 'text\ntext\ntext'), + (' text\n\ttext\r\n text', 'text\n text\ntext'), + ], ) def test_indentation_removal(test_params): """Test that indentation remover works in different conditions.""" diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index 2572136a1..3859f3c59 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -1,4 +1,5 @@ """Provides WPS CLI application class.""" + from typing import final from wemake_python_styleguide.cli.commands.base import AbstractCommand diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 01d438f6a..e40d4005c 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -10,8 +10,7 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: """Configures CLI arguments and subcommands.""" parser = argparse.ArgumentParser( - prog='wps', - description='WPS command line tool' + prog='wps', description='WPS command line tool' ) sub_parsers = parser.add_subparsers(help='sub-command help') diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 3b8784a3e..ab8ea675f 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -45,8 +45,7 @@ def _get_greatest_common_indent(text: str) -> int: if len(line.strip()) == 0: continue greatest_common_indent = min( - greatest_common_indent, - _get_whitespace_prefix(line) + greatest_common_indent, _get_whitespace_prefix(line) ) if isinstance(greatest_common_indent, float): greatest_common_indent = 0 diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 628647241..d068da226 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -46,7 +46,7 @@ def _is_a_violation(class_object) -> bool: def _get_violations_of_submodule( - module: ModuleType + module: ModuleType, ) -> Collection[type[BaseViolation]]: """Get all violation classes of defined module.""" return [ @@ -57,9 +57,7 @@ def _get_violations_of_submodule( def _create_violation_info( - class_object, - submodule_name: str, - submodule_path: str + class_object, submodule_name: str, submodule_path: str ) -> ViolationInfo: """Create violation info DTO from violation class and metadata.""" return ViolationInfo( diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py index 5a66a66b1..dc9f38608 100644 --- a/wemake_python_styleguide/cli/output.py +++ b/wemake_python_styleguide/cli/output.py @@ -24,10 +24,7 @@ class BufferedStreamWriter(Writable): """Writes to provided buffered text streams.""" def __init__( - self, - out_stream: TextIO, - err_stream: TextIO, - newline_sym: str = '\n' + self, out_stream: TextIO, err_stream: TextIO, newline_sym: str = '\n' ): """Create stream writer.""" self._out = out_stream From 6365a2ba6d49dafb81b173d292bb04612205bd5f Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 6 Jan 2025 00:19:03 +0500 Subject: [PATCH 12/44] WIP #3241 Add explain command (with code review and updated docs) --- CHANGELOG.md | 8 ++ docs/index.rst | 1 + docs/pages/usage/cli.rst | 34 +++++++ poetry.lock | 1 - pyproject.toml | 3 + tests/test_cli/test_explain.py | 72 ++++++++++++++ wemake_python_styleguide/cli/__init__.py | 1 + wemake_python_styleguide/cli/application.py | 26 +++++ wemake_python_styleguide/cli/cli_app.py | 39 ++++++++ .../cli/commands/__init__.py | 1 + wemake_python_styleguide/cli/commands/base.py | 18 ++++ .../cli/commands/explain/__init__.py | 1 + .../cli/commands/explain/command.py | 42 ++++++++ .../cli/commands/explain/message_formatter.py | 71 ++++++++++++++ .../cli/commands/explain/violation_loader.py | 96 +++++++++++++++++++ wemake_python_styleguide/cli/output.py | 50 ++++++++++ 16 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 docs/pages/usage/cli.rst create mode 100644 tests/test_cli/test_explain.py create mode 100644 wemake_python_styleguide/cli/__init__.py create mode 100644 wemake_python_styleguide/cli/application.py create mode 100644 wemake_python_styleguide/cli/cli_app.py create mode 100644 wemake_python_styleguide/cli/commands/__init__.py create mode 100644 wemake_python_styleguide/cli/commands/base.py create mode 100644 wemake_python_styleguide/cli/commands/explain/__init__.py create mode 100644 wemake_python_styleguide/cli/commands/explain/command.py create mode 100644 wemake_python_styleguide/cli/commands/explain/message_formatter.py create mode 100644 wemake_python_styleguide/cli/commands/explain/violation_loader.py create mode 100644 wemake_python_styleguide/cli/output.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b81c97079..c3a0ef8f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ Semantic versioning in our case means: change the client facing API, change code conventions significantly, etc. +## 1.1.0 WIP + +### Command line utility + +This version introduces `wps` CLI tool. +- `wps explain ` command can be used to access WPS violation docs (same as on website) without internet access + + ## 1.0.1 WIP ### Bugfixes diff --git a/docs/index.rst b/docs/index.rst index 88f24121b..f52f705a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ pages/usage/configuration.rst pages/usage/violations/index.rst pages/usage/formatter.rst + pages/usage/cli.rst .. toctree:: diff --git a/docs/pages/usage/cli.rst b/docs/pages/usage/cli.rst new file mode 100644 index 000000000..5f9cc7b27 --- /dev/null +++ b/docs/pages/usage/cli.rst @@ -0,0 +1,34 @@ +Command line tool +================= + +.. versionadded:: 1.1.0 + +WPS has a command-line utility named ``wps`` + +Here are listed all the subcommands it has. + +.. rubric:: ``wps explain`` + +This command can be used to get description of violation. +It will be the same description that is located on the website. + +Syntax: ``wps explain `` + +Examples: + +.. code:: + $ wps explain WPS115 + WPS115 (UpperCaseAttributeViolation) + + WPS115 - Require ``snake_case`` for naming class attributes. + ... + +.. code:: + $ wps explain 116 + WPS116 (ConsecutiveUnderscoresInNameViolation) + + WPS116 - Forbid using more than one consecutive underscore in variable names. + + + +.. versionadded:: 1.1.0 diff --git a/poetry.lock b/poetry.lock index 18a54ee73..9df31b326 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2053,4 +2053,3 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "a99c4d38221a83066d6846541880bb1b64248f9cff5d906d33a77a3240e81cf4" diff --git a/pyproject.toml b/pyproject.toml index 5e75243d7..7d7e9fa8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -272,3 +272,6 @@ disallow_any_explicit = false module = "wemake_python_styleguide.compat.packaging" # We allow unused `ignore` comments, because we cannot sync it between versions: warn_unused_ignores = false + +[tool.poetry.scripts] +wps = "wemake_python_styleguide.cli.cli_app:main" diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py new file mode 100644 index 000000000..aaa44eb0e --- /dev/null +++ b/tests/test_cli/test_explain.py @@ -0,0 +1,72 @@ +"""Test that wps explain command works fine.""" + +import pytest + +from wemake_python_styleguide.cli.commands.explain import ( + message_formatter, + violation_loader, +) +from wemake_python_styleguide.violations.best_practices import ( + InitModuleHasLogicViolation, +) +from wemake_python_styleguide.violations.naming import ( + UpperCaseAttributeViolation, +) +from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation + + +@pytest.mark.parametrize( + 'violation_params', + [ + (115, UpperCaseAttributeViolation), + (412, InitModuleHasLogicViolation), + (600, BuiltinSubclassViolation), + ] +) +def test_violation_getter(violation_params): + """Test that violation loader can get violation by their codes.""" + violation_code, expected_class = violation_params + violation = violation_loader.get_violation(violation_code) + assert violation.code is not None + assert violation.docstring == expected_class.__doc__ + + +@pytest.mark.parametrize( + 'test_params', + [ + ( + ' text\n text\n text', + 'text\ntext\ntext' + ), + ( + ' text\n\ttext\r\n text', + 'text\n text\ntext' + ), + ] +) +def test_indentation_removal(test_params): + """Test that indentation remover works in different conditions.""" + input_text, expected = test_params + actual = message_formatter._remove_indentation(input_text) # noqa: SLF001 + assert actual == expected + + +violation_mock = violation_loader.ViolationInfo( + identifier='Mock', + code=100, + docstring='docstring', + fully_qualified_id='mock.Mock', + section='mock', +) +violation_string = ( + 'WPS100 (Mock)\n' + 'docstring\n' + 'See at website: https://wemake-python-styleguide.readthedocs.io/en/' + 'latest/pages/usage/violations/mock.html#mock.Mock' +) + + +def test_formatter(): + """Test that formatter formats violations as expected.""" + formatted = message_formatter.format_violation(violation_mock) + assert formatted == violation_string diff --git a/wemake_python_styleguide/cli/__init__.py b/wemake_python_styleguide/cli/__init__.py new file mode 100644 index 000000000..e08834af0 --- /dev/null +++ b/wemake_python_styleguide/cli/__init__.py @@ -0,0 +1 @@ +"""Contains all files related to WPS CLI utility.""" diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py new file mode 100644 index 000000000..2572136a1 --- /dev/null +++ b/wemake_python_styleguide/cli/application.py @@ -0,0 +1,26 @@ +"""Provides WPS CLI application class.""" +from typing import final + +from wemake_python_styleguide.cli.commands.base import AbstractCommand +from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand +from wemake_python_styleguide.cli.output import Writable + + +@final +class Application: + """WPS CLI application class.""" + + def __init__(self, writer: Writable) -> None: + """Create application.""" + self._writer = writer + + def run_explain(self, args) -> int: + """Run explain command.""" + return self._get_command(ExplainCommand).run(args) + + def _get_command( + self, + command_class: type[AbstractCommand], + ) -> AbstractCommand: + """Create command from its class and inject the selected writer.""" + return command_class(writer=self._writer) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py new file mode 100644 index 000000000..01d438f6a --- /dev/null +++ b/wemake_python_styleguide/cli/cli_app.py @@ -0,0 +1,39 @@ +"""Main CLI utility file.""" + +import argparse +import sys + +from wemake_python_styleguide.cli.application import Application +from wemake_python_styleguide.cli.output import BufferedStreamWriter + + +def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: + """Configures CLI arguments and subcommands.""" + parser = argparse.ArgumentParser( + prog='wps', + description='WPS command line tool' + ) + sub_parsers = parser.add_subparsers(help='sub-command help') + + parser_explain = sub_parsers.add_parser( + 'explain', + help='Get violation description', + ) + parser_explain.add_argument( + 'violation_code', + help='Desired violation code', + ) + parser_explain.set_defaults(func=app.run_explain) + + return parser + + +def main() -> int: + """Main function.""" + app = Application(BufferedStreamWriter(sys.stdout, sys.stderr)) + args = _configure_arg_parser(app).parse_args() + return int(args.func(args)) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/wemake_python_styleguide/cli/commands/__init__.py b/wemake_python_styleguide/cli/commands/__init__.py new file mode 100644 index 000000000..e7f103bea --- /dev/null +++ b/wemake_python_styleguide/cli/commands/__init__.py @@ -0,0 +1 @@ +"""Contains all files related to wps console commands.""" diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py new file mode 100644 index 000000000..284fcd496 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/base.py @@ -0,0 +1,18 @@ +"""Contains files common for all wps commands.""" + +from abc import ABC, abstractmethod + +from wemake_python_styleguide.cli.output import Writable + + +class AbstractCommand(ABC): + """ABC for all commands.""" + + def __init__(self, writer: Writable) -> None: + """Create a command and define its writer.""" + self.writer = writer + + @abstractmethod + def run(self, args) -> int: + """Run the command.""" + raise NotImplementedError diff --git a/wemake_python_styleguide/cli/commands/explain/__init__.py b/wemake_python_styleguide/cli/commands/explain/__init__.py new file mode 100644 index 000000000..4774bf5fc --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/__init__.py @@ -0,0 +1 @@ +"""Contains files related to wps explain command.""" diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py new file mode 100644 index 000000000..2117a31d1 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -0,0 +1,42 @@ +"""Contains command implementation.""" + +from wemake_python_styleguide.cli.commands.base import AbstractCommand +from wemake_python_styleguide.cli.commands.explain import ( + message_formatter, + violation_loader, +) + + +def _clean_violation_code(violation_str: str) -> int: + """ + Get int violation code from str violation code. + + Args: + violation_str: violation code expressed as string + WPS412, 412 - both acceptable + + Returns: + integer violation code + + Throws: + ValueError: violation str is not an integer (except WPS prefix). + """ + violation_str = violation_str.removeprefix('WPS') + return int(violation_str) + + +class ExplainCommand(AbstractCommand): + """Explain command impl.""" + + def run(self, args) -> int: + """Run command.""" + code = _clean_violation_code(args.violation_code) + violation = violation_loader.get_violation(code) + if violation is None: + self.writer.write_err('Violation not found') + self.writer.flush() + return 1 + message = message_formatter.format_violation(violation) + self.writer.write_out(message) + self.writer.flush() + return 0 diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py new file mode 100644 index 000000000..3b8784a3e --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -0,0 +1,71 @@ +"""Provides tools for formatting explanations.""" + +from typing import Final + +from wemake_python_styleguide.cli.commands.explain.violation_loader import ( + ViolationInfo, +) + +_DOCS_URL: Final = 'https://pyflak.es/{0}' + + +def _clean_text(text: str) -> str: + """ + Cleans provided text. + + Args: + text: target text + + Returns: + text with normalized newlines (CRs and CRLFs transformed to LFs). + """ + return text.replace('\r\n', '\n').replace('\r', '\n') + + +def _replace_tabs(text: str, tab_size: int = 4) -> str: + """Replace all tabs with defined amount of spaces.""" + return text.replace('\t', ' ' * tab_size) + + +def _get_whitespace_prefix(line: str) -> int: + """Get length of whitespace prefix of string.""" + for char_index, char in enumerate(line): + if char != ' ': + return char_index + return len(line) + + +def _get_greatest_common_indent(text: str) -> int: + """Get the greatest common whitespace prefix length of all lines.""" + lines = text.split('\n') + if not lines: + return 0 + greatest_common_indent = float('+inf') + for line in lines: + if len(line.strip()) == 0: + continue + greatest_common_indent = min( + greatest_common_indent, + _get_whitespace_prefix(line) + ) + if isinstance(greatest_common_indent, float): + greatest_common_indent = 0 + return greatest_common_indent + + +def _remove_indentation(text: str, tab_size: int = 4) -> str: + """Remove excessive indentation.""" + text = _replace_tabs(_clean_text(text), tab_size) + max_indent = _get_greatest_common_indent(text) + return '\n'.join(line[max_indent:] for line in text.split('\n')) + + +def format_violation(violation: ViolationInfo) -> str: + """Format violation information.""" + cleaned_docstring = _remove_indentation(violation.docstring) + violation_url = _DOCS_URL.format(f'WPS{violation.code}') + return ( + f'WPS{violation.code} ({violation.identifier})\n' + f'{cleaned_docstring}\n' + f'See at website: {violation_url}' + ) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py new file mode 100644 index 000000000..628647241 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -0,0 +1,96 @@ +"""Provides tools to extract violation info.""" + +import importlib +import inspect +from collections.abc import Collection, Mapping +from types import ModuleType +from typing import Final, final + +from wemake_python_styleguide.violations.base import BaseViolation + +_VIOLATION_SUBMODULES: Final = ( + 'best_practices', + 'complexity', + 'consistency', + 'naming', + 'oop', + 'refactoring', + 'system', +) +_VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' + + +@final +class ViolationInfo: + """Contains violation info.""" + + def __init__( + self, + identifier: str, + fully_qualified_id: str, + code: int, + docstring: str, + section: str, + ) -> None: + """Create dataclass.""" + self.identifier = identifier + self.fully_qualified_id = fully_qualified_id + self.code = code + self.docstring = docstring + self.section = section + + +def _is_a_violation(class_object) -> bool: + """Dumb check if class is a violation class.""" + return hasattr(class_object, 'code') + + +def _get_violations_of_submodule( + module: ModuleType +) -> Collection[type[BaseViolation]]: + """Get all violation classes of defined module.""" + return [ + class_ + for name, class_ in inspect.getmembers(module, inspect.isclass) + if _is_a_violation(class_) + ] + + +def _create_violation_info( + class_object, + submodule_name: str, + submodule_path: str +) -> ViolationInfo: + """Create violation info DTO from violation class and metadata.""" + return ViolationInfo( + identifier=class_object.__name__, + fully_qualified_id=f'{submodule_path}.{class_object.__name__}', + code=class_object.code, + docstring=class_object.__doc__, + section=submodule_name, + ) + + +def _get_all_violations() -> Mapping[int, ViolationInfo]: + """Get all violations inside all defined WPS violation modules.""" + all_violations = {} + for submodule_name in _VIOLATION_SUBMODULES: + submodule_path = f'{_VIOLATION_MODULE_BASE}.{submodule_name}' + violations = _get_violations_of_submodule( + importlib.import_module(submodule_path) + ) + for violation in violations: + all_violations[violation.code] = _create_violation_info( + violation, + submodule_name, + submodule_path, + ) + return all_violations + + +def get_violation(code: int) -> ViolationInfo | None: + """Get a violation by its integer code.""" + violations = _get_all_violations() + if code not in violations: + return None + return violations[code] diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py new file mode 100644 index 000000000..5a66a66b1 --- /dev/null +++ b/wemake_python_styleguide/cli/output.py @@ -0,0 +1,50 @@ +"""Provides tool for outputting data.""" + +from abc import abstractmethod +from typing import Protocol, TextIO + + +class Writable(Protocol): + """Interface for outputting text data.""" + + @abstractmethod + def write_out(self, *args) -> None: + """Write usual text. Works as print.""" + + @abstractmethod + def write_err(self, *args) -> None: + """Write error text. Works as print.""" + + @abstractmethod + def flush(self) -> None: + """Flush all outputs.""" + + +class BufferedStreamWriter(Writable): + """Writes to provided buffered text streams.""" + + def __init__( + self, + out_stream: TextIO, + err_stream: TextIO, + newline_sym: str = '\n' + ): + """Create stream writer.""" + self._out = out_stream + self._err = err_stream + self._newline = newline_sym.encode() + + def write_out(self, *args) -> None: + """Write usual text. Works as print.""" + self._out.buffer.write(' '.join(args).encode()) + self._out.buffer.write(self._newline) + + def write_err(self, *args) -> None: + """Write error text. Works as print.""" + self._err.buffer.write(' '.join(args).encode()) + self._err.buffer.write(self._newline) + + def flush(self) -> None: + """Flush all outputs.""" + self._out.flush() + self._err.flush() From f9b983b97e31ca68ecd98ffc76b83989c5ae49f8 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 11 Jan 2025 13:01:32 +0500 Subject: [PATCH 13/44] Re-lock poetry --- poetry.lock | 120 +++++++++++++++++++++++++--------------------------- 1 file changed, 58 insertions(+), 62 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9df31b326..cda21abac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -820,59 +820,54 @@ test = ["coverage", "hypothesis", "pytest", "pytest-cov", "tox"] [[package]] name = "libcst" -version = "1.5.1" +version = "1.6.0" description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." optional = false python-versions = ">=3.9" files = [ - {file = "libcst-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab83633e61ee91df575a3838b1e73c371f19d4916bf1816554933235553d41ea"}, - {file = "libcst-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b58a49895d95ec1fd34fad041a142d98edf9b51fcaf632337c13befeb4d51c7c"}, - {file = "libcst-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9ec764aa781ef35ab96b693569ac3dced16df9feb40ee6c274d13e86a1472e"}, - {file = "libcst-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99bbffd8596d192bc0e844a4cf3c4fc696979d4e20ab1c0774a01768a59b47ed"}, - {file = "libcst-1.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec6ee607cfe4cc4cc93e56e0188fdb9e50399d61a1262d58229752946f288f5e"}, - {file = "libcst-1.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72132756f985a19ef64d702a821099d4afc3544974662772b44cbc55b7279727"}, - {file = "libcst-1.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:40b75bf2d70fc0bc26b1fa73e61bdc46fef59f5c71aedf16128e7c33db8d5e40"}, - {file = "libcst-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:56c944acaa781b8e586df3019374f5cf117054d7fc98f85be1ba84fe810005dc"}, - {file = "libcst-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db7711a762b0327b581be5a963908fecd74412bdda34db34553faa521563c22d"}, - {file = "libcst-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa524bd012aaae1f485fd44490ef5abf708b14d2addc0f06b28de3e4585c4b9e"}, - {file = "libcst-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffb8135c09e41e8cf710b152c33e9b7f1d0d0b9f242bae0c502eb082fdb1fb"}, - {file = "libcst-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76a8ac7a84f9b6f678a668bff85b360e0a93fa8d7f25a74a206a28110734bb2a"}, - {file = "libcst-1.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89c808bdb5fa9ca02df41dd234cbb0e9de0d2e0c029c7063d5435a9f6781cc10"}, - {file = "libcst-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40fbbaa8b839bfbfa5b300623ca2b6b0768b58bbc31b341afbc99110c9bee232"}, - {file = "libcst-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c7021e3904d8d088c369afc3fe17c279883e583415ef07edacadba76cfbecd27"}, - {file = "libcst-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f053a5deb6a214972dbe9fa26ecd8255edb903de084a3d7715bf9e9da8821c50"}, - {file = "libcst-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:666813950b8637af0c0e96b1ca46f5d5f183d2fe50bbac2186f5b283a99f3529"}, - {file = "libcst-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b58b36022ae77a5a00002854043ae95c03e92f6062ad08473eff326f32efa0"}, - {file = "libcst-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb13d7c598fe9a798a1d22eae56ab3d3d599b38b83436039bd6ae229fc854d7"}, - {file = "libcst-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5987daff8389b0df60b5c20499ff4fb73fc03cb3ae1f6a746eefd204ed08df85"}, - {file = "libcst-1.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00f3d2f32ee081bad3394546b0b9ac5e31686d3b5cfe4892d716d2ba65f9ec08"}, - {file = "libcst-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ff21005c33b634957a98db438e882522febf1cacc62fa716f29e163a3f5871a"}, - {file = "libcst-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:15697ea9f1edbb9a263364d966c72abda07195d1c1a6838eb79af057f1040770"}, - {file = "libcst-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:cedd4c8336e01c51913113fbf5566b8f61a86d90f3d5cc5b1cb5049575622c5f"}, - {file = "libcst-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:06a9b4c9b76da4a7399e6f1f3a325196fb5febd3ea59fac1f68e2116f3517cd8"}, - {file = "libcst-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:940ec4c8db4c2d620a7268d6c83e64ff646e4afd74ae5183d0f0ef3b80e05be0"}, - {file = "libcst-1.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbccb016b1ac6d892344300dcccc8a16887b71bb7f875ba56c0ed6c1a7ade8be"}, - {file = "libcst-1.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c615af2117320e9a218083c83ec61227d3547e38a0de80329376971765f27a9e"}, - {file = "libcst-1.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02b38fa4d9f13e79fe69e9b5407b9e173557bcfb5960f7866cf4145af9c7ae09"}, - {file = "libcst-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3334afe9e7270e175de01198f816b0dc78dda94d9d72152b61851c323e4e741e"}, - {file = "libcst-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26c804fa8091747128579013df0b5f8e6b0c7904d9c4ee83841f136f53e18684"}, - {file = "libcst-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:b5a0d3c632aa2b21c5fa145e4e8dbf86f45c9b37a64c0b7221a5a45caf58915a"}, - {file = "libcst-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1cc7393aaac733e963f0ee00466d059db74a38e15fc7e6a46dddd128c5be8d08"}, - {file = "libcst-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bbaf5755be50fa9b35a3d553d1e62293fbb2ee5ce2c16c7e7ffeb2746af1ab88"}, - {file = "libcst-1.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e397f5b6c0fc271acea44579f154b0f3ab36011050f6db75ab00cef47441946"}, - {file = "libcst-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1947790a4fd7d96bcc200a6ecaa528045fcb26a34a24030d5859c7983662289e"}, - {file = "libcst-1.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:697eabe9f5ffc40f76d6d02e693274e0a382826d0cf8183bd44e7407dfb0ab90"}, - {file = "libcst-1.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dc06b7c60d086ef1832aebfd31b64c3c8a645adf0c5638d6243e5838f6a9356e"}, - {file = "libcst-1.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:19e39cfef4316599ca20d1c821490aeb783b52e8a8543a824972a525322a85d0"}, - {file = "libcst-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:01e01c04f0641188160d3b99c6526436e93a3fbf9783dba970f9885a77ec9b38"}, - {file = "libcst-1.5.1.tar.gz", hash = "sha256:71cb294db84df9e410208009c732628e920111683c2f2b2e0c5b71b98464f365"}, + {file = "libcst-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f02d0da6dfbad44e6ec4d1e5791e17afe95d9fe89bce4374bf109fd9c103a50"}, + {file = "libcst-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48406225378ee9208edb1e5a10451bea810262473af1a2f2473737fd16d34e3a"}, + {file = "libcst-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf59a21e9968dc4e7c301fac660bf54bc7d4dcadc0b1abf31b1cac34e800555"}, + {file = "libcst-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d65550ac686bff9395398afacbc88fe812363703a4161108e8a6db066d30b96e"}, + {file = "libcst-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5ac6d68364031f0b554d8920a69b33f25ec6ef351fa31b4e8f3676abb729ce36"}, + {file = "libcst-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c0fb2f7b74605832cc38d79e9d104f92a8aaeec7bf8f2759b20c5ba3786a321"}, + {file = "libcst-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:1bd11863889b630fe41543b4eb5e2dd445447a7f89e6b58229e83c9e52a74942"}, + {file = "libcst-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a9e71a046b4a91950125967f5ee67389f25a2511103e5595508f0591a5f50bc0"}, + {file = "libcst-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df3f452e074893dfad7746a041caeb3cde75bd9fbca4ea7b223012e112d1da8c"}, + {file = "libcst-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31e45f88d4a9a8e5b690ed14a564fcbace14b10f5e7b6797d6d97f4226b395da"}, + {file = "libcst-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bd00399d20bf93590b6f02647f8be08e2b730e050e6b7360f669254e69c98f5"}, + {file = "libcst-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25132f24edc24895082589645dbb8972c0eff6c9716ff71932fa72643d7c74f"}, + {file = "libcst-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38f3f25d4f5d8713cdb6a7bd41d75299de3c2416b9890a34d9b05417b8e64c1d"}, + {file = "libcst-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:91242ccbae6e7a070b33ebe03d3677c54bf678653538fbaa89597a59e4a13b2d"}, + {file = "libcst-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd2b28688dabf0f7a166b47ab1c7d5c0b6ef8c9a05ad932618471a33fe591a4a"}, + {file = "libcst-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a12a4766ce5874ccb31a1cc095cff47e2fb35755954965fe77458d9e5b361a8"}, + {file = "libcst-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfcd78a5e775f155054ed50d047a260cd23f0f6a89ef2a57e10bdb9c697680b8"}, + {file = "libcst-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5786240358b122ad901bb0b7e6b7467085b2317333233d7c7d7cac46388fbd77"}, + {file = "libcst-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c527472093b5b64ffa65d33c472da38952827abbca18c786d559d6d6122bc891"}, + {file = "libcst-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:63a8893dfc344b9b08bfaf4e433b16a7e2e9361f8362fa73eaecc4d379c328ba"}, + {file = "libcst-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:4cd011fcd79b76be216440ec296057780223674bc2566662c4bc50d3c5ecd58e"}, + {file = "libcst-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96506807dc01c9efcea8ab57d9ea18fdc87b85514cc8ee2f8568fab6df861f02"}, + {file = "libcst-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dac722aade8796a1e78662c3ed424f0ab9f1dc0e8fdf3088610354cdd709e53f"}, + {file = "libcst-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8370d0f7092a17b7fcda0e1539d0162cf35a0c19af94842b09c9dddc382acd"}, + {file = "libcst-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e4fcd791cab0fe8287b6edd0d78512b6475b87d906562a5d2d0999cb6d23b8d"}, + {file = "libcst-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3fb953fc0155532f366ff40f6a23f191250134d6928e02074ae4eb3531fa6c30"}, + {file = "libcst-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f3c85602e5a6d3aec0a8fc74230363f943004d7c2b2a6a1c09b320b61692241"}, + {file = "libcst-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:c4486921bebd33d67bbbd605aff8bfaefd2d13dc73c20c1fde2fb245880b7fd6"}, + {file = "libcst-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b3d274115d134a550fe8a0b38780a28a659d4a35ac6068c7c92fffe6661b519c"}, + {file = "libcst-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d45513f6cd3dbb2a80cf21a53bc6e6e560414edea17c474c784100e10aebe921"}, + {file = "libcst-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8c70a124d7a7d326abdc9a6261013c57d36f21c6c6370de5dd3e6a040c4ee5e"}, + {file = "libcst-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc95df61838d708adb37e18af1615491f6cac59557fd11077664dd956fe4528"}, + {file = "libcst-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05c32de72553cb93ff606c7d2421ce1eab1f0740c8c4b715444e2ae42f42b1b6"}, + {file = "libcst-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69b705f5b1faa66f115ede52a970d7613d3a8fb988834f853f7fb46870a041d2"}, + {file = "libcst-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:984512829a80f963bfc1803342219a4264a8d4206df0a30eae9bce921357a938"}, + {file = "libcst-1.6.0.tar.gz", hash = "sha256:e80ecdbe3fa43b3793cae8fa0b07a985bd9a693edbe6e9d076f5422ecadbf0db"}, ] [package.dependencies] pyyaml = ">=5.2" [package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.1)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.4)", "jupyter (>=1.0.0)", "maturin (>=1.7.0,<1.8)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.7.3)", "usort (==1.0.8.post1)"] +dev = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.1)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.5)", "jupyter (>=1.0.0)", "maturin (>=1.7.0,<1.8)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools_scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] [[package]] name = "lxml" @@ -1635,29 +1630,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.9.0" +version = "0.9.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.9.0-py3-none-linux_armv6l.whl", hash = "sha256:949b3513f931741e006cf267bf89611edff04e1f012013424022add3ce78f319"}, - {file = "ruff-0.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:99fbcb8c7fe94ae1e462ab2a1ef17cb20b25fb6438b9f198b1bcf5207a0a7916"}, - {file = "ruff-0.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b022afd8eb0fcfce1e0adec84322abf4d6ce3cd285b3b99c4f17aae7decf749"}, - {file = "ruff-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:336567ce92c9ca8ec62780d07b5fa11fbc881dc7bb40958f93a7d621e7ab4589"}, - {file = "ruff-0.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d338336c44bda602dc8e8766836ac0441e5b0dfeac3af1bd311a97ebaf087a75"}, - {file = "ruff-0.9.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b3ececf523d733e90b540e7afcc0494189e8999847f8855747acd5a9a8c45f"}, - {file = "ruff-0.9.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a11c0872a31232e473e2e0e2107f3d294dbadd2f83fb281c3eb1c22a24866924"}, - {file = "ruff-0.9.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5fd06220c17a9cc0dc7fc6552f2ac4db74e8e8bff9c401d160ac59d00566f54"}, - {file = "ruff-0.9.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0457e775c74bf3976243f910805242b7dcd389e1d440deccbd1194ca17a5728c"}, - {file = "ruff-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05415599bbcb318f730ea1b46a39e4fbf71f6a63fdbfa1dda92efb55f19d7ecf"}, - {file = "ruff-0.9.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fbf9864b009e43cfc1c8bed1a6a4c529156913105780af4141ca4342148517f5"}, - {file = "ruff-0.9.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:37b3da222b12e2bb2ce628e02586ab4846b1ed7f31f42a5a0683b213453b2d49"}, - {file = "ruff-0.9.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:733c0fcf2eb0c90055100b4ed1af9c9d87305b901a8feb6a0451fa53ed88199d"}, - {file = "ruff-0.9.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8221a454bfe5ccdf8017512fd6bb60e6ec30f9ea252b8a80e5b73619f6c3cefd"}, - {file = "ruff-0.9.0-py3-none-win32.whl", hash = "sha256:d345f2178afd192c7991ddee59155c58145e12ad81310b509bd2e25c5b0247b3"}, - {file = "ruff-0.9.0-py3-none-win_amd64.whl", hash = "sha256:0cbc0905d94d21305872f7f8224e30f4bbcd532bc21b2225b2446d8fc7220d19"}, - {file = "ruff-0.9.0-py3-none-win_arm64.whl", hash = "sha256:7b1148771c6ca88f820d761350a053a5794bc58e0867739ea93eb5e41ad978cd"}, - {file = "ruff-0.9.0.tar.gz", hash = "sha256:143f68fa5560ecf10fc49878b73cee3eab98b777fcf43b0e62d43d42f5ef9d8b"}, + {file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"}, + {file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"}, + {file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"}, + {file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"}, + {file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"}, + {file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"}, + {file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"}, ] [[package]] @@ -2053,3 +2048,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" +content-hash = "a99c4d38221a83066d6846541880bb1b64248f9cff5d906d33a77a3240e81cf4" From ff23e7ba56d45682faa55bfc5b9289c87f58a28d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 08:01:35 +0000 Subject: [PATCH 14/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/test_explain.py | 14 ++++---------- wemake_python_styleguide/cli/application.py | 1 + wemake_python_styleguide/cli/cli_app.py | 3 +-- .../cli/commands/explain/message_formatter.py | 3 +-- .../cli/commands/explain/violation_loader.py | 6 ++---- wemake_python_styleguide/cli/output.py | 5 +---- 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index aaa44eb0e..3fcc01294 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -21,7 +21,7 @@ (115, UpperCaseAttributeViolation), (412, InitModuleHasLogicViolation), (600, BuiltinSubclassViolation), - ] + ], ) def test_violation_getter(violation_params): """Test that violation loader can get violation by their codes.""" @@ -34,15 +34,9 @@ def test_violation_getter(violation_params): @pytest.mark.parametrize( 'test_params', [ - ( - ' text\n text\n text', - 'text\ntext\ntext' - ), - ( - ' text\n\ttext\r\n text', - 'text\n text\ntext' - ), - ] + (' text\n text\n text', 'text\ntext\ntext'), + (' text\n\ttext\r\n text', 'text\n text\ntext'), + ], ) def test_indentation_removal(test_params): """Test that indentation remover works in different conditions.""" diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index 2572136a1..3859f3c59 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -1,4 +1,5 @@ """Provides WPS CLI application class.""" + from typing import final from wemake_python_styleguide.cli.commands.base import AbstractCommand diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 01d438f6a..e40d4005c 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -10,8 +10,7 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: """Configures CLI arguments and subcommands.""" parser = argparse.ArgumentParser( - prog='wps', - description='WPS command line tool' + prog='wps', description='WPS command line tool' ) sub_parsers = parser.add_subparsers(help='sub-command help') diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 3b8784a3e..ab8ea675f 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -45,8 +45,7 @@ def _get_greatest_common_indent(text: str) -> int: if len(line.strip()) == 0: continue greatest_common_indent = min( - greatest_common_indent, - _get_whitespace_prefix(line) + greatest_common_indent, _get_whitespace_prefix(line) ) if isinstance(greatest_common_indent, float): greatest_common_indent = 0 diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 628647241..d068da226 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -46,7 +46,7 @@ def _is_a_violation(class_object) -> bool: def _get_violations_of_submodule( - module: ModuleType + module: ModuleType, ) -> Collection[type[BaseViolation]]: """Get all violation classes of defined module.""" return [ @@ -57,9 +57,7 @@ def _get_violations_of_submodule( def _create_violation_info( - class_object, - submodule_name: str, - submodule_path: str + class_object, submodule_name: str, submodule_path: str ) -> ViolationInfo: """Create violation info DTO from violation class and metadata.""" return ViolationInfo( diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py index 5a66a66b1..dc9f38608 100644 --- a/wemake_python_styleguide/cli/output.py +++ b/wemake_python_styleguide/cli/output.py @@ -24,10 +24,7 @@ class BufferedStreamWriter(Writable): """Writes to provided buffered text streams.""" def __init__( - self, - out_stream: TextIO, - err_stream: TextIO, - newline_sym: str = '\n' + self, out_stream: TextIO, err_stream: TextIO, newline_sym: str = '\n' ): """Create stream writer.""" self._out = out_stream From 5060acece7945a9e16f4bfe8b3f8235f60d43a76 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 11 Jan 2025 13:05:45 +0500 Subject: [PATCH 15/44] Update tests accordingly. Fix docs --- docs/pages/usage/cli.rst | 4 ++-- tests/test_cli/test_explain.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/pages/usage/cli.rst b/docs/pages/usage/cli.rst index 5f9cc7b27..5c0a994a9 100644 --- a/docs/pages/usage/cli.rst +++ b/docs/pages/usage/cli.rst @@ -16,14 +16,14 @@ Syntax: ``wps explain `` Examples: -.. code:: +.. code:: bash $ wps explain WPS115 WPS115 (UpperCaseAttributeViolation) WPS115 - Require ``snake_case`` for naming class attributes. ... -.. code:: +.. code:: bash $ wps explain 116 WPS116 (ConsecutiveUnderscoresInNameViolation) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 3fcc01294..2dd994589 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -55,8 +55,7 @@ def test_indentation_removal(test_params): violation_string = ( 'WPS100 (Mock)\n' 'docstring\n' - 'See at website: https://wemake-python-styleguide.readthedocs.io/en/' - 'latest/pages/usage/violations/mock.html#mock.Mock' + 'See at website: https://pyflak.es/WPS100' ) From 4eca1fa68bbfa2471d160b3c1ef19d170f8857d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 08:06:10 +0000 Subject: [PATCH 16/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/test_explain.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 2dd994589..c063c089c 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -53,9 +53,7 @@ def test_indentation_removal(test_params): section='mock', ) violation_string = ( - 'WPS100 (Mock)\n' - 'docstring\n' - 'See at website: https://pyflak.es/WPS100' + 'WPS100 (Mock)\ndocstring\nSee at website: https://pyflak.es/WPS100' ) From c167f0b8170e4063b8f0afcbfac76468e9c7434b Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 11 Jan 2025 13:51:44 +0500 Subject: [PATCH 17/44] Try fixing docs again. Add more tests --- docs/pages/usage/cli.rst | 2 + .../test_cli/__snapshots__/test_explain.ambr | 31 +++++++ tests/test_cli/test_explain.py | 81 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 tests/test_cli/__snapshots__/test_explain.ambr diff --git a/docs/pages/usage/cli.rst b/docs/pages/usage/cli.rst index 5c0a994a9..366152f8e 100644 --- a/docs/pages/usage/cli.rst +++ b/docs/pages/usage/cli.rst @@ -17,6 +17,7 @@ Syntax: ``wps explain `` Examples: .. code:: bash + $ wps explain WPS115 WPS115 (UpperCaseAttributeViolation) @@ -24,6 +25,7 @@ Examples: ... .. code:: bash + $ wps explain 116 WPS116 (ConsecutiveUnderscoresInNameViolation) diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr new file mode 100644 index 000000000..17b976aba --- /dev/null +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_command + ''' + WPS123 (WrongUnusedVariableNameViolation) + + WPS123 — Forbid unused variables with multiple underscores. + + Reasoning: + We only use ``_`` as a special definition for an unused variable. + Other variables are hard to read. It is unclear why would one use it. + + Solution: + Rename unused variables to ``_`` + or give it some more context with an explicit name: ``_context``. + + Example:: + + # Correct: + some_element, _next_element, _ = some_tuple() + some_element, _, _ = some_tuple() + some_element, _ = some_tuple() + + # Wrong: + some_element, _, __ = some_tuple() + + .. versionadded:: 0.12.0 + + + See at website: https://pyflak.es/WPS123 + ''' +# --- diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index c063c089c..48bca6d86 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,4 +1,6 @@ """Test that wps explain command works fine.""" +from io import BytesIO +from typing import TextIO import pytest @@ -6,6 +8,8 @@ message_formatter, violation_loader, ) +from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand +from wemake_python_styleguide.cli.output import BufferedStreamWriter, Writable from wemake_python_styleguide.violations.best_practices import ( InitModuleHasLogicViolation, ) @@ -61,3 +65,80 @@ def test_formatter(): """Test that formatter formats violations as expected.""" formatted = message_formatter.format_violation(violation_mock) assert formatted == violation_string + + +class MockWriter(Writable): + """Writer for testing.""" + + def __init__(self): + """Create writer.""" + self.out = '' + self.err = '' + + def write_out(self, *args) -> None: + """Write stdout.""" + self.out += ' '.join(map(str, args)) + + def write_err(self, *args) -> None: + """Write stderr.""" + self.err += ' '.join(map(str, args)) + + def flush(self) -> None: + """Blank method. Flushing not needed.""" + + +class MockArgs: + """Arguments for explain command.""" + + def __init__(self, code): + """Create mock explain arguments.""" + self.violation_code = code + + +def test_command(snapshot): + """Test that command works and formats violations as expected.""" + writer = MockWriter() + command = ExplainCommand(writer) + command.run(MockArgs('WPS123')) + assert writer.out == snapshot + + +class MockBufferedStringIO(TextIO): + """IO for testing BufferedStreamWriter.""" + + def __init__(self): + """Create IO.""" + self._buffer = BytesIO() + + @property + def buffer(self): + """Get IO buffer.""" + return self._buffer + + def flush(self): + """Flush buffer.""" + self._buffer.flush() + + def writable(self): + """Is IO writable.""" + return True + + def write(self, text): + """Write into buffer.""" + self._buffer.write(text.encode()) + + def get_string(self) -> str: + """Get string value written into buffer.""" + return self._buffer.getvalue().decode() + + +def test_buffered_stream_writer(): + """Test that stream writer works as expected.""" + io_out = MockBufferedStringIO() + io_err = MockBufferedStringIO() + writer = BufferedStreamWriter(io_out, io_err) + writer.write_out('Test', 'text') + writer.write_err('Test', 'error') + writer.flush() + assert io_out.get_string() == 'Test text\n' + assert io_err.get_string() == 'Test error\n' From c85ae4535034d8f043f489b3b2837528a1751ea0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 08:52:33 +0000 Subject: [PATCH 18/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/__snapshots__/test_explain.ambr | 18 +++++++++--------- tests/test_cli/test_explain.py | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr index 17b976aba..cf6a6c567 100644 --- a/tests/test_cli/__snapshots__/test_explain.ambr +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -2,30 +2,30 @@ # name: test_command ''' WPS123 (WrongUnusedVariableNameViolation) - + WPS123 — Forbid unused variables with multiple underscores. - + Reasoning: We only use ``_`` as a special definition for an unused variable. Other variables are hard to read. It is unclear why would one use it. - + Solution: Rename unused variables to ``_`` or give it some more context with an explicit name: ``_context``. - + Example:: - + # Correct: some_element, _next_element, _ = some_tuple() some_element, _, _ = some_tuple() some_element, _ = some_tuple() - + # Wrong: some_element, _, __ = some_tuple() - + .. versionadded:: 0.12.0 - - + + See at website: https://pyflak.es/WPS123 ''' # --- diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 48bca6d86..df500dc43 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,4 +1,5 @@ """Test that wps explain command works fine.""" + from io import BytesIO from typing import TextIO From c062e764993a8c507be3040c04ca587efcfe5304 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 11 Jan 2025 21:43:34 +0500 Subject: [PATCH 19/44] More tests to cover almost all cli utility --- tests/test_cli/test_explain.py | 36 +++++++++++++------ .../cli/commands/explain/command.py | 5 ++- .../cli/commands/explain/message_formatter.py | 8 ++--- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 48bca6d86..8bb4f3302 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -4,11 +4,11 @@ import pytest +from wemake_python_styleguide.cli.application import Application from wemake_python_styleguide.cli.commands.explain import ( message_formatter, violation_loader, ) -from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand from wemake_python_styleguide.cli.output import BufferedStreamWriter, Writable from wemake_python_styleguide.violations.best_practices import ( InitModuleHasLogicViolation, @@ -40,6 +40,12 @@ def test_violation_getter(violation_params): [ (' text\n text\n text', 'text\ntext\ntext'), (' text\n\ttext\r\n text', 'text\n text\ntext'), + (' text\n \n\n text', 'text\n \n\ntext'), + ('\n\n', '\n\n'), + ('text', 'text'), + ('text\ntext', 'text\ntext'), + ('', ''), + (' ', ' '), ], ) def test_indentation_removal(test_params): @@ -98,11 +104,27 @@ def __init__(self, code): def test_command(snapshot): """Test that command works and formats violations as expected.""" writer = MockWriter() - command = ExplainCommand(writer) - command.run(MockArgs('WPS123')) + command = Application(writer) + command.run_explain(MockArgs('WPS123')) assert writer.out == snapshot +@pytest.mark.parametrize( + 'non_existent_code', + [ + '10000', + 'NOT_A_CODE', + 'WPS10000', + ] +) +def test_command_on_not_found(non_existent_code): + """Test command works when violation code is wrong.""" + writer = MockWriter() + command = Application(writer) + command.run_explain(MockArgs(non_existent_code)) + assert writer.err.strip() == 'Violation not found' + + class MockBufferedStringIO(TextIO): """IO for testing BufferedStreamWriter.""" @@ -119,14 +141,6 @@ def flush(self): """Flush buffer.""" self._buffer.flush() - def writable(self): - """Is IO writable.""" - return True - - def write(self, text): - """Write into buffer.""" - self._buffer.write(text.encode()) - def get_string(self) -> str: """Get string value written into buffer.""" return self._buffer.getvalue().decode() diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index 2117a31d1..963f31eb5 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -22,7 +22,10 @@ def _clean_violation_code(violation_str: str) -> int: ValueError: violation str is not an integer (except WPS prefix). """ violation_str = violation_str.removeprefix('WPS') - return int(violation_str) + try: + return int(violation_str) + except ValueError: + return -1 class ExplainCommand(AbstractCommand): diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index ab8ea675f..69ce5c05d 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -27,23 +27,19 @@ def _replace_tabs(text: str, tab_size: int = 4) -> str: return text.replace('\t', ' ' * tab_size) -def _get_whitespace_prefix(line: str) -> int: +def _get_whitespace_prefix(line: str) -> int | float: """Get length of whitespace prefix of string.""" for char_index, char in enumerate(line): if char != ' ': return char_index - return len(line) + return float('+inf') def _get_greatest_common_indent(text: str) -> int: """Get the greatest common whitespace prefix length of all lines.""" lines = text.split('\n') - if not lines: - return 0 greatest_common_indent = float('+inf') for line in lines: - if len(line.strip()) == 0: - continue greatest_common_indent = min( greatest_common_indent, _get_whitespace_prefix(line) ) From 4533f632e1d4326b18bf51d28957561a48b9aa94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:44:11 +0000 Subject: [PATCH 20/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/test_explain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 291da245d..597e47995 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -116,7 +116,7 @@ def test_command(snapshot): '10000', 'NOT_A_CODE', 'WPS10000', - ] + ], ) def test_command_on_not_found(non_existent_code): """Test command works when violation code is wrong.""" From a8695cb0451aa8ea7690c5bbf1076f9ba8e880dd Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 11 Jan 2025 22:01:54 +0500 Subject: [PATCH 21/44] Cover 100% CLI --- .../test_cli/__snapshots__/test_explain.ambr | 18 ++++++------ tests/test_cli/test_explain.py | 29 ++++++++----------- wemake_python_styleguide/cli/cli_app.py | 10 +++++-- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr index cf6a6c567..17b976aba 100644 --- a/tests/test_cli/__snapshots__/test_explain.ambr +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -2,30 +2,30 @@ # name: test_command ''' WPS123 (WrongUnusedVariableNameViolation) - + WPS123 — Forbid unused variables with multiple underscores. - + Reasoning: We only use ``_`` as a special definition for an unused variable. Other variables are hard to read. It is unclear why would one use it. - + Solution: Rename unused variables to ``_`` or give it some more context with an explicit name: ``_context``. - + Example:: - + # Correct: some_element, _next_element, _ = some_tuple() some_element, _, _ = some_tuple() some_element, _ = some_tuple() - + # Wrong: some_element, _, __ = some_tuple() - + .. versionadded:: 0.12.0 - - + + See at website: https://pyflak.es/WPS123 ''' # --- diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 291da245d..1d123dd7c 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -5,6 +5,7 @@ import pytest +from wemake_python_styleguide.cli import cli_app from wemake_python_styleguide.cli.application import Application from wemake_python_styleguide.cli.commands.explain import ( message_formatter, @@ -94,35 +95,29 @@ def flush(self) -> None: """Blank method. Flushing not needed.""" -class MockArgs: - """Arguments for explain command.""" - - def __init__(self, code): - """Create mock explain arguments.""" - self.violation_code = code - - def test_command(snapshot): """Test that command works and formats violations as expected.""" writer = MockWriter() - command = Application(writer) - command.run_explain(MockArgs('WPS123')) + application = Application(writer) + args = cli_app.parse_args('explain WPS123'.split(), application) + application.run_explain(args) assert writer.out == snapshot @pytest.mark.parametrize( - 'non_existent_code', + 'arguments', [ - '10000', - 'NOT_A_CODE', - 'WPS10000', + 'explain 10000', + 'explain NOT_A_CODE', + 'explain WPS10000', ] ) -def test_command_on_not_found(non_existent_code): +def test_command_on_not_found(arguments): """Test command works when violation code is wrong.""" writer = MockWriter() - command = Application(writer) - command.run_explain(MockArgs(non_existent_code)) + application = Application(writer) + args = cli_app.parse_args(arguments.split(), application) + application.run_explain(args) assert writer.err.strip() == 'Violation not found' diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index e40d4005c..89151bd11 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -2,6 +2,7 @@ import argparse import sys +from collections.abc import Sequence from wemake_python_styleguide.cli.application import Application from wemake_python_styleguide.cli.output import BufferedStreamWriter @@ -27,10 +28,15 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: return parser -def main() -> int: +def parse_args(args: Sequence[str], app: Application) -> argparse.Namespace: + parser = _configure_arg_parser(app) + return parser.parse_args(args) + + +def main() -> int: # pragma: no cover """Main function.""" app = Application(BufferedStreamWriter(sys.stdout, sys.stderr)) - args = _configure_arg_parser(app).parse_args() + args = parse_args(sys.argv[1:], app) return int(args.func(args)) From 4ef4db1d68fb4daf93a3aff98ddd870b4db9cb73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:03:17 +0000 Subject: [PATCH 22/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/__snapshots__/test_explain.ambr | 18 +++++++++--------- tests/test_cli/test_explain.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr index 17b976aba..cf6a6c567 100644 --- a/tests/test_cli/__snapshots__/test_explain.ambr +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -2,30 +2,30 @@ # name: test_command ''' WPS123 (WrongUnusedVariableNameViolation) - + WPS123 — Forbid unused variables with multiple underscores. - + Reasoning: We only use ``_`` as a special definition for an unused variable. Other variables are hard to read. It is unclear why would one use it. - + Solution: Rename unused variables to ``_`` or give it some more context with an explicit name: ``_context``. - + Example:: - + # Correct: some_element, _next_element, _ = some_tuple() some_element, _, _ = some_tuple() some_element, _ = some_tuple() - + # Wrong: some_element, _, __ = some_tuple() - + .. versionadded:: 0.12.0 - - + + See at website: https://pyflak.es/WPS123 ''' # --- diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 13a9693ca..8e41b124c 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -99,7 +99,7 @@ def test_command(snapshot): """Test that command works and formats violations as expected.""" writer = MockWriter() application = Application(writer) - args = cli_app.parse_args('explain WPS123'.split(), application) + args = cli_app.parse_args(['explain', 'WPS123'], application) application.run_explain(args) assert writer.out == snapshot From fcf7350753aff02a30e06f45257dca24c8cc07a2 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 11 Jan 2025 22:08:12 +0500 Subject: [PATCH 23/44] Finally, found and fixed what breaks my pytest snapshots --- .pre-commit-config.yaml | 2 +- wemake_python_styleguide/cli/cli_app.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbd5ec64a..5b140396a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: hooks: - id: check-useless-excludes -exclude: ^(tests/fixtures/|tests/test_formatter/__snapshots__/) +exclude: ^(tests/fixtures/|tests/test_formatter/__snapshots__/|tests/test_cli/__snapshots__/) ci: autofix_commit_msg: "[pre-commit.ci] auto fixes from pre-commit.com hooks" diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 89151bd11..8fd06b7cf 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -29,6 +29,7 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: def parse_args(args: Sequence[str], app: Application) -> argparse.Namespace: + """Parse CLI arguments.""" parser = _configure_arg_parser(app) return parser.parse_args(args) From 815ffd255346b4c68770e874ca513f9cb569546e Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 11 Jan 2025 22:12:47 +0500 Subject: [PATCH 24/44] Bring back whitespaces in snapshot --- tests/test_cli/__snapshots__/test_explain.ambr | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr index cf6a6c567..17b976aba 100644 --- a/tests/test_cli/__snapshots__/test_explain.ambr +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -2,30 +2,30 @@ # name: test_command ''' WPS123 (WrongUnusedVariableNameViolation) - + WPS123 — Forbid unused variables with multiple underscores. - + Reasoning: We only use ``_`` as a special definition for an unused variable. Other variables are hard to read. It is unclear why would one use it. - + Solution: Rename unused variables to ``_`` or give it some more context with an explicit name: ``_context``. - + Example:: - + # Correct: some_element, _next_element, _ = some_tuple() some_element, _, _ = some_tuple() some_element, _ = some_tuple() - + # Wrong: some_element, _, __ = some_tuple() - + .. versionadded:: 0.12.0 - - + + See at website: https://pyflak.es/WPS123 ''' # --- From e6fbf52fa63757c10db5417f5e1fe732deff7055 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Tue, 21 Jan 2025 20:50:34 +0500 Subject: [PATCH 25/44] Re-lock poetry --- poetry.lock | 105 +++++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/poetry.lock b/poetry.lock index de4de1ae3..7864490f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -84,13 +84,13 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "autopep8" -version = "2.3.1" +version = "2.3.2" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "autopep8-2.3.1-py2.py3-none-any.whl", hash = "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d"}, - {file = "autopep8-2.3.1.tar.gz", hash = "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda"}, + {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, + {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, ] [package.dependencies] @@ -820,59 +820,54 @@ test = ["coverage", "hypothesis", "pytest", "pytest-cov", "tox"] [[package]] name = "libcst" -version = "1.5.1" +version = "1.6.0" description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." optional = false python-versions = ">=3.9" files = [ - {file = "libcst-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab83633e61ee91df575a3838b1e73c371f19d4916bf1816554933235553d41ea"}, - {file = "libcst-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b58a49895d95ec1fd34fad041a142d98edf9b51fcaf632337c13befeb4d51c7c"}, - {file = "libcst-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9ec764aa781ef35ab96b693569ac3dced16df9feb40ee6c274d13e86a1472e"}, - {file = "libcst-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99bbffd8596d192bc0e844a4cf3c4fc696979d4e20ab1c0774a01768a59b47ed"}, - {file = "libcst-1.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec6ee607cfe4cc4cc93e56e0188fdb9e50399d61a1262d58229752946f288f5e"}, - {file = "libcst-1.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72132756f985a19ef64d702a821099d4afc3544974662772b44cbc55b7279727"}, - {file = "libcst-1.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:40b75bf2d70fc0bc26b1fa73e61bdc46fef59f5c71aedf16128e7c33db8d5e40"}, - {file = "libcst-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:56c944acaa781b8e586df3019374f5cf117054d7fc98f85be1ba84fe810005dc"}, - {file = "libcst-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db7711a762b0327b581be5a963908fecd74412bdda34db34553faa521563c22d"}, - {file = "libcst-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa524bd012aaae1f485fd44490ef5abf708b14d2addc0f06b28de3e4585c4b9e"}, - {file = "libcst-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffb8135c09e41e8cf710b152c33e9b7f1d0d0b9f242bae0c502eb082fdb1fb"}, - {file = "libcst-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76a8ac7a84f9b6f678a668bff85b360e0a93fa8d7f25a74a206a28110734bb2a"}, - {file = "libcst-1.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89c808bdb5fa9ca02df41dd234cbb0e9de0d2e0c029c7063d5435a9f6781cc10"}, - {file = "libcst-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40fbbaa8b839bfbfa5b300623ca2b6b0768b58bbc31b341afbc99110c9bee232"}, - {file = "libcst-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c7021e3904d8d088c369afc3fe17c279883e583415ef07edacadba76cfbecd27"}, - {file = "libcst-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f053a5deb6a214972dbe9fa26ecd8255edb903de084a3d7715bf9e9da8821c50"}, - {file = "libcst-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:666813950b8637af0c0e96b1ca46f5d5f183d2fe50bbac2186f5b283a99f3529"}, - {file = "libcst-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b58b36022ae77a5a00002854043ae95c03e92f6062ad08473eff326f32efa0"}, - {file = "libcst-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb13d7c598fe9a798a1d22eae56ab3d3d599b38b83436039bd6ae229fc854d7"}, - {file = "libcst-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5987daff8389b0df60b5c20499ff4fb73fc03cb3ae1f6a746eefd204ed08df85"}, - {file = "libcst-1.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00f3d2f32ee081bad3394546b0b9ac5e31686d3b5cfe4892d716d2ba65f9ec08"}, - {file = "libcst-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ff21005c33b634957a98db438e882522febf1cacc62fa716f29e163a3f5871a"}, - {file = "libcst-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:15697ea9f1edbb9a263364d966c72abda07195d1c1a6838eb79af057f1040770"}, - {file = "libcst-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:cedd4c8336e01c51913113fbf5566b8f61a86d90f3d5cc5b1cb5049575622c5f"}, - {file = "libcst-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:06a9b4c9b76da4a7399e6f1f3a325196fb5febd3ea59fac1f68e2116f3517cd8"}, - {file = "libcst-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:940ec4c8db4c2d620a7268d6c83e64ff646e4afd74ae5183d0f0ef3b80e05be0"}, - {file = "libcst-1.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbccb016b1ac6d892344300dcccc8a16887b71bb7f875ba56c0ed6c1a7ade8be"}, - {file = "libcst-1.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c615af2117320e9a218083c83ec61227d3547e38a0de80329376971765f27a9e"}, - {file = "libcst-1.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02b38fa4d9f13e79fe69e9b5407b9e173557bcfb5960f7866cf4145af9c7ae09"}, - {file = "libcst-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3334afe9e7270e175de01198f816b0dc78dda94d9d72152b61851c323e4e741e"}, - {file = "libcst-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26c804fa8091747128579013df0b5f8e6b0c7904d9c4ee83841f136f53e18684"}, - {file = "libcst-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:b5a0d3c632aa2b21c5fa145e4e8dbf86f45c9b37a64c0b7221a5a45caf58915a"}, - {file = "libcst-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1cc7393aaac733e963f0ee00466d059db74a38e15fc7e6a46dddd128c5be8d08"}, - {file = "libcst-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bbaf5755be50fa9b35a3d553d1e62293fbb2ee5ce2c16c7e7ffeb2746af1ab88"}, - {file = "libcst-1.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e397f5b6c0fc271acea44579f154b0f3ab36011050f6db75ab00cef47441946"}, - {file = "libcst-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1947790a4fd7d96bcc200a6ecaa528045fcb26a34a24030d5859c7983662289e"}, - {file = "libcst-1.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:697eabe9f5ffc40f76d6d02e693274e0a382826d0cf8183bd44e7407dfb0ab90"}, - {file = "libcst-1.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dc06b7c60d086ef1832aebfd31b64c3c8a645adf0c5638d6243e5838f6a9356e"}, - {file = "libcst-1.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:19e39cfef4316599ca20d1c821490aeb783b52e8a8543a824972a525322a85d0"}, - {file = "libcst-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:01e01c04f0641188160d3b99c6526436e93a3fbf9783dba970f9885a77ec9b38"}, - {file = "libcst-1.5.1.tar.gz", hash = "sha256:71cb294db84df9e410208009c732628e920111683c2f2b2e0c5b71b98464f365"}, + {file = "libcst-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f02d0da6dfbad44e6ec4d1e5791e17afe95d9fe89bce4374bf109fd9c103a50"}, + {file = "libcst-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48406225378ee9208edb1e5a10451bea810262473af1a2f2473737fd16d34e3a"}, + {file = "libcst-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf59a21e9968dc4e7c301fac660bf54bc7d4dcadc0b1abf31b1cac34e800555"}, + {file = "libcst-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d65550ac686bff9395398afacbc88fe812363703a4161108e8a6db066d30b96e"}, + {file = "libcst-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5ac6d68364031f0b554d8920a69b33f25ec6ef351fa31b4e8f3676abb729ce36"}, + {file = "libcst-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c0fb2f7b74605832cc38d79e9d104f92a8aaeec7bf8f2759b20c5ba3786a321"}, + {file = "libcst-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:1bd11863889b630fe41543b4eb5e2dd445447a7f89e6b58229e83c9e52a74942"}, + {file = "libcst-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a9e71a046b4a91950125967f5ee67389f25a2511103e5595508f0591a5f50bc0"}, + {file = "libcst-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df3f452e074893dfad7746a041caeb3cde75bd9fbca4ea7b223012e112d1da8c"}, + {file = "libcst-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31e45f88d4a9a8e5b690ed14a564fcbace14b10f5e7b6797d6d97f4226b395da"}, + {file = "libcst-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bd00399d20bf93590b6f02647f8be08e2b730e050e6b7360f669254e69c98f5"}, + {file = "libcst-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25132f24edc24895082589645dbb8972c0eff6c9716ff71932fa72643d7c74f"}, + {file = "libcst-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38f3f25d4f5d8713cdb6a7bd41d75299de3c2416b9890a34d9b05417b8e64c1d"}, + {file = "libcst-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:91242ccbae6e7a070b33ebe03d3677c54bf678653538fbaa89597a59e4a13b2d"}, + {file = "libcst-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd2b28688dabf0f7a166b47ab1c7d5c0b6ef8c9a05ad932618471a33fe591a4a"}, + {file = "libcst-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a12a4766ce5874ccb31a1cc095cff47e2fb35755954965fe77458d9e5b361a8"}, + {file = "libcst-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfcd78a5e775f155054ed50d047a260cd23f0f6a89ef2a57e10bdb9c697680b8"}, + {file = "libcst-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5786240358b122ad901bb0b7e6b7467085b2317333233d7c7d7cac46388fbd77"}, + {file = "libcst-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c527472093b5b64ffa65d33c472da38952827abbca18c786d559d6d6122bc891"}, + {file = "libcst-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:63a8893dfc344b9b08bfaf4e433b16a7e2e9361f8362fa73eaecc4d379c328ba"}, + {file = "libcst-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:4cd011fcd79b76be216440ec296057780223674bc2566662c4bc50d3c5ecd58e"}, + {file = "libcst-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96506807dc01c9efcea8ab57d9ea18fdc87b85514cc8ee2f8568fab6df861f02"}, + {file = "libcst-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dac722aade8796a1e78662c3ed424f0ab9f1dc0e8fdf3088610354cdd709e53f"}, + {file = "libcst-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8370d0f7092a17b7fcda0e1539d0162cf35a0c19af94842b09c9dddc382acd"}, + {file = "libcst-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e4fcd791cab0fe8287b6edd0d78512b6475b87d906562a5d2d0999cb6d23b8d"}, + {file = "libcst-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3fb953fc0155532f366ff40f6a23f191250134d6928e02074ae4eb3531fa6c30"}, + {file = "libcst-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f3c85602e5a6d3aec0a8fc74230363f943004d7c2b2a6a1c09b320b61692241"}, + {file = "libcst-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:c4486921bebd33d67bbbd605aff8bfaefd2d13dc73c20c1fde2fb245880b7fd6"}, + {file = "libcst-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b3d274115d134a550fe8a0b38780a28a659d4a35ac6068c7c92fffe6661b519c"}, + {file = "libcst-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d45513f6cd3dbb2a80cf21a53bc6e6e560414edea17c474c784100e10aebe921"}, + {file = "libcst-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8c70a124d7a7d326abdc9a6261013c57d36f21c6c6370de5dd3e6a040c4ee5e"}, + {file = "libcst-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc95df61838d708adb37e18af1615491f6cac59557fd11077664dd956fe4528"}, + {file = "libcst-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05c32de72553cb93ff606c7d2421ce1eab1f0740c8c4b715444e2ae42f42b1b6"}, + {file = "libcst-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69b705f5b1faa66f115ede52a970d7613d3a8fb988834f853f7fb46870a041d2"}, + {file = "libcst-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:984512829a80f963bfc1803342219a4264a8d4206df0a30eae9bce921357a938"}, + {file = "libcst-1.6.0.tar.gz", hash = "sha256:e80ecdbe3fa43b3793cae8fa0b07a985bd9a693edbe6e9d076f5422ecadbf0db"}, ] [package.dependencies] pyyaml = ">=5.2" [package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.1)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.4)", "jupyter (>=1.0.0)", "maturin (>=1.7.0,<1.8)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.7.3)", "usort (==1.0.8.post1)"] +dev = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.1)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.5)", "jupyter (>=1.0.0)", "maturin (>=1.7.0,<1.8)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools_scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] [[package]] name = "lxml" @@ -1179,13 +1174,13 @@ files = [ [[package]] name = "more-itertools" -version = "10.5.0" +version = "10.6.0" description = "More routines for operating on iterables, beyond itertools" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, - {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, ] [[package]] @@ -1403,13 +1398,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.48" +version = "3.0.50" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, - {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, + {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, + {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, ] [package.dependencies] From 1755eb0a7a177de96c6f28fcec6782a0fac057ba Mon Sep 17 00:00:00 2001 From: Tapeline Date: Tue, 21 Jan 2025 20:59:46 +0500 Subject: [PATCH 26/44] Remove unnecessary text and newlines in wps explain --- tests/test_cli/__snapshots__/test_explain.ambr | 4 ---- tests/test_cli/test_explain.py | 2 +- .../cli/commands/explain/message_formatter.py | 7 ++++++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr index 17b976aba..47944a7ee 100644 --- a/tests/test_cli/__snapshots__/test_explain.ambr +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -1,8 +1,6 @@ # serializer version: 1 # name: test_command ''' - WPS123 (WrongUnusedVariableNameViolation) - WPS123 — Forbid unused variables with multiple underscores. Reasoning: @@ -24,8 +22,6 @@ some_element, _, __ = some_tuple() .. versionadded:: 0.12.0 - - See at website: https://pyflak.es/WPS123 ''' # --- diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 8e41b124c..d02e79392 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -65,7 +65,7 @@ def test_indentation_removal(test_params): section='mock', ) violation_string = ( - 'WPS100 (Mock)\ndocstring\nSee at website: https://pyflak.es/WPS100' + 'docstring\nSee at website: https://pyflak.es/WPS100' ) diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 69ce5c05d..8dfdb484b 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -55,12 +55,17 @@ def _remove_indentation(text: str, tab_size: int = 4) -> str: return '\n'.join(line[max_indent:] for line in text.split('\n')) +def _remove_newlines_at_ends(text: str) -> str: + """Remove leading and trailing newlines.""" + return text.strip('\n\r') + + def format_violation(violation: ViolationInfo) -> str: """Format violation information.""" cleaned_docstring = _remove_indentation(violation.docstring) + cleaned_docstring = _remove_newlines_at_ends(cleaned_docstring) violation_url = _DOCS_URL.format(f'WPS{violation.code}') return ( - f'WPS{violation.code} ({violation.identifier})\n' f'{cleaned_docstring}\n' f'See at website: {violation_url}' ) From f27d7260ba5394c887415dcc9e607f83d8daa2d0 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Tue, 21 Jan 2025 21:06:06 +0500 Subject: [PATCH 27/44] Remove duplicated versionadded label from cli.rst --- docs/pages/usage/cli.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/pages/usage/cli.rst b/docs/pages/usage/cli.rst index 366152f8e..56b815b55 100644 --- a/docs/pages/usage/cli.rst +++ b/docs/pages/usage/cli.rst @@ -30,7 +30,3 @@ Examples: WPS116 (ConsecutiveUnderscoresInNameViolation) WPS116 - Forbid using more than one consecutive underscore in variable names. - - - -.. versionadded:: 1.1.0 From c037010e0d33a8e412154b74a90e3adcef43d9af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:06:39 +0000 Subject: [PATCH 28/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/test_explain.py | 4 +--- .../cli/commands/explain/message_formatter.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index d02e79392..ce084c4f7 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -64,9 +64,7 @@ def test_indentation_removal(test_params): fully_qualified_id='mock.Mock', section='mock', ) -violation_string = ( - 'docstring\nSee at website: https://pyflak.es/WPS100' -) +violation_string = 'docstring\nSee at website: https://pyflak.es/WPS100' def test_formatter(): diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 8dfdb484b..1ec37d17a 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -65,7 +65,4 @@ def format_violation(violation: ViolationInfo) -> str: cleaned_docstring = _remove_indentation(violation.docstring) cleaned_docstring = _remove_newlines_at_ends(cleaned_docstring) violation_url = _DOCS_URL.format(f'WPS{violation.code}') - return ( - f'{cleaned_docstring}\n' - f'See at website: {violation_url}' - ) + return f'{cleaned_docstring}\nSee at website: {violation_url}' From 173e821fc3b0cbf73416a1baec76e750ce6c629b Mon Sep 17 00:00:00 2001 From: Tapeline Date: Thu, 23 Jan 2025 17:26:34 +0500 Subject: [PATCH 29/44] Remove Writable abstraction. Cosmetic changes to formatting. More testing --- .../test_cli/__snapshots__/test_explain.ambr | 26 ++++ tests/test_cli/test_explain.py | 115 +++++++----------- wemake_python_styleguide/cli/application.py | 15 +-- wemake_python_styleguide/cli/cli_app.py | 7 +- wemake_python_styleguide/cli/commands/base.py | 6 - .../cli/commands/explain/command.py | 7 +- .../cli/commands/explain/message_formatter.py | 2 +- .../cli/commands/explain/violation_loader.py | 30 ++--- wemake_python_styleguide/cli/output.py | 51 ++------ 9 files changed, 100 insertions(+), 159 deletions(-) diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr index 47944a7ee..d2a486680 100644 --- a/tests/test_cli/__snapshots__/test_explain.ambr +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -22,6 +22,32 @@ some_element, _, __ = some_tuple() .. versionadded:: 0.12.0 + See at website: https://pyflak.es/WPS123 + + ''' +# --- +# name: test_command_on_not_found[wps explain 10000] + ''' + Violation not found + + ''' +# --- +# name: test_command_on_not_found[wps explain NOT_A_CODE] + ''' + Violation not found + + ''' +# --- +# name: test_command_on_not_found[wps explain WPS10000] + ''' + Violation not found + + ''' +# --- +# name: test_no_command_specified + ''' + Command not specified. Usage: wps help + ''' # --- diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index d02e79392..dce3f4201 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,17 +1,14 @@ """Test that wps explain command works fine.""" - -from io import BytesIO -from typing import TextIO +import os +import platform +import subprocess import pytest -from wemake_python_styleguide.cli import cli_app -from wemake_python_styleguide.cli.application import Application from wemake_python_styleguide.cli.commands.explain import ( message_formatter, violation_loader, ) -from wemake_python_styleguide.cli.output import BufferedStreamWriter, Writable from wemake_python_styleguide.violations.best_practices import ( InitModuleHasLogicViolation, ) @@ -65,7 +62,7 @@ def test_indentation_removal(test_params): section='mock', ) violation_string = ( - 'docstring\nSee at website: https://pyflak.es/WPS100' + 'docstring\n\nSee at website: https://pyflak.es/WPS100' ) @@ -75,80 +72,52 @@ def test_formatter(): assert formatted == violation_string -class MockWriter(Writable): - """Writer for testing.""" - - def __init__(self): - """Create writer.""" - self.out = '' - self.err = '' - - def write_out(self, *args) -> None: - """Write stdout.""" - self.out += ' '.join(map(str, args)) - - def write_err(self, *args) -> None: - """Write stderr.""" - self.err += ' '.join(map(str, args)) - - def flush(self) -> None: - """Blank method. Flushing not needed.""" +def _popen_in_shell(args: str) -> subprocess.Popen: # pragma: no cover + """Run command in shell.""" + encoding = 'utf-8' + # Some encoding magic. Calling with shell=True on Windows + # causes everything to be in cp1251. shell=True is needed + # for subprocess.Popen to locate the installed wps command. + if platform.system() == 'Windows': + encoding = 'cp1251' + return subprocess.Popen( # noqa: S602 (insecure shell=True) + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding=encoding, + env=os.environ, + shell=True, + ) def test_command(snapshot): """Test that command works and formats violations as expected.""" - writer = MockWriter() - application = Application(writer) - args = cli_app.parse_args(['explain', 'WPS123'], application) - application.run_explain(args) - assert writer.out == snapshot + process = _popen_in_shell('wps explain WPS123') + stdout, stderr = process.communicate() + assert process.returncode == 0, (stdout, stderr) + assert stdout == snapshot @pytest.mark.parametrize( - 'arguments', + 'command', [ - 'explain 10000', - 'explain NOT_A_CODE', - 'explain WPS10000', + 'wps explain 10000', + 'wps explain NOT_A_CODE', + 'wps explain WPS10000', ], ) -def test_command_on_not_found(arguments): +def test_command_on_not_found(command, snapshot): """Test command works when violation code is wrong.""" - writer = MockWriter() - application = Application(writer) - args = cli_app.parse_args(arguments.split(), application) - application.run_explain(args) - assert writer.err.strip() == 'Violation not found' - - -class MockBufferedStringIO(TextIO): - """IO for testing BufferedStreamWriter.""" - - def __init__(self): - """Create IO.""" - self._buffer = BytesIO() - - @property - def buffer(self): - """Get IO buffer.""" - return self._buffer - - def flush(self): - """Flush buffer.""" - self._buffer.flush() - - def get_string(self) -> str: - """Get string value written into buffer.""" - return self._buffer.getvalue().decode() - - -def test_buffered_stream_writer(): - """Test that stream writer works as expected.""" - io_out = MockBufferedStringIO() - io_err = MockBufferedStringIO() - writer = BufferedStreamWriter(io_out, io_err) - writer.write_out('Test', 'text') - writer.write_err('Test', 'error') - writer.flush() - assert io_out.get_string() == 'Test text\n' - assert io_err.get_string() == 'Test error\n' + process = _popen_in_shell(command) + stdout, stderr = process.communicate() + assert process.returncode == 1, (stdout, stderr) + assert stderr == snapshot + + +def test_no_command_specified(snapshot): + """Test command displays error message when no subcommand provided.""" + process = _popen_in_shell('wps') + stdout, stderr = process.communicate() + assert process.returncode == 1, (stdout, stderr) + assert stderr == snapshot diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index 3859f3c59..53829ed53 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -2,26 +2,13 @@ from typing import final -from wemake_python_styleguide.cli.commands.base import AbstractCommand from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand -from wemake_python_styleguide.cli.output import Writable @final class Application: """WPS CLI application class.""" - def __init__(self, writer: Writable) -> None: - """Create application.""" - self._writer = writer - def run_explain(self, args) -> int: """Run explain command.""" - return self._get_command(ExplainCommand).run(args) - - def _get_command( - self, - command_class: type[AbstractCommand], - ) -> AbstractCommand: - """Create command from its class and inject the selected writer.""" - return command_class(writer=self._writer) + return ExplainCommand().run(args) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 8fd06b7cf..a60c956a9 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from wemake_python_styleguide.cli.application import Application -from wemake_python_styleguide.cli.output import BufferedStreamWriter +from wemake_python_styleguide.cli.output import print_stderr def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: @@ -36,7 +36,10 @@ def parse_args(args: Sequence[str], app: Application) -> argparse.Namespace: def main() -> int: # pragma: no cover """Main function.""" - app = Application(BufferedStreamWriter(sys.stdout, sys.stderr)) + app = Application() + if len(sys.argv) == 1: + print_stderr('Command not specified. Usage: wps help') + return 1 args = parse_args(sys.argv[1:], app) return int(args.func(args)) diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py index 284fcd496..d66b0cc21 100644 --- a/wemake_python_styleguide/cli/commands/base.py +++ b/wemake_python_styleguide/cli/commands/base.py @@ -2,16 +2,10 @@ from abc import ABC, abstractmethod -from wemake_python_styleguide.cli.output import Writable - class AbstractCommand(ABC): """ABC for all commands.""" - def __init__(self, writer: Writable) -> None: - """Create a command and define its writer.""" - self.writer = writer - @abstractmethod def run(self, args) -> int: """Run the command.""" diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index 963f31eb5..d60b0e44e 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -5,6 +5,7 @@ message_formatter, violation_loader, ) +from wemake_python_styleguide.cli.output import print_stderr, print_stdout def _clean_violation_code(violation_str: str) -> int: @@ -36,10 +37,8 @@ def run(self, args) -> int: code = _clean_violation_code(args.violation_code) violation = violation_loader.get_violation(code) if violation is None: - self.writer.write_err('Violation not found') - self.writer.flush() + print_stderr('Violation not found') return 1 message = message_formatter.format_violation(violation) - self.writer.write_out(message) - self.writer.flush() + print_stdout(message) return 0 diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 8dfdb484b..1c41101cf 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -66,6 +66,6 @@ def format_violation(violation: ViolationInfo) -> str: cleaned_docstring = _remove_newlines_at_ends(cleaned_docstring) violation_url = _DOCS_URL.format(f'WPS{violation.code}') return ( - f'{cleaned_docstring}\n' + f'{cleaned_docstring}\n\n' f'See at website: {violation_url}' ) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index d068da226..7aabc4a47 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -6,6 +6,8 @@ from types import ModuleType from typing import Final, final +from attrs import frozen + from wemake_python_styleguide.violations.base import BaseViolation _VIOLATION_SUBMODULES: Final = ( @@ -21,28 +23,22 @@ @final +@frozen class ViolationInfo: """Contains violation info.""" - - def __init__( - self, - identifier: str, - fully_qualified_id: str, - code: int, - docstring: str, - section: str, - ) -> None: - """Create dataclass.""" - self.identifier = identifier - self.fully_qualified_id = fully_qualified_id - self.code = code - self.docstring = docstring - self.section = section + identifier: str + fully_qualified_id: str + code: int + docstring: str + section: str def _is_a_violation(class_object) -> bool: - """Dumb check if class is a violation class.""" - return hasattr(class_object, 'code') + """Check if class is a violation class.""" + return ( + issubclass(class_object, BaseViolation) and + hasattr(class_object, 'code') # Not all subclasses have code + ) def _get_violations_of_submodule( diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py index dc9f38608..452920889 100644 --- a/wemake_python_styleguide/cli/output.py +++ b/wemake_python_styleguide/cli/output.py @@ -1,47 +1,14 @@ """Provides tool for outputting data.""" +import sys -from abc import abstractmethod -from typing import Protocol, TextIO +def print_stdout(*args: str) -> None: + """Write usual text. Works as print.""" + sys.stdout.write(' '.join(args)) + sys.stdout.write('\n') -class Writable(Protocol): - """Interface for outputting text data.""" - @abstractmethod - def write_out(self, *args) -> None: - """Write usual text. Works as print.""" - - @abstractmethod - def write_err(self, *args) -> None: - """Write error text. Works as print.""" - - @abstractmethod - def flush(self) -> None: - """Flush all outputs.""" - - -class BufferedStreamWriter(Writable): - """Writes to provided buffered text streams.""" - - def __init__( - self, out_stream: TextIO, err_stream: TextIO, newline_sym: str = '\n' - ): - """Create stream writer.""" - self._out = out_stream - self._err = err_stream - self._newline = newline_sym.encode() - - def write_out(self, *args) -> None: - """Write usual text. Works as print.""" - self._out.buffer.write(' '.join(args).encode()) - self._out.buffer.write(self._newline) - - def write_err(self, *args) -> None: - """Write error text. Works as print.""" - self._err.buffer.write(' '.join(args).encode()) - self._err.buffer.write(self._newline) - - def flush(self) -> None: - """Flush all outputs.""" - self._out.flush() - self._err.flush() +def print_stderr(*args: str) -> None: + """Write error text. Works as print.""" + sys.stderr.write(' '.join(args)) + sys.stderr.write('\n') From 4392a6ff7b0b1627d5301329f67e607b83a2c28f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:44:51 +0000 Subject: [PATCH 30/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/test_explain.py | 1 + .../cli/commands/explain/violation_loader.py | 5 +++-- wemake_python_styleguide/cli/output.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 25c92933b..04ef31813 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,4 +1,5 @@ """Test that wps explain command works fine.""" + import os import platform import subprocess diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 7aabc4a47..124029c67 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -26,6 +26,7 @@ @frozen class ViolationInfo: """Contains violation info.""" + identifier: str fully_qualified_id: str code: int @@ -36,8 +37,8 @@ class ViolationInfo: def _is_a_violation(class_object) -> bool: """Check if class is a violation class.""" return ( - issubclass(class_object, BaseViolation) and - hasattr(class_object, 'code') # Not all subclasses have code + issubclass(class_object, BaseViolation) + and hasattr(class_object, 'code') # Not all subclasses have code ) diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py index 452920889..162c1f073 100644 --- a/wemake_python_styleguide/cli/output.py +++ b/wemake_python_styleguide/cli/output.py @@ -1,4 +1,5 @@ """Provides tool for outputting data.""" + import sys From 88f7f824fe348d9b08f4671d645313644a32ecb9 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Fri, 24 Jan 2025 18:01:45 +0500 Subject: [PATCH 31/44] Remove unused pragma no cover from cli_app.py --- wemake_python_styleguide/cli/cli_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index a60c956a9..b95257b95 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -34,7 +34,7 @@ def parse_args(args: Sequence[str], app: Application) -> argparse.Namespace: return parser.parse_args(args) -def main() -> int: # pragma: no cover +def main() -> int: """Main function.""" app = Application() if len(sys.argv) == 1: From 2978cd77e77c33806f720fe3114f1f8c5f9387a0 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 25 Jan 2025 16:48:12 +0500 Subject: [PATCH 32/44] Delegate no argument corner-case to argparse. Move doc link to constants (formatter.py also affected). Move from using hardcoded module names to module discovery with glob. --- .../test_cli/__snapshots__/test_explain.ambr | 9 ++-- tests/test_cli/test_explain.py | 3 +- wemake_python_styleguide/cli/__init__.py | 1 - wemake_python_styleguide/cli/cli_app.py | 12 ++--- .../cli/commands/__init__.py | 1 - .../cli/commands/explain/__init__.py | 1 - .../cli/commands/explain/command.py | 16 +------ .../cli/commands/explain/message_formatter.py | 22 +++------ .../cli/commands/explain/module_loader.py | 47 +++++++++++++++++++ .../cli/commands/explain/violation_loader.py | 34 ++++---------- wemake_python_styleguide/cli/output.py | 2 + wemake_python_styleguide/constants.py | 3 ++ wemake_python_styleguide/formatter.py | 6 +-- 13 files changed, 81 insertions(+), 76 deletions(-) create mode 100644 wemake_python_styleguide/cli/commands/explain/module_loader.py diff --git a/tests/test_cli/__snapshots__/test_explain.ambr b/tests/test_cli/__snapshots__/test_explain.ambr index d2a486680..71d3e1418 100644 --- a/tests/test_cli/__snapshots__/test_explain.ambr +++ b/tests/test_cli/__snapshots__/test_explain.ambr @@ -29,25 +29,26 @@ # --- # name: test_command_on_not_found[wps explain 10000] ''' - Violation not found + Violation "10000" not found ''' # --- # name: test_command_on_not_found[wps explain NOT_A_CODE] ''' - Violation not found + Violation "NOT_A_CODE" not found ''' # --- # name: test_command_on_not_found[wps explain WPS10000] ''' - Violation not found + Violation "WPS10000" not found ''' # --- # name: test_no_command_specified ''' - Command not specified. Usage: wps help + usage: wps [-h] {explain} ... + wps: error: the following arguments are required: {explain} ''' # --- diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 04ef31813..fdfa1c675 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -59,7 +59,6 @@ def test_indentation_removal(test_params): identifier='Mock', code=100, docstring='docstring', - fully_qualified_id='mock.Mock', section='mock', ) violation_string = 'docstring\n\nSee at website: https://pyflak.es/WPS100' @@ -118,5 +117,5 @@ def test_no_command_specified(snapshot): """Test command displays error message when no subcommand provided.""" process = _popen_in_shell('wps') stdout, stderr = process.communicate() - assert process.returncode == 1, (stdout, stderr) + assert process.returncode != 0, (stdout, stderr) assert stderr == snapshot diff --git a/wemake_python_styleguide/cli/__init__.py b/wemake_python_styleguide/cli/__init__.py index e08834af0..e69de29bb 100644 --- a/wemake_python_styleguide/cli/__init__.py +++ b/wemake_python_styleguide/cli/__init__.py @@ -1 +0,0 @@ -"""Contains all files related to WPS CLI utility.""" diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index b95257b95..773e30927 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -2,10 +2,8 @@ import argparse import sys -from collections.abc import Sequence from wemake_python_styleguide.cli.application import Application -from wemake_python_styleguide.cli.output import print_stderr def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: @@ -14,6 +12,7 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: prog='wps', description='WPS command line tool' ) sub_parsers = parser.add_subparsers(help='sub-command help') + sub_parsers.required = True parser_explain = sub_parsers.add_parser( 'explain', @@ -28,19 +27,16 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: return parser -def parse_args(args: Sequence[str], app: Application) -> argparse.Namespace: +def parse_args(app: Application) -> argparse.Namespace: """Parse CLI arguments.""" parser = _configure_arg_parser(app) - return parser.parse_args(args) + return parser.parse_args() def main() -> int: """Main function.""" app = Application() - if len(sys.argv) == 1: - print_stderr('Command not specified. Usage: wps help') - return 1 - args = parse_args(sys.argv[1:], app) + args = parse_args(app) return int(args.func(args)) diff --git a/wemake_python_styleguide/cli/commands/__init__.py b/wemake_python_styleguide/cli/commands/__init__.py index e7f103bea..e69de29bb 100644 --- a/wemake_python_styleguide/cli/commands/__init__.py +++ b/wemake_python_styleguide/cli/commands/__init__.py @@ -1 +0,0 @@ -"""Contains all files related to wps console commands.""" diff --git a/wemake_python_styleguide/cli/commands/explain/__init__.py b/wemake_python_styleguide/cli/commands/explain/__init__.py index 4774bf5fc..e69de29bb 100644 --- a/wemake_python_styleguide/cli/commands/explain/__init__.py +++ b/wemake_python_styleguide/cli/commands/explain/__init__.py @@ -1 +0,0 @@ -"""Contains files related to wps explain command.""" diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index d60b0e44e..50d735110 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -9,19 +9,7 @@ def _clean_violation_code(violation_str: str) -> int: - """ - Get int violation code from str violation code. - - Args: - violation_str: violation code expressed as string - WPS412, 412 - both acceptable - - Returns: - integer violation code - - Throws: - ValueError: violation str is not an integer (except WPS prefix). - """ + """Get int violation code from str violation code.""" violation_str = violation_str.removeprefix('WPS') try: return int(violation_str) @@ -37,7 +25,7 @@ def run(self, args) -> int: code = _clean_violation_code(args.violation_code) violation = violation_loader.get_violation(code) if violation is None: - print_stderr('Violation not found') + print_stderr(f'Violation "{args.violation_code}" not found') return 1 message = message_formatter.format_violation(violation) print_stdout(message) diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index da64471eb..e8bbf4877 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -1,24 +1,13 @@ """Provides tools for formatting explanations.""" -from typing import Final - from wemake_python_styleguide.cli.commands.explain.violation_loader import ( ViolationInfo, ) - -_DOCS_URL: Final = 'https://pyflak.es/{0}' +from wemake_python_styleguide.constants import SHORTLINK_TEMPLATE def _clean_text(text: str) -> str: - """ - Cleans provided text. - - Args: - text: target text - - Returns: - text with normalized newlines (CRs and CRLFs transformed to LFs). - """ + """Normalize line endings and clean text.""" return text.replace('\r\n', '\n').replace('\r', '\n') @@ -62,7 +51,8 @@ def _remove_newlines_at_ends(text: str) -> str: def format_violation(violation: ViolationInfo) -> str: """Format violation information.""" - cleaned_docstring = _remove_indentation(violation.docstring) - cleaned_docstring = _remove_newlines_at_ends(cleaned_docstring) - violation_url = _DOCS_URL.format(f'WPS{violation.code}') + cleaned_docstring = _remove_newlines_at_ends( + _remove_indentation(violation.docstring) + ) + violation_url = SHORTLINK_TEMPLATE.format(f'WPS{violation.code}') return f'{cleaned_docstring}\n\nSee at website: {violation_url}' diff --git a/wemake_python_styleguide/cli/commands/explain/module_loader.py b/wemake_python_styleguide/cli/commands/explain/module_loader.py new file mode 100644 index 000000000..439b98c98 --- /dev/null +++ b/wemake_python_styleguide/cli/commands/explain/module_loader.py @@ -0,0 +1,47 @@ +import importlib +from collections.abc import Collection +from contextlib import suppress +from pathlib import Path +from types import ModuleType +from typing import Final + +_VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' + + +def get_violation_submodules() -> Collection[ModuleType]: + """Get all possible violation submodules.""" + return _safely_get_all_submodules(_VIOLATION_MODULE_BASE) + + +def _safely_get_all_submodules(module_name: str) -> Collection[ModuleType]: + """Get all submodules of given module. Ignore missing.""" + submodule_names = _get_all_possible_submodule_names(module_name) + modules = [] + for submodule_name in submodule_names: + # just in case if there are some bad module paths + # (which generally should not happen) + with suppress(ModuleNotFoundError): + modules.append(importlib.import_module(submodule_name)) + return modules + + +def _get_all_possible_submodule_names(module_name: str) -> Collection[str]: + """Get .py submodule names listed in given module.""" + root_module = importlib.import_module(module_name) + root_paths = root_module.__path__ + names = [] + for root in root_paths: + names.extend([ + f'{module_name}.{name}' + for name in _get_all_possible_names_in_root(root) + ]) + return names + + +def _get_all_possible_names_in_root(root: str) -> Collection[str]: + """Get .py submodule names listed in given root path.""" + return [ + path.name.removesuffix('.py') + for path in Path(root).glob('*.py') + if '__' not in path.name # filter dunder files like __init__.py + ] diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 124029c67..3e47e2a6e 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -1,25 +1,15 @@ """Provides tools to extract violation info.""" - -import importlib import inspect from collections.abc import Collection, Mapping from types import ModuleType -from typing import Final, final +from typing import final from attrs import frozen -from wemake_python_styleguide.violations.base import BaseViolation - -_VIOLATION_SUBMODULES: Final = ( - 'best_practices', - 'complexity', - 'consistency', - 'naming', - 'oop', - 'refactoring', - 'system', +from wemake_python_styleguide.cli.commands.explain.module_loader import ( + get_violation_submodules, ) -_VIOLATION_MODULE_BASE: Final = 'wemake_python_styleguide.violations' +from wemake_python_styleguide.violations.base import BaseViolation @final @@ -28,7 +18,6 @@ class ViolationInfo: """Contains violation info.""" identifier: str - fully_qualified_id: str code: int docstring: str section: str @@ -38,7 +27,7 @@ def _is_a_violation(class_object) -> bool: """Check if class is a violation class.""" return ( issubclass(class_object, BaseViolation) - and hasattr(class_object, 'code') # Not all subclasses have code + and hasattr(class_object, 'code') # Only end-user classes have code ) @@ -54,12 +43,11 @@ def _get_violations_of_submodule( def _create_violation_info( - class_object, submodule_name: str, submodule_path: str + class_object, submodule_name: str ) -> ViolationInfo: """Create violation info DTO from violation class and metadata.""" return ViolationInfo( identifier=class_object.__name__, - fully_qualified_id=f'{submodule_path}.{class_object.__name__}', code=class_object.code, docstring=class_object.__doc__, section=submodule_name, @@ -69,16 +57,12 @@ def _create_violation_info( def _get_all_violations() -> Mapping[int, ViolationInfo]: """Get all violations inside all defined WPS violation modules.""" all_violations = {} - for submodule_name in _VIOLATION_SUBMODULES: - submodule_path = f'{_VIOLATION_MODULE_BASE}.{submodule_name}' - violations = _get_violations_of_submodule( - importlib.import_module(submodule_path) - ) + for submodule in get_violation_submodules(): + violations = _get_violations_of_submodule(submodule) for violation in violations: all_violations[violation.code] = _create_violation_info( violation, - submodule_name, - submodule_path, + submodule.__name__, ) return all_violations diff --git a/wemake_python_styleguide/cli/output.py b/wemake_python_styleguide/cli/output.py index 162c1f073..b4458157b 100644 --- a/wemake_python_styleguide/cli/output.py +++ b/wemake_python_styleguide/cli/output.py @@ -7,9 +7,11 @@ def print_stdout(*args: str) -> None: """Write usual text. Works as print.""" sys.stdout.write(' '.join(args)) sys.stdout.write('\n') + sys.stdout.flush() def print_stderr(*args: str) -> None: """Write error text. Works as print.""" sys.stderr.write(' '.join(args)) sys.stderr.write('\n') + sys.stderr.flush() diff --git a/wemake_python_styleguide/constants.py b/wemake_python_styleguide/constants.py index 72b2b5fe3..3fdfcd40f 100644 --- a/wemake_python_styleguide/constants.py +++ b/wemake_python_styleguide/constants.py @@ -43,6 +43,9 @@ # Values beyond this line are public and should be used. # ------------------------------------------------------ +#: This url points to the specific violation page. +SHORTLINK_TEMPLATE: Final = 'https://pyflak.es/{0}' + #: List of functions we forbid to use. FUNCTIONS_BLACKLIST: Final = frozenset( ( diff --git a/wemake_python_styleguide/formatter.py b/wemake_python_styleguide/formatter.py index d0a6f55a3..b85f2cd7c 100644 --- a/wemake_python_styleguide/formatter.py +++ b/wemake_python_styleguide/formatter.py @@ -36,6 +36,7 @@ from pygments.formatters import TerminalFormatter from pygments.lexers import PythonLexer +from wemake_python_styleguide.constants import SHORTLINK_TEMPLATE from wemake_python_styleguide.version import pkg_version #: That url is generated and hosted by Sphinx. @@ -43,9 +44,6 @@ 'https://wemake-python-styleguide.rtfd.io/en/{0}/pages/usage/violations/' ) -#: This url points to the specific violation page. -_SHORTLINK_TEMPLATE: Final = 'https://pyflak.es/{0}' - #: Option to disable any code highlight and text output format. #: See https://no-color.org _NO_COLOR: Final = os.environ.get('NO_COLOR', '0') == '1' @@ -162,7 +160,7 @@ def _show_link(self, error: Violation) -> str: return ' {spacing}-> {link}'.format( spacing=' ' * 9, - link=_SHORTLINK_TEMPLATE.format(error.code), + link=SHORTLINK_TEMPLATE.format(error.code), ) def _print_header(self, filename: str) -> None: From 9ea66b733ac3dcf8bd963fdebca5027acbe69b86 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 11:50:41 +0000 Subject: [PATCH 33/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- .../cli/commands/explain/violation_loader.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 3e47e2a6e..81323a73e 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -1,4 +1,5 @@ """Provides tools to extract violation info.""" + import inspect from collections.abc import Collection, Mapping from types import ModuleType @@ -42,9 +43,7 @@ def _get_violations_of_submodule( ] -def _create_violation_info( - class_object, submodule_name: str -) -> ViolationInfo: +def _create_violation_info(class_object, submodule_name: str) -> ViolationInfo: """Create violation info DTO from violation class and metadata.""" return ViolationInfo( identifier=class_object.__name__, From 82100b28658d398be7181b3c57193644ad790b21 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 25 Jan 2025 16:57:56 +0500 Subject: [PATCH 34/44] Update conditions so python 3.10 tests pass too --- .../cli/commands/explain/violation_loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 3e47e2a6e..a8189291a 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -26,7 +26,8 @@ class ViolationInfo: def _is_a_violation(class_object) -> bool: """Check if class is a violation class.""" return ( - issubclass(class_object, BaseViolation) + isinstance(class_object, type) # py 3.10 tests don't pass w/o that + and issubclass(class_object, BaseViolation) and hasattr(class_object, 'code') # Only end-user classes have code ) From 8c68b6d3326a3c8f275ad16c7f3515f078193cf4 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 25 Jan 2025 17:49:37 +0500 Subject: [PATCH 35/44] Update so python 3.10 tests pass too --- .../cli/commands/explain/violation_loader.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index 5a41fe012..f4c739da2 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -26,11 +26,13 @@ class ViolationInfo: def _is_a_violation(class_object) -> bool: """Check if class is a violation class.""" - return ( - isinstance(class_object, type) # py 3.10 tests don't pass w/o that - and issubclass(class_object, BaseViolation) - and hasattr(class_object, 'code') # Only end-user classes have code - ) + try: + return ( + issubclass(class_object, BaseViolation) + and hasattr(class_object, 'code') # Only end-user classes have code + ) + except TypeError: # py 3.10 bug raises a type error + return False def _get_violations_of_submodule( From 2a54118b99ca1a64970f14c3fa4ff68fddf2b2db Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 25 Jan 2025 17:54:24 +0500 Subject: [PATCH 36/44] Add pragma no cover to previous fix as it only runs on python 3.10 --- .../cli/commands/explain/violation_loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wemake_python_styleguide/cli/commands/explain/violation_loader.py b/wemake_python_styleguide/cli/commands/explain/violation_loader.py index f4c739da2..babe8a65a 100644 --- a/wemake_python_styleguide/cli/commands/explain/violation_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/violation_loader.py @@ -31,7 +31,8 @@ def _is_a_violation(class_object) -> bool: issubclass(class_object, BaseViolation) and hasattr(class_object, 'code') # Only end-user classes have code ) - except TypeError: # py 3.10 bug raises a type error + except TypeError: # pragma: no cover + # py 3.10 bug raises a type error return False From b03371eca5f7bb890cd99ddd780b7f4da9a40989 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 25 Jan 2025 20:17:44 +0500 Subject: [PATCH 37/44] Separate unit and integration tests. Delegate indentation removal to textwrap library --- tests/test_cli/test_explain.py | 97 +++---------------- tests/test_cli/test_explain_internals.py | 64 ++++++++++++ wemake_python_styleguide/cli/cli_app.py | 5 - .../cli/commands/explain/message_formatter.py | 28 +----- 4 files changed, 83 insertions(+), 111 deletions(-) create mode 100644 tests/test_cli/test_explain_internals.py diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index fdfa1c675..1a4630f9f 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,100 +1,34 @@ -"""Test that wps explain command works fine.""" +"""Integration testing of wps explain command.""" -import os -import platform import subprocess import pytest -from wemake_python_styleguide.cli.commands.explain import ( - message_formatter, - violation_loader, -) -from wemake_python_styleguide.violations.best_practices import ( - InitModuleHasLogicViolation, -) -from wemake_python_styleguide.violations.naming import ( - UpperCaseAttributeViolation, -) -from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation - - -@pytest.mark.parametrize( - 'violation_params', - [ - (115, UpperCaseAttributeViolation), - (412, InitModuleHasLogicViolation), - (600, BuiltinSubclassViolation), - ], -) -def test_violation_getter(violation_params): - """Test that violation loader can get violation by their codes.""" - violation_code, expected_class = violation_params - violation = violation_loader.get_violation(violation_code) - assert violation.code is not None - assert violation.docstring == expected_class.__doc__ - -@pytest.mark.parametrize( - 'test_params', - [ - (' text\n text\n text', 'text\ntext\ntext'), - (' text\n\ttext\r\n text', 'text\n text\ntext'), - (' text\n \n\n text', 'text\n \n\ntext'), - ('\n\n', '\n\n'), - ('text', 'text'), - ('text\ntext', 'text\ntext'), - ('', ''), - (' ', ' '), - ], -) -def test_indentation_removal(test_params): - """Test that indentation remover works in different conditions.""" - input_text, expected = test_params - actual = message_formatter._remove_indentation(input_text) # noqa: SLF001 - assert actual == expected - - -violation_mock = violation_loader.ViolationInfo( - identifier='Mock', - code=100, - docstring='docstring', - section='mock', -) -violation_string = 'docstring\n\nSee at website: https://pyflak.es/WPS100' - - -def test_formatter(): - """Test that formatter formats violations as expected.""" - formatted = message_formatter.format_violation(violation_mock) - assert formatted == violation_string - - -def _popen_in_shell(args: str) -> subprocess.Popen: # pragma: no cover +def _popen_in_shell( + args: str +) -> tuple[subprocess.Popen, str, str]: """Run command in shell.""" - encoding = 'utf-8' - # Some encoding magic. Calling with shell=True on Windows - # causes everything to be in cp1251. shell=True is needed - # for subprocess.Popen to locate the installed wps command. - if platform.system() == 'Windows': - encoding = 'cp1251' - return subprocess.Popen( # noqa: S602 (insecure shell=True) + # shell=True is needed for subprocess.Popen to + # locate the installed wps command. + process = subprocess.Popen( # noqa: S602 (insecure shell=True) args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, - encoding=encoding, - env=os.environ, + text=True, shell=True, ) + stdin, stdout = process.communicate() + return process, stdin, stdout def test_command(snapshot): """Test that command works and formats violations as expected.""" - process = _popen_in_shell('wps explain WPS123') - stdout, stderr = process.communicate() + process, stdout, stderr = _popen_in_shell('wps explain WPS123') assert process.returncode == 0, (stdout, stderr) assert stdout == snapshot + assert not stderr @pytest.mark.parametrize( @@ -107,15 +41,16 @@ def test_command(snapshot): ) def test_command_on_not_found(command, snapshot): """Test command works when violation code is wrong.""" - process = _popen_in_shell(command) - stdout, stderr = process.communicate() + process, stdout, stderr = _popen_in_shell(command) assert process.returncode == 1, (stdout, stderr) + assert not stdout assert stderr == snapshot def test_no_command_specified(snapshot): """Test command displays error message when no subcommand provided.""" - process = _popen_in_shell('wps') + process, stdout, stderr = _popen_in_shell('wps') stdout, stderr = process.communicate() assert process.returncode != 0, (stdout, stderr) + assert not stdout assert stderr == snapshot diff --git a/tests/test_cli/test_explain_internals.py b/tests/test_cli/test_explain_internals.py new file mode 100644 index 000000000..e03e93e81 --- /dev/null +++ b/tests/test_cli/test_explain_internals.py @@ -0,0 +1,64 @@ +"""Unit testing of wps explain command.""" + +import pytest + +from wemake_python_styleguide.cli.commands.explain import ( + message_formatter, + violation_loader, +) +from wemake_python_styleguide.violations.best_practices import ( + InitModuleHasLogicViolation, +) +from wemake_python_styleguide.violations.naming import ( + UpperCaseAttributeViolation, +) +from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation + + +@pytest.mark.parametrize( + 'violation_params', + [ + (115, UpperCaseAttributeViolation), + (412, InitModuleHasLogicViolation), + (600, BuiltinSubclassViolation), + ], +) +def test_violation_getter(violation_params): + """Test that violation loader can get violation by their codes.""" + violation_code, expected_class = violation_params + violation = violation_loader.get_violation(violation_code) + assert violation.code is not None + assert violation.docstring == expected_class.__doc__ + + +@pytest.mark.parametrize( + 'test_params', + [ + (' text\n text\n text', 'text\ntext\ntext'), + (' text\n\ttext\r\n text', 'text\n text\ntext'), + (' text\n \n\n text', 'text\n\n\ntext'), + ('\n\n', '\n\n'), + ('text', 'text'), + ('text\ntext', 'text\ntext'), + ('', ''), + (' ', ''), + ], +) +def test_indentation_removal(test_params): + """Test that indentation remover works in different conditions.""" + input_text, expected = test_params + actual = message_formatter._remove_indentation(input_text) # noqa: SLF001 + assert actual == expected + + +def test_formatter(): + """Test that formatter formats violations as expected.""" + violation_mock = violation_loader.ViolationInfo( + identifier='Mock', + code=100, + docstring='docstring', + section='mock', + ) + violation_string = 'docstring\n\nSee at website: https://pyflak.es/WPS100' + formatted = message_formatter.format_violation(violation_mock) + assert formatted == violation_string diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 773e30927..7e71c351f 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -1,7 +1,6 @@ """Main CLI utility file.""" import argparse -import sys from wemake_python_styleguide.cli.application import Application @@ -38,7 +37,3 @@ def main() -> int: app = Application() args = parse_args(app) return int(args.func(args)) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index e8bbf4877..49e7f9d16 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -1,4 +1,5 @@ """Provides tools for formatting explanations.""" +import textwrap from wemake_python_styleguide.cli.commands.explain.violation_loader import ( ViolationInfo, @@ -16,32 +17,9 @@ def _replace_tabs(text: str, tab_size: int = 4) -> str: return text.replace('\t', ' ' * tab_size) -def _get_whitespace_prefix(line: str) -> int | float: - """Get length of whitespace prefix of string.""" - for char_index, char in enumerate(line): - if char != ' ': - return char_index - return float('+inf') - - -def _get_greatest_common_indent(text: str) -> int: - """Get the greatest common whitespace prefix length of all lines.""" - lines = text.split('\n') - greatest_common_indent = float('+inf') - for line in lines: - greatest_common_indent = min( - greatest_common_indent, _get_whitespace_prefix(line) - ) - if isinstance(greatest_common_indent, float): - greatest_common_indent = 0 - return greatest_common_indent - - def _remove_indentation(text: str, tab_size: int = 4) -> str: """Remove excessive indentation.""" - text = _replace_tabs(_clean_text(text), tab_size) - max_indent = _get_greatest_common_indent(text) - return '\n'.join(line[max_indent:] for line in text.split('\n')) + return textwrap.dedent(_replace_tabs(_clean_text(text), tab_size)) def _remove_newlines_at_ends(text: str) -> str: @@ -52,7 +30,7 @@ def _remove_newlines_at_ends(text: str) -> str: def format_violation(violation: ViolationInfo) -> str: """Format violation information.""" cleaned_docstring = _remove_newlines_at_ends( - _remove_indentation(violation.docstring) + textwrap.dedent(violation.docstring) ) violation_url = SHORTLINK_TEMPLATE.format(f'WPS{violation.code}') return f'{cleaned_docstring}\n\nSee at website: {violation_url}' From 2969b6c14d52fd2e53fb848011399b27d3fb7775 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 15:18:15 +0000 Subject: [PATCH 38/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/test_cli/test_explain.py | 4 +--- .../cli/commands/explain/message_formatter.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index 1a4630f9f..5abdeacec 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -5,9 +5,7 @@ import pytest -def _popen_in_shell( - args: str -) -> tuple[subprocess.Popen, str, str]: +def _popen_in_shell(args: str) -> tuple[subprocess.Popen, str, str]: """Run command in shell.""" # shell=True is needed for subprocess.Popen to # locate the installed wps command. diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 49e7f9d16..b7c8580c2 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -1,4 +1,5 @@ """Provides tools for formatting explanations.""" + import textwrap from wemake_python_styleguide.cli.commands.explain.violation_loader import ( From 97694488ada58bf3f5ddfd7f97fb5ede8cb5a1c4 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Sat, 25 Jan 2025 20:17:44 +0500 Subject: [PATCH 39/44] Separate unit and integration tests. Delegate indentation removal to textwrap library. Add typed arguments for subcommands --- tests/test_cli/test_explain.py | 98 +++---------------- tests/test_cli/test_explain_internals.py | 30 ++++++ wemake_python_styleguide/cli/application.py | 29 +++++- wemake_python_styleguide/cli/cli_app.py | 17 ++-- wemake_python_styleguide/cli/commands/base.py | 13 ++- .../cli/commands/explain/command.py | 14 ++- .../cli/commands/explain/message_formatter.py | 41 +------- 7 files changed, 101 insertions(+), 141 deletions(-) create mode 100644 tests/test_cli/test_explain_internals.py diff --git a/tests/test_cli/test_explain.py b/tests/test_cli/test_explain.py index fdfa1c675..5c7e7451b 100644 --- a/tests/test_cli/test_explain.py +++ b/tests/test_cli/test_explain.py @@ -1,100 +1,33 @@ -"""Test that wps explain command works fine.""" +"""Integration testing of wps explain command.""" -import os -import platform import subprocess import pytest -from wemake_python_styleguide.cli.commands.explain import ( - message_formatter, - violation_loader, -) -from wemake_python_styleguide.violations.best_practices import ( - InitModuleHasLogicViolation, -) -from wemake_python_styleguide.violations.naming import ( - UpperCaseAttributeViolation, -) -from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation - - -@pytest.mark.parametrize( - 'violation_params', - [ - (115, UpperCaseAttributeViolation), - (412, InitModuleHasLogicViolation), - (600, BuiltinSubclassViolation), - ], -) -def test_violation_getter(violation_params): - """Test that violation loader can get violation by their codes.""" - violation_code, expected_class = violation_params - violation = violation_loader.get_violation(violation_code) - assert violation.code is not None - assert violation.docstring == expected_class.__doc__ - -@pytest.mark.parametrize( - 'test_params', - [ - (' text\n text\n text', 'text\ntext\ntext'), - (' text\n\ttext\r\n text', 'text\n text\ntext'), - (' text\n \n\n text', 'text\n \n\ntext'), - ('\n\n', '\n\n'), - ('text', 'text'), - ('text\ntext', 'text\ntext'), - ('', ''), - (' ', ' '), - ], -) -def test_indentation_removal(test_params): - """Test that indentation remover works in different conditions.""" - input_text, expected = test_params - actual = message_formatter._remove_indentation(input_text) # noqa: SLF001 - assert actual == expected - - -violation_mock = violation_loader.ViolationInfo( - identifier='Mock', - code=100, - docstring='docstring', - section='mock', -) -violation_string = 'docstring\n\nSee at website: https://pyflak.es/WPS100' - - -def test_formatter(): - """Test that formatter formats violations as expected.""" - formatted = message_formatter.format_violation(violation_mock) - assert formatted == violation_string - - -def _popen_in_shell(args: str) -> subprocess.Popen: # pragma: no cover +def _popen_in_shell( + args: str +) -> tuple[subprocess.Popen, str, str]: """Run command in shell.""" - encoding = 'utf-8' - # Some encoding magic. Calling with shell=True on Windows - # causes everything to be in cp1251. shell=True is needed - # for subprocess.Popen to locate the installed wps command. - if platform.system() == 'Windows': - encoding = 'cp1251' - return subprocess.Popen( # noqa: S602 (insecure shell=True) + # shell=True is needed for subprocess.Popen to + # locate the installed wps command. + process = subprocess.Popen( # noqa: S602 (insecure shell=True) args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True, - encoding=encoding, - env=os.environ, + text=True, shell=True, ) + stdin, stdout = process.communicate() + return process, stdin, stdout def test_command(snapshot): """Test that command works and formats violations as expected.""" - process = _popen_in_shell('wps explain WPS123') - stdout, stderr = process.communicate() + process, stdout, stderr = _popen_in_shell('wps explain WPS123') assert process.returncode == 0, (stdout, stderr) assert stdout == snapshot + assert not stderr @pytest.mark.parametrize( @@ -107,15 +40,16 @@ def test_command(snapshot): ) def test_command_on_not_found(command, snapshot): """Test command works when violation code is wrong.""" - process = _popen_in_shell(command) - stdout, stderr = process.communicate() + process, stdout, stderr = _popen_in_shell(command) assert process.returncode == 1, (stdout, stderr) + assert not stdout assert stderr == snapshot def test_no_command_specified(snapshot): """Test command displays error message when no subcommand provided.""" - process = _popen_in_shell('wps') + process, stdout, stderr = _popen_in_shell('wps') stdout, stderr = process.communicate() assert process.returncode != 0, (stdout, stderr) + assert not stdout assert stderr == snapshot diff --git a/tests/test_cli/test_explain_internals.py b/tests/test_cli/test_explain_internals.py new file mode 100644 index 000000000..95409783d --- /dev/null +++ b/tests/test_cli/test_explain_internals.py @@ -0,0 +1,30 @@ +"""Unit testing of wps explain command.""" + +import pytest + +from wemake_python_styleguide.cli.commands.explain import ( + violation_loader, +) +from wemake_python_styleguide.violations.best_practices import ( + InitModuleHasLogicViolation, +) +from wemake_python_styleguide.violations.naming import ( + UpperCaseAttributeViolation, +) +from wemake_python_styleguide.violations.oop import BuiltinSubclassViolation + + +@pytest.mark.parametrize( + 'violation_params', + [ + (115, UpperCaseAttributeViolation), + (412, InitModuleHasLogicViolation), + (600, BuiltinSubclassViolation), + ], +) +def test_violation_getter(violation_params): + """Test that violation loader can get violation by their codes.""" + violation_code, expected_class = violation_params + violation = violation_loader.get_violation(violation_code) + assert violation.code is not None + assert violation.docstring == expected_class.__doc__ diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index 53829ed53..2e4000c97 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -1,7 +1,10 @@ """Provides WPS CLI application class.""" +import functools +from argparse import Namespace +from collections.abc import Callable, Mapping +from typing import final, Any -from typing import final - +from wemake_python_styleguide.cli.commands.base import AbstractCommand, Initialisable from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand @@ -9,6 +12,22 @@ class Application: """WPS CLI application class.""" - def run_explain(self, args) -> int: - """Run explain command.""" - return ExplainCommand().run(args) + def __init__(self) -> None: + """Create application and init commands.""" + self.commands: Mapping[str, AbstractCommand[Any]] = { + 'explain': ExplainCommand(), + } + + def run_subcommand(self, subcommand: str, args: Namespace) -> int: + """Run subcommand with provided arguments.""" + cmd = self.commands[subcommand] + args_dict = vars(args) # noqa: WPS421 + args_dict.pop('func') # argument classes do not expect that + cmd_args = cmd.args_type(**args_dict) + return cmd.run(cmd_args) + + def curry_run_subcommand( + self, subcommand: str + ) -> Callable[[Namespace], int]: + """Helper func for easy use of argparse config.""" + return functools.partial(self.run_subcommand, subcommand=subcommand) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index 773e30927..efa2a365a 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -1,7 +1,4 @@ -"""Main CLI utility file.""" - import argparse -import sys from wemake_python_styleguide.cli.application import Application @@ -11,8 +8,10 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog='wps', description='WPS command line tool' ) - sub_parsers = parser.add_subparsers(help='sub-command help') - sub_parsers.required = True + sub_parsers = parser.add_subparsers( + help='sub-parser for exact wps commands', + required=True, + ) parser_explain = sub_parsers.add_parser( 'explain', @@ -22,7 +21,7 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: 'violation_code', help='Desired violation code', ) - parser_explain.set_defaults(func=app.run_explain) + parser_explain.set_defaults(func=app.curry_run_subcommand('explain')) return parser @@ -37,8 +36,4 @@ def main() -> int: """Main function.""" app = Application() args = parse_args(app) - return int(args.func(args)) - - -if __name__ == '__main__': - sys.exit(main()) + return int(args.func(args=args)) diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py index d66b0cc21..fb56f43df 100644 --- a/wemake_python_styleguide/cli/commands/base.py +++ b/wemake_python_styleguide/cli/commands/base.py @@ -1,12 +1,21 @@ """Contains files common for all wps commands.""" from abc import ABC, abstractmethod +from typing import Protocol -class AbstractCommand(ABC): +class Initialisable(Protocol): + """Represents a class that can be initialised with kwargs.""" + + def __init__(self, **kwargs) -> None: + ... + + +class AbstractCommand[_ArgsT: Initialisable](ABC): """ABC for all commands.""" + args_type: type[_ArgsT] @abstractmethod - def run(self, args) -> int: + def run(self, args: _ArgsT) -> int: """Run the command.""" raise NotImplementedError diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index 50d735110..809da43d3 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -1,4 +1,5 @@ """Contains command implementation.""" +from attrs import frozen from wemake_python_styleguide.cli.commands.base import AbstractCommand from wemake_python_styleguide.cli.commands.explain import ( @@ -17,10 +18,19 @@ def _clean_violation_code(violation_str: str) -> int: return -1 -class ExplainCommand(AbstractCommand): +@frozen +class ExplainCommandArgs: + """Arguments for wps explain command.""" + + violation_code: str + + +class ExplainCommand(AbstractCommand[ExplainCommandArgs]): """Explain command impl.""" - def run(self, args) -> int: + args_type = ExplainCommandArgs + + def run(self, args: ExplainCommandArgs) -> int: """Run command.""" code = _clean_violation_code(args.violation_code) violation = violation_loader.get_violation(code) diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index e8bbf4877..2d11604e7 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -1,4 +1,5 @@ """Provides tools for formatting explanations.""" +import textwrap from wemake_python_styleguide.cli.commands.explain.violation_loader import ( ViolationInfo, @@ -6,44 +7,6 @@ from wemake_python_styleguide.constants import SHORTLINK_TEMPLATE -def _clean_text(text: str) -> str: - """Normalize line endings and clean text.""" - return text.replace('\r\n', '\n').replace('\r', '\n') - - -def _replace_tabs(text: str, tab_size: int = 4) -> str: - """Replace all tabs with defined amount of spaces.""" - return text.replace('\t', ' ' * tab_size) - - -def _get_whitespace_prefix(line: str) -> int | float: - """Get length of whitespace prefix of string.""" - for char_index, char in enumerate(line): - if char != ' ': - return char_index - return float('+inf') - - -def _get_greatest_common_indent(text: str) -> int: - """Get the greatest common whitespace prefix length of all lines.""" - lines = text.split('\n') - greatest_common_indent = float('+inf') - for line in lines: - greatest_common_indent = min( - greatest_common_indent, _get_whitespace_prefix(line) - ) - if isinstance(greatest_common_indent, float): - greatest_common_indent = 0 - return greatest_common_indent - - -def _remove_indentation(text: str, tab_size: int = 4) -> str: - """Remove excessive indentation.""" - text = _replace_tabs(_clean_text(text), tab_size) - max_indent = _get_greatest_common_indent(text) - return '\n'.join(line[max_indent:] for line in text.split('\n')) - - def _remove_newlines_at_ends(text: str) -> str: """Remove leading and trailing newlines.""" return text.strip('\n\r') @@ -52,7 +15,7 @@ def _remove_newlines_at_ends(text: str) -> str: def format_violation(violation: ViolationInfo) -> str: """Format violation information.""" cleaned_docstring = _remove_newlines_at_ends( - _remove_indentation(violation.docstring) + textwrap.dedent(violation.docstring) ) violation_url = SHORTLINK_TEMPLATE.format(f'WPS{violation.code}') return f'{cleaned_docstring}\n\nSee at website: {violation_url}' From 4a976cacff2a1bf80945d27548b7e77d47f618d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 20:39:44 +0000 Subject: [PATCH 40/44] [pre-commit.ci] auto fixes from pre-commit.com hooks --- wemake_python_styleguide/cli/application.py | 7 +++++-- wemake_python_styleguide/cli/commands/base.py | 4 ++-- wemake_python_styleguide/cli/commands/explain/command.py | 1 + .../cli/commands/explain/message_formatter.py | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py index 2e4000c97..5b997d201 100644 --- a/wemake_python_styleguide/cli/application.py +++ b/wemake_python_styleguide/cli/application.py @@ -1,10 +1,13 @@ """Provides WPS CLI application class.""" + import functools from argparse import Namespace from collections.abc import Callable, Mapping -from typing import final, Any +from typing import Any, final -from wemake_python_styleguide.cli.commands.base import AbstractCommand, Initialisable +from wemake_python_styleguide.cli.commands.base import ( + AbstractCommand, +) from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py index fb56f43df..fc14098a4 100644 --- a/wemake_python_styleguide/cli/commands/base.py +++ b/wemake_python_styleguide/cli/commands/base.py @@ -7,12 +7,12 @@ class Initialisable(Protocol): """Represents a class that can be initialised with kwargs.""" - def __init__(self, **kwargs) -> None: - ... + def __init__(self, **kwargs) -> None: ... class AbstractCommand[_ArgsT: Initialisable](ABC): """ABC for all commands.""" + args_type: type[_ArgsT] @abstractmethod diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index 809da43d3..9e474c4ad 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -1,4 +1,5 @@ """Contains command implementation.""" + from attrs import frozen from wemake_python_styleguide.cli.commands.base import AbstractCommand diff --git a/wemake_python_styleguide/cli/commands/explain/message_formatter.py b/wemake_python_styleguide/cli/commands/explain/message_formatter.py index 2d11604e7..f8b58625c 100644 --- a/wemake_python_styleguide/cli/commands/explain/message_formatter.py +++ b/wemake_python_styleguide/cli/commands/explain/message_formatter.py @@ -1,4 +1,5 @@ """Provides tools for formatting explanations.""" + import textwrap from wemake_python_styleguide.cli.commands.explain.violation_loader import ( From 017e2714d1ac4d2c761b59e72c06abfb3a9941d9 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 27 Jan 2025 20:22:28 +0500 Subject: [PATCH 41/44] Clean up redundant abstractions. Move to old generics syntax to support python 3.10. --- wemake_python_styleguide/cli/application.py | 33 ------------------- wemake_python_styleguide/cli/cli_app.py | 13 ++++---- wemake_python_styleguide/cli/commands/base.py | 23 +++++++------ .../cli/commands/explain/command.py | 8 +++-- .../cli/commands/explain/module_loader.py | 12 +++---- 5 files changed, 29 insertions(+), 60 deletions(-) delete mode 100644 wemake_python_styleguide/cli/application.py diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py deleted file mode 100644 index 2e4000c97..000000000 --- a/wemake_python_styleguide/cli/application.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Provides WPS CLI application class.""" -import functools -from argparse import Namespace -from collections.abc import Callable, Mapping -from typing import final, Any - -from wemake_python_styleguide.cli.commands.base import AbstractCommand, Initialisable -from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand - - -@final -class Application: - """WPS CLI application class.""" - - def __init__(self) -> None: - """Create application and init commands.""" - self.commands: Mapping[str, AbstractCommand[Any]] = { - 'explain': ExplainCommand(), - } - - def run_subcommand(self, subcommand: str, args: Namespace) -> int: - """Run subcommand with provided arguments.""" - cmd = self.commands[subcommand] - args_dict = vars(args) # noqa: WPS421 - args_dict.pop('func') # argument classes do not expect that - cmd_args = cmd.args_type(**args_dict) - return cmd.run(cmd_args) - - def curry_run_subcommand( - self, subcommand: str - ) -> Callable[[Namespace], int]: - """Helper func for easy use of argparse config.""" - return functools.partial(self.run_subcommand, subcommand=subcommand) diff --git a/wemake_python_styleguide/cli/cli_app.py b/wemake_python_styleguide/cli/cli_app.py index efa2a365a..b1a3da17c 100644 --- a/wemake_python_styleguide/cli/cli_app.py +++ b/wemake_python_styleguide/cli/cli_app.py @@ -1,9 +1,9 @@ import argparse -from wemake_python_styleguide.cli.application import Application +from wemake_python_styleguide.cli.commands.explain.command import ExplainCommand -def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: +def _configure_arg_parser() -> argparse.ArgumentParser: """Configures CLI arguments and subcommands.""" parser = argparse.ArgumentParser( prog='wps', description='WPS command line tool' @@ -21,19 +21,18 @@ def _configure_arg_parser(app: Application) -> argparse.ArgumentParser: 'violation_code', help='Desired violation code', ) - parser_explain.set_defaults(func=app.curry_run_subcommand('explain')) + parser_explain.set_defaults(func=ExplainCommand()) return parser -def parse_args(app: Application) -> argparse.Namespace: +def parse_args() -> argparse.Namespace: """Parse CLI arguments.""" - parser = _configure_arg_parser(app) + parser = _configure_arg_parser() return parser.parse_args() def main() -> int: """Main function.""" - app = Application() - args = parse_args(app) + args = parse_args() return int(args.func(args=args)) diff --git a/wemake_python_styleguide/cli/commands/base.py b/wemake_python_styleguide/cli/commands/base.py index fb56f43df..6880ded75 100644 --- a/wemake_python_styleguide/cli/commands/base.py +++ b/wemake_python_styleguide/cli/commands/base.py @@ -1,21 +1,24 @@ """Contains files common for all wps commands.""" from abc import ABC, abstractmethod -from typing import Protocol +from argparse import Namespace +from typing import Generic, TypeVar +_ArgsT = TypeVar('_ArgsT') -class Initialisable(Protocol): - """Represents a class that can be initialised with kwargs.""" - def __init__(self, **kwargs) -> None: - ... - - -class AbstractCommand[_ArgsT: Initialisable](ABC): +class AbstractCommand(ABC, Generic[_ArgsT]): """ABC for all commands.""" - args_type: type[_ArgsT] + _args_type: type[_ArgsT] + + def __call__(self, args: Namespace) -> int: + """Parse arguments into the generic namespace.""" + args_dict = vars(args) # noqa: WPS421 + args_dict.pop('func') # argument classes do not expect that + cmd_args = self._args_type(**args_dict) + return self._run(cmd_args) @abstractmethod - def run(self, args: _ArgsT) -> int: + def _run(self, args: _ArgsT) -> int: """Run the command.""" raise NotImplementedError diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index 809da43d3..58e79acdc 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -1,4 +1,6 @@ """Contains command implementation.""" +from typing import final + from attrs import frozen from wemake_python_styleguide.cli.commands.base import AbstractCommand @@ -18,6 +20,7 @@ def _clean_violation_code(violation_str: str) -> int: return -1 +@final @frozen class ExplainCommandArgs: """Arguments for wps explain command.""" @@ -25,12 +28,13 @@ class ExplainCommandArgs: violation_code: str +@final class ExplainCommand(AbstractCommand[ExplainCommandArgs]): """Explain command impl.""" - args_type = ExplainCommandArgs + _args_type = ExplainCommandArgs - def run(self, args: ExplainCommandArgs) -> int: + def _run(self, args: ExplainCommandArgs) -> int: """Run command.""" code = _clean_violation_code(args.violation_code) violation = violation_loader.get_violation(code) diff --git a/wemake_python_styleguide/cli/commands/explain/module_loader.py b/wemake_python_styleguide/cli/commands/explain/module_loader.py index 439b98c98..22d8d968c 100644 --- a/wemake_python_styleguide/cli/commands/explain/module_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/module_loader.py @@ -1,6 +1,5 @@ import importlib from collections.abc import Collection -from contextlib import suppress from pathlib import Path from types import ModuleType from typing import Final @@ -16,13 +15,10 @@ def get_violation_submodules() -> Collection[ModuleType]: def _safely_get_all_submodules(module_name: str) -> Collection[ModuleType]: """Get all submodules of given module. Ignore missing.""" submodule_names = _get_all_possible_submodule_names(module_name) - modules = [] - for submodule_name in submodule_names: - # just in case if there are some bad module paths - # (which generally should not happen) - with suppress(ModuleNotFoundError): - modules.append(importlib.import_module(submodule_name)) - return modules + return [ + importlib.import_module(submodule_name) + for submodule_name in submodule_names + ] def _get_all_possible_submodule_names(module_name: str) -> Collection[str]: From 56fc859d3897cc7ec341ede6b1063dd2336076c7 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 27 Jan 2025 20:24:11 +0500 Subject: [PATCH 42/44] Remove application that came back after merging --- wemake_python_styleguide/cli/application.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 wemake_python_styleguide/cli/application.py diff --git a/wemake_python_styleguide/cli/application.py b/wemake_python_styleguide/cli/application.py deleted file mode 100644 index e69de29bb..000000000 From 7950dc900fdf2652c1f97134b86eb25cfe7ab704 Mon Sep 17 00:00:00 2001 From: Tapeline Date: Mon, 27 Jan 2025 20:25:08 +0500 Subject: [PATCH 43/44] Fix some things that broke after merge --- wemake_python_styleguide/cli/commands/explain/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wemake_python_styleguide/cli/commands/explain/command.py b/wemake_python_styleguide/cli/commands/explain/command.py index f03ba05b7..4b93eb2c9 100644 --- a/wemake_python_styleguide/cli/commands/explain/command.py +++ b/wemake_python_styleguide/cli/commands/explain/command.py @@ -35,7 +35,7 @@ class ExplainCommand(AbstractCommand[ExplainCommandArgs]): _args_type = ExplainCommandArgs - def run(self, args: ExplainCommandArgs) -> int: + def _run(self, args: ExplainCommandArgs) -> int: """Run command.""" code = _clean_violation_code(args.violation_code) violation = violation_loader.get_violation(code) From 481212e7ab0fb766c32f9b6bc350e25296bb5984 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 28 Jan 2025 19:39:40 +0300 Subject: [PATCH 44/44] Apply suggestions from code review --- .../cli/commands/explain/module_loader.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/wemake_python_styleguide/cli/commands/explain/module_loader.py b/wemake_python_styleguide/cli/commands/explain/module_loader.py index 22d8d968c..6674e161c 100644 --- a/wemake_python_styleguide/cli/commands/explain/module_loader.py +++ b/wemake_python_styleguide/cli/commands/explain/module_loader.py @@ -9,12 +9,7 @@ def get_violation_submodules() -> Collection[ModuleType]: """Get all possible violation submodules.""" - return _safely_get_all_submodules(_VIOLATION_MODULE_BASE) - - -def _safely_get_all_submodules(module_name: str) -> Collection[ModuleType]: - """Get all submodules of given module. Ignore missing.""" - submodule_names = _get_all_possible_submodule_names(module_name) + submodule_names = _get_all_possible_submodule_names(_VIOLATION_MODULE_BASE) return [ importlib.import_module(submodule_name) for submodule_name in submodule_names