Repository: python/mypy (Python)

Issue:
AssertionError: Must not defer during final iteration & TypeError: '<' not supported between instances of 'ArgKind' and 'ArgKind'
**Crash Report**

Basically, I am trying to program a game with the Trio asynchronous library. Using the mypy daemon and type checking different files makes it crash. Trying to make excerpt files for demonstrating the issue, I also found that I would encounter `AssertionError: Must not defer during final iteration` if I had the setup slightly different.

**Traceback**

If you type check `main.py` and then type check `with_trio.py`, you get this traceback from the log file:
```
/home/<my_username>/.local/lib/python3.11/site-packages/trio-stubs/__init__.pyi:285: error: INTERNAL ERROR -- Please try using mypy master on GitHub:
https://mypy.readthedocs.io/en/stable/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.991
/home/<my_username>/.local/lib/python3.11/site-packages/trio-stubs/__init__.pyi: error: INTERNAL ERROR -- Please try using mypy master on GitHub:
https://mypy.readthedocs.io/en/stable/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.991
Traceback (most recent call last):
  File "mypy/dmypy_server.py", line 229, in serve
  File "mypy/dmypy_server.py", line 274, in run_command
  File "mypy/dmypy_server.py", line 342, in cmd_run
  File "mypy/dmypy_server.py", line 411, in check
  File "mypy/dmypy_server.py", line 595, in fine_grained_increment_follow_imports
  File "mypy/server/update.py", line 263, in update
  File "mypy/server/update.py", line 365, in update_one
  File "mypy/server/update.py", line 427, in update_module
  File "mypy/server/update.py", line 650, in update_module_isolated
  File "mypy/semanal_main.py", line 85, in semantic_analysis_for_scc
  File "mypy/semanal_main.py", line 243, in process_functions
  File "mypy/semanal_main.py", line 282, in process_top_level_function
  File "mypy/semanal_main.py", line 339, in semantic_analyze_target
  File "mypy/semanal.py", line 581, in refresh_partial
  File "mypy/semanal.py", line 6049, in accept
  File "mypy/errors.py", line 1149, in report_internal_error
  File "mypy/semanal.py", line 6047, in accept
  File "mypy/nodes.py", line 822, in accept
  File "mypy/semanal.py", line 808, in visit_func_def
  File "mypy/semanal.py", line 837, in analyze_func_def
  File "mypy/typeanal.py", line 865, in visit_callable_type
  File "mypy/typeanal.py", line 1391, in anal_array
  File "mypy/typeanal.py", line 1400, in anal_type
  File "mypy/types.py", line 2738, in accept
  File "mypy/typeanal.py", line 1050, in visit_type_type
  File "mypy/typeanal.py", line 1400, in anal_type
  File "mypy/types.py", line 2172, in accept
  File "mypy/typeanal.py", line 979, in visit_tuple_type
  File "mypy/typeanal.py", line 1391, in anal_array
  File "mypy/typeanal.py", line 1400, in anal_type
  File "mypy/types.py", line 2782, in accept
  File "mypy/typeanal.py", line 1055, in visit_placeholder_type
  File "mypy/semanal.py", line 5724, in defer
AssertionError: Must not defer during final iteration
```

