Skip to content

TSP getComputedType resolves a re-exported class (unittest.TestCase) to the containing module instead of the class #3979

Description

@rchiodo

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.

Metadata

Metadata

Assignees

Labels

language-serverIssues specific to our IDE integration rather than type checking

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions