forked from Unidata/MetPy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CI: Add custom flake8 plugin (Fixes Unidata#1818)
This gives us a custom flake8 plugin we can use to detect custom errors, style violations that we want. Right now this checks for multiplication by units on the right.
- Loading branch information
1 parent
6ff8b8e
commit eb6de07
Showing
4 changed files
with
127 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# Copyright (c) 2021 MetPy Developers. | ||
# Distributed under the terms of the BSD 3-Clause License. | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
""" | ||
Custom flake8 plugin to catch MetPy-specific bad style/practice. | ||
Currently this only looks for multiplication or division by units, since that can break | ||
masked arrays and is slower than calling ``Quantity()``. | ||
""" | ||
|
||
import ast | ||
from collections import namedtuple | ||
|
||
try: | ||
from importlib.metadata import version | ||
except ImportError: | ||
from importlib_metadata import version | ||
|
||
|
||
Error = namedtuple('Error', 'lineno col code') | ||
|
||
|
||
class MetPyVisitor(ast.NodeVisitor): | ||
"""Visit nodes of the AST looking for violations.""" | ||
|
||
def __init__(self): | ||
"""Initialize the visitor.""" | ||
self.errors = [] | ||
|
||
@staticmethod | ||
def _is_unit(node): | ||
"""Check whether a node should be considered to represent "units".""" | ||
# Looking for a .units attribute, a units.myunit, or a call to units() | ||
is_units_attr = isinstance(node, ast.Attribute) and node.attr == 'units' | ||
is_reg_attr = (isinstance(node, ast.Attribute) | ||
and isinstance(node.value, ast.Name) and node.value.id == 'units') | ||
is_reg_call = (isinstance(node, ast.Call) | ||
and isinstance(node.func, ast.Name) and node.func.id == 'units') | ||
is_unit_alias = isinstance(node, ast.Name) and 'unit' in node.id | ||
|
||
return is_units_attr or is_reg_attr or is_reg_call or is_unit_alias | ||
|
||
def visit_BinOp(self, node): | ||
"""Visit binary operations.""" | ||
# Check whether this is multiplying or dividing by units | ||
if (isinstance(node.op, (ast.Mult, ast.Div)) | ||
and (self._is_unit(node.right) or self._is_unit(node.left))): | ||
self.error(node.lineno, node.col_offset, 1) | ||
|
||
super().generic_visit(node) | ||
|
||
def error(self, lineno, col, code): | ||
"""Add an error to our output list.""" | ||
self.errors.append(Error(lineno, col, code)) | ||
|
||
|
||
class MetPyChecker: | ||
"""Flake8 plugin class to check MetPy style/best practice.""" | ||
|
||
name = __name__ | ||
version = version(__name__) | ||
|
||
def __init__(self, tree): | ||
"""Initialize the plugin.""" | ||
self.tree = tree | ||
|
||
def run(self): | ||
"""Run the plugin and yield errors.""" | ||
visitor = MetPyVisitor() | ||
visitor.visit(self.tree) | ||
for err in visitor.errors: | ||
yield self.error(err) | ||
|
||
def error(self, err): | ||
"""Format errors into Flake8's required format.""" | ||
return (err.lineno, err.col, | ||
f'METPY{err.code:03d}: Multiplying/dividing by units--use units.Quantity()', | ||
type(self)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Copyright (c) 2021 MetPy Developers. | ||
# Distributed under the terms of the BSD 3-Clause License. | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
"""Setup script for installing MetPy custom flake8 plugin.""" | ||
|
||
from setuptools import setup | ||
|
||
setup( | ||
name='flake8-metpy', | ||
version='1.0', | ||
license='BSD 3 Clause', | ||
py_modules=['flake8_metpy'], | ||
install_requires=['flake8 > 3.0.0'], | ||
entry_points={ | ||
'flake8.extension': ['METPY00 = flake8_metpy:MetPyChecker'], | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Copyright (c) 2021 MetPy Developers. | ||
# Distributed under the terms of the BSD 3-Clause License. | ||
# SPDX-License-Identifier: BSD-3-Clause | ||
"""Tests for custom Flake8 plugin for MetPy.""" | ||
|
||
import ast | ||
|
||
import pytest | ||
|
||
from flake8_metpy import MetPyChecker | ||
|
||
|
||
@pytest.mark.parametrize('source, errs', [ | ||
('5 * pressure.units', 1), | ||
('pw = -1. * (np.trapz(w.magnitude, pres.magnitude) * (w.units * pres.units))', 1), | ||
("""def foo(): | ||
return ret * moist_adiabat_temperatures.units""", 1), | ||
('p_interp = np.sort(np.append(p_interp.m, top_pressure.m)) * pressure.units', 1), | ||
('parameter = data[ob_type][subset].values * units(self.data.units[ob_type])', 1), | ||
('np.nan * pressure.units', 1), | ||
('np.array([1, 2, 3]) * units.m', 1), | ||
('np.arange(4) * units.s', 1), | ||
('np.ma.array([1, 2, 3]) * units.hPa', 1) | ||
]) | ||
def test_plugin(source, errs): | ||
"""Test that the flake8 checker works correctly.""" | ||
checker = MetPyChecker(ast.parse(source)) | ||
assert len(list(checker.run())) == errs |