Repository: python/mypy

Issue:
dmypy crash on followup check of file importing `multiprocessing`
<!--
  Use this form only if mypy reports an "INTERNAL ERROR" and/or gives a traceback.
  Please include the traceback and all other messages below (use `mypy --show-traceback`).
-->

**Crash Report**

The `dmypy` daemon crashes if a check is performed on a file importing `multiprocessing.util` after an earlier check of any other set of files.

```
Traceback (most recent call last):
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 229, in serve
    resp = self.run_command(command, data)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 272, in run_command
    return method(self, **data)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 331, in cmd_run
    return self.check(sources, is_tty, terminal_width)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 393, in check
    messages = self.fine_grained_increment_follow_imports(sources)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 568, in fine_grained_increment_follow_imports
    messages = fine_grained_manager.update(changed, [])
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 245, in update
    result = self.update_one(changed_modules, initial_set, removed_set, blocking_error)
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 328, in update_one
    result = self.update_module(next_id, next_path, next_id in removed_set)
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 414, in update_module
    remaining += propagate_changes_using_dependencies(
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 808, in propagate_changes_using_dependencies
    raise RuntimeError('Max number of iterations (%d) reached (endless loop?)' % MAX_ITER)
RuntimeError: Max number of iterations (1000) reached (endless loop?)
```

If a check of a file importing of `multiprocessing.util` is the **first** check run using the daemon, the daemon does not crash and correctly errors that the `util` module is not present in the `multiprocessing` `typeshed` stubs:
```
Daemon started
b.py:1: error: Cannot find implementation or library stub for module named "multiprocessing.util"
b.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
```

**To Reproduce**

```
# Create the files
touch a.py
echo "import multiprocessing.util" > b.py

# Clean up any old daemon
dmypy kill || true

# Perform the first check
dmypy run -- --show-traceback a.py

# Perform the second check on a file containing "import multiprocessing.util"
dmypy run -- --show-traceback b.py
```

**Your Environment**

<!-- Include as many relevant details about the environment you experienced the bug in -->
Dependency versions:
```
mypy-extensions==0.4.3
tomli==2.0.0
typing_extensions==4.0.1
```

- Mypy version used: `mypy 0.940+dev.63abf4b5b26edb2c7db2a9ff50dfa0e63a43434a`. Also reproduced on `0.920`.
- Mypy command-line flags: `--show-traceback`
- Mypy configuration options from `mypy.ini` (and other config files): None
- Python version used: `3.9.8`
- Operating system and version: MacOS 11.6 (Big Sur)

<!--
You can freely edit this text, please remove all the lines
you believe are unnecessary.
-->



Discussion:
Having looked at this again, it's actually worse. The crash appears to happen simply with an import of `multiprocessing`:
```bash
# Create the files
touch a.py
echo "import multiprocessing" > b.py

# Clean up any old daemon
dmypy kill || true

# Perform the first check
dmypy run -- --show-traceback a.py

# Perform the second check on a file containing "import multiprocessing"
dmypy run -- --show-traceback b.py
```
```
Daemon crashed!
Traceback (most recent call last):
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 229, in serve
    resp = self.run_command(command, data)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 272, in run_command
    return method(self, **data)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 331, in cmd_run
    return self.check(sources, is_tty, terminal_width)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 393, in check
    messages = self.fine_grained_increment_follow_imports(sources)
  File "/Users/lwfitzgerald/mypy/mypy/dmypy_server.py", line 568, in fine_grained_increment_follow_imports
    messages = fine_grained_manager.update(changed, [])
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 245, in update
    result = self.update_one(changed_modules, initial_set, removed_set, blocking_error)
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 328, in update_one
    result = self.update_module(next_id, next_path, next_id in removed_set)
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 414, in update_module
    remaining += propagate_changes_using_dependencies(
  File "/Users/lwfitzgerald/mypy/mypy/server/update.py", line 808, in propagate_changes_using_dependencies
    raise RuntimeError('Max number of iterations (%d) reached (endless loop?)' % MAX_ITER)
RuntimeError: Max number of iterations (1000) reached (endless loop?)
```

I will update the issue title accordingly.
I can reproduce the issue. This seems to have been around for some time, at least from 0.900.
I've just run into this issue and did some initial debugging. Mypy seems to get stuck inside of `propagate_changes_using_dependencies` on the following nodes:

```
LOG:  fine-grained: triggered: ['<multiprocessing.Value>', '<multiprocessing.RawArray>', '<multiprocessing.RawValue>', '<multiprocessing.Array>']
LOG:  fine-grained: triggered: ['<multiprocessing.Value>', '<multiprocessing.RawArray>', '<multiprocessing.RawValue>', '<multiprocessing.Array>']
LOG:  fine-grained: triggered: ['<multiprocessing.Value>', '<multiprocessing.RawArray>', '<multiprocessing.RawValue>', '<multiprocessing.Array>']
LOG:  fine-grained: triggered: ['<multiprocessing.Value>', '<multiprocessing.RawArray>', '<multiprocessing.RawValue>', '<multiprocessing.Array>']
...
```

Setting a breakpoint within `compare_symbol_table_snapshots` reveals the following:

```
> /Users/kjagiello/Dev/private/mypy/mypy/server/astdiff.py(121)compare_symbol_table_snapshots()
-> triggers.add(item_name)
(Pdb) item_name
'multiprocessing.Array'
(Pdb) snapshot1[name]
('Var', ('multiprocessing.Array', 1, True), ('Overloaded', (('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 107, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), False)), ('TypeVar', '_CT', 'multiprocessing.context._CT', 107, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 108, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('AnyType',), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True)))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('Instance', 'builtins.str', (), ('None',)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('UnionType', (('Instance', 'builtins.str', (), ('None',)), ('TypeType', ('Instance', 'ctypes._CData', (), ('None',))))), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('Instance', 'builtins.bool', (), ('None',)), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED_OPT: 5>), False, False))), False)
(Pdb) snapshot2[name]
('Var', ('multiprocessing.Array', 1, True), ('Overloaded', (('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 113, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), False)), ('TypeVar', '_CT', 'multiprocessing.context._CT', 113, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 114, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('AnyType',), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True)))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('Instance', 'builtins.str', (), ('None',)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('UnionType', (('Instance', 'builtins.str', (), ('None',)), ('TypeType', ('Instance', 'ctypes._CData', (), ('None',))))), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('Instance', 'builtins.bool', (), ('None',)), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED_OPT: 5>), False, False))), False)

... next occurence:

-> triggers.add(item_name)
(Pdb) item_name
'multiprocessing.Array'
(Pdb) snapshot1[name]
('Var', ('multiprocessing.Array', 1, True), ('Overloaded', (('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 113, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), False)), ('TypeVar', '_CT', 'multiprocessing.context._CT', 113, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 114, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('AnyType',), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True)))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('Instance', 'builtins.str', (), ('None',)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('UnionType', (('Instance', 'builtins.str', (), ('None',)), ('TypeType', ('Instance', 'ctypes._CData', (), ('None',))))), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('Instance', 'builtins.bool', (), ('None',)), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED_OPT: 5>), False, False))), False)
(Pdb) snapshot2[name]
('Var', ('multiprocessing.Array', 1, True), ('Overloaded', (('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 119, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), False)), ('TypeVar', '_CT', 'multiprocessing.context._CT', 119, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('TypeType', ('TypeVar', '_CT', 'multiprocessing.context._CT', 120, 0, (), ('Instance', 'ctypes._CData', (), ('None',)), 0)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('AnyType',), ('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True)))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('Instance', 'builtins.str', (), ('None',)), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('LiteralType', ('Instance', 'builtins.bool', (), ('None',)), True), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED: 3>), False, False), ('CallableType', (('UnionType', (('Instance', 'builtins.str', (), ('None',)), ('TypeType', ('Instance', 'ctypes._CData', (), ('None',))))), ('UnionType', (('Instance', 'builtins.int', (), ('None',)), ('Instance', 'typing.Sequence', (('AnyType',),), ('None',)))), ('UnionType', (('Instance', 'builtins.bool', (), ('None',)), ('TypeAliasType', 'multiprocessing.context._LockLike', ())))), ('AnyType',), ('typecode_or_type', 'size_or_initializer', 'lock'), (<ArgKind.ARG_POS: 0>, <ArgKind.ARG_POS: 0>, <ArgKind.ARG_NAMED_OPT: 5>), False, False))), False)
```

As you can see the ID of the `multiprocessing.context._CT` TypeVar seems to always differ between the snapshots.
A self-contained test case:
```
[case testBoundGenericMethodFine]
import lib
[file lib/__init__.pyi]
from lib import context
foo = context.test.foo
[file lib/context.pyi]
from typing import TypeVar
import lib.other

T = TypeVar("T")
class Test:
    def foo(self, x: T, n: lib.other.C) -> T: ...
test: Test

[file lib/other.pyi]
class C: ...
[file lib/other.pyi.2]
class B: ...
class C(B): ...
[out]
==
```
Unfortunately, this is a fundamental problem. Each time we look up a method, we call `freshen_function_type_vars()` on it, because otherwise there may be type variable clashes. Therefore, there may be situations where we never reach a fixed point.
I think our type variable identity system needs to be seriously cleaned up/re-implemented. There is a meta issue on this https://github.com/python/mypy/issues/4814, and it may be a quite large project (and requires good understanding of mypy internals).
Actually I think there may be a simple short term solution for this. We can "anonimize" generic callables before comparisons. Anyway, their type variables are purely internal, and can leak only if there is a bug (like famous https://github.com/python/mypy/issues/1317)

Resolving patch:
diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py
--- a/mypy/server/astdiff.py
+++ b/mypy/server/astdiff.py
@@ -52,9 +52,10 @@ class level -- these are handled at attribute level (say, 'mod.Cls.method'
 
 from __future__ import annotations
 
-from typing import Sequence, Tuple
+from typing import Sequence, Tuple, cast
 from typing_extensions import TypeAlias as _TypeAlias
 
+from mypy.expandtype import expand_type
 from mypy.nodes import (
     UNBOUND_IMPORTED,
     Decorator,
@@ -88,6 +89,8 @@ class level -- these are handled at attribute level (say, 'mod.Cls.method'
     TypeAliasType,
     TypedDictType,
     TypeType,
+    TypeVarId,
+    TypeVarLikeType,
     TypeVarTupleType,
     TypeVarType,
     TypeVisitor,
@@ -388,7 +391,8 @@ def visit_parameters(self, typ: Parameters) -> SnapshotItem:
         )
 
     def visit_callable_type(self, typ: CallableType) -> SnapshotItem:
-        # FIX generics
+        if typ.is_generic():
+            typ = self.normalize_callable_variables(typ)
         return (
             "CallableType",
             snapshot_types(typ.arg_types),
@@ -397,8 +401,26 @@ def visit_callable_type(self, typ: CallableType) -> SnapshotItem:
             tuple(typ.arg_kinds),
             typ.is_type_obj(),
             typ.is_ellipsis_args,
+            snapshot_types(typ.variables),
         )
 
+    def normalize_callable_variables(self, typ: CallableType) -> CallableType:
+        """Normalize all type variable ids to run from -1 to -len(variables)."""
+        tvs = []
+        tvmap: dict[TypeVarId, Type] = {}
+        for i, v in enumerate(typ.variables):
+            tid = TypeVarId(-1 - i)
+            if isinstance(v, TypeVarType):
+                tv: TypeVarLikeType = v.copy_modified(id=tid)
+            elif isinstance(v, TypeVarTupleType):
+                tv = v.copy_modified(id=tid)
+            else:
+                assert isinstance(v, ParamSpecType)
+                tv = v.copy_modified(id=tid)
+            tvs.append(tv)
+            tvmap[v.id] = tv
+        return cast(CallableType, expand_type(typ, tvmap)).copy_modified(variables=tvs)
+
     def visit_tuple_type(self, typ: TupleType) -> SnapshotItem:
         return ("TupleType", snapshot_types(typ.items))
 
diff --git a/mypy/types.py b/mypy/types.py
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -517,15 +517,23 @@ def __init__(
     @staticmethod
     def new_unification_variable(old: TypeVarType) -> TypeVarType:
         new_id = TypeVarId.new(meta_level=1)
+        return old.copy_modified(id=new_id)
+
+    def copy_modified(
+        self,
+        values: Bogus[list[Type]] = _dummy,
+        upper_bound: Bogus[Type] = _dummy,
+        id: Bogus[TypeVarId | int] = _dummy,
+    ) -> TypeVarType:
         return TypeVarType(
-            old.name,
-            old.fullname,
-            new_id,
-            old.values,
-            old.upper_bound,
-            old.variance,
-            old.line,
-            old.column,
+            self.name,
+            self.fullname,
+            self.id if id is _dummy else id,
+            self.values if values is _dummy else values,
+            self.upper_bound if upper_bound is _dummy else upper_bound,
+            self.variance,
+            self.line,
+            self.column,
         )
 
     def accept(self, visitor: TypeVisitor[T]) -> T:
@@ -616,16 +624,7 @@ def __init__(
     @staticmethod
     def new_unification_variable(old: ParamSpecType) -> ParamSpecType:
         new_id = TypeVarId.new(meta_level=1)
-        return ParamSpecType(
-            old.name,
-            old.fullname,
-            new_id,
-            old.flavor,
-            old.upper_bound,
-            line=old.line,
-            column=old.column,
-            prefix=old.prefix,
-        )
+        return old.copy_modified(id=new_id)
 
     def with_flavor(self, flavor: int) -> ParamSpecType:
         return ParamSpecType(
@@ -737,8 +736,16 @@ def __eq__(self, other: object) -> bool:
     @staticmethod
     def new_unification_variable(old: TypeVarTupleType) -> TypeVarTupleType:
         new_id = TypeVarId.new(meta_level=1)
+        return old.copy_modified(id=new_id)
+
+    def copy_modified(self, id: Bogus[TypeVarId | int] = _dummy) -> TypeVarTupleType:
         return TypeVarTupleType(
-            old.name, old.fullname, new_id, old.upper_bound, line=old.line, column=old.column
+            self.name,
+            self.fullname,
+            self.id if id is _dummy else id,
+            self.upper_bound,
+            line=self.line,
+            column=self.column,
         )
 
 


Test patch:
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -9969,3 +9969,65 @@ m.py:9: note:     Expected:
 m.py:9: note:         def update() -> bool
 m.py:9: note:     Got:
 m.py:9: note:         def update() -> str
+
+[case testBoundGenericMethodFine]
+import main
+[file main.py]
+import lib
+[file main.py.3]
+import lib
+reveal_type(lib.foo(42))
+[file lib/__init__.pyi]
+from lib import context
+foo = context.test.foo
+[file lib/context.pyi]
+from typing import TypeVar
+import lib.other
+
+T = TypeVar("T")
+class Test:
+    def foo(self, x: T, n: lib.other.C = ...) -> T: ...
+test: Test
+
+[file lib/other.pyi]
+class C: ...
+[file lib/other.pyi.2]
+class B: ...
+class C(B): ...
+[out]
+==
+==
+main.py:2: note: Revealed type is "builtins.int"
+
+[case testBoundGenericMethodParamSpecFine]
+import main
+[file main.py]
+import lib
+[file main.py.3]
+from typing import Callable
+import lib
+f: Callable[[], int]
+reveal_type(lib.foo(f))
+[file lib/__init__.pyi]
+from lib import context
+foo = context.test.foo
+[file lib/context.pyi]
+from typing_extensions import ParamSpec
+from typing import Callable
+import lib.other
+
+P = ParamSpec("P")
+class Test:
+    def foo(self, x: Callable[P, int], n: lib.other.C = ...) -> Callable[P, str]: ...
+test: Test
+
+[file lib/other.pyi]
+class C: ...
+[file lib/other.pyi.2]
+class B: ...
+class C(B): ...
+[builtins fixtures/dict.pyi]
+[out]
+==
+==
+main.py:4: note: Revealed type is "def () -> builtins.str"