Repository: deppen8/pandas-vet

Issue:
Method for off-by-default linter checks
As discussed in #64, it would be good to have some checks that can be implemented but are "off" by default. These would be the most opinionated checks that would be a bit too strict to be activated out-of-the-box.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b26e92..fb5f7e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,12 +5,42 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (to the best of our ability).
 
+## Unreleased
+
+### Added
+
+- New check `PD901 'df' is a bad variable name. Be kinder to your future self.` ([#69](https://github.com/deppen8/pandas-vet/pull/69))
+- An `--annoy` flag that can be used to activate checks that set to "off" by default. The off-by-default checks should use the convention `PD9xx` ([#69](https://github.com/deppen8/pandas-vet/pull/69))
+- Added `PD901` to README along with an example use of the `--annoy` flag ([#69](https://github.com/deppen8/pandas-vet/pull/69))
+
+### Changed
+
+- `test_PD012.py` had test cases that used `df = <something>`, which conflicted with the new `PD901` check. These were changed to `employees = <something>` ([#69](https://github.com/deppen8/pandas-vet/pull/69))
+- Applied the `black` formatter to the entire pandas-vet package.
+
+### Deprecated
+
+- None
+
+### Removed
+
+- A few extraneous variables ([455d1f0](https://github.com/deppen8/pandas-vet/pull/69/commits/455d1f0525dd4e9590cd10efdcd39c9d9a7923a2))
+
+### Fixed
+
+- None
+
+### Security
+
+- None
+
+
 ## [0.2.1] - 2019-07-27
 
 ### Added
 
-- Leandro Leites added as contributor (#66)
-- This CHANGELOG.md added
+- Leandro Leites added as contributor ([#66](https://github.com/deppen8/pandas-vet/pull/66))
+- This CHANGELOG.md added ([#68](https://github.com/deppen8/pandas-vet/pull/68))
 
 ### Changed
 
@@ -22,12 +52,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Removed
 
-- Unnecessary commented line from `setup.py` (#67)
+- Unnecessary commented line from `setup.py` ([#67](https://github.com/deppen8/pandas-vet/pull/67))
 
 ### Fixed
 
-- PD015 would fail if `node.func.value` did not have an `id`. Fixed with #65
-- `version.py` now correctly uses v0.2.x. This version file was not bumped with the last release. (#67)
+- PD015 would fail if `node.func.value` did not have an `id`. Fixed with [#65](https://github.com/deppen8/pandas-vet/pull/65)
+- `version.py` now correctly uses v0.2.x. This version file was not bumped with the last release. ([#67](https://github.com/deppen8/pandas-vet/pull/67))
 
 ### Security
 
diff --git a/README.md b/README.md
index bf5cb1c..f875ae7 100644
--- a/README.md
+++ b/README.md
@@ -138,3 +138,13 @@ flake8 pandas_vet setup.py tests --exclude tests/data
 **PD013** '.melt' is preferred to '.stack'; provides same functionality
 
 **PD015** Use '.merge' method instead of 'pd.merge' function. They have equivalent functionality.
+
+### *Very* Opinionated Warnings
+
+These warnings are turned off by default. To enable them, add the `-annoy` flag to your command, e.g.,
+
+```bash
+$ flake8 --annoy my_file.py
+```
+
+**PD901** 'df' is a bad variable name. Be kinder to your future self.
diff --git a/pandas_vet/__init__.py b/pandas_vet/__init__.py
index af81139..fd7bee9 100644
--- a/pandas_vet/__init__.py
+++ b/pandas_vet/__init__.py
@@ -18,6 +18,7 @@ class Visitor(ast.NodeVisitor):
     The `check` functions should be called from the `visit_` method that
     would produce a 'fail' condition.
     """
+
     errors = attr.ib(default=attr.Factory(list))
 
     def visit_Import(self, node):
@@ -56,8 +57,16 @@ class Visitor(ast.NodeVisitor):
         """
         Called for `.attribute` nodes.
         """
+        self.generic_visit(node)  # continue checking children
         self.errors.extend(check_for_values(node))
 
+    def visit_Name(self, node):
+        """
+        Called for `Assignment` nodes.
+        """
+        self.generic_visit(node)  # continue checking children
+        self.errors.extend(check_for_df(node))
+
     def check(self, node):
         self.errors = []
         self.visit(node)
@@ -81,6 +90,22 @@ class VetPlugin:
         except Exception as e:
             raise PandasVetException(e)
 
+    @staticmethod
+    def add_options(optmanager):
+        """Informs flake8 to ignore PD9xx by default."""
+        optmanager.extend_default_ignore(disabled_by_default)
+
+        optmanager.add_option(
+            long_option_name="--annoy",
+            action="store_true",
+            dest="annoy",
+            default=False,
+        )
+
+        options, xargs = optmanager.parse_args()
+        if options.annoy:
+            optmanager.remove_from_default_ignore(disabled_by_default)
+
 
 def check_import_name(node: ast.Import) -> List:
     """Check AST for imports of pandas not using the preferred alias 'pd'.
@@ -163,26 +188,23 @@ def check_for_arithmetic_methods(node: ast.Call) -> List:
     Error/warning message to recommend use of binary arithmetic operators.
     """
     arithmetic_methods = [
-        'add',
-        'sub', 'subtract',
-        'mul', 'multiply',
-        'div', 'divide', 'truediv',
-        'pow',
-        'floordiv',
-        'mod',
-        ]
-    arithmetic_operators = [
-        '+',
-        '-',
-        '*',
-        '/',
-        '**',
-        '//',
-        '%',
-        ]
-
-    if isinstance(node.func, ast.Attribute) and \
-       node.func.attr in arithmetic_methods:
+        "add",
+        "sub",
+        "subtract",
+        "mul",
+        "multiply",
+        "div",
+        "divide",
+        "truediv",
+        "pow",
+        "floordiv",
+        "mod",
+    ]
+
+    if (
+        isinstance(node.func, ast.Attribute)
+        and node.func.attr in arithmetic_methods
+    ):
         return [PD005(node.lineno, node.col_offset)]
     return []
 
@@ -193,11 +215,12 @@ def check_for_comparison_methods(node: ast.Call) -> List:
 
     Error/warning message to recommend use of binary comparison operators.
     """
-    comparison_methods = ['gt', 'lt', 'ge', 'le', 'eq', 'ne']
-    comparison_operators = ['>',  '<',  '>=', '<=', '==', '!=']
+    comparison_methods = ["gt", "lt", "ge", "le", "eq", "ne"]
 
-    if isinstance(node.func, ast.Attribute) and \
-       node.func.attr in comparison_methods:
+    if (
+        isinstance(node.func, ast.Attribute)
+        and node.func.attr in comparison_methods
+    ):
         return [PD006(node.lineno, node.col_offset)]
     return []
 
@@ -304,42 +327,55 @@ def check_for_merge(node: ast.Call) -> List:
     # object.  If the object name is `pd`, and if the `.merge()` method has at
     # least two arguments (left, right, ... ) we will assume that it matches
     # the pattern that we are trying to check, `pd.merge(left, right)`
-    if not hasattr(node.func, 'value'):
-        return []   # ignore functions
-    elif not hasattr(node.func.value, 'id'):
-        return [] # it could be the case that id is not present
+    if not hasattr(node.func, "value"):
+        return []  # ignore functions
+    elif not hasattr(node.func.value, "id"):
+        return []  # it could be the case that id is not present
 
-    if node.func.value.id != 'pd': return[]     # assume object name is `pd`
+    if node.func.value.id != "pd":
+        return []  # assume object name is `pd`
 
-    if not len(node.args) >= 2: return []           # at least two arguments
+    if not len(node.args) >= 2:
+        return []  # at least two arguments
 
-    if isinstance(node.func, ast.Attribute) and \
-       node.func.attr == "merge":
+    if isinstance(node.func, ast.Attribute) and node.func.attr == "merge":
         return [PD015(node.lineno, node.col_offset)]
     return []
 
 
+def check_for_df(node: ast.Name) -> List:
+    """
+    Check for variables named `df`
+    """
+    if node.id == "df" and isinstance(node.ctx, ast.Store):
+        return [PD901(node.lineno, node.col_offset)]
+    return []
+
+
 error = namedtuple("Error", ["lineno", "col", "message", "type"])
 VetError = partial(partial, error, type=VetPlugin)
 
+disabled_by_default = ["PD9"]
+
 PD001 = VetError(
     message="PD001 pandas should always be imported as 'import pandas as pd'"
 )
+
 PD002 = VetError(
     message="PD002 'inplace = True' should be avoided; it has inconsistent behavior"
 )
+
 PD003 = VetError(
     message="PD003 '.isna' is preferred to '.isnull'; functionality is equivalent"
 )
+
 PD004 = VetError(
     message="PD004 '.notna' is preferred to '.notnull'; functionality is equivalent"
 )
-PD005 = VetError(
-    message="PD005 Use arithmetic operator instead of method"
-)
-PD006 = VetError(
-    message="PD006 Use comparison operator instead of method"
-)
+PD005 = VetError(message="PD005 Use arithmetic operator instead of method")
+
+PD006 = VetError(message="PD006 Use comparison operator instead of method")
+
 PD007 = VetError(
     message="PD007 '.ix' is deprecated; use more explicit '.loc' or '.iloc'"
 )
@@ -364,3 +400,7 @@ PD013 = VetError(
 PD015 = VetError(
     message="PD015 Use '.merge' method instead of 'pd.merge' function. They have equivalent functionality."
 )
+
+PD901 = VetError(
+    message="PD901 'df' is a bad variable name. Be kinder to your future self."
+)
diff --git a/setup.py b/setup.py
index 96c1e4c..10e1b69 100644
--- a/setup.py
+++ b/setup.py
@@ -1,15 +1,10 @@
 import setuptools
 
-requires = [
-    "flake8 > 3.0.0",
-    "attrs",
-]
+requires = ["flake8 > 3.0.0", "attrs"]
 
-tests_requires = [
-    "pytest > 4.0.0"
-]
+tests_requires = ["pytest > 4.0.0"]
 
-flake8_entry_point = 'flake8.extension'
+flake8_entry_point = "flake8.extension"
 
 with open("README.md", "rt") as f:
     long_description = f.read()
@@ -26,16 +21,10 @@ setuptools.setup(
     author="Jacob Deppen",
     author_email="deppen.8@gmail.com",
     url="https://github.com/deppen8/pandas-vet",
-    packages=[
-        "pandas_vet",
-    ],
+    packages=["pandas_vet"],
     install_requires=requires,
     tests_require=tests_requires,
-    entry_points={
-        flake8_entry_point: [
-            'PD = pandas_vet:VetPlugin',
-        ],
-    },
+    entry_points={flake8_entry_point: ["PD = pandas_vet:VetPlugin"]},
     classifiers=[
         "Development Status :: 4 - Beta",
         "Environment :: Console",


Test patch:
diff --git a/tests/test_PD005.py b/tests/test_PD005.py
index 1fcc596..78b3eca 100644
--- a/tests/test_PD005.py
+++ b/tests/test_PD005.py
@@ -14,17 +14,9 @@ def test_PD005_pass_arithmetic_operator():
     Test that explicit use of binary arithmetic operator does not
     result in an error.
     """
-    arithmetic_operators = [
-        '+',
-        '-',
-        '*',
-        '/',
-        '**',
-        '//',
-        '%',
-        ]
+    arithmetic_operators = ["+", "-", "*", "/", "**", "//", "%"]
     for op in arithmetic_operators:
-        statement = 'C = A {0} B'.format(op)
+        statement = "C = A {0} B".format(op)
         tree = ast.parse(statement)
         actual = list(VetPlugin(tree).run())
         expected = []
@@ -36,16 +28,20 @@ def test_PD005_fail_arithmetic_method():
     Test that using arithmetic method results in an error.
     """
     arithmetic_methods = [
-        'add',
-        'sub', 'subtract',
-        'mul', 'multiply',
-        'div', 'divide', 'truediv',
-        'pow',
-        'floordiv',
-        'mod',
-        ]
+        "add",
+        "sub",
+        "subtract",
+        "mul",
+        "multiply",
+        "div",
+        "divide",
+        "truediv",
+        "pow",
+        "floordiv",
+        "mod",
+    ]
     for op in arithmetic_methods:
-        statement = 'C = A.{0}(B)'.format(op)
+        statement = "C = A.{0}(B)".format(op)
         tree = ast.parse(statement)
         actual = list(VetPlugin(tree).run())
         expected = [PD005(1, 4)]
diff --git a/tests/test_PD006.py b/tests/test_PD006.py
index 7dae856..b1e1b62 100644
--- a/tests/test_PD006.py
+++ b/tests/test_PD006.py
@@ -14,9 +14,9 @@ def test_PD006_pass_comparison_operator():
     Test that explicit use of binary comparison operator does not
     result in an error.
     """
-    comparison_operators = ['>',  '<',  '>=', '<=', '==', '!=']
+    comparison_operators = [">", "<", ">=", "<=", "==", "!="]
     for op in comparison_operators:
-        statement = 'C = A {0} B'.format(op)
+        statement = "C = A {0} B".format(op)
         tree = ast.parse(statement)
         actual = list(VetPlugin(tree).run())
         expected = []
@@ -27,9 +27,9 @@ def test_PD006_fail_comparison_method():
     """
     Test that using comparison method results in an error.
     """
-    comparison_methods = ['gt', 'lt', 'ge', 'le', 'eq', 'ne']
+    comparison_methods = ["gt", "lt", "ge", "le", "eq", "ne"]
     for op in comparison_methods:
-        statement = 'C = A.{0}(B)'.format(op)
+        statement = "C = A.{0}(B)".format(op)
         tree = ast.parse(statement)
         actual = list(VetPlugin(tree).run())
         expected = [PD006(1, 4)]
diff --git a/tests/test_PD012.py b/tests/test_PD012.py
index 176b2d4..d669b42 100644
--- a/tests/test_PD012.py
+++ b/tests/test_PD012.py
@@ -12,7 +12,7 @@ def test_PD012_pass_read_csv():
     """
     Test that using .read_csv() explicitly does not result in an error.
     """
-    statement = "df = pd.read_csv(input_file)"
+    statement = "employees = pd.read_csv(input_file)"
     tree = ast.parse(statement)
     actual = list(VetPlugin(tree).run())
     expected = []
@@ -23,10 +23,10 @@ def test_PD012_fail_read_table():
     """
     Test that using .read_table() method results in an error.
     """
-    statement = "df = pd.read_table(input_file)"
+    statement = "employees = pd.read_table(input_file)"
     tree = ast.parse(statement)
     actual = list(VetPlugin(tree).run())
-    expected = [PD012(1, 5)]
+    expected = [PD012(1, 12)]
     assert actual == expected
 
 
@@ -34,7 +34,7 @@ def test_PD012_node_Name_pass():
     """
     Test that where 'read_table' is a Name does NOT raise an error
     """
-    statement = "df = read_table"
+    statement = "employees = read_table"
     tree = ast.parse(statement)
     actual = list(VetPlugin(tree).run())
     expected = []
diff --git a/tests/test_PD901.py b/tests/test_PD901.py
new file mode 100644
index 0000000..f4093f1
--- /dev/null
+++ b/tests/test_PD901.py
@@ -0,0 +1,36 @@
+import ast
+
+from pandas_vet import VetPlugin
+from pandas_vet import PD901
+
+
+def test_PD901_pass_non_df():
+    statement = "employees = pd.DataFrame(employee_dict)"
+    tree = ast.parse(statement)
+    actual = list(VetPlugin(tree).run())
+    expected = []
+    assert actual == expected
+
+
+def test_PD901_pass_part_df():
+    statement = "employees_df = pd.DataFrame(employee_dict)"
+    tree = ast.parse(statement)
+    actual = list(VetPlugin(tree).run())
+    expected = []
+    assert actual == expected
+
+
+def test_PD901_pass_df_param():
+    statement = "my_function(df=data)"
+    tree = ast.parse(statement)
+    actual = list(VetPlugin(tree).run())
+    expected = []
+    assert actual == expected
+
+
+def test_PD901_fail_df_var():
+    statement = "df = pd.DataFrame()"
+    tree = ast.parse(statement)
+    actual = list(VetPlugin(tree).run())
+    expected = [PD901(1, 0)]
+    assert actual == expected