Summary
When acting as a Type Server Protocol (TSP) server, pyrefly answers typeServer/getComputedType for the member-access expression unittest.TestCase with the unittest package module instead of the TestCase class that the package re-exports. Because the consumer (Pylance) uses this computed type to build the subclass's MRO, the base class is lost and every inherited member becomes unresolvable.
This was originally reported against pyrefly 1.0.0 (microsoft/pylance-release#8086). It still reproduces on a locally built pyrefly 1.1.1.
Environment
- pyrefly: 1.0.0 and 1.1.1 (locally built from source,
target/release)
- Consumer: Pylance using pyrefly as an external TSP type server (TSP protocol 0.4.x)
- Platform: Windows
- typeshed: pyrefly-bundled (
pyrefly_bundled_typeshed_*/unittest/__init__.pyi)
Repro
# test_mro.py
import unittest
class MyTest(unittest.TestCase):
def test_something(self) -> None:
self.assertEqual(1, 1) # go-to-definition here returns nothing
unittest/__init__.pyi re-exports the class:
from .case import TestCase as TestCase
Expected
typeServer/getComputedType on the base-class expression unittest.TestCase (the class MyTest(unittest.TestCase) line) returns the class unittest.case.TestCase:
TypeKind.Class (kind = 3)
flags = Instantiable | Callable (5)
Actual
pyrefly returns the module:
TypeKind.Module (kind = 5)
moduleName = "unittest"
uri = .../pyrefly_bundled_typeshed_*/unittest/__init__.pyi
flags = 0
Wire-level evidence
Same file, same node (test_mro.py, line 3, chars 13–30 — the unittest.TestCase base expression), same typeServer/getComputedType request, two different servers:
| Server |
kind |
moduleName |
uri |
flags |
| pyright (as TSP, control) |
3 (Class) |
— |
— |
5 (Instantiable|Callable) |
| pyrefly 1.0.0 |
5 (Module) |
unittest |
.../unittest/__init__.pyi |
0 |
| pyrefly 1.1.1 (local build) |
5 (Module) |
unittest |
.../unittest/__init__.pyi |
0 |
The bundled pyright type server returns the correct Class for the identical query, which isolates the gap to pyrefly's getComputedType resolution of the member-access node, not to the consumer's protocol decoding (Pylance maps Module → ModuleType faithfully).
Impact
Any subclass of a class imported through a package-level re-export (from .submodule import X as X) loses that base in its MRO. Concretely, unittest.TestCase subclasses report an MRO of roughly [MyTest, <Unknown>…, object], so every inherited member (assertEqual, setUp, assertTrue, …) fails go-to-definition, hover, and completion. This affects the extremely common unittest.TestCase pattern.
Suggested check
When computing the type of a member-access expression (pkg.Name) where Name resolves to a re-exported symbol (from .submodule import Name as Name), follow the re-export to the underlying class/symbol rather than returning the left-hand-side package module. The expression's computed type should be the re-exported Name, not the pkg module.
Summary
When acting as a Type Server Protocol (TSP) server, pyrefly answers
typeServer/getComputedTypefor the member-access expressionunittest.TestCasewith theunittestpackage module instead of theTestCaseclass that the package re-exports. Because the consumer (Pylance) uses this computed type to build the subclass's MRO, the base class is lost and every inherited member becomes unresolvable.This was originally reported against pyrefly 1.0.0 (microsoft/pylance-release#8086). It still reproduces on a locally built pyrefly 1.1.1.
Environment
target/release)pyrefly_bundled_typeshed_*/unittest/__init__.pyi)Repro
unittest/__init__.pyire-exports the class:Expected
typeServer/getComputedTypeon the base-class expressionunittest.TestCase(theclass MyTest(unittest.TestCase)line) returns the classunittest.case.TestCase:TypeKind.Class(kind =3)flags = Instantiable | Callable(5)Actual
pyrefly returns the module:
TypeKind.Module(kind =5)moduleName = "unittest"uri = .../pyrefly_bundled_typeshed_*/unittest/__init__.pyiflags = 0Wire-level evidence
Same file, same node (
test_mro.py, line 3, chars 13–30 — theunittest.TestCasebase expression), sametypeServer/getComputedTyperequest, two different servers:kindmoduleNameuriflags3(Class)5(Instantiable|Callable)5(Module)unittest.../unittest/__init__.pyi05(Module)unittest.../unittest/__init__.pyi0The bundled pyright type server returns the correct
Classfor the identical query, which isolates the gap to pyrefly'sgetComputedTyperesolution of the member-access node, not to the consumer's protocol decoding (Pylance mapsModule → ModuleTypefaithfully).Impact
Any subclass of a class imported through a package-level re-export (
from .submodule import X as X) loses that base in its MRO. Concretely,unittest.TestCasesubclasses report an MRO of roughly[MyTest, <Unknown>…, object], so every inherited member (assertEqual,setUp,assertTrue, …) fails go-to-definition, hover, and completion. This affects the extremely commonunittest.TestCasepattern.Suggested check
When computing the type of a member-access expression (
pkg.Name) whereNameresolves to a re-exported symbol (from .submodule import Name as Name), follow the re-export to the underlying class/symbol rather than returning the left-hand-side package module. The expression's computed type should be the re-exportedName, not thepkgmodule.