diff --git a/README.md b/README.md deleted file mode 100644 index 7ea5d70..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# flake8-bugbear -A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pep8. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3646340 --- /dev/null +++ b/README.rst @@ -0,0 +1,58 @@ +============== +flake8-bugbear +============== + +A plugin for flake8 finding likely bugs and design problems in your +program. Contains warnings that don't belong in pyflakes and pep8:: + + bug·bear (bŭg′bâr′) + n. + 1. A cause of fear, anxiety, or irritation: *Overcrowding is often + a bugbear for train commuters.* + 2. A difficult or persistent problem: *"One of the major bugbears of + traditional AI is the difficulty of programming computers to + recognize that different but similar objects are instances of the + same type of thing" (Jack Copeland).* + 3. A fearsome imaginary creature, especially one evoked to frighten + children. + +List of warnings +---------------- + +B001 +~~~~ + +Do not use bare ``except:``, it also catches unexpected events " "like +memory errors, interrupts, system exit, and so on. Prefer ``except +Exception:``. If you're sure what you're doing, be explicit and write +``except BaseException:``. + +Tests +----- + +Just run:: + + python setup.py test + + +License +------- + +MIT + + +Change Log +---------- + +16.4.0 +~~~~~~ + +* first published version + +* date-versioned + + +Authors +------- + +Glued together by `Łukasz Langa `_. diff --git a/bugbear.py b/bugbear.py new file mode 100644 index 0000000..7fc174a --- /dev/null +++ b/bugbear.py @@ -0,0 +1,89 @@ +import ast +from collections import namedtuple +from functools import partial + +import attr +import pep8 + + +__version__ = '16.4.0' + + +@attr.s +class BugBearChecker(object): + name = 'flake8-bugbear' + version = __version__ + + tree = attr.ib(default=None) + filename = attr.ib(default='(none)') + builtins = attr.ib(default=None) + lines = attr.ib(default=None) + visitor = attr.ib(default=attr.Factory(lambda: BugBearVisitor)) + + def run(self): + if not self.tree or not self.lines: + self.load_file() + visitor = self.visitor( + filename=self.filename, + lines=self.lines, + ) + visitor.visit(self.tree) + for e in visitor.errors: + if pep8.noqa(self.lines[e.lineno - 1]): + continue + + yield e + + def load_file(self): + """Loads the file in a way that auto-detects source encoding and deals + with broken terminal encodings for stdin. + + Stolen from flake8_import_order because it's good. + """ + + if self.filename in ("stdin", "-", None): + self.filename = "stdin" + self.lines = pep8.stdin_get_value().splitlines(True) + else: + self.lines = pep8.readlines(self.filename) + + if not self.tree: + self.tree = ast.parse("".join(self.lines)) + + +@attr.s +class BugBearVisitor(ast.NodeVisitor): + filename = attr.ib() + lines = attr.ib() + node_stack = attr.ib(default=attr.Factory(list)) + errors = attr.ib(default=attr.Factory(list)) + + if False: + # Useful for tracing what the hell is going on. + + def __getattr__(self, name): + print(name) + return self.__getattribute__(name) + + def visit(self, node): + self.node_stack.append(node) + super().visit(node) + self.node_stack.pop() + + def visit_ExceptHandler(self, node): + if node.type is None: + self.errors.append( + B001(node.lineno, node.col_offset) + ) + + +error = namedtuple('error', 'lineno col message type') +B001 = partial( + error, + message="B001: Do not use bare `except:`, it also catches unexpected " + "events like memory errors, interrupts, system exit, and so on. " + "Prefer `except Exception:`. If you're sure what you're doing, " + "be explicit and write `except BaseException:`.", + type=BugBearChecker, +) + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5686c8e --- /dev/null +++ b/setup.py @@ -0,0 +1,57 @@ +# Copyright (C) 2016 Łukasz Langa + +import ast +import os +import re +from setuptools import setup + + +current_dir = os.path.abspath(os.path.dirname(__file__)) +ld_file = open(os.path.join(current_dir, 'README.rst')) +try: + long_description = ld_file.read() +finally: + ld_file.close() + + +_version_re = re.compile(r'__version__\s+=\s+(?P.*)') + + +with open('bugbear.py', 'rb') as f: + version = _version_re.search(f.read().decode('utf-8')).group('version') + version = str(ast.literal_eval(version)) + + +setup( + name='flake8-bugbear', + version=version, + description="A plugin for flake8 finding likely bugs and design problems " + "in your program. Contains warnings that don't belong in " + "pyflakes and pep8.", + long_description=long_description, + keywords='flake8 bugbear bugs pyflakes pylint linter qa', + author='Łukasz Langa', + author_email='lukasz@langa.pl', + url='https://github.com/ambv/flake8-bugbear', + license='MIT', + py_modules=['bugbear'], + zip_safe=False, + install_requires = ['flake8', 'attrs'], + test_suite='tests.test_bugbear', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Software Development :: Quality Assurance', + ], + entry_points={ + 'flake8.extension': [ + 'B00 = bugbear:BugBearChecker', + ], + }, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/b001.py b/tests/b001.py new file mode 100644 index 0000000..b10535d --- /dev/null +++ b/tests/b001.py @@ -0,0 +1,49 @@ +""" +Should emit: +B001 - on lines 8 and 40 +""" + +try: + import something +except: + # should be except ImportError: + import something_else as something + +try: + pass +except ValueError: + # no warning here, all good + pass + +try: + pass +except (KeyError, IndexError): + # no warning here, all good + pass + +try: + pass +except BaseException as be: + # no warning here, all good + pass + +try: + pass +except BaseException: + # no warning here, all good + pass + + +def func(**kwargs): + try: + is_debug = kwargs['debug'] + except: + # should be except KeyError: + return + + results = something.call(debug=is_debug) + try: + results['ok'] + except: # noqa + # warning silenced + return diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py new file mode 100644 index 0000000..1c93ee4 --- /dev/null +++ b/tests/test_bugbear.py @@ -0,0 +1,21 @@ +from pathlib import Path +import unittest + +from bugbear import BugBearChecker, B001 + + +class BugbearTestCase(unittest.TestCase): + maxDiff = None + + def test_b001(self): + filename = Path(__file__).absolute().parent / 'b001.py' + bbc = BugBearChecker(filename=str(filename)) + errors = list(bbc.run()) + self.assertEqual( + errors, + [B001(8, 0), B001(40, 4)], + ) + + +if __name__ == '__main__': + unittest.main()