diff --git a/.gitignore b/.gitignore index b6e4761..e522b4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.ruff_cache # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 025810a..55e8de1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -73,10 +73,16 @@ repos: - id: trailing-whitespace args: ['--markdown-linebreak-ext=md,markdown'] + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.269 + hooks: + - id: ruff + - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 + # this doesn't seem to work, pyi files don't get checked with --all-files types_or: [python, pyi] language_version: python3 additional_dependencies: @@ -86,26 +92,14 @@ repos: - flake8-comprehensions - flake8-datetimez - flake8-docstrings - - flake8-mutable - - flake8-noqa + - flake8-mutable # not official supported by ruff - flake8-pie - flake8-pyi - flake8-pytest-style - flake8-return - flake8-simplify - flake8-type-checking - -# Pinned to flake8 5.0.4 since flake8-eradicate is not updated to work with flake8 6 -# https://github.com/wemake-services/flake8-eradicate/pull/271 - - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 - hooks: - - id: flake8 - name: flake8-eradicate - args: [--select=E800] - language_version: python3 - additional_dependencies: - - flake8-eradicate + # all other are - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt rev: 0.2.2 diff --git a/flake8_trio/__init__.py b/flake8_trio/__init__.py index 20ba88d..42135a0 100644 --- a/flake8_trio/__init__.py +++ b/flake8_trio/__init__.py @@ -312,7 +312,7 @@ def get_matching_codes( all_codes: set[str] = { err_code for err_class in (*ERROR_CLASSES, *ERROR_CLASSES_CST) - for err_code in err_class.error_codes.keys() # type: ignore[attr-defined] + for err_code in err_class.error_codes # type: ignore[attr-defined] if len(err_code) == 7 # exclude e.g. TRIO103_anyio_trio } @@ -367,7 +367,7 @@ def parse_trio200_dict(raw_value: str) -> dict[str, str]: # if we raise it as ValueError raise ArgumentTypeError( f"Invalid number ({len(split_values)-1}) of splitter " - + f"tokens {splitter!r} in {value!r}" + f"tokens {splitter!r} in {value!r}" ) res[split_values[0]] = split_values[1] return res diff --git a/flake8_trio/visitors/flake8triovisitor.py b/flake8_trio/visitors/flake8triovisitor.py index 9d3fb9f..14eb78e 100644 --- a/flake8_trio/visitors/flake8triovisitor.py +++ b/flake8_trio/visitors/flake8triovisitor.py @@ -153,7 +153,7 @@ def library_str(self) -> str: def add_library(self, name: str) -> None: if name not in self.__state.library: - self.__state.library = self.__state.library + (name,) + self.__state.library = (*self.__state.library, name) class Flake8TrioVisitor_cst(cst.CSTTransformer, ABC): @@ -200,8 +200,9 @@ def restore_state(self, node: cst.CSTNode): def is_noqa(self, node: cst.CSTNode, code: str): if self.options.disable_noqa: return False - pos = self.get_metadata(PositionProvider, node).start - noqas = self.noqas.get(pos.line) + # pyright + get_metadata is acting up + pos = self.get_metadata(PositionProvider, node).start # type: ignore + noqas = self.noqas.get(pos.line) # type: ignore return noqas is not None and (noqas == set() or code in noqas) def error( @@ -223,13 +224,14 @@ def error( if self.is_noqa(node, error_code): return False - pos = self.get_metadata(PositionProvider, node).start + # pyright + get_metadata is acting up + pos = self.get_metadata(PositionProvider, node).start # type: ignore self.__state.problems.append( Error( # 7 == len('TRIO...'), so alt messages raise the original code error_code[:7], - pos.line, - pos.column, + pos.line, # type: ignore + pos.column, # type: ignore self.error_codes[error_code], *args, ) @@ -253,4 +255,4 @@ def library(self) -> tuple[str, ...]: def add_library(self, name: str) -> None: if name not in self.__state.library: - self.__state.library = self.__state.library + (name,) + self.__state.library = (*self.__state.library, name) diff --git a/flake8_trio/visitors/visitor100.py b/flake8_trio/visitors/visitor100.py index 9e37f1c..db698ab 100644 --- a/flake8_trio/visitors/visitor100.py +++ b/flake8_trio/visitors/visitor100.py @@ -9,7 +9,7 @@ from typing import Any -import libcst as cst +import libcst as cst # noqa: TCH002 import libcst.matchers as m from .flake8triovisitor import Flake8TrioVisitor_cst diff --git a/flake8_trio/visitors/visitor91x.py b/flake8_trio/visitors/visitor91x.py index 77ffd57..6eb0a6c 100644 --- a/flake8_trio/visitors/visitor91x.py +++ b/flake8_trio/visitors/visitor91x.py @@ -284,9 +284,9 @@ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # this should improve performance on codebases with many sync functions return any(m.findall(node, m.FunctionDef(asynchronous=m.Asynchronous()))) - pos = self.get_metadata(PositionProvider, node).start + pos = self.get_metadata(PositionProvider, node).start # type: ignore self.uncheckpointed_statements = { - Statement("function definition", pos.line, pos.column) + Statement("function definition", pos.line, pos.column) # type: ignore } return True @@ -419,8 +419,10 @@ def leave_Yield( self.add_statement = self.checkpoint_statement() # mark as requiring checkpoint after - pos = self.get_metadata(PositionProvider, original_node).start - self.uncheckpointed_statements = {Statement("yield", pos.line, pos.column)} + pos = self.get_metadata(PositionProvider, original_node).start # type: ignore + self.uncheckpointed_statements = { + Statement("yield", pos.line, pos.column) # type: ignore + } # return original to avoid problems with identity equality assert original_node.deep_equals(updated_node) return original_node @@ -442,9 +444,9 @@ def visit_Try(self, node: cst.Try): ) # yields inside `try` can always be uncheckpointed for inner_node in m.findall(node.body, m.Yield()): - pos = self.get_metadata(PositionProvider, inner_node).start + pos = self.get_metadata(PositionProvider, inner_node).start # type: ignore self.try_state.body_uncheckpointed_statements.add( - Statement("yield", pos.line, pos.column) + Statement("yield", pos.line, pos.column) # type: ignore ) def leave_Try_body(self, node: cst.Try): @@ -657,7 +659,7 @@ def leave_While_orelse(self, node: cst.For | cst.While): else: # We may exit from: # orelse (covering: no body, body until continue, and all body) - # break + # `break` self.uncheckpointed_statements.update( self.loop_state.uncheckpointed_before_break ) diff --git a/flake8_trio/visitors/visitor_utility.py b/flake8_trio/visitors/visitor_utility.py index 0f6df09..c92d30f 100644 --- a/flake8_trio/visitors/visitor_utility.py +++ b/flake8_trio/visitors/visitor_utility.py @@ -7,7 +7,6 @@ import re from typing import TYPE_CHECKING, Any -import libcst as cst import libcst.matchers as m from libcst.metadata import PositionProvider @@ -17,6 +16,8 @@ if TYPE_CHECKING: from re import Match + import libcst as cst + @utility_visitor class VisitorTypeTracker(Flake8TrioVisitor): diff --git a/pyproject.toml b/pyproject.toml index b51de0e..f2a001c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,75 @@ reportShadowedImports = true reportUninitializedInstanceVariable = true reportUnusedCallResult = false strict = ["*.py", "tests/*.py", "flake8_trio/**/*.py"] + +[tool.ruff] +extend-exclude = [ + ".*", + "tests/eval_files/*", + "tests/autofix_files/*" +] +ignore = [ + "COM", # flake8-comma, handled by black + "ANN", # annotations, handled by pyright/mypy + "T20", # flake8-print + "TID252", # relative imports from parent modules https://github.com/Zac-HD/flake8-trio/pull/196#discussion_r1200413372 + "D101", + "D102", + "D103", + "D105", + "D106", + "D107", + "S101", + "D203", # one-blank-line-before-class + "D213", # multi-line-summary-second-line + "EM101", # exception must not use a string literal + "EM102", # exception must not use an f-string literal + 'PGH001', # No builtin `eval()` allowed + 'N802', # function name should be lowercase - not an option with inheritance + 'PTH123', # `open()` should be replaced by `Path.open()` + 'PYI021', # docstring in stub + 'S603', # `subprocess` call: check for execution of untrusted input + 'N815', # Variable `visit_AsyncWith` in class scope should not be mixedCase + 'PLR091', # Too many return statements/branches/arguments + 'C901', # function is too complex + # maybe should be ignored in-place + 'N806', # Variable `MyDict` in function should be lowercase + # --- maybe should be fixed / ignored in-place --- + 'ARG001', # Unused function argument + 'ARG002', # Unused method argument + 'B904', # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + 'B905', # zip without explicit strict parameter + 'BLE001', # Do not catch blind exception: `Exception` + 'FBT001', # Boolean positional arg in function definition + 'FBT002', # Boolean default value in function definition + 'N801', # Class name {} should use CapWords convention + 'PGH003', # Use specific rule codes when ignoring type issues + 'PLR2004', # Magic value used in comparison + 'PLW2901', # Outer `for` loop variable `err` overwritten by inner `for` loop target + 'PTH118', # `os.path.join()` should be replaced by `Path` with `/` operator + 'S607', # Starting a process with a partial executable path + 'SLF001', # Private member accessed: `_tree` + 'TD002', # missing author in TODO + 'TD003', # missing issue link in TODO + 'TRY003', # Avoid specifying long messages outside the exception class + 'TRY200', # Use `raise from` to specify exception cause + 'TRY201', # Use `raise` without specifying exception name + # enable if flake8/plugins are removed + 'PGH004', # Use specific rule codes when using `noqa` + 'RUF100' # Unused `noqa` - enable only if flake8 is no longer used +] +line-length = 90 +select = ["ALL"] +target-version = "py39" + +[tool.ruff.per-file-ignores] +# docstrings, and arguments we can't modify +"*.pyi" = ["D", 'FBT001', 'PLR0913'] +# imports +"flake8_trio/visitors/__init__.py" = [ + "F401", + "E402" +] +# visitor_utility contains comments specifying how it parses noqa comments, which get +# parsed as noqa comments +"flake8_trio/visitors/visitor_utility.py" = ["RUF100", "PGH004"] diff --git a/tests/test_changelog_and_version.py b/tests/test_changelog_and_version.py index 80e3387..46358dd 100755 --- a/tests/test_changelog_and_version.py +++ b/tests/test_changelog_and_version.py @@ -83,7 +83,7 @@ def ensure_tagged() -> None: def update_version() -> None: # If we've added a new version to the changelog, update __version__ to match last_version = next(iter(get_releases())) - if VERSION != last_version: + if last_version != VERSION: INIT_FILE = ROOT_PATH / "flake8_trio" / "__init__.py" subs = (f'__version__ = "{VERSION}"', f'__version__ = "{last_version}"') INIT_FILE.write_text(INIT_FILE.read_text().replace(*subs)) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 562d4e8..3fd66e8 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -94,7 +94,7 @@ def test_pep614(): def test_command_line_1(capfd: pytest.CaptureFixture[str]): - Application().run(common_flags + ["--no-checkpoint-warning-decorators=app.route"]) + Application().run([*common_flags, "--no-checkpoint-warning-decorators=app.route"]) assert capfd.readouterr() == ("", "") @@ -115,7 +115,7 @@ def test_command_line_1(capfd: pytest.CaptureFixture[str]): def test_command_line_2(capfd: pytest.CaptureFixture[str]): - Application().run(common_flags + ["--no-checkpoint-warning-decorators=app"]) + Application().run([*common_flags, "--no-checkpoint-warning-decorators=app"]) assert capfd.readouterr() == (expected_out, "") diff --git a/tests/test_flake8_trio.py b/tests/test_flake8_trio.py index d5ab0d8..cc6dfa3 100644 --- a/tests/test_flake8_trio.py +++ b/tests/test_flake8_trio.py @@ -13,10 +13,10 @@ import tokenize import unittest from argparse import ArgumentParser -from collections import deque +from collections import defaultdict, deque from dataclasses import dataclass, fields from pathlib import Path -from typing import TYPE_CHECKING, Any, DefaultDict +from typing import TYPE_CHECKING, Any import libcst as cst import pytest @@ -66,7 +66,7 @@ def check_version(test: str): ERROR_CODES: dict[str, Flake8TrioVisitor] = { err_code: err_class # type: ignore[misc] for err_class in (*ERROR_CLASSES, *ERROR_CLASSES_CST) - for err_code in err_class.error_codes.keys() # type: ignore[attr-defined] + for err_code in err_class.error_codes # type: ignore[attr-defined] } @@ -500,11 +500,11 @@ def print_first_diff(errors: Sequence[Error], expected: Sequence[Error]): def assert_correct_lines_and_codes(errors: Iterable[Error], expected: Iterable[Error]): """Check that errors are on correct lines.""" - MyDict = DefaultDict[int, DefaultDict[str, int]] # TypeAlias + MyDict = defaultdict[int, defaultdict[str, int]] # TypeAlias all_lines = sorted({e.line for e in (*errors, *expected)}) - error_dict: MyDict = DefaultDict(lambda: DefaultDict(int)) + error_dict: MyDict = defaultdict(lambda: defaultdict(int)) expected_dict = copy.deepcopy(error_dict) # populate dicts with number of errors per line @@ -622,7 +622,6 @@ def test_line_numbers_match_end_result(): plugin = Plugin.from_source(text) initialize_options(plugin, args=["--enable=TRIO100,TRIO115", "--autofix=TRIO100"]) errors = tuple(plugin.run()) - plugin.module.code assert errors[1].line != plugin.module.code.split("\n").index("trio.sleep(0)") + 1 diff --git a/tests/test_messages_documented.py b/tests/test_messages_documented.py old mode 100644 new mode 100755 diff --git a/typings/flake8/options/manager.pyi b/typings/flake8/options/manager.pyi index 51d9fca..914a91c 100644 --- a/typings/flake8/options/manager.pyi +++ b/typings/flake8/options/manager.pyi @@ -7,7 +7,7 @@ Generated for flake8 5, so OptionManager signature is incorrect for flake8 6 import argparse import enum from collections.abc import Callable, Mapping, Sequence -from typing import Any, TypeAlias +from typing import Any from flake8.plugins.finder import Plugins