Repository: jparise/flake8-assertive

Issue:
can not handle keyword arguments
For the following test case 
```python
class TestCase:
    def test_one(
        self,
    ):
        self.assertTrue(
            expr=some_value,
        )
```

flake8 raises the following exception.

```python
Traceback (most recent call last):
  File "/home/wavenator/.local/bin/flake8", line 8, in <module>
    sys.exit(main())
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/main/cli.py", line 18, in main
    app.run(argv)
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/main/application.py", line 393, in run
    self._run(argv)
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/main/application.py", line 381, in _run
    self.run_checks()
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/main/application.py", line 300, in run_checks
    self.file_checker_manager.run()
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/checker.py", line 331, in run
    self.run_serial()
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/checker.py", line 315, in run_serial
    checker.run_checks()
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/checker.py", line 598, in run_checks
    self.run_ast_checks()
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8/checker.py", line 502, in run_ast_checks
    for (line_number, offset, text, check) in runner:
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8_assertive.py", line 120, in run
    for error in func(node):
  File "/home/wavenator/.local/lib/python3.7/site-packages/flake8_assertive.py", line 160, in check_asserttrue
    if (isinstance(node.args[0], ast.Compare) and
IndexError: list index out of range
```

It happens because in `check_asserttrue` you assume `.args` exists although it does not in functions with keyword arguments.

I can produce a fix if you want. I also assume that this behavior might happen in other tests in your code. I can fix them if you want me to. 

Thanks and I really appreciate your work!

EDIT:
Yep it happens in `check_assertfalse` too. 

Discussion:

Patch:
diff --git a/flake8_assertive.py b/flake8_assertive.py
index e2dff4c..841356c 100644
--- a/flake8_assertive.py
+++ b/flake8_assertive.py
@@ -49,6 +49,13 @@ def is_assert_method_call(node):
             node.func.attr.startswith('assert'))
 
 
+def args(node):
+    for arg in node.args:
+        yield arg
+    for arg in node.keywords:
+        yield arg.value
+
+
 def wrap_deprecated(func, name):
     """Return a check function for a deprecated assert method call.
 
@@ -121,56 +128,56 @@ class Checker(object):
                         yield error
 
     def check_assertequal(self, node):
-        if any(arg for arg in node.args if is_constant(arg, None)):
+        if any(arg for arg in args(node) if is_constant(arg, None)):
             yield self.error(node, 'A502', 'assertIsNone', obj=None)
-        elif any(arg for arg in node.args if is_constant(arg, True)):
+        elif any(arg for arg in args(node) if is_constant(arg, True)):
             yield self.error(node, 'A502', 'assertTrue', obj=True)
-        elif any(arg for arg in node.args if is_constant(arg, False)):
+        elif any(arg for arg in args(node) if is_constant(arg, False)):
             yield self.error(node, 'A502', 'assertFalse', obj=False)
-        elif any(arg for arg in node.args if is_function_call(arg, 'round')):
+        elif any(arg for arg in args(node) if is_function_call(arg, 'round')):
             yield self.error(node, 'A501',
                              'built-in rounding of assertAlmostEqual',
                              op='round')
 
     def check_assertalmostequal(self, node):
-        if any(arg for arg in node.args if is_function_call(arg, 'round')):
+        if any(arg for arg in args(node) if is_function_call(arg, 'round')):
             yield self.error(node, 'A501',
                              'built-in rounding of assertAlmostEqual',
                              op='round')
 
     def check_assertnotequal(self, node):
-        if any(arg for arg in node.args if is_constant(arg, None)):
+        if any(arg for arg in args(node) if is_constant(arg, None)):
             yield self.error(node, 'A502', 'assertIsNotNone', obj=None)
-        elif any(arg for arg in node.args if is_constant(arg, True)):
+        elif any(arg for arg in args(node) if is_constant(arg, True)):
             yield self.error(node, 'A502', 'assertFalse', obj=True)
-        elif any(arg for arg in node.args if is_constant(arg, False)):
+        elif any(arg for arg in args(node) if is_constant(arg, False)):
             yield self.error(node, 'A502', 'assertTrue', obj=False)
-        elif any(arg for arg in node.args if is_function_call(arg, 'round')):
+        elif any(arg for arg in args(node) if is_function_call(arg, 'round')):
             yield self.error(node, 'A501',
                              'built-in rounding of assertNotAlmostEqual',
                              op='round')
 
     def check_assertnotalmostequal(self, node):
-        if any(arg for arg in node.args if is_function_call(arg, 'round')):
+        if any(arg for arg in args(node) if is_function_call(arg, 'round')):
             yield self.error(node, 'A501',
                              'built-in rounding of assertNotAlmostEqual',
                              op='round')
 
     def check_asserttrue(self, node):
-        if (isinstance(node.args[0], ast.Compare) and
-                len(node.args[0].ops) == 1):
-            op = node.args[0].ops[0]
+        arg = next(args(node), None)
+        if arg and isinstance(arg, ast.Compare) and len(arg.ops) == 1:
+            op = arg.ops[0]
             if isinstance(op, ast.In):
                 yield self.error(node, 'A501', 'assertIn', op='in')
             elif isinstance(op, ast.NotIn):
                 yield self.error(node, 'A501', 'assertNotIn', op='in')
             elif isinstance(op, ast.Is):
-                if is_constant(node.args[0].comparators[0], None):
+                if is_constant(arg.comparators[0], None):
                     yield self.error(node, 'A502', 'assertIsNone', obj=None)
                 else:
                     yield self.error(node, 'A501', 'assertIs', op='is')
             elif isinstance(op, ast.IsNot):
-                if is_constant(node.args[0].comparators[0], None):
+                if is_constant(arg.comparators[0], None):
                     yield self.error(node, 'A502', 'assertIsNotNone', obj=None)
                 else:
                     yield self.error(node, 'A501', 'assertIsNot', op='is')
@@ -186,25 +193,25 @@ class Checker(object):
                 yield self.error(node, 'A500', 'assertGreater', op='>')
             elif isinstance(op, ast.GtE):
                 yield self.error(node, 'A500', 'assertGreaterEqual', op='>=')
-        elif is_function_call(node.args[0], 'isinstance'):
+        elif is_function_call(arg, 'isinstance'):
             yield self.error(
                 node, 'A501', 'assertIsInstance', op='isinstance()')
 
     def check_assertfalse(self, node):
-        if (isinstance(node.args[0], ast.Compare) and
-                len(node.args[0].ops) == 1):
-            op = node.args[0].ops[0]
+        arg = next(args(node), None)
+        if arg and isinstance(arg, ast.Compare) and len(arg.ops) == 1:
+            op = arg.ops[0]
             if isinstance(op, ast.In):
                 yield self.error(node, 'A501', 'assertNotIn', op='in')
             elif isinstance(op, ast.NotIn):
                 yield self.error(node, 'A501', 'assertIn', op='in')
             elif isinstance(op, ast.Is):
-                if is_constant(node.args[0].comparators[0], None):
+                if is_constant(arg.comparators[0], None):
                     yield self.error(node, 'A502', 'assertIsNotNone', obj=None)
                 else:
                     yield self.error(node, 'A501', 'assertIsNot', op='is')
             elif isinstance(op, ast.IsNot):
-                if is_constant(node.args[0].comparators[0], None):
+                if is_constant(arg.comparators[0], None):
                     yield self.error(node, 'A502', 'assertIsNone', obj=None)
                 else:
                     yield self.error(node, 'A501', 'assertIs', op='is')
@@ -212,7 +219,7 @@ class Checker(object):
                 yield self.error(node, 'A500', 'assertNotEqual', op='==')
             elif isinstance(op, ast.NotEq):
                 yield self.error(node, 'A500', 'assertEqual', op='!=')
-        elif is_function_call(node.args[0], 'isinstance'):
+        elif is_function_call(arg, 'isinstance'):
             yield self.error(
                 node, 'A501', 'assertNotIsInstance', op='isinstance()')
 


Test patch:
diff --git a/tests/test_checker.py b/tests/test_checker.py
index 4118ad3..0752a24 100644
--- a/tests/test_checker.py
+++ b/tests/test_checker.py
@@ -148,6 +148,15 @@ class TestChecks(unittest.TestCase):
         self.check(
             "self.assertFalse(1 != 0)", "A500", "assertEqual() for '!='")
 
+    def test_keyword_args(self):
+        self.check("self.assertTrue(expr=1)", expected=None)
+        self.check("self.assertTrue(expr=(True is True))", expected="A501")
+        self.check("self.assertEqual(first=1, second=1)", expected=None)
+        self.check("self.assertEqual(first=1, second=None)", "A502")
+        self.check("self.assertEqual(first=None, second=1)", "A502")
+        self.check("self.assertEqual(1, second=1)", expected=None)
+        self.check("self.assertEqual(None, second=1)", "A502")
+
     def test_multiple_comparison_ops(self):
         self.check("self.assertTrue(1 == 1 == 1)", expected=None)
         self.check("self.assertFalse(1 == 1 == 1)", expected=None)