From d0eff1ea177deb10abc844a2f672eb9055221ff6 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 7 Sep 2022 00:54:46 -0400 Subject: [PATCH] feat: configurable max length for string literals (#6) This is a way to allow short literals like: ```python raise RuntimeError("Little msg") ``` Signed-off-by: Henry Schreiner --- README.md | 7 +++++ noxfile.py | 23 +++++++++++++++- src/flake8_errmsg/__init__.py | 49 ++++++++++++++++++++++++++++------- tests/test_package.py | 14 ++++++++++ 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a1c002c..bae674f 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,13 @@ applications should print nice errors, usually _without_ a traceback, unless something _unexpected_ occurred. An app should not print a traceback for an error that is known to be triggerable by a user. +## Options + +There is one option, `--errmsg-max-string-length`, which defaults to 0 but can +be set to a larger value. The check will ignore string literals shorter than +this length. This option is supported in configuration mode as well. This will +only affect string literals and not f-strings. + ## Usage Just add this to your `.pre-commit-config.yaml` `flake8` check under diff --git a/noxfile.py b/noxfile.py index f7152fa..c5de8ed 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,7 +2,7 @@ import nox -nox.options.sessions = ["lint", "pylint", "tests"] +nox.options.sessions = ["lint", "pylint", "tests", "tests_flake8"] @nox.session @@ -42,3 +42,24 @@ def build(session: nox.Session) -> None: session.install("build") session.run("python", "-m", "build") + + +@nox.session +def tests_flake8(session: nox.Session) -> None: + """ + Run the flake8 tests. + """ + session.install(".", "flake8") + result = session.run("flake8", "tests/example1.py", silent=True, success_codes=[1]) + if len(result.splitlines()) != 2: + session.error(f"Expected 2 errors from flake8\n{result}") + + result = session.run( + "flake8", + "--errmsg-max-string-length=30", + "tests/example1.py", + silent=True, + success_codes=[1], + ) + if len(result.splitlines()) != 1: + session.error(f"Expected 1 errors from flake8\n{result}") diff --git a/src/flake8_errmsg/__init__.py b/src/flake8_errmsg/__init__.py index 133fd55..165a677 100644 --- a/src/flake8_errmsg/__init__.py +++ b/src/flake8_errmsg/__init__.py @@ -8,17 +8,17 @@ from __future__ import annotations +import argparse import ast import dataclasses -import sys import traceback from collections.abc import Iterator from pathlib import Path -from typing import NamedTuple +from typing import Any, ClassVar, NamedTuple __all__ = ("__version__", "run_on_file", "main", "ErrMsgASTPlugin") -__version__ = "0.2.4" +__version__ = "0.3.0" class Flake8ASTErrorInfo(NamedTuple): @@ -29,13 +29,15 @@ class Flake8ASTErrorInfo(NamedTuple): class Visitor(ast.NodeVisitor): - def __init__(self) -> None: + def __init__(self, max_string_len: int) -> None: self.errors: list[Flake8ASTErrorInfo] = [] + self.max_string_len = max_string_len def visit_Raise(self, node: ast.Raise) -> None: match node.exc: - case ast.Call(args=[ast.Constant(value=str()), *_]): - self.errors.append(EM101(node)) + case ast.Call(args=[ast.Constant(value=str(value)), *_]): + if len(value) >= self.max_string_len: + self.errors.append(EM101(node)) case ast.Call(args=[ast.JoinedStr(), *_]): self.errors.append(EM102(node)) case _: @@ -52,21 +54,41 @@ def EM102(node: ast.AST) -> Flake8ASTErrorInfo: return Flake8ASTErrorInfo(node.lineno, node.col_offset, msg, Visitor) +MAX_STRING_LENGTH = 0 + + @dataclasses.dataclass class ErrMsgASTPlugin: + max_string_length: ClassVar[int] = 0 + tree: ast.AST _: dataclasses.KW_ONLY name: str = "flake8_errmsg" version: str = "0.1.0" + options: Any = None def run(self) -> Iterator[Flake8ASTErrorInfo]: - visitor = Visitor() + visitor = Visitor(self.max_string_length) visitor.visit(self.tree) yield from visitor.errors + @staticmethod + def add_options(optmanager: Any) -> None: + optmanager.add_option( + "--errmsg-max-string-length", + parse_from_config=True, + default=0, + type=int, + help="Set a maximum string length to allow inline strings. Default 0 (always disallow).", + ) + + @classmethod + def parse_options(cls, options: argparse.Namespace) -> None: + cls.max_string_length = options.errmsg_max_string_length -def run_on_file(path: str) -> None: + +def run_on_file(path: str, max_string_length: int = 0) -> None: code = Path(path).read_text(encoding="utf-8") try: @@ -78,13 +100,20 @@ def run_on_file(path: str) -> None: raise SystemExit(1) from None plugin = ErrMsgASTPlugin(node) + ErrMsgASTPlugin.max_string_length = max_string_length + for err in plugin.run(): print(f"{path}:{err.line_number}:{err.offset} {err.msg}") def main() -> None: - for item in sys.argv[1:]: - run_on_file(item) + parser = argparse.ArgumentParser() + parser.add_argument("--errmsg-max-string-length", type=int, default=0) + parser.add_argument("files", nargs="+") + namespace = parser.parse_args() + + for item in namespace.files: + run_on_file(item, namespace.errmsg_max_string_length) if __name__ == "__main__": diff --git a/tests/test_package.py b/tests/test_package.py index f5b9872..2054377 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -31,3 +31,17 @@ def test_err1(): results[1].msg == "EM102 Exception must not use an f-string literal, assign to variable first" ) + + +def test_string_length(): + node = ast.parse(ERR1) + plugin = m.ErrMsgASTPlugin(node) + m.ErrMsgASTPlugin.max_string_length = 10 + results = list(plugin.run()) + assert len(results) == 1 + assert results[0].line_number == 2 + + assert ( + results[0].msg + == "EM102 Exception must not use an f-string literal, assign to variable first" + )