Repository: PyCQA/pyflakes (python)

Issue:
F811 (redefinition of unused variable) incorrectly triggered with @overload decorator
PEP 484 [introduced](https://www.python.org/dev/peps/pep-0484/#function-method-overloading) the `@overload` decorator for type annotations.

> [...] a series of `@overload`-decorated definitions must be followed by exactly one non-@overload-decorated definition (for the same function/method). The `@overload`-decorated definitions are for the benefit of the type checker only, since they will be overwritten by the non-`@overload`-decorated definition, while the latter is used at runtime but should be ignored by a type checker.

The following should pass PyFlakes without errors, but currently triggers F811 errors:

```python
from typing import overload


@overload
def utf8(value: None) -> None:
    pass


@overload
def utf8(value: bytes) -> bytes:
    pass


@overload
def utf8(value: str) -> bytes:
    pass


def utf8(value):
    pass  # actual implementation
```
```
test.py:9:1: F811 redefinition of unused 'utf8' from line 4
test.py:14:1: F811 redefinition of unused 'utf8' from line 9
test.py:19:1: F811 redefinition of unused 'utf8' from line 14
```

Pull request description:
Exempt typing.overload from duplicate detection
Resolves #320

Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index f520325..ad25b5e 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -495,6 +495,28 @@ def getNodeName(node):
         return node.name
 
 
+def is_typing_overload(value, scope):
+    def is_typing_overload_decorator(node):
+        return (
+            (
+                isinstance(node, ast.Name) and
+                node.id in scope and
+                scope[node.id].fullName == 'typing.overload'
+            ) or (
+                isinstance(node, ast.Attribute) and
+                isinstance(node.value, ast.Name) and
+                node.value.id == 'typing' and
+                node.attr == 'overload'
+            )
+        )
+
+    return (
+        isinstance(value.source, ast.FunctionDef) and
+        len(value.source.decorator_list) == 1 and
+        is_typing_overload_decorator(value.source.decorator_list[0])
+    )
+
+
 class Checker(object):
     """
     I check the cleanliness and sanity of Python code.
@@ -747,8 +769,9 @@ class Checker(object):
                                 node, value.name, existing.source)
                 elif not existing.used and value.redefines(existing):
                     if value.name != '_' or isinstance(existing, Importation):
-                        self.report(messages.RedefinedWhileUnused,
-                                    node, value.name, existing.source)
+                        if not is_typing_overload(existing, self.scope):
+                            self.report(messages.RedefinedWhileUnused,
+                                        node, value.name, existing.source)
 
             elif isinstance(existing, Importation) and value.redefines(existing):
                 existing.redefined.append(node)


Test patch:
diff --git a/pyflakes/test/test_doctests.py b/pyflakes/test/test_doctests.py
index aed7957..b6d0f65 100644
--- a/pyflakes/test/test_doctests.py
+++ b/pyflakes/test/test_doctests.py
@@ -33,7 +33,8 @@ class _DoctestMixin(object):
                   line.startswith('except ') or
                   line.startswith('finally:') or
                   line.startswith('else:') or
-                  line.startswith('elif ')):
+                  line.startswith('elif ') or
+                  (lines and lines[-1].startswith(('>>> @', '... @')))):
                 line = "... %s" % line
             else:
                 line = ">>> %s" % line
diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index e5da024..b8301ea 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -291,6 +291,35 @@ class Test(TestCase):
                 pass
         """)
 
+    def test_typingOverload(self):
+        """Allow intentional redefinitions via @typing.overload"""
+        self.flakes("""
+        import typing
+        from typing import overload
+
+        @overload
+        def f(s):  # type: (None) -> None
+            pass
+
+        @overload
+        def f(s):  # type: (int) -> int
+            pass
+
+        def f(s):
+            return s
+
+        @typing.overload
+        def g(s):  # type: (None) -> None
+            pass
+
+        @typing.overload
+        def g(s):  # type: (int) -> int
+            pass
+
+        def g(s):
+            return s
+        """)
+
     def test_unaryPlus(self):
         """Don't die on unary +."""
         self.flakes('+1')