If you type check `with_trio.py` and then type check `main.py`, you get this traceback from the log file:
```
Traceback (most recent call last):
  File "mypy/dmypy_server.py", line 229, in serve
  File "mypy/dmypy_server.py", line 274, in run_command
  File "mypy/dmypy_server.py", line 342, in cmd_run
  File "mypy/dmypy_server.py", line 411, in check
  File "mypy/dmypy_server.py", line 659, in fine_grained_increment_follow_imports
  File "mypy/server/update.py", line 263, in update
  File "mypy/server/update.py", line 365, in update_one
  File "mypy/server/update.py", line 422, in update_module
  File "mypy/server/astdiff.py", line 205, in snapshot_symbol_table
  File "mypy/server/astdiff.py", line 278, in snapshot_definition
  File "mypy/server/astdiff.py", line 205, in snapshot_symbol_table
  File "mypy/server/astdiff.py", line 249, in snapshot_definition
  File "mypy/server/astdiff.py", line 294, in snapshot_optional_type
  File "mypy/server/astdiff.py", line 289, in snapshot_type
  File "mypy/types.py", line 1792, in accept
  File "mypy/server/astdiff.py", line 406, in visit_callable_type
  File "mypy/server/astdiff.py", line 300, in snapshot_types
  File "mypy/server/astdiff.py", line 289, in snapshot_type
  File "mypy/types.py", line 2592, in accept
  File "mypy/server/astdiff.py", line 447, in visit_union_type
TypeError: '<' not supported between instances of 'ArgKind' and 'ArgKind'
```

**To Reproduce**

With the following files in a folder with nothing else

In `main.py`:
```python3
import does_not_exist
```

In `with_trio.py`:
```python3
import trio
```

In `recreate.sh`:
```console
#!/bin/bash
# -*- coding: utf-8 -*-

# "AssertionError: Must not defer during final iteration" one:
dmypy start -- --warn-unused-ignores --show-error-codes --show-traceback --strict --show-absolute-path --disallow-untyped-calls --no-warn-no-return --show-column-numbers --no-color-output --warn-redundant-casts --disallow-untyped-defs --no-implicit-reexport --warn-unreachable --no-error-summary
dmypy check main.py --export-types
dmypy check with_trio.py --export-types

# "TypeError: '<' not supported between instances of 'ArgKind' and 'ArgKind'" one:
dmypy start -- --warn-unused-ignores --show-error-codes --show-traceback --strict --show-absolute-path --disallow-untyped-calls --no-warn-no-return --show-column-numbers --no-color-output --warn-redundant-casts --disallow-untyped-defs --no-implicit-reexport --warn-unreachable --no-error-summary
dmypy check with_trio.py --export-types
dmypy check main.py --export-types
```

Assuming you don't already have a daemon running, Run `recreate.sh`. There are more than one `dmypy start` command because it crashes in-between the two setups.

**Your Environment**

trio==0.22.0
trio-typing==0.7.0

- Mypy version used: 0.991
- Mypy command-line flags: See listed `recreate.sh` file for all daemon flags
- Mypy configuration options from `mypy.ini` (and other config files): No other configuration files
- Python version used: Python 3.11.0rc1 (main, Aug 12 2022, 10:02:14) [GCC 11.2.0]
- Operating system and version: 64 bit Linux Ubuntu Budgie 10.6.1 (Ubuntu 22.04.1 LTS)


Hints:

Patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1049,7 +1049,12 @@ def setup_self_type(self) -> None:
         if info.self_type is not None:
             if has_placeholder(info.self_type.upper_bound):
                 # Similar to regular (user defined) type variables.
-                self.defer(force_progress=True)
+                self.process_placeholder(
+                    None,
+                    "Self upper bound",
+                    info,
+                    force_progress=info.self_type.upper_bound != fill_typevars(info),
+                )
             else:
                 return
         info.self_type = TypeVarType("Self", f"{info.fullname}.Self", 0, [], fill_typevars(info))
@@ -2132,7 +2137,9 @@ def configure_tuple_base_class(self, defn: ClassDef, base: TupleType) -> Instanc
             self.fail("Class has two incompatible bases derived from tuple", defn)
             defn.has_incompatible_baseclass = True
         if info.special_alias and has_placeholder(info.special_alias.target):
-            self.defer(force_progress=True)
+            self.process_placeholder(
+                None, "tuple base", defn, force_progress=base != info.tuple_type
+            )
         info.update_tuple_type(base)
         self.setup_alias_type_vars(defn)
 
@@ -3913,12 +3920,16 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> bool:
             type_var = TypeVarExpr(name, self.qualified_name(name), values, upper_bound, variance)
             type_var.line = call.line
             call.analyzed = type_var
+            updated = True
         else:
             assert isinstance(call.analyzed, TypeVarExpr)
+            updated = values != call.analyzed.values or upper_bound != call.analyzed.upper_bound
             call.analyzed.upper_bound = upper_bound
             call.analyzed.values = values
         if any(has_placeholder(v) for v in values) or has_placeholder(upper_bound):
-            self.defer(force_progress=True)
+            self.process_placeholder(
+                None, f"TypeVar {'values' if values else 'upper bound'}", s, force_progress=updated
+            )
 
         self.add_symbol(name, call.analyzed, s)
         return True
@@ -5931,7 +5942,9 @@ def is_incomplete_namespace(self, fullname: str) -> bool:
         """
         return fullname in self.incomplete_namespaces
 
-    def process_placeholder(self, name: str, kind: str, ctx: Context) -> None:
+    def process_placeholder(
+        self, name: str | None, kind: str, ctx: Context, force_progress: bool = False
+    ) -> None:
         """Process a reference targeting placeholder node.
 
         If this is not a final iteration, defer current node,
@@ -5943,10 +5956,11 @@ def process_placeholder(self, name: str, kind: str, ctx: Context) -> None:
         if self.final_iteration:
             self.cannot_resolve_name(name, kind, ctx)
         else:
-            self.defer(ctx)
+            self.defer(ctx, force_progress=force_progress)
 
-    def cannot_resolve_name(self, name: str, kind: str, ctx: Context) -> None:
-        self.fail(f'Cannot resolve {kind} "{name}" (possible cyclic definition)', ctx)
+    def cannot_resolve_name(self, name: str | None, kind: str, ctx: Context) -> None:
+        name_format = f' "{name}"' if name else ""
+        self.fail(f"Cannot resolve {kind}{name_format} (possible cyclic definition)", ctx)
         if not self.options.disable_recursive_aliases and self.is_func_scope():
             self.note("Recursive types are not allowed at function scope", ctx)
 
diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py
--- a/mypy/semanal_namedtuple.py
+++ b/mypy/semanal_namedtuple.py
@@ -501,7 +501,9 @@ def build_namedtuple_typeinfo(
         info.is_named_tuple = True
         tuple_base = TupleType(types, fallback)
         if info.special_alias and has_placeholder(info.special_alias.target):
-            self.api.defer(force_progress=True)
+            self.api.process_placeholder(
+                None, "NamedTuple item", info, force_progress=tuple_base != info.tuple_type
+            )
         info.update_tuple_type(tuple_base)
         info.line = line
         # For use by mypyc.
diff --git a/mypy/semanal_newtype.py b/mypy/semanal_newtype.py
--- a/mypy/semanal_newtype.py
+++ b/mypy/semanal_newtype.py
@@ -249,10 +249,16 @@ def build_newtype_typeinfo(
         init_func = FuncDef("__init__", args, Block([]), typ=signature)
         init_func.info = info
         init_func._fullname = info.fullname + ".__init__"
+        if not existing_info:
+            updated = True
+        else:
+            previous_sym = info.names["__init__"].node
+            assert isinstance(previous_sym, FuncDef)
+            updated = old_type != previous_sym.arguments[1].variable.type
         info.names["__init__"] = SymbolTableNode(MDEF, init_func)
 
-        if has_placeholder(old_type) or info.tuple_type and has_placeholder(info.tuple_type):
-            self.api.defer(force_progress=True)
+        if has_placeholder(old_type):
+            self.api.process_placeholder(None, "NewType base", info, force_progress=updated)
         return info
 
     # Helpers
diff --git a/mypy/semanal_shared.py b/mypy/semanal_shared.py
--- a/mypy/semanal_shared.py
+++ b/mypy/semanal_shared.py
@@ -232,6 +232,12 @@ def qualified_name(self, n: str) -> str:
     def is_typeshed_stub_file(self) -> bool:
         raise NotImplementedError
 
+    @abstractmethod
+    def process_placeholder(
+        self, name: str | None, kind: str, ctx: Context, force_progress: bool = False
+    ) -> None:
+        raise NotImplementedError
+
 
 def set_callable_name(sig: Type, fdef: FuncDef) -> ProperType:
     sig = get_proper_type(sig)
diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py
--- a/mypy/semanal_typeddict.py
+++ b/mypy/semanal_typeddict.py
@@ -535,7 +535,9 @@ def build_typeddict_typeinfo(
         info = existing_info or self.api.basic_new_typeinfo(name, fallback, line)
         typeddict_type = TypedDictType(dict(zip(items, types)), required_keys, fallback)
         if info.special_alias and has_placeholder(info.special_alias.target):
-            self.api.defer(force_progress=True)
+            self.api.process_placeholder(
+                None, "TypedDict item", info, force_progress=typeddict_type != info.typeddict_type
+            )
         info.update_typeddict_type(typeddict_type)
         return info
 
diff --git a/mypy/types.py b/mypy/types.py
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -2857,6 +2857,14 @@ def accept(self, visitor: TypeVisitor[T]) -> T:
         assert isinstance(visitor, SyntheticTypeVisitor)
         return cast(T, visitor.visit_placeholder_type(self))
 
+    def __hash__(self) -> int:
+        return hash((self.fullname, tuple(self.args)))
+
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, PlaceholderType):
+            return NotImplemented
+        return self.fullname == other.fullname and self.args == other.args
+
     def serialize(self) -> str:
         # We should never get here since all placeholders should be replaced
         # during semantic analysis.


Test patch:
diff --git a/test-data/unit/fine-grained-follow-imports.test b/test-data/unit/fine-grained-follow-imports.test
--- a/test-data/unit/fine-grained-follow-imports.test
+++ b/test-data/unit/fine-grained-follow-imports.test
@@ -769,3 +769,80 @@ from . import mod3
 ==
 main.py:1: error: Cannot find implementation or library stub for module named "pkg"
 main.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
+
+[case testNewImportCycleTypeVarBound]
+# flags: --follow-imports=normal
+# cmd: mypy main.py
+# cmd2: mypy other.py
+
+[file main.py]
+# empty
+
+[file other.py.2]
+import trio
+
+[file trio/__init__.py.2]
+from typing import TypeVar
+import trio
+from . import abc as abc
+
+T = TypeVar("T", bound=trio.abc.A)
+
+[file trio/abc.py.2]
+import trio
+class A: ...
+[out]
+==
+
+[case testNewImportCycleTupleBase]
+# flags: --follow-imports=normal
+# cmd: mypy main.py
+# cmd2: mypy other.py
+
+[file main.py]
+# empty
+
+[file other.py.2]
+import trio
+
+[file trio/__init__.py.2]
+from typing import TypeVar, Tuple
+import trio
+from . import abc as abc
+
+class C(Tuple[trio.abc.A, trio.abc.A]): ...
+
+[file trio/abc.py.2]
+import trio
+class A: ...
+[builtins fixtures/tuple.pyi]
+[out]
+==
+
+[case testNewImportCycleTypedDict]
+# flags: --follow-imports=normal
+# cmd: mypy main.py
+# cmd2: mypy other.py
+
+[file main.py]
+# empty
+
+[file other.py.2]
+import trio
+
+[file trio/__init__.py.2]
+from typing import TypeVar
+from typing_extensions import TypedDict
+import trio
+from . import abc as abc
+
+class C(TypedDict):
+    x: trio.abc.A
+    y: trio.abc.A
+
+[file trio/abc.py.2]
+import trio
+class A: ...
+[builtins fixtures/dict.pyi]
+[out]
+==