Repository: larsoner/flake8-array-spacing

Issue:
BUG: Regex should be more complete/selective
It should be more selective (matching `[` and maybe ensuring any `i/j` are immediately preceded by numbers, currently it's pretty basic:
```
ARRAY_LIKE_REGEX = re.compile(
    r'[\[(][0-9 ij,+\-*^\/&|\[\]]*[\]),;]'
)
```

@jnothman I know you work on sklearn, @agramfort mentioned this might be useful for you all, too. Any interest in improving the regex [here](https://github.com/larsoner/flake8-array-spacing/blob/master/flake8_array_spacing/__init__.py#L12-L14), and maybe adopting this for sklearn?

Basically this plugin will replace `E` error variants:
```
    extraneous_whitespace,  # 201, 202
    whitespace_around_operator,  # 221, 222
    whitespace_around_comma,  # 241
```
With `A2XX` versions, where the ones above are ignored if they occur within an array-like list of (list of) numerical values. So you basically run an equivalent of `flake8 --ignore E201,E202,E203,E221,E222,E241 --select=A` and it should work. This came up originally for [scipy](https://github.com/scipy/scipy/issues/12367) (WIP PR to add this plugin [here](https://github.com/scipy/scipy/pull/12516/files)) but I imagine you all might have been annoyed about this at some point, too, so wanted to loop you in.

Comments:

Patch:
diff --git a/flake8_array_spacing/__init__.py b/flake8_array_spacing/__init__.py
index 052b4b0..b19694d 100644
--- a/flake8_array_spacing/__init__.py
+++ b/flake8_array_spacing/__init__.py
@@ -9,9 +9,50 @@ __version__ = '0.1.dev0'
 
 # This could almost certainly be better, it just checks for [...]
 # (no pairing within, or checking that i/j are attached to a number, etc.)
-ARRAY_LIKE_REGEX = re.compile(
-    r'[\[(][0-9 ij,+\-*^\/&|\[\]]*[\]),;]'
+_FMT = dict(
+    VARIABLE=r'[_a-z]+[_a-z0-9]*',
+    OPERATORS=r'[+\-*\/^\|&]',
+    SEPARATORS=r'[+\-*\/^\|& ,\[\]()]',
+    NUMERICAL=r"""
+(
+[0-9.]+(e[+-]?[0-9.]+)?j?|
+(np\.|numpy\.)?(nan|inf)|
 )
+""")
+
+_FMT['NUMERICAL_LIST'] = r"""
+[\[(]+  # >= 1 left bracket or paren
+({SEPARATORS}*{NUMERICAL}{SEPARATORS}*)+  # >= 1 numerical
+[\])]+  # >= 1 right bracket or paren
+""".format(**_FMT)
+
+_FMT['ARRAY_WITH_BINARY_OPS'] = r"""
+array\([\[(]+  # open array
+(
+    {SEPARATORS}*
+    (
+        {NUMERICAL}|
+        (
+            ({VARIABLE}|{NUMERICAL})
+            \ *
+            {OPERATORS}
+            \ *
+            ({VARIABLE}|{NUMERICAL})
+        )
+
+    )
+    {SEPARATORS}*
+)+  # at least one numerical-or-variable-with-binary-op
+[\])]+\)  # close array
+""".format(**_FMT)
+
+ARRAY_LIKE_REGEXP = re.compile("""(?x)
+(
+{NUMERICAL_LIST}
+|
+{ARRAY_WITH_BINARY_OPS}
+)""".format(**_FMT))
+
 FUNC_KINDS = (
     (extraneous_whitespace, ('E201', 'E202')),
     (whitespace_around_operator, ('E221', 'E222')),
@@ -35,7 +76,7 @@ class ArraySpacing(object):
         if self._array_ranges is None:
             self._array_ranges = [
                 (match.start() + 1, match.end() + 1)
-                for match in ARRAY_LIKE_REGEX.finditer(self.logical_line)]
+                for match in ARRAY_LIKE_REGEXP.finditer(self.logical_line)]
         return any(start <= idx <= end for start, end in self._array_ranges)
 
     def ignoring(self, kind):


Test patch:
diff --git a/flake8_array_spacing/tests/test_array_spacing.py b/flake8_array_spacing/tests/test_array_spacing.py
index d411f3f..269699e 100644
--- a/flake8_array_spacing/tests/test_array_spacing.py
+++ b/flake8_array_spacing/tests/test_array_spacing.py
@@ -1,4 +1,5 @@
 import subprocess
+from textwrap import dedent
 import pytest
 
 
@@ -7,8 +8,8 @@ def flake8(path, *args):
     import os
     os.chdir(str(path))
     proc = subprocess.run(
-        ['flake8', '--select', 'A2',
-         '--ignore', 'E201,E202,E203,E221,E222,E241', '.'] + list(args),
+        ['flake8', '--select', 'F,A2',
+         '--ignore', 'E201,E202,E203,E221,E222,E241,F821', '.'] + list(args),
         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     stderr = proc.stderr.decode().strip()
     assert stderr == ''
@@ -22,25 +23,45 @@ def test_installed():
     assert 'array-spacing' in out
 
 
-a_b_pre = 'a = 1\nb = 2\n'
-
-
-@pytest.mark.parametrize('line, want_output, want_code', [
-    ('[ -3  + 4j ,  -10 +  20j ]', '', 0),
-    (a_b_pre + '[a, b]', '', 0),
-    (a_b_pre + '[ a, b]', "./bad.py:3:2: A201 whitespace after '['", 1),
-    (a_b_pre + '[a, b ]', "./bad.py:3:6: A202 whitespace before ']'", 1),
-    (a_b_pre + '[a  + b, b]', "./bad.py:3:3: A221 multiple spaces before operator", 1),  # noqa: E501
-    (a_b_pre + '[a +  b, b]', "./bad.py:3:5: A222 multiple spaces after operator", 1),  # noqa: E501
-    (a_b_pre + '[a,  b]', "./bad.py:3:4: A241 multiple spaces after ','", 1),
-    (a_b_pre + '[a,  b]  # noqa', '', 0),
-    (a_b_pre + '[a,  b]  # noqa: E241', '', 0),
+@pytest.mark.parametrize('content, output', [
+    ("""
+     array([[x / 2.0,       0.],
+            [0.,      -y / 2.0]])'""", ''),
+    ("""
+     np.array([[x / 2.0,       0.],
+               [0.,      -y / 2.0]])'""", ''),
+    ('[[x / 2.0,       0.], [0.,      -y / 2.0]]', """\
+     ./bad.py:1:11: A241 multiple spaces after ','
+     ./bad.py:1:27: A241 multiple spaces after ','"""),
+    ('[ -3.  + 4.1j + 2,  -10e-2 +  20e+2j ]', ''),
+    ('[[ 1  , 2 ]  ,  [3, 4]]', ''),
+    ('[[ a  , b ]  ,  [a, b]]', """\
+     ./bad.py:1:3: A201 whitespace after '['
+     ./bad.py:1:10: A202 whitespace before ']'
+     ./bad.py:1:15: A241 multiple spaces after ','"""),
+    ('[ np.inf , 1  , 2 , numpy.nan , -inf  ]', ''),
+    ('[a, b]', ''),
+    ('[ a, b]', "./bad.py:1:2: A201 whitespace after '['"),
+    ('[a, b ]', "./bad.py:1:6: A202 whitespace before ']'"),
+    ('[a  + b, b]',
+     "./bad.py:1:3: A221 multiple spaces before operator"),
+    ('[a +  b, b]',
+    "./bad.py:1:5: A222 multiple spaces after operator"),  # noqa: E501
+    ('[ a,  b ]', """\
+     ./bad.py:1:2: A201 whitespace after '['
+     ./bad.py:1:5: A241 multiple spaces after ','
+     ./bad.py:1:8: A202 whitespace before ']'"""),
+    ('[a,  b]', "./bad.py:1:4: A241 multiple spaces after ','"),
+    ('[a,  b]  # noqa', ''),
+    ('[a,  b]  # noqa: E241', ''),
 ])
-def test_array_spacing(line, want_output, want_code, tmpdir):
+def test_array_spacing(content, output, tmpdir):
     """Test some cases."""
+    content = dedent(content)
+    output = dedent(output)
     fname = tmpdir.join('bad.py')
     with open(fname, 'w') as fid:
-        fid.write(line)
+        fid.write(content)
     got_output, got_code = flake8(tmpdir)
-    assert got_output == want_output
-    assert got_code == want_code
+    assert got_output == output
+    assert got_code == (1 if output else 0)