Custom Python linting through AST expressions

Overview

bellybutton

Build Status PyPI version PyPI - Python Version

bellybutton is a customizable, easy-to-configure linting engine for Python.

What is this good for?

Tools like pylint and flake8 provide, out-of-the-box, a wide variety of rules for enforcing Python best practices, ensuring PEP-8 compliance, and avoiding frequent sources of bugs. However, many projects have project-specific candidates for static analysis, such as internal style guides, areas of deprecated functionality, or common sources of error. This is especially true of those projects with many contributors or with large or legacy codebases.

bellybutton allows custom linting rules to be specified on a per-project basis and detected as part of your normal build, test and deployment process and, further, makes specifying these rules highly accessible, greatly lowering the cost of adoption.

Give bellybutton a try if:

  • You find yourself making the same PR comments over and over again
  • You need a means of gradually deprecating legacy functionality
  • You're looking to build a self-enforcing style guide
  • Your project needs to onboard new or junior developers more quickly and effectively
  • You have Python nitpicks that go beyond what standard linting tools enforce

Installation & getting started

bellybutton can be installed via:

pip install bellybutton

Once installed, running

bellybutton init

in your project's root directory will create a .bellybutton.yml configuration file with an example rule for you to begin adapting. bellybutton will also try to provide additional rule settings based on the directory structure of your project.

Once you have configured bellybutton for your project, running

bellybutton lint

will lint the project against the rules specified in your .bellybutton.yml. Additionally, running

bellybutton lint --modified-only

will, if using git, only lint those files that differ from origin/master.

For adding bellybutton to your CI pipeline, take a look at this repository's tox configuration and .travis.yml as an example.

Concepts

Rules

Rules in bellybutton supply patterns that should be caught and cause linting to fail. Rules as specified in your .bellybutton.yml configuration must consist of:

  • A description description, expressing the meaning of the rule
  • An expression expr, specifying the pattern to be caught - either as an astpath expression or as a regular expression (!regex ...).

Additionally, the key used for the rule within the rules mapping serves as its name.

Rules may also consist of:

  • Settings settings that specify on which files the rule is to be enforced, as well as whether it can be ignored via a # bb: ignore comment
  • An example example of Python code that would be matched by the rule
  • A counter-example instead of an alternative piece of code, for guiding the developer in fixing their linting error.

These example and instead clauses are checked at run-time to ensure that they respectively are and are not matched by the rule's expr.

As an example, a rule to lint for a deprecated function call using an astpath expression might look like:

DeprecatedFnCall:
  description: `deprecated_fn` will be deprecated in v9.1.2. Please use `new_fn` instead.
  expr: //Call[func/Name/@id='deprecated_fn']
  example: "deprecated_fn(*values)"
  instead: "new_fn(values)"

Settings

!settings nodes specify:

  • included paths on which rules are to be run, using glob notation
  • excluded paths on which rules are not to be run (even when matching the included paths)
  • A boolean allow_ignore which determines whether rules can be ignored, providing the line matching the rule has a # bb: ignore comment.

Additionally, at the root level of .bellybutton.yml, a default_settings setting may be specified which will be used by rules without explicit settings. Each rule must either have a settings parameter or be able to fall back on the default_settings.

As an example, a !settings node to lint only a specific module might look like:

my_module_settings: !settings
  included:
    - ~+/my_package/my_module.py
  excluded: []
  allow_ignore: no

Example usage

Check out this repository's .bellybutton.yml as an example bellybutton configuration file, and astpath's README for examples of the types of patterns you can lint for using bellybutton.

Development status

bellybutton is in an alpha release and, as such, is missing some key features, documentation, and full test coverage. Further, bellybutton is not optimized for performance on extremely large codebases and may contain breaking bugs. Please report any bugs encountered.

Known issues:

  • The !chain and !verbal expression nodes are not yet implemented

Contacts

Comments
  • Feature suggestion: Run on files given from terminal, not on files specified in configuration file

    Feature suggestion: Run on files given from terminal, not on files specified in configuration file

    While having an option to specify these files using configuration file is cool, in my use case, it is not the best.

    I am creating a tool for code quality checks, which uses pylint and mypy and I want to be able to integrate bellybutton to it. Unfortunately bellybutton doesn't allow to specify, on which files it should run using command line. This turns out to be very limiting, because my tool could be launched on specified files only.

    I think, it would be better, if user could specify the files using command line, not only using the configuration file.

    I am willing to implement this myself, if you don't {want|have time} to do it yourself, if you like it.

    opened by tomascapek 7
  • Bump pyyaml and specify explicit FullLoader for yaml

    Bump pyyaml and specify explicit FullLoader for yaml

    Version for pyyaml has been restricted because it has started to require an explicit loader for loading yaml files (see #20). By applying this commit, we can now remove the restriction, since we explicitly specify the loader.

    We had to bump the lower bound for pyyaml to >=4.0 because 3.x versions did not have a FullLoader, which is required for this to work properly.

    opened by palicand 5
  • Multi-line rules

    Multi-line rules

    Bellybutton doesn't like this:

      filter_by:
        description: Use SQLAlchemy's filter_by for concision
        # Find calls to "filter" that:
        # 
        # - are chained after a call to "query" with one argument,
        # - have one argument,
        # - which is an equality comparison
        #   * which has a LHS which is an attribute access on a name
        expr: >
          //Call[ ./func/Attribute[ @attr='filter'
                                    and ./value/Call[./func/Attribute[@attr='query']
                                    and ./args[count(child::*) = 1]]
                                  ]
                 and ./args[count(child::*) = 1]
                 and ./args/Compare[ ./ops/Eq
                                     and ./left/Attribute[ @attr != 'attributes'
                                                           and ./value/Name
                                                         ]
                                   ]
                ]
        example: |
          session.query(Model).filter(Model.attr == baz)
        instead: |
          session.query(Model).filter_by(attr = baz)
        settings: *all_files
    

    it says:

    ERROR: /x/frontend/.bellybutton.yml, rule `filter_by`: 'str' object has no attribute 'match'
    

    It works fine if you inline expr, but this is a pain because expr is huge:

        expr: //Call[./func/Attribute[ @attr='filter' and ./value/Call[./func/Attribute[@attr='query'] and ./args[count(child::*) = 1]]] and ./args[count(child::*) = 1] and ./args/Compare[ ./ops/Eq and ./left/Attribute[ @attr != 'attributes' and ./value/Name]]]
    

    This is version 0.3.0.

    opened by langston-barrett 2
  • --modified-only flag walks over all file types in diff

    --modified-only flag walks over all file types in diff

    Noticed that when specifying the --modified-only flag, bellybutton no longer filters for only '.py' files, which causes it to try and parse unwanted files.

    opened by fergusleahytab 2
  • Crash when using regular expressions

    Crash when using regular expressions

    I created a simple rule from which I expected to fail on every single file in my project:

    settings:
      all_files: &all_files !settings
        included:
          - ~+/*
        excluded:
          - ~+/.tox/*
        allow_ignore: yes
    
    default_settings: *all_files
    
    rules:
      SimpleRule:
        description: "Simple rule"
        expr: !regex .*
    

    And then I launched it:

    bellybutton lint
    

    Which results in a crash:

    Traceback (most recent call last):
      File "/home/dev_intern/.virtualenvs/bellybutton-test/bin/bellybutton", line 11, in <module>
        sys.exit(main())
      File "/home/dev_intern/.virtualenvs/bellybutton-test/lib/python3.5/site-packages/bellybutton/cli.py", line 203, in main
        for arg in args.func.__code__.co_varnames[:args.func.__code__.co_argcount]
      File "/home/dev_intern/.virtualenvs/bellybutton-test/lib/python3.5/site-packages/bellybutton/cli.py", line 167, in lint
        linting_results = list(lint_file(filepath, file_contents, rules))
      File "/home/dev_intern/.virtualenvs/bellybutton-test/lib/python3.5/site-packages/bellybutton/linting.py", line 65, in lint_file
        for match in re.finditer(rule.expr)
    TypeError: finditer() missing 1 required positional argument: 'string'
    

    Can this be fixed soon?

    opened by tomascapek 2
  • Add support for pre-commit

    Add support for pre-commit

    First of all, great work with bellybutton, thanks for that!

    We are using pre-commit and I was wondering if we could add bellybutton as a hook as well. We can use our own fork, but other people might want to use it like this too, so here it is. The addition of the .pre-commit-hooks.yaml file allows this repo to be used with pre-commit. It will simply run bellybutton lint for you in your repository and you can add args if you like into your own pre-commit config.

    It can be used like this:

    # .pre-commit-config.yaml
    repos:
      - repo: https://github.com/hchasestevens/bellybutton
        rev: '9653582' # git rev (commit hash in this case)
        hooks:
          - id: bellybutton
    
    opened by jelleklaver 1
  • Support non-master default branches

    Support non-master default branches

    More and more projects use a default branch other than 'master'. This PR guesses the default branch based on HEAD of the origin remote.

    This PR also removes the 'fetch' call, which shaves off two seconds. When you're working on a relatively up-to-date clone, this shouldn't change the behavior too much.

    opened by remcokranenburg 1
  • Introduce FileManager and open files as-needed

    Introduce FileManager and open files as-needed

    In this PR, we introduce a simple FileManager class that allows you to open a file as-needed, and caches the contents for future use.

    Reasoning: opening all Python files indiscriminately slows us down and is an unnecessary risk.

    Longer explanation:

    Big repositories have a lot of Python files, many of which might not be covered by the include patterns. It makes no sense to open all these files up front. In the company I work for, bellybutton would actually crash on some test files that contain invalid UTF-8 sequences (by design). These files are not covered by any bellybutton rule, but still crashed it. With this PR, we only open the files when the linter has decided to apply a rule to it.

    opened by remcokranenburg 1
  • Support multiple config files (from cli)

    Support multiple config files (from cli)

    Hi,

    I would love to be able to explicitly tell bellybutton which configuration file to use. Furthermore, I believe it might be sensible to allow for multiple configuration files to be loaded, which might help to apply the same rules over multiple projects w/o config duplication.

    bellybutton lint --config /some/path/shared_bellybutton.yml .bellybutton.yml
    

    This could simply mean to have the rules from the files concatenated or result with rules in .bellybutton.yml overwriting rules with the same names in /some/path/shared_bellybutton.yml.

    What do you think?

    opened by backbord 0
  • Exclude doesn't exclude

    Exclude doesn't exclude

    I have a project where I put lots of virtual environments under .mam. I've generated a config using bellybutton init and then excluded that directory with the following settings file:

    settings:
      all_files: &all_files !settings
        included:
          - ~+/*
        excluded:
          - ~+/.tox/*
          - ~+/.mam/*
        allow_ignore: yes
    
    default_settings: *all_files
    
    rules:
      ExampleRule:
        description: "Empty module."
        expr: /Module/body[not(./*)]
        example: ""
        instead: |
          """This module has a docstring."""
    

    I get the error in #22 from these files. I know this because I changed cli#109 to use a try except and raise the file name as a new error. The problem file is:

    .\.mam\bandit\Lib\functools.py
    

    I'm guessing I'm doing something wrong. But it's not excluding the file.

    opened by Peilonrayz 4
  • Bellybutton breaks on non-ascii characters in source

    Bellybutton breaks on non-ascii characters in source

    Traceback:

    Traceback (most recent call last):
      File "/usr/local/bin/bellybutton", line 10, in <module>
        sys.exit(main())
      File "/usr/local/lib/python3.6/dist-packages/bellybutton/cli.py", line 218, in main
        for arg in args.func.__code__.co_varnames[:args.func.__code__.co_argcount]
      File "/usr/local/lib/python3.6/dist-packages/bellybutton/cli.py", line 191, in lint
        for failure in linting_failures(filepaths, rules):
      File "/usr/local/lib/python3.6/dist-packages/bellybutton/cli.py", line 134, in linting_failures
        for filepath, file_contents in files:
      File "/usr/local/lib/python3.6/dist-packages/bellybutton/cli.py", line 109, in open_python_files
        contents = f.read()
      File "/usr/lib/python3.6/encodings/ascii.py", line 26, in decode
        return codecs.ascii_decode(input, self.errors)[0]
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 3487: ordinal not in range(128)
    
    opened by hchasestevens 0
Owner
H. Chase Stevens
Metaprogramming, natural language processing, and global optimization technique enthusiast.
H. Chase Stevens
Simple Python style checker in one Python file

pycodestyle (formerly called pep8) - Python style guide checker pycodestyle is a tool to check your Python code against some of the style conventions

Python Code Quality Authority 4.7k Jan 1, 2023
Optional static typing for Python 3 and 2 (PEP 484)

Mypy: Optional Static Typing for Python Got a question? Join us on Gitter! We don't have a mailing list; but we are always happy to answer questions o

Python 14.4k Jan 8, 2023
A Python Parser

parso - A Python Parser Parso is a Python parser that supports error recovery and round-trip parsing for different Python versions (in multiple Python

Dave Halter 520 Dec 26, 2022
A simple program which checks Python source files for errors

Pyflakes A simple program which checks Python source files for errors. Pyflakes analyzes programs and detects various errors. It works by parsing the

Python Code Quality Authority 1.2k Dec 30, 2022
Performant type-checking for python.

Pyre is a performant type checker for Python compliant with PEP 484. Pyre can analyze codebases with millions of lines of code incrementally – providi

Facebook 6.2k Jan 4, 2023
A static type analyzer for Python code

pytype - ?? ✔ Pytype checks and infers types for your Python code - without requiring type annotations. Pytype can: Lint plain Python code, flagging c

Google 4k Dec 31, 2022
The strictest and most opinionated python linter ever!

wemake-python-styleguide Welcome to the strictest and most opinionated python linter ever. wemake-python-styleguide is actually a flake8 plugin with s

wemake.services 2.1k Jan 1, 2023
Static type checker for Python

Static type checker for Python Speed Pyright is a fast type checker meant for large Python source bases. It can run in a “watch” mode and performs fas

Microsoft 9.2k Jan 3, 2023
Tool to check the completeness of MANIFEST.in for Python packages

check-manifest Are you a Python developer? Have you uploaded packages to the Python Package Index? Have you accidentally uploaded broken packages with

Marius Gedminas 270 Dec 26, 2022
A python documentation linter which checks that the docstring description matches the definition.

Darglint A functional docstring linter which checks whether a docstring's description matches the actual function/method implementation. Darglint expe

Terrence Reilly 463 Dec 31, 2022
Flake8 plugin that checks import order against various Python Style Guides

flake8-import-order A flake8 and Pylama plugin that checks the ordering of your imports. It does not check anything else about the imports. Merely tha

Python Code Quality Authority 270 Nov 24, 2022
Flake8 extension for checking quotes in python

Flake8 Extension to lint for quotes. Major update in 2.0.0 We automatically encourage avoiding escaping quotes as per PEP 8. To disable this, use --no

Zachary Heller 157 Dec 13, 2022
Check for python builtins being used as variables or parameters

Flake8 Builtins plugin Check for python builtins being used as variables or parameters. Imagine some code like this: def max_values(list, list2):

Gil Forcada Codinachs 98 Jan 8, 2023
flake8 plugin to run black for checking Python coding style

flake8-black Introduction This is an MIT licensed flake8 plugin for validating Python code style with the command line code formatting tool black. It

Peter Cock 146 Dec 15, 2022
Unbearably fast O(1) runtime type-checking in pure Python.

Look for the bare necessities, the simple bare necessities. Forget about your worries and your strife. — The Jungle Book.

beartype 1.4k Jan 1, 2023
Naming Convention checker for Python

PEP 8 Naming Conventions Check your code against PEP 8 naming conventions. This module provides a plugin for flake8, the Python code checker. (It repl

Python Code Quality Authority 411 Dec 23, 2022
Code audit tool for python.

Pylama Code audit tool for Python and JavaScript. Pylama wraps these tools: pycodestyle (formerly pep8) © 2012-2013, Florent Xicluna; pydocstyle (form

Kirill Klenov 967 Jan 7, 2023
Flake8 extension for enforcing trailing commas in python

Flake8 Extension to enforce better comma placement. Usage If you are using flake8 it's as easy as: pip install flake8-commas Now you can avoid those a

Python Code Quality Authority 127 Sep 3, 2022
Tool for pinpointing circular imports in Python. Find cyclic imports in any project

Pycycle: Find and fix circular imports in python projects Pycycle is an experimental project that aims to help python developers fix their circular de

Vadim Kravcenko 311 Dec 15, 2022