Repository: python/mypy

Issue:
Daemon crash: comparing tuple and None
In encountered a mypy daemon crash (when using `dmypy suggest`, not sure if it's related):

```
Traceback (most recent call last):
  File "mypy/suggestions.py", line 212, in restore_after
  File "mypy/suggestions.py", line 187, in suggest
  File "mypy/suggestions.py", line 355, in get_suggestion
  File "mypy/suggestions.py", line 325, in find_best
  File "mypy/suggestions.py", line 524, in try_type
  File "mypy/server/update.py", line 275, in trigger
  File "mypy/server/update.py", line 804, in propagate_changes_using_dependencies
  File "mypy/server/update.py", line 951, in reprocess_nodes
  File "mypy/server/astdiff.py", line 160, in snapshot_symbol_table
  File "mypy/server/astdiff.py", line 174, in snapshot_definition
  File "mypy/server/astdiff.py", line 231, in snapshot_type
  File "mypy/types.py", line 1061, in accept
  File "mypy/server/astdiff.py", line 302, in visit_callable_type
  File "mypy/server/astdiff.py", line 242, in snapshot_types
  File "mypy/server/astdiff.py", line 231, in snapshot_type
  File "mypy/types.py", line 1681, in accept
  File "mypy/server/astdiff.py", line 325, in visit_union_type
TypeError: '<' not supported between instances of 'tuple' and 'NoneType'
```

It seems possible that type snapshots contain `None` values, which breaks sorting.

I don't have a repro, but the traceback may contain enough hints to fix the issue.


Discussion:

Resolving patch:
diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py
--- a/mypy/server/astdiff.py
+++ b/mypy/server/astdiff.py
@@ -246,6 +246,13 @@ def snapshot_simple_type(typ: Type) -> SnapshotItem:
     return (type(typ).__name__,)
 
 
+def encode_optional_str(s: Optional[str]) -> str:
+    if s is None:
+        return '<None>'
+    else:
+        return s
+
+
 class SnapshotTypeVisitor(TypeVisitor[SnapshotItem]):
     """Creates a read-only, self-contained snapshot of a type object.
 
@@ -256,6 +263,9 @@ class SnapshotTypeVisitor(TypeVisitor[SnapshotItem]):
     - Has no references to mutable or non-primitive objects.
     - Two snapshots represent the same object if and only if they are
       equal.
+    - Results must be sortable. It's important that tuples have
+      consistent types and can't arbitrarily mix str and None values,
+      for example, since they can't be compared.
     """
 
     def visit_unbound_type(self, typ: UnboundType) -> SnapshotItem:
@@ -282,9 +292,9 @@ def visit_deleted_type(self, typ: DeletedType) -> SnapshotItem:
 
     def visit_instance(self, typ: Instance) -> SnapshotItem:
         return ('Instance',
-                typ.type.fullname(),
+                encode_optional_str(typ.type.fullname()),
                 snapshot_types(typ.args),
-                None if typ.last_known_value is None else snapshot_type(typ.last_known_value))
+                ('None',) if typ.last_known_value is None else snapshot_type(typ.last_known_value))
 
     def visit_type_var(self, typ: TypeVarType) -> SnapshotItem:
         return ('TypeVar',
@@ -301,7 +311,7 @@ def visit_callable_type(self, typ: CallableType) -> SnapshotItem:
         return ('CallableType',
                 snapshot_types(typ.arg_types),
                 snapshot_type(typ.ret_type),
-                tuple(typ.arg_names),
+                tuple([encode_optional_str(name) for name in typ.arg_names]),
                 tuple(typ.arg_kinds),
                 typ.is_type_obj(),
                 typ.is_ellipsis_args)
@@ -316,7 +326,7 @@ def visit_typeddict_type(self, typ: TypedDictType) -> SnapshotItem:
         return ('TypedDictType', items, required)
 
     def visit_literal_type(self, typ: LiteralType) -> SnapshotItem:
-        return ('LiteralType', typ.value, snapshot_type(typ.fallback))
+        return ('LiteralType', snapshot_type(typ.fallback), typ.value)
 
     def visit_union_type(self, typ: UnionType) -> SnapshotItem:
         # Sort and remove duplicates so that we can use equality to test for


Test patch:
diff --git a/test-data/unit/diff.test b/test-data/unit/diff.test
--- a/test-data/unit/diff.test
+++ b/test-data/unit/diff.test
@@ -1443,3 +1443,25 @@ class C:
 [out]
 __main__.C.method
 __main__.func
+
+[case testUnionOfLiterals]
+from typing_extensions import Literal
+x: Literal[1, '2']
+[file next.py]
+from typing_extensions import Literal
+x: Literal[1, 2]
+[out]
+__main__.x
+
+[case testUnionOfCallables]
+from typing import Callable, Union
+from mypy_extensions import Arg
+x: Union[Callable[[Arg(int, 'x')], None],
+         Callable[[int], None]]
+[file next.py]
+from typing import Callable, Union
+from mypy_extensions import Arg
+x: Union[Callable[[Arg(int, 'y')], None],
+         Callable[[int], None]]
+[out]
+__main__.x