diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index b3aef4f..6ac8351 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -7,7 +7,6 @@ jobs: max-parallel: 4 matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" diff --git a/README.md b/README.md index 06c1c26..eeb2c6b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## Tools ### Production -- python 3.9+ +- python 3.10+ - [flake8](http://flake8.pycqa.org/en/latest/) ### Development diff --git a/flake8_debug/errors.py b/flake8_debug/errors.py index 6f75e7e..8c8a66a 100644 --- a/flake8_debug/errors.py +++ b/flake8_debug/errors.py @@ -1,4 +1,7 @@ -class Error: +from abc import ABC + + +class Error(ABC): code: str func_name: str diff --git a/flake8_debug/plugin.py b/flake8_debug/plugin.py index 6636aaf..1e02107 100644 --- a/flake8_debug/plugin.py +++ b/flake8_debug/plugin.py @@ -1,4 +1,5 @@ import ast +import sys from typing import List, Tuple, Generator, Type, Any, Optional from flake8_debug.errors import ERRORS, Error @@ -6,6 +7,11 @@ TDebug = Generator[Tuple[int, int, str, Type[Any]], None, None] +# Func names meaningful only as attribute calls (e.g. pdb.set_trace()) +_ATTR_DETECTABLE: frozenset = frozenset({'set_trace'}) +# Only flag attribute calls when the object looks like a known debugger module +_DEBUGGER_MODULES: frozenset = frozenset({'pdb', 'ipdb'}) + class DebugVisitor(ast.NodeVisitor): def __init__(self, errors: Tuple[Type[Error], ...]) -> None: @@ -14,14 +20,20 @@ def __init__(self, errors: Tuple[Type[Error], ...]) -> None: def visit_Call(self, node: ast.Call) -> None: for error in self._errors: - if ( + is_bare_call = ( isinstance(node.func, ast.Name) and node.func.id == error.func_name - ) or ( + ) + is_attr_call = ( isinstance(node.func, ast.Attribute) and node.func.attr == error.func_name - ): + and error.func_name in _ATTR_DETECTABLE + and isinstance(node.func.value, ast.Name) + and node.func.value.id in _DEBUGGER_MODULES + ) + if is_bare_call or is_attr_call: self.issues.append((node.lineno, node.col_offset, error().msg)) + self.generic_visit(node) class NoDebug: @@ -32,10 +44,16 @@ def __init__( self, tree: ast.Module, filename: Optional[str] = None ) -> None: self._tree = tree - self._filename = filename def run(self) -> TDebug: debug = DebugVisitor(ERRORS) - debug.visit(self._tree) + old_limit = sys.getrecursionlimit() + try: + sys.setrecursionlimit(max(old_limit, 2000)) + debug.visit(self._tree) + except RecursionError: + pass + finally: + sys.setrecursionlimit(old_limit) for lineno, column, msg in debug.issues: # type: int, int, str yield lineno, column, msg, type(self) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1583d15..7de1398 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,8 @@ black==25.9.0 flake8>=4.0.1 pytest==8.4.2 -pytest-cov==3.0.0 -coverage==5.3 -coveralls==2.1.2 +pytest-cov==7.1.0 +coverage==7.13.5 +coveralls==4.1.0 setuptools==80.9.0 -wheel==0.45.1 \ No newline at end of file +wheel==0.46.3 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 394f984..2fe82b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ flake8>=4.0.1 -astpretty==2.1.0 diff --git a/setup.py b/setup.py index 8286828..42a5305 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,6 @@ from setuptools import find_packages, setup -from flake8_debug.meta import Meta - def __load_readme() -> str: """Returns project description.""" @@ -19,6 +17,8 @@ def __load_requirements() -> Sequence[str]: if __name__ == '__main__': + from flake8_debug.meta import Meta + setup( name=Meta.name, version=Meta.version, diff --git a/tests/flake8_debug_test.py b/tests/flake8_debug_test.py index c1affe4..cd20124 100644 --- a/tests/flake8_debug_test.py +++ b/tests/flake8_debug_test.py @@ -94,3 +94,27 @@ def test_present_bare_set_trace(): ) == ( _out(line=3, column=5, err=PdbError()), ) + + +def test_nested_print_is_detected(): + assert _plugin_results('foo(print(0))') == ( + _out(line=1, column=5, err=PrintError()), + ) + + +def test_nested_breakpoint_is_detected(): + assert _plugin_results('foo(breakpoint())') == ( + _out(line=1, column=5, err=BreakpointError()), + ) + + +def test_no_false_positive_on_arbitrary_object_print(): + assert not _plugin_results('logger.print("msg")') + + +def test_no_false_positive_on_arbitrary_object_breakpoint(): + assert not _plugin_results('self.breakpoint()') + + +def test_no_false_positive_on_arbitrary_object_set_trace(): + assert not _plugin_results('cursor.set_trace()')