Repository: PyCQA/pyflakes (python)

Issue:
F822 (flake8): assigning result of adding tuples with wrong names to `__all__` doesn't warn
This properly warns (`example.py:4:1: F822 undefined name 'Test2' in __all__`):
```py
Test1 = "Test1"

__all__ = ("Test1",)
__all__ += ("Test2",)
```
And this doesn't:
```py
Test1 = "Test1"

__all__ = ("Test1",) + ("Test2",)
```

Version info:
```
$ flake8 --version
3.8.1 (mccabe: 0.6.1, pycodestyle: 2.6.0, pyflakes: 2.2.0) CPython 3.8.2 on Windows
```

Pull request description:
Extend F822 to support tuple concatenation
Fixes #543 

// Thanks asottile for pointing me in the right direction


Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index 1738253..2d907d6 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -2,12 +2,12 @@
 install:
   - python -m pip install --upgrade tox virtualenv
 
-  - ps: (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.1.1-win32.zip', "$env:appveyor_build_folder\pypy2.7-v7.1.1-win32.zip")
+  - ps: (New-Object Net.WebClient).DownloadFile('https://downloads.python.org/pypy/pypy2.7-v7.1.1-win32.zip', "$env:appveyor_build_folder\pypy2.7-v7.1.1-win32.zip")
   - ps: 7z x pypy2.7-v7.1.1-win32.zip | Out-Null
   - move pypy2.7-v7.1.1-win32 C:\
   - 'SET PATH=C:\pypy2.7-v7.1.1-win32\;%PATH%'
 
-  - ps: (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.1.1-win32.zip', "$env:appveyor_build_folder\pypy3.6-v7.1.1-win32.zip")
+  - ps: (New-Object Net.WebClient).DownloadFile('https://downloads.python.org/pypy/pypy3.6-v7.1.1-win32.zip', "$env:appveyor_build_folder\pypy3.6-v7.1.1-win32.zip")
   - ps: 7z x pypy3.6-v7.1.1-win32.zip | Out-Null
   - move pypy3.6-v7.1.1-win32 C:\
   - 'SET PATH=C:\pypy3.6-v7.1.1-win32\;%PATH%'
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 5af956c..1fbad48 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -542,7 +542,7 @@ class ExportBinding(Binding):
     can be determined statically, they will be treated as names for export and
     additional checking applied to them.
 
-    The only recognized C{__all__} assignment via list concatenation is in the
+    The only recognized C{__all__} assignment via list/tuple concatenation is in the
     following format:
 
         __all__ = ['a'] + ['b'] + ['c']
@@ -564,10 +564,10 @@ class ExportBinding(Binding):
 
         if isinstance(source.value, (ast.List, ast.Tuple)):
             _add_to_names(source.value)
-        # If concatenating lists
+        # If concatenating lists or tuples
         elif isinstance(source.value, ast.BinOp):
             currentValue = source.value
-            while isinstance(currentValue.right, ast.List):
+            while isinstance(currentValue.right, (ast.List, ast.Tuple)):
                 left = currentValue.left
                 right = currentValue.right
                 _add_to_names(right)
@@ -575,7 +575,7 @@ class ExportBinding(Binding):
                 if isinstance(left, ast.BinOp):
                     currentValue = left
                 # If just two lists are being added
-                elif isinstance(left, ast.List):
+                elif isinstance(left, (ast.List, ast.Tuple)):
                     _add_to_names(left)
                     # All lists accounted for - done
                     break


Test patch:
diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py
index 13e7bef..d5be269 100644
--- a/pyflakes/test/test_imports.py
+++ b/pyflakes/test/test_imports.py
@@ -1084,7 +1084,7 @@ class TestSpecialAll(TestCase):
             __all__ += ['c', 'd']
         ''', m.UndefinedExport, m.UndefinedExport)
 
-    def test_concatenationAssignment(self):
+    def test_list_concatenation_assignment(self):
         """
         The C{__all__} variable is defined through list concatenation.
         """
@@ -1093,6 +1093,15 @@ class TestSpecialAll(TestCase):
         __all__ = ['a'] + ['b'] + ['c']
         ''', m.UndefinedExport, m.UndefinedExport, m.UndefinedExport, m.UnusedImport)
 
+    def test_tuple_concatenation_assignment(self):
+        """
+        The C{__all__} variable is defined through tuple concatenation.
+        """
+        self.flakes('''
+        import sys
+        __all__ = ('a',) + ('b',) + ('c',)
+        ''', m.UndefinedExport, m.UndefinedExport, m.UndefinedExport, m.UnusedImport)
+
     def test_all_with_attributes(self):
         self.flakes('''
         from foo import bar