Repository: google/pytype

Issue:
merge-pyi support for "import collections.abc as abc" style imports
Fixes https://github.com/google/pytype/issues/706.


Discussion:

Resolving patch:
diff --git a/pytype/abstract.py b/pytype/abstract.py
--- a/pytype/abstract.py
+++ b/pytype/abstract.py
@@ -3453,6 +3453,16 @@ def property_get(self, callself, is_class=False):
   def is_coroutine(self):
     return self.code.has_coroutine() or self.code.has_iterable_coroutine()
 
+  def has_empty_body(self):
+    ops = self.code.co_code
+    if len(ops) != 2:
+      # This check isn't strictly necessary but prevents us from wastefully
+      # building a list of opcode names for a long method.
+      return False
+    if [op.name for op in ops] != ["LOAD_CONST", "RETURN_VALUE"]:
+      return False
+    return self.code.co_consts[ops[0].arg] is None
+
 
 class SimpleFunction(SignedFunction):
   """An abstract value representing a function with a particular signature.
diff --git a/pytype/tools/merge_pyi/merge_pyi.py b/pytype/tools/merge_pyi/merge_pyi.py
--- a/pytype/tools/merge_pyi/merge_pyi.py
+++ b/pytype/tools/merge_pyi/merge_pyi.py
@@ -798,9 +798,9 @@ def parse(cls, text):
 
   @classmethod
   def _parse_import_alias(cls, leaves):
-    assert [leaf.type for leaf in leaves] == [token.NAME] * 3
-    assert leaves[1].value == 'as'
-    return (leaves[0].value, leaves[2].value)
+    assert leaves[-2].value == 'as'
+    name = ''.join(leaf.value for leaf in leaves[:-2])
+    return (name, leaves[-1].value)
 
   @classmethod
   def parse_top_import(cls, results):
@@ -820,6 +820,9 @@ def parse_top_import(cls, results):
     'import pkg as pkg2' gives
     (('pkg', 'pkg2'), [])
 
+    'import pkg.a as b' gives
+    (('pkg.a', 'b'), [])
+
     Args:
       results: The values from import_pattern.
 
diff --git a/pytype/vm.py b/pytype/vm.py
--- a/pytype/vm.py
+++ b/pytype/vm.py
@@ -652,8 +652,18 @@ def _check_defaults(self, node, method):
         for name, value in e.bad_call.passed_args:
           if name != arg_name:
             continue
-          self.errorlog.annotation_type_mismatch(
-              self.frames, expected_type, value.to_binding(node), arg_name)
+          if value == self.convert.ellipsis:
+            # `...` should be a valid default parameter value for overloads.
+            # Unfortunately, the is_overload attribute is not yet set when
+            # _check_defaults runs, so we instead check that the method body is
+            # empty. As a side effect, `...` is allowed as a default value for
+            # any method that does nothing except return None.
+            should_report = not method.has_empty_body()
+          else:
+            should_report = True
+          if should_report:
+            self.errorlog.annotation_type_mismatch(
+                self.frames, expected_type, value.to_binding(node), arg_name)
           # Replace the bad default with Any so we can call match_args again to
           # find other type errors.
           try:


Test patch:
diff --git a/pytype/tests/py3/test_functions.py b/pytype/tests/py3/test_functions.py
--- a/pytype/tests/py3/test_functions.py
+++ b/pytype/tests/py3/test_functions.py
@@ -205,6 +205,25 @@ def f(x: int = '', y: str = 0):  # annotation-type-mismatch[e1]  # annotation-ty
     self.assertErrorRegexes(errors, {"e1": r"Annotation: int.*Assignment: str",
                                      "e2": r"Annotation: str.*Assignment: int"})
 
+  def test_ellipsis(self):
+    self.CheckWithErrors("""
+      def f(x: int = ...):  # annotation-type-mismatch
+        return x
+    """)
+
+  def test_overload_ellipsis(self):
+    self.Check("""
+      from typing import overload
+
+      @overload
+      def f(x: int = ...): ...
+      @overload
+      def f(x: str = ...): ...
+
+      def f(x):
+        return x
+    """)
+
 
 class TestCheckParameterAssignment(test_base.TargetPython3BasicTest):
   """Tests for checking assignments to annotated parameters."""
diff --git a/pytype/tools/merge_pyi/test_data/imports_alias.comment.py b/pytype/tools/merge_pyi/test_data/imports_alias.comment.py
--- a/pytype/tools/merge_pyi/test_data/imports_alias.comment.py
+++ b/pytype/tools/merge_pyi/test_data/imports_alias.comment.py
@@ -3,7 +3,9 @@
 from m2 import B_old as B
 from m3 import C_old as C
 import m4_old as m4
+import m5.D_old as D
+import m5.something.E_old as E
 
-def f(a, b, c):
-    # type: (A, B, C) -> m4.D
+def f(a, b, c, d, e):
+    # type: (A, B, C, D, E) -> m4.D
     pass
diff --git a/pytype/tools/merge_pyi/test_data/imports_alias.pep484.py b/pytype/tools/merge_pyi/test_data/imports_alias.pep484.py
--- a/pytype/tools/merge_pyi/test_data/imports_alias.pep484.py
+++ b/pytype/tools/merge_pyi/test_data/imports_alias.pep484.py
@@ -3,6 +3,8 @@
 from m2 import B_old as B
 from m3 import C_old as C
 import m4_old as m4
+import m5.D_old as D
+import m5.something.E_old as E
 
-def f(a: A, b: B, c: C) -> m4.D:
+def f(a: A, b: B, c: C, d: D, e: E) -> m4.D:
     pass
diff --git a/pytype/tools/merge_pyi/test_data/imports_alias.py b/pytype/tools/merge_pyi/test_data/imports_alias.py
--- a/pytype/tools/merge_pyi/test_data/imports_alias.py
+++ b/pytype/tools/merge_pyi/test_data/imports_alias.py
@@ -1,4 +1,4 @@
 """Test import-as."""
 
-def f(a, b, c):
+def f(a, b, c, d, e):
     pass
diff --git a/pytype/tools/merge_pyi/test_data/imports_alias.pyi b/pytype/tools/merge_pyi/test_data/imports_alias.pyi
--- a/pytype/tools/merge_pyi/test_data/imports_alias.pyi
+++ b/pytype/tools/merge_pyi/test_data/imports_alias.pyi
@@ -2,5 +2,7 @@ from m1 import A_old as A
 from m2 import something, B_old as B
 from m3 import C_old as C, otherthing as Other
 import m4_old as m4
+import m5.D_old as D
+import m5.something.E_old as E
 
-def f(a: A, b: B, c: C) -> m4.D: ...
+def f(a: A, b: B, c: C, d: D, e: E) -> m4.D: ...