Repository: quora/pyanalyze
Pull request #386 (2022-01-03 07:05:01)

Stub-related improvements


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 2e7ff21..831ad57 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,10 @@
 
 ## Unreleased
 
+- Support some imports from stub-only modules (#386)
+- Support type evaluation functions in stubs (#386)
+- Support `TypedDict` in stubs (#386)
+- Support `TypeAlias` (PEP 612) (#386)
 - Small improvements to `ParamSpec` support (#385)
 - Allow `CustomCheck` to customize what values
   a value can be assigned to (#383)
diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py
index d32b12f..f2d15fd 100644
--- a/pyanalyze/annotations.py
+++ b/pyanalyze/annotations.py
@@ -30,7 +30,9 @@ import typing_inspect
 import qcore
 import ast
 import builtins
+import inspect
 from collections.abc import Callable, Iterable
+import textwrap
 from typing import (
     Any,
     Container,
@@ -46,7 +48,7 @@ from typing import (
     Union,
     TYPE_CHECKING,
 )
-from typing_extensions import ParamSpec
+from typing_extensions import ParamSpec, TypedDict
 
 from .error_code import ErrorCode
 from .extensions import (
@@ -57,8 +59,11 @@ from .extensions import (
     NoReturnGuard,
     ParameterTypeGuard,
     TypeGuard,
+    get_type_evaluation,
 )
 from .find_unused import used
+from .functions import FunctionDefNode
+from .node_visitor import ErrorContext
 from .signature import ELLIPSIS_PARAM, SigParameter, Signature, ParameterKind
 from .safe import is_typing_name, is_instance_of_typing_name
 from . import type_evaluation
@@ -67,7 +72,6 @@ from .value import (
     AnySource,
     AnyValue,
     CallableValue,
-    CanAssignContext,
     CustomCheckExtension,
     Extension,
     HasAttrGuardExtension,
@@ -152,11 +156,9 @@ class Context:
 
 
 @dataclass
-class TypeEvaluationContext(Context, type_evaluation.Context):
-    variables: type_evaluation.VarMap
-    positions: Mapping[str, type_evaluation.Position]
-    can_assign_context: CanAssignContext
+class RuntimeEvaluator(type_evaluation.Evaluator, Context):
     globals: Mapping[str, object] = field(repr=False)
+    func: typing.Callable[..., Any]
 
     def evaluate_type(self, node: ast.AST) -> Value:
         return type_from_ast(node, ctx=self)
@@ -168,13 +170,30 @@ class TypeEvaluationContext(Context, type_evaluation.Context):
         """Return the :class:`Value <pyanalyze.value.Value>` corresponding to a name."""
         return self.get_name_from_globals(node.id, self.globals)
 
+    @classmethod
+    def get_for(cls, func: typing.Callable[..., Any]) -> Optional["RuntimeEvaluator"]:
+        try:
+            key = f"{func.__module__}.{func.__qualname__}"
+        except AttributeError:
+            return None
+        evaluation_func = get_type_evaluation(key)
+        if evaluation_func is None or not hasattr(evaluation_func, "__globals__"):
+            return None
+        lines, _ = inspect.getsourcelines(evaluation_func)
+        code = textwrap.dedent("".join(lines))
+        body = ast.parse(code)
+        if not body.body:
+            return None
+        evaluator = body.body[0]
+        if not isinstance(evaluator, ast.FunctionDef):
+            return None
+        return RuntimeEvaluator(evaluator, evaluation_func.__globals__, evaluation_func)
+
 
 @dataclass
-class EvaluatorValidationContext(Context, type_evaluation.Context):
-    variables: type_evaluation.VarMap
-    positions: Mapping[str, type_evaluation.Position]
-    can_assign_context: "NameCheckVisitor"
-    node: ast.AST
+class SyntheticEvaluator(type_evaluation.Evaluator):
+    error_ctx: ErrorContext
+    annotations_context: Context
 
     def show_error(
         self,
@@ -182,22 +201,27 @@ class EvaluatorValidationContext(Context, type_evaluation.Context):
         error_code: ErrorCode = ErrorCode.invalid_annotation,
         node: Optional[ast.AST] = None,
     ) -> None:
-        self.can_assign_context.show_error(
-            node or self.node, message, error_code=error_code
-        )
+        self.error_ctx.show_error(node or self.node, message, error_code=error_code)
 
     def evaluate_type(self, node: ast.AST) -> Value:
-        return type_from_ast(node, ctx=self)
+        return type_from_ast(node, ctx=self.annotations_context)
 
     def evaluate_value(self, node: ast.AST) -> Value:
-        return value_from_ast(node, ctx=self, error_on_unrecognized=False)
+        return value_from_ast(
+            node, ctx=self.annotations_context, error_on_unrecognized=False
+        )
 
     def get_name(self, node: ast.Name) -> Value:
         """Return the :class:`Value <pyanalyze.value.Value>` corresponding to a name."""
-        val, _ = self.can_assign_context.resolve_name(
-            node, suppress_errors=self.should_suppress_undefined_names
+        return self.annotations_context.get_name(node)
+
+    @classmethod
+    def from_visitor(
+        cls, node: FunctionDefNode, visitor: "NameCheckVisitor"
+    ) -> "SyntheticEvaluator":
+        return cls(
+            node, visitor, _DefaultContext(visitor, node, use_name_node_for_error=True)
         )
-        return val
 
 
 @used  # part of an API
@@ -223,6 +247,25 @@ def type_from_ast(
     return _type_from_ast(ast_node, ctx)
 
 
+def type_from_annotations(
+    annotations: Mapping[str, object],
+    key: str,
+    *,
+    globals: Optional[Mapping[str, object]] = None,
+    ctx: Optional[Context] = None,
+) -> Optional[Value]:
+    try:
+        annotation = annotations[key]
+    except Exception:
+        # Malformed __annotations__
+        return None
+    else:
+        maybe_val = type_from_runtime(annotation, globals=globals, ctx=ctx)
+        if maybe_val != AnyValue(AnySource.incomplete_annotation):
+            return maybe_val
+    return None
+
+
 def type_from_runtime(
     val: object,
     visitor: Optional["NameCheckVisitor"] = None,
@@ -490,11 +533,15 @@ def _type_from_runtime(val: Any, ctx: Context, is_typeddict: bool = False) -> Va
     elif is_instance_of_typing_name(val, "_MaybeRequired"):
         required = is_instance_of_typing_name(val, "_Required")
         if is_typeddict:
-            return _Pep655Value(required, _type_from_runtime(val.__type__, ctx))
+            return Pep655Value(required, _type_from_runtime(val.__type__, ctx))
         else:
             cls = "Required" if required else "NotRequired"
             ctx.show_error(f"{cls}[] used in unsupported context")
             return AnyValue(AnySource.error)
+    elif is_typing_name(val, "TypeAlias"):
+        return AnyValue(AnySource.incomplete_annotation)
+    elif is_typing_name(val, "TypedDict"):
+        return KnownValue(TypedDict)
     else:
         origin = get_origin(val)
         if isinstance(origin, type):
@@ -571,7 +618,7 @@ def _get_typeddict_value(
     total: bool,
 ) -> Tuple[bool, Value]:
     val = _type_from_runtime(value, ctx, is_typeddict=True)
-    if isinstance(val, _Pep655Value):
+    if isinstance(val, Pep655Value):
         return (val.required, val.value)
     if required_keys is None:
         required = total
@@ -605,6 +652,8 @@ def _type_from_value(value: Value, ctx: Context, is_typeddict: bool = False) ->
         )
     elif isinstance(value, AnyValue):
         return value
+    elif isinstance(value, SubclassValue) and value.exactly:
+        return value.typ
     elif isinstance(value, TypedValue) and isinstance(value.typ, str):
         # Synthetic type
         return value
@@ -634,6 +683,15 @@ def _type_from_subscripted_value(
                 for subval in root.vals
             ]
         )
+    if (
+        isinstance(root, SubclassValue)
+        and root.exactly
+        and isinstance(root.typ, TypedValue)
+    ):
+        return GenericValue(
+            root.typ.typ, [_type_from_value(elt, ctx) for elt in members]
+        )
+
     if isinstance(root, TypedValue) and isinstance(root.typ, str):
         return GenericValue(root.typ, [_type_from_value(elt, ctx) for elt in members])
 
@@ -691,7 +749,7 @@ def _type_from_subscripted_value(
         if len(members) != 1:
             ctx.show_error("Required[] requires a single argument")
             return AnyValue(AnySource.error)
-        return _Pep655Value(True, _type_from_value(members[0], ctx))
+        return Pep655Value(True, _type_from_value(members[0], ctx))
     elif is_typing_name(root, "NotRequired"):
         if not is_typeddict:
             ctx.show_error("NotRequired[] used in unsupported context")
@@ -699,7 +757,7 @@ def _type_from_subscripted_value(
         if len(members) != 1:
             ctx.show_error("NotRequired[] requires a single argument")
             return AnyValue(AnySource.error)
-        return _Pep655Value(False, _type_from_value(members[0], ctx))
+        return Pep655Value(False, _type_from_value(members[0], ctx))
     elif root is Callable or root is typing.Callable:
         if len(members) == 2:
             args, return_value = members
@@ -739,11 +797,13 @@ class _DefaultContext(Context):
         visitor: "NameCheckVisitor",
         node: Optional[ast.AST],
         globals: Optional[Mapping[str, object]] = None,
+        use_name_node_for_error: bool = False,
     ) -> None:
         super().__init__()
         self.visitor = visitor
         self.node = node
         self.globals = globals
+        self.use_name_node_for_error = use_name_node_for_error
 
     def show_error(
         self,
@@ -760,7 +820,7 @@ class _DefaultContext(Context):
         if self.visitor is not None:
             val, _ = self.visitor.resolve_name(
                 node,
-                error_node=self.node,
+                error_node=node if self.use_name_node_for_error else self.node,
                 suppress_errors=self.should_suppress_undefined_names,
             )
             return val
@@ -786,7 +846,7 @@ class _SubscriptedValue(Value):
 
 
 @dataclass
-class _Pep655Value(Value):
+class Pep655Value(Value):
     required: bool
     value: Value
 
@@ -901,7 +961,6 @@ class _Visitor(ast.NodeVisitor):
                     if isinstance(kwarg_value, KnownValue):
                         kwargs[name] = kwarg_value.val
                     else:
-                        print(kwarg_value)
                         return None
             return KnownValue(func.val(*args, **kwargs))
         elif func.val == TypeVar:
@@ -1038,7 +1097,7 @@ def _value_of_origin_args(
         if len(args) != 1:
             ctx.show_error("Required[] requires a single argument")
             return AnyValue(AnySource.error)
-        return _Pep655Value(True, _type_from_runtime(args[0], ctx))
+        return Pep655Value(True, _type_from_runtime(args[0], ctx))
     elif is_typing_name(origin, "NotRequired"):
         if not is_typeddict:
             ctx.show_error("NotRequired[] used in unsupported context")
@@ -1046,7 +1105,7 @@ def _value_of_origin_args(
         if len(args) != 1:
             ctx.show_error("NotRequired[] requires a single argument")
             return AnyValue(AnySource.error)
-        return _Pep655Value(False, _type_from_runtime(args[0], ctx))
+        return Pep655Value(False, _type_from_runtime(args[0], ctx))
     elif origin is None and isinstance(val, type):
         # This happens for SupportsInt in 3.7.
         return _maybe_typed_value(val)
diff --git a/pyanalyze/arg_spec.py b/pyanalyze/arg_spec.py
index 502dabd..8611fc4 100644
--- a/pyanalyze/arg_spec.py
+++ b/pyanalyze/arg_spec.py
@@ -7,7 +7,7 @@ Implementation of extended argument specifications used by test_scope.
 from .options import Options, PyObjectSequenceOption
 from .analysis_lib import is_positional_only_arg_name
 from .extensions import CustomCheck, get_overloads
-from .annotations import Context, type_from_runtime
+from .annotations import Context, RuntimeEvaluator, type_from_runtime
 from .config import Config
 from .find_unused import used
 from . import implementation
@@ -33,7 +33,6 @@ from .signature import (
     Signature,
     ParameterKind,
 )
-from .type_evaluation import get_evaluator
 from .typeshed import TypeshedFinder
 from .value import (
     AnySource,
@@ -94,9 +93,9 @@ def with_implementation(fn: object, implementation_fn: Impl) -> Iterator[None]:
         ):
             yield
     else:
-        argspec = ArgSpecCache(Options.from_option_list([], Config())).get_argspec(
-            fn, impl=implementation_fn
-        )
+        options = Options.from_option_list([], Config())
+        tsf = TypeshedFinder.make(options)
+        argspec = ArgSpecCache(options, tsf).get_argspec(fn, impl=implementation_fn)
         if argspec is None:
             # builtin or something, just use a generic argspec
             argspec = Signature.make(
@@ -288,13 +287,14 @@ class ArgSpecCache:
     def __init__(
         self,
         options: Options,
+        ts_finder: TypeshedFinder,
         *,
         vnv_provider: Callable[[str], Optional[Value]] = lambda _: None,
     ) -> None:
         self.vnv_provider = vnv_provider
         self.options = options
         self.config = options.fallback
-        self.ts_finder = TypeshedFinder(verbose=False)
+        self.ts_finder = ts_finder
         self.known_argspecs = {}
         self.generic_bases_cache = {}
         self.default_context = AnnotationsContext(self)
@@ -509,7 +509,7 @@ class ArgSpecCache:
                     ]
                     if all_of_type(sigs, Signature):
                         return OverloadedSignature(sigs)
-                evaluator = get_evaluator(obj)
+                evaluator = RuntimeEvaluator.get_for(obj)
                 if evaluator is not None:
                     sig = self._cached_get_argspec(
                         evaluator.func, impl, is_asynq, in_overload_resolution=True
diff --git a/pyanalyze/attributes.py b/pyanalyze/attributes.py
index 7b9a205..ca1c10d 100644
--- a/pyanalyze/attributes.py
+++ b/pyanalyze/attributes.py
@@ -14,7 +14,7 @@ import types
 from typing import Any, Generic, Sequence, Tuple, Optional, Union
 
 
-from .annotations import type_from_runtime, Context
+from .annotations import type_from_annotations, type_from_runtime, Context
 from .safe import safe_isinstance, safe_issubclass
 from .signature import Signature, MaybeSignature
 from .stacked_scopes import Composite
@@ -376,13 +376,14 @@ def _get_attribute_from_mro(
             pass
     elif safe_isinstance(typ, types.ModuleType):
         try:
-            annotation = typ.__annotations__[ctx.attr]
+            annotations = typ.__annotations__
         except Exception:
-            # Module doesn't have annotations or it's not in there
             pass
         else:
-            attr_type = type_from_runtime(annotation, ctx=AnnotationsContext(ctx, typ))
-            if attr_type != AnyValue(AnySource.incomplete_annotation):
+            attr_type = type_from_annotations(
+                annotations, ctx.attr, ctx=AnnotationsContext(ctx, typ)
+            )
+            if attr_type is not None:
                 return (attr_type, typ, False)
 
     try:
@@ -407,15 +408,15 @@ def _get_attribute_from_mro(
             try:
                 # Make sure to use only __annotations__ that are actually on this
                 # class, not ones inherited from a base class.
-                annotation = base_cls.__dict__["__annotations__"][ctx.attr]
+                annotations = base_cls.__dict__["__annotations__"]
             except Exception:
                 # no __annotations__, or it's not a dict, or the attr isn't there
                 pass
             else:
-                attr_type = type_from_runtime(
-                    annotation, ctx=AnnotationsContext(ctx, base_cls)
+                attr_type = type_from_annotations(
+                    annotations, ctx.attr, ctx=AnnotationsContext(ctx, base_cls)
                 )
-                if attr_type != AnyValue(AnySource.incomplete_annotation):
+                if attr_type is not None:
                     return (attr_type, base_cls, False)
             try:
                 # Make sure we use only the object from this class, but do invoke
diff --git a/pyanalyze/checker.py b/pyanalyze/checker.py
index da03ce9..6b4dd9e 100644
--- a/pyanalyze/checker.py
+++ b/pyanalyze/checker.py
@@ -20,8 +20,6 @@ from typing import (
     Dict,
 )
 
-from pyanalyze.shared_options import VariableNameValues
-
 from .options import Options, PyObjectSequenceOption
 from .node_visitor import Failure
 from .value import TypedValue, VariableNameValue
@@ -29,6 +27,8 @@ from .arg_spec import ArgSpecCache
 from .config import Config
 from .reexport import ImplicitReexportTracker
 from .safe import is_instance_of_typing_name, is_typing_name, safe_getattr
+from .shared_options import VariableNameValues
+from .typeshed import TypeshedFinder
 from .type_object import TypeObject, get_mro
 from .suggested_type import CallableTracker
 
@@ -63,6 +63,7 @@ class Checker:
     raw_options: InitVar[Optional[Options]] = None
     options: Options = field(init=False)
     arg_spec_cache: ArgSpecCache = field(init=False)
+    ts_finder: TypeshedFinder = field(init=False)
     reexport_tracker: ImplicitReexportTracker = field(init=False)
     callable_tracker: CallableTracker = field(init=False)
     type_object_cache: Dict[Union[type, super, str], TypeObject] = field(
@@ -78,8 +79,11 @@ class Checker:
             self.options = Options.from_option_list([], self.config)
         else:
             self.options = raw_options
+        self.ts_finder = TypeshedFinder.make(self.options)
         self.arg_spec_cache = ArgSpecCache(
-            self.options, vnv_provider=self.maybe_get_variable_name_value
+            self.options,
+            self.ts_finder,
+            vnv_provider=self.maybe_get_variable_name_value,
         )
         self.reexport_tracker = ImplicitReexportTracker(self.options)
         self.callable_tracker = CallableTracker()
@@ -130,7 +134,7 @@ class Checker:
         else:
             additional_bases = self.get_additional_bases(typ)
             # Is it marked as a protocol in stubs? If so, use the stub definition.
-            if self.arg_spec_cache.ts_finder.is_protocol(typ):
+            if self.ts_finder.is_protocol(typ):
                 bases = self._get_typeshed_bases(typ)
                 return TypeObject(
                     typ,
@@ -155,13 +159,13 @@ class Checker:
             return TypeObject(typ, additional_bases)
 
     def _get_typeshed_bases(self, typ: Union[type, str]) -> Set[Union[type, str]]:
-        base_values = self.arg_spec_cache.ts_finder.get_bases_recursively(typ)
+        base_values = self.ts_finder.get_bases_recursively(typ)
         return set(base.typ for base in base_values if isinstance(base, TypedValue))
 
     def _get_protocol_members(self, bases: Iterable[Union[type, str]]) -> Set[str]:
         return set(
             itertools.chain.from_iterable(
-                self.arg_spec_cache.ts_finder.get_all_attributes(base) for base in bases
+                self.ts_finder.get_all_attributes(base) for base in bases
             )
         )
 
diff --git a/pyanalyze/functions.py b/pyanalyze/functions.py
index ebaa940..50467a2 100644
--- a/pyanalyze/functions.py
+++ b/pyanalyze/functions.py
@@ -37,7 +37,8 @@ from .value import (
     CanAssignError,
 )
 
-FunctionNode = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda]
+FunctionDefNode = Union[ast.FunctionDef, ast.AsyncFunctionDef]
+FunctionNode = Union[FunctionDefNode, ast.Lambda]
 IMPLICIT_CLASSMETHODS = ("__init_subclass__", "__new__")
 
 
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index 58f7da8..fafdb03 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -31,6 +31,7 @@ import sys
 import tempfile
 import traceback
 import types
+import typeshed_client
 from typing import (
     ClassVar,
     ContextManager,
@@ -56,9 +57,9 @@ import qcore
 
 from . import attributes, format_strings, node_visitor, importer, method_return_type
 from .annotations import (
-    EvaluatorValidationContext,
+    SyntheticEvaluator,
     is_instance_of_typing_name,
-    type_from_runtime,
+    type_from_annotations,
     type_from_value,
     is_typing_name,
 )
@@ -133,6 +134,7 @@ from .functions import (
     AsyncFunctionKind,
     FunctionInfo,
     FunctionResult,
+    FunctionDefNode,
     FunctionNode,
     compute_function_info,
     IMPLICIT_CLASSMETHODS,
@@ -303,7 +305,7 @@ class _AttrContext(attributes.AttrContext):
         return AnyValue(AnySource.inference)
 
     def get_attribute_from_typeshed(self, typ: type, *, on_class: bool) -> Value:
-        typeshed_type = self.visitor.arg_spec_cache.ts_finder.get_attribute(
+        typeshed_type = self.visitor.checker.ts_finder.get_attribute(
             typ, self.attr, on_class=on_class
         )
         if (
@@ -316,7 +318,7 @@ class _AttrContext(attributes.AttrContext):
     def get_attribute_from_typeshed_recursively(
         self, fq_name: str, *, on_class: bool
     ) -> Tuple[Value, object]:
-        return self.visitor.arg_spec_cache.ts_finder.get_attribute_recursively(
+        return self.visitor.checker.ts_finder.get_attribute_recursively(
             fq_name, self.attr, on_class=on_class
         )
 
@@ -1546,9 +1548,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
     def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> Value:
         return self.visit_FunctionDef(node)
 
-    def visit_FunctionDef(
-        self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
-    ) -> Value:
+    def visit_FunctionDef(self, node: FunctionDefNode) -> Value:
         potential_function = self._get_potential_function(node)
         info = compute_function_info(
             node,
@@ -1674,9 +1674,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
             return
         self._argspec_to_retval[id(sig)] = return_value
 
-    def _get_potential_function(
-        self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
-    ) -> Optional[object]:
+    def _get_potential_function(self, node: FunctionDefNode) -> Optional[object]:
         scope_type = self.scopes.scope_type()
         if scope_type == ScopeType.module_scope and self.module is not None:
             potential_function = safe_getattr(self.module, node.name, None)
@@ -1730,16 +1728,16 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
             return FunctionResult(parameters=params)
 
         if function_info.is_evaluated:
-            if self._is_collecting():
+            if self._is_collecting() or isinstance(node, ast.Lambda):
                 return FunctionResult(parameters=params)
             with self.scopes.allow_only_module_scope():
-                ctx = EvaluatorValidationContext(
+                evaluator = SyntheticEvaluator.from_visitor(node, self)
+                ctx = type_evaluation.EvalContext(
                     variables={param.name: param.annotation for param in params},
                     positions={param.name: type_evaluation.DEFAULT for param in params},
                     can_assign_context=self,
-                    node=node,
                 )
-                for error in type_evaluation.validate(node, ctx):
+                for error in evaluator.validate(ctx):
                     self.show_error(
                         error.node, error.message, error_code=ErrorCode.bad_evaluator
                     )
@@ -1747,10 +1745,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
                     with self.catch_errors(), self.scopes.add_scope(
                         ScopeType.function_scope, scope_node=node
                     ):
-                        if isinstance(node, ast.Lambda):
-                            self.generic_visit(node.body)
-                        else:
-                            self._generic_visit_list(node.body)
+                        self._generic_visit_list(node.body)
             return FunctionResult(parameters=params)
 
         # We pass in the node to add_scope() and visit the body once in collecting
@@ -2119,6 +2114,30 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
             self._handle_imports(node.names, force_public=force_public)
             return
 
+        # See if we can get the names from the stub instead
+        if (
+            isinstance(node, ast.ImportFrom)
+            and node.module is not None
+            and node.level == 0
+        ):
+            path = typeshed_client.ModulePath(tuple(node.module.split(".")))
+            finder = self.checker.ts_finder
+            mod = finder.resolver.get_module(path)
+            if mod.exists:
+                for alias in node.names:
+                    val = finder.resolve_name(node.module, alias.name)
+                    if val is UNINITIALIZED_VALUE:
+                        self._show_error_if_checking(
+                            node,
+                            f"Cannot import name {alias.name!r} from {node.module!r}",
+                            ErrorCode.import_failed,
+                        )
+                        val = AnyValue(AnySource.error)
+                    self._set_name_in_scope(
+                        alias.asname or alias.name, alias, val, private=not force_public
+                    )
+                return
+
         source_code = decompile(node)
 
         if self._is_unimportable_module(node):
@@ -4105,7 +4124,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
             return True
         if issubclass(typ, enum.Enum):
             return True
-        ts_finder = self.arg_spec_cache.ts_finder
+        ts_finder = self.checker.ts_finder
         if (
             ts_finder.has_stubs(typ)
             and not ts_finder.has_attribute(typ, "__getattr__")
@@ -4593,9 +4612,11 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
         return {}
 
     @classmethod
-    def prepare_constructor_kwargs(cls, kwargs: Mapping[str, Any]) -> Mapping[str, Any]:
+    def prepare_constructor_kwargs(
+        cls, kwargs: Mapping[str, Any], extra_options: Sequence[ConfigOption] = ()
+    ) -> Mapping[str, Any]:
         kwargs = dict(kwargs)
-        instances = []
+        instances = [*extra_options]
         if "settings" in kwargs:
             for error_code, value in kwargs["settings"].items():
                 option_cls = ConfigOption.registry[error_code.name]
@@ -4757,17 +4778,9 @@ def build_stacked_scopes(
         module_vars = {}
         annotations = getattr(module, "__annotations__", {})
         for key, value in module.__dict__.items():
-            try:
-                annotation = annotations[key]
-            except Exception:
-                # Malformed __annotations__
+            val = type_from_annotations(annotations, key, globals=module.__dict__)
+            if val is None:
                 val = KnownValue(value)
-            else:
-                maybe_val = type_from_runtime(annotation, globals=module.__dict__)
-                if maybe_val == AnyValue(AnySource.incomplete_annotation):
-                    val = KnownValue(value)
-                else:
-                    val = maybe_val
             module_vars[key] = val
     return StackedScopes(module_vars, module, simplification_limit=simplification_limit)
 
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index 0337d9f..66c7d0b 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -6,8 +6,7 @@ calls.
 
 """
 
-from . import annotations
-from .type_evaluation import Evaluator
+from .type_evaluation import EvalContext, Evaluator
 from .error_code import ErrorCode
 from .safe import all_of_type
 from .stacked_scopes import (
@@ -924,9 +923,7 @@ class Signature:
                 positions = {
                     param: position for param, (position, _) in bound_args.items()
                 }
-                ctx = annotations.TypeEvaluationContext(
-                    varmap, positions, visitor, self.evaluator.globals
-                )
+                ctx = EvalContext(varmap, positions, visitor)
                 return_value, errors = self.evaluator.evaluate(ctx)
                 for error in errors:
                     error_node = node
diff --git a/pyanalyze/stubs/README b/pyanalyze/stubs/README
new file mode 100644
index 0000000..5ec3f06
--- /dev/null
+++ b/pyanalyze/stubs/README
@@ -0,0 +1,1 @@
+Dummy stubs directory used in pyanalyze's tests.
diff --git a/pyanalyze/stubs/pyanalyze-stubs/extensions.pyi b/pyanalyze/stubs/pyanalyze-stubs/extensions.pyi
new file mode 100644
index 0000000..b8cdeaf
--- /dev/null
+++ b/pyanalyze/stubs/pyanalyze-stubs/extensions.pyi
@@ -0,0 +1,17 @@
+# Has to exist for stubs to be able to import
+# from it, because typeshed_client doesn't let
+# stubs import from non-stub files.
+
+from typing import Any, Callable, Optional, List
+
+def reveal_type(value: object) -> None: ...
+def get_overloads(fully_qualified_name: str) -> List[Callable[..., Any]]: ...
+def get_type_evaluation(fully_qualified_name: str) -> Optional[Callable[..., Any]]: ...
+def overload(func: Callable[..., Any]) -> Callable[..., Any]: ...
+def evaluated(func: Callable[..., Any]) -> Callable[..., Any]: ...
+def is_provided(arg: Any) -> bool: ...
+def is_positional(arg: Any) -> bool: ...
+def is_keyword(arg: Any) -> bool: ...
+def is_of_type(arg: Any, type: Any, *, exclude_any: bool = ...) -> bool: ...
+def show_error(message: str, *, argument: Optional[Any] = ...) -> bool: ...
+def __getattr__(self, __arg: str) -> Any: ...
diff --git a/pyanalyze/type_evaluation.py b/pyanalyze/type_evaluation.py
index 129681c..dfd043a 100644
--- a/pyanalyze/type_evaluation.py
+++ b/pyanalyze/type_evaluation.py
@@ -8,11 +8,9 @@ import ast
 from contextlib import contextmanager
 import contextlib
 from dataclasses import dataclass, field
-import inspect
 import operator
 import qcore
 import sys
-import textwrap
 from typing_extensions import Literal
 from typing import (
     Any,
@@ -35,7 +33,6 @@ from .stacked_scopes import (
     constrain_value,
     IsAssignablePredicate,
 )
-from .extensions import get_type_evaluation
 from .safe import all_of_type
 from .value import (
     NO_RETURN_VALUE,
@@ -261,17 +258,12 @@ class UserRaisedError:
 EvaluateError = Union[InvalidEvaluation, UserRaisedError]
 
 
-class Context:
+@dataclass
+class EvalContext:
     variables: VarMap
     positions: Mapping[str, Position]
     can_assign_context: CanAssignContext
 
-    def evaluate_type(self, __node: ast.AST) -> Value:
-        raise NotImplementedError
-
-    def evaluate_value(self, __node: ast.AST) -> Value:
-        raise NotImplementedError
-
     @contextmanager
     def narrow_variables(self, varmap: Optional[VarMap]) -> Iterator[None]:
         if varmap is None:
@@ -288,16 +280,27 @@ class Context:
 
 @dataclass
 class Evaluator:
-    func: Callable[..., Any]
-    node: ast.FunctionDef
-    globals: Mapping[str, object]
+    node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
 
-    def evaluate(self, ctx: Context) -> Tuple[Value, Sequence[UserRaisedError]]:
-        visitor = EvaluateVisitor(ctx)
-        result = visitor.run(self.node)
+    def evaluate(self, ctx: EvalContext) -> Tuple[Value, Sequence[UserRaisedError]]:
+        visitor = EvaluateVisitor(self, ctx)
+        result = visitor.run()
         errors = [e for e in visitor.errors if isinstance(e, UserRaisedError)]
         return result, errors
 
+    def validate(self, ctx: EvalContext) -> List[InvalidEvaluation]:
+        visitor = EvaluateVisitor(self, ctx, validation_mode=True)
+        visitor.run()
+        return [
+            error for error in visitor.errors if isinstance(error, InvalidEvaluation)
+        ]
+
+    def evaluate_type(self, __node: ast.AST) -> Value:
+        raise NotImplementedError
+
+    def evaluate_value(self, __node: ast.AST) -> Value:
+        raise NotImplementedError
+
 
 @dataclass
 class CombinedReturn:
@@ -337,7 +340,8 @@ class ConditionReturn:
 
 @dataclass
 class ConditionEvaluator(ast.NodeVisitor):
-    ctx: Context
+    evaluator: Evaluator
+    ctx: EvalContext
     validation_mode: bool = False
     errors: List[EvaluateError] = field(default_factory=list, init=False)
 
@@ -384,7 +388,7 @@ class ConditionEvaluator(ast.NodeVisitor):
                     "is_of_type() takes two positional arguments", node
                 )
             varname_node = node.args[0]
-            typ = self.ctx.evaluate_type(node.args[1])
+            typ = self.evaluator.evaluate_type(node.args[1])
             exclude_any = True
             for keyword in node.keywords:
                 if keyword.arg == "exclude_any":
@@ -591,7 +595,7 @@ class ConditionEvaluator(ast.NodeVisitor):
             )
 
     def evaluate_literal(self, node: ast.expr) -> Optional[KnownValue]:
-        val = self.ctx.evaluate_value(node)
+        val = self.evaluator.evaluate_value(node)
         if (
             isinstance(val, SequenceIncompleteValue)
             and isinstance(val.typ, type)
@@ -613,14 +617,15 @@ class ConditionEvaluator(ast.NodeVisitor):
 
 @dataclass
 class EvaluateVisitor(ast.NodeVisitor):
-    ctx: Context
+    evaluator: Evaluator
+    ctx: EvalContext
     errors: List[EvaluateError] = field(default_factory=list)
     active_conditions: List[Condition] = field(default_factory=list)
     validation_mode: bool = False
 
-    def run(self, node: ast.AST) -> Value:
-        ret = self.visit(node)
-        return self._evaluate_ret(ret, node)
+    def run(self) -> Value:
+        ret = self.visit(self.evaluator.node)
+        return self._evaluate_ret(ret, self.evaluator.node)
 
     def _evaluate_ret(self, ret: EvalReturn, node: ast.AST) -> Value:
         if ret is None:
@@ -677,7 +682,7 @@ class EvaluateVisitor(ast.NodeVisitor):
         if node.value is None:
             self.add_invalid("return statement must have a value", node)
             return KnownValue(None)
-        return self.ctx.evaluate_type(node.value)
+        return self.evaluator.evaluate_type(node.value)
 
     def visit_Expr(self, node: ast.Expr) -> EvalReturn:
         if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name):
@@ -746,7 +751,7 @@ class EvaluateVisitor(ast.NodeVisitor):
         return None
 
     def visit_If(self, node: ast.If) -> EvalReturn:
-        visitor = ConditionEvaluator(self.ctx, self.validation_mode)
+        visitor = ConditionEvaluator(self.evaluator, self.ctx, self.validation_mode)
         condition = visitor.visit(node.test)
         self.errors += visitor.errors
         if self.validation_mode:
@@ -782,31 +787,6 @@ class EvaluateVisitor(ast.NodeVisitor):
         return None
 
 
-def get_evaluator(func: Callable[..., Any]) -> Optional[Evaluator]:
-    try:
-        key = f"{func.__module__}.{func.__qualname__}"
-    except AttributeError:
-        return None
-    evaluation_func = get_type_evaluation(key)
-    if evaluation_func is None or not hasattr(evaluation_func, "__globals__"):
-        return None
-    lines, _ = inspect.getsourcelines(evaluation_func)
-    code = textwrap.dedent("".join(lines))
-    body = ast.parse(code)
-    if not body.body:
-        return None
-    evaluator = body.body[0]
-    if not isinstance(evaluator, ast.FunctionDef):
-        return None
-    return Evaluator(evaluation_func, evaluator, evaluation_func.__globals__)
-
-
-def validate(node: ast.AST, ctx: Context) -> List[InvalidEvaluation]:
-    visitor = EvaluateVisitor(ctx, validation_mode=True)
-    visitor.run(node)
-    return [error for error in visitor.errors if isinstance(error, InvalidEvaluation)]
-
-
 def decompose_union(
     expected_type: Value, parent_value: Value, ctx: CanAssignContext, exclude_any: bool
 ) -> Optional[Tuple[TypeVarMap, Value]]:
diff --git a/pyanalyze/typeshed.py b/pyanalyze/typeshed.py
index 3eba0e8..b8b77c1 100644
--- a/pyanalyze/typeshed.py
+++ b/pyanalyze/typeshed.py
@@ -4,8 +4,17 @@ Code for getting annotations from typeshed (and from third-party stubs generally
 
 """
 
+from .node_visitor import Failure
+from .options import Options, PathSequenceOption
+from .extensions import evaluated
 from .analysis_lib import is_positional_only_arg_name
-from .annotations import Context, type_from_value, value_from_ast
+from .annotations import (
+    Context,
+    Pep655Value,
+    SyntheticEvaluator,
+    type_from_value,
+    value_from_ast,
+)
 from .error_code import ErrorCode
 from .safe import is_typing_name
 from .stacked_scopes import uniq_chain
@@ -20,6 +29,8 @@ from .value import (
     AnySource,
     AnyValue,
     CallableValue,
+    SubclassValue,
+    TypedDictValue,
     TypedValue,
     GenericValue,
     KnownValue,
@@ -42,12 +53,14 @@ from collections.abc import (
 from contextlib import AbstractContextManager
 from dataclasses import dataclass, field, replace
 import collections.abc
+from enum import Enum
 import qcore
 import inspect
 import sys
 from types import GeneratorType
 from typing import (
     Dict,
+    Sequence,
     Set,
     Tuple,
     Any,
@@ -63,6 +76,7 @@ from typing import (
 from typing_extensions import Protocol, TypedDict
 import typeshed_client
 
+
 try:
     # 3.7+
     from contextlib import AbstractAsyncContextManager
@@ -93,6 +107,28 @@ class _AnnotationContext(Context):
         return self.finder.resolve_name(self.module, node.id)
 
 
+class _DummyErrorContext:
+    all_failures: List[Failure] = []
+
+    def show_error(
+        self,
+        node: ast.AST,
+        e: str,
+        error_code: Enum,
+        *,
+        detail: Optional[str] = None,
+        save: bool = True,
+        extra_metadata: Optional[Dict[str, Any]] = None,
+    ) -> Optional[Failure]:
+        return None
+
+
+class StubPath(PathSequenceOption):
+    """Extra paths in which to look for stubs."""
+
+    name = "stub_path"
+
+
 # These are specified as just "List = _Alias()" in typing.pyi. Redirect
 # them to the proper runtime equivalent.
 _TYPING_ALIASES = {
@@ -120,6 +156,16 @@ class TypeshedFinder:
         default_factory=dict, repr=False, init=False
     )
 
+    @classmethod
+    def make(cls, options: Options, *, verbose: bool = False) -> "TypeshedFinder":
+        extra_paths = options.get_value_for(StubPath)
+        ctx = typeshed_client.get_search_context()
+        ctx = typeshed_client.get_search_context(
+            search_path=[*ctx.search_path, *extra_paths]
+        )
+        resolver = typeshed_client.Resolver(ctx)
+        return TypeshedFinder(verbose, resolver)
+
     def log(self, message: str, obj: object) -> None:
         if not self.verbose:
             return
@@ -348,6 +394,7 @@ class TypeshedFinder:
             val = getattr(builtins, name)
             if val is None or isinstance(val, type):
                 return KnownValue(val)
+        # TODO change to UNINITIALIZED_VALUE
         return AnyValue(AnySource.inference)
 
     def _get_attribute_from_info(
@@ -357,6 +404,7 @@ class TypeshedFinder:
         attr: str,
         *,
         on_class: bool,
+        is_typeddict: bool = False,
     ) -> Value:
         if info is None:
             return UNINITIALIZED_VALUE
@@ -370,7 +418,11 @@ class TypeshedFinder:
                     child_info = info.child_nodes[attr]
                     if isinstance(child_info, typeshed_client.NameInfo):
                         if isinstance(child_info.ast, ast.AnnAssign):
-                            return self._parse_type(child_info.ast.annotation, mod)
+                            return self._parse_type(
+                                child_info.ast.annotation,
+                                mod,
+                                is_typeddict=is_typeddict,
+                            )
                         elif isinstance(child_info.ast, ast.FunctionDef):
                             decorators = [
                                 self._parse_expr(decorator, mod)
@@ -636,7 +688,7 @@ class TypeshedFinder:
         autobind: bool = False,
         allow_call: bool = False,
     ) -> Optional[Signature]:
-        is_classmethod = is_staticmethod = False
+        is_classmethod = is_staticmethod = is_evaluated = False
         for decorator_ast in node.decorator_list:
             decorator = self._parse_expr(decorator_ast, mod)
             if decorator == KnownValue(abstractmethod) or decorator == KnownValue(
@@ -651,6 +703,9 @@ class TypeshedFinder:
                 is_staticmethod = True
                 if autobind:  # TODO support staticmethods otherwise
                     continue
+            elif decorator == KnownValue(evaluated):
+                is_evaluated = True
+                continue
             # might be @overload or something else we don't recognize
             return None
         if node.returns is None:
@@ -702,6 +757,11 @@ class TypeshedFinder:
             else:
                 seen_non_positional = True
             cleaned_arguments.append(arg)
+        if is_evaluated:
+            ctx = _AnnotationContext(self, mod)
+            evaluator = SyntheticEvaluator(node, _DummyErrorContext(), ctx)
+        else:
+            evaluator = None
         return Signature.make(
             cleaned_arguments,
             callable=obj,
@@ -709,6 +769,7 @@ class TypeshedFinder:
             if isinstance(node, ast.AsyncFunctionDef)
             else return_value,
             allow_call=allow_call,
+            evaluator=evaluator,
         )
 
     def _parse_param_list(
@@ -768,10 +829,12 @@ class TypeshedFinder:
         ctx = _AnnotationContext(finder=self, module=module)
         return value_from_ast(node, ctx=ctx)
 
-    def _parse_type(self, node: ast.AST, module: str) -> Value:
+    def _parse_type(
+        self, node: ast.AST, module: str, *, is_typeddict: bool = False
+    ) -> Value:
         val = self._parse_expr(node, module)
         ctx = _AnnotationContext(finder=self, module=module)
-        typ = type_from_value(val, ctx=ctx)
+        typ = type_from_value(val, ctx=ctx, is_typeddict=is_typeddict)
         if self.verbose and isinstance(typ, AnyValue):
             self.log("Got Any", (ast.dump(node), module))
         return typ
@@ -793,6 +856,54 @@ class TypeshedFinder:
         ctx = _AnnotationContext(finder=self, module=module)
         return value_from_ast(info.ast.value, ctx=ctx)
 
+    def make_synthetic_type(self, module: str, info: typeshed_client.NameInfo) -> Value:
+        fq_name = f"{module}.{info.name}"
+        bases = self.get_bases_for_fq_name(fq_name)
+        typ = TypedValue(fq_name)
+        if bases is not None:
+            if any(
+                (isinstance(base, KnownValue) and is_typing_name(base.val, "TypedDict"))
+                or isinstance(base, TypedDictValue)
+                for base in bases
+            ):
+                typ = self._make_typeddict(module, info, bases)
+        return SubclassValue(typ, exactly=True)
+
+    def _make_typeddict(
+        self, module: str, info: typeshed_client.NameInfo, bases: Sequence[Value]
+    ) -> TypedDictValue:
+        total = True
+        if isinstance(info.ast, ast.ClassDef):
+            for keyword in info.ast.keywords:
+                if keyword.arg == "total":
+                    val = self._parse_expr(keyword.value, module)
+                    if isinstance(val, KnownValue) and isinstance(val.val, bool):
+                        total = val.val
+        attrs = self._get_all_attributes_from_info(info, module)
+        fields = [
+            self._get_attribute_from_info(
+                info, module, attr, on_class=True, is_typeddict=True
+            )
+            for attr in attrs
+        ]
+        items = {}
+        for base in bases:
+            if isinstance(base, TypedDictValue):
+                items.update(base.items)
+        items.update(
+            {
+                attr: self._make_td_value(field, total)
+                for attr, field in zip(attrs, fields)
+            }
+        )
+        return TypedDictValue(items)
+
+    def _make_td_value(self, field: Value, total: bool) -> Tuple[bool, Value]:
+        if isinstance(field, Pep655Value):
+            return (field.required, field.value)
+        else:
+            return (total, field)
+
     def _value_from_info(
         self, info: typeshed_client.resolver.ResolvedName, module: str
     ) -> Value:
@@ -825,9 +936,17 @@ class TypeshedFinder:
                 return KnownValue(getattr(mod, info.name))
             except Exception:
                 if isinstance(info.ast, ast.ClassDef):
-                    return TypedValue(f"{module}.{info.name}")
+                    return self.make_synthetic_type(module, info)
                 elif isinstance(info.ast, ast.AnnAssign):
-                    return self._parse_type(info.ast.annotation, module)
+                    val = self._parse_type(info.ast.annotation, module)
+                    if val != AnyValue(AnySource.incomplete_annotation):
+                        return val
+                    if info.ast.value:
+                        return self._parse_expr(info.ast.value, module)
+                elif isinstance(info.ast, ast.FunctionDef):
+                    sig = self._get_signature_from_info(info, None, fq_name, module)
+                    if sig is not None:
+                        return CallableValue(sig)
                 self.log("Unable to import", (module, info))
                 return AnyValue(AnySource.inference)
         elif isinstance(info, tuple):
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index 686cc3b..9e60b2e 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -1112,7 +1112,7 @@ class TypedDictValue(GenericValue):
             return unify_typevar_maps(tv_maps)
         return super().can_assign(other, ctx)
 
-    def substitute_typevars(self, typevars: TypeVarMap) -> Value:
+    def substitute_typevars(self, typevars: TypeVarMap) -> "TypedDictValue":
         return TypedDictValue(
             {
                 key: (is_required, value.substitute_typevars(typevars))
@@ -1228,9 +1228,11 @@ class SubclassValue(Value):
 
     typ: Union[TypedValue, "TypeVarValue"]
     """The underlying type."""
+    exactly: bool = False
+    """If True, represents exactly this class and not a subclass."""
 
     def substitute_typevars(self, typevars: TypeVarMap) -> Value:
-        return self.make(self.typ.substitute_typevars(typevars))
+        return self.make(self.typ.substitute_typevars(typevars), exactly=self.exactly)
 
     def walk_values(self) -> Iterable["Value"]:
         yield self
@@ -1283,14 +1285,16 @@ class SubclassValue(Value):
         return f"Type[{self.typ}]"
 
     @classmethod
-    def make(cls, origin: Value) -> Value:
+    def make(cls, origin: Value, *, exactly: bool = False) -> Value:
         if isinstance(origin, MultiValuedValue):
-            return unite_values(*[cls.make(val) for val in origin.vals])
+            return unite_values(
+                *[cls.make(val, exactly=exactly) for val in origin.vals]
+            )
         elif isinstance(origin, AnyValue):
             # Type[Any] is equivalent to plain type
             return TypedValue(type)
         elif isinstance(origin, (TypeVarValue, TypedValue)):
-            return cls(origin)
+            return cls(origin, exactly=exactly)
         else:
             return AnyValue(AnySource.inference)
 


Test patch:
diff --git a/pyanalyze/stubs/_pyanalyze_tests-stubs/aliases.pyi b/pyanalyze/stubs/_pyanalyze_tests-stubs/aliases.pyi
new file mode 100644
index 0000000..4bf491e
--- /dev/null
+++ b/pyanalyze/stubs/_pyanalyze_tests-stubs/aliases.pyi
@@ -0,0 +1,8 @@
+from typing_extensions import TypeAlias
+
+Alias = int
+ExplicitAlias: TypeAlias = int
+
+constant: int
+aliased_constant: Alias
+explicitly_aliased_constant: ExplicitAlias
diff --git a/pyanalyze/stubs/_pyanalyze_tests-stubs/evaluated.pyi b/pyanalyze/stubs/_pyanalyze_tests-stubs/evaluated.pyi
new file mode 100644
index 0000000..1ea1d34
--- /dev/null
+++ b/pyanalyze/stubs/_pyanalyze_tests-stubs/evaluated.pyi
@@ -0,0 +1,11 @@
+from pyanalyze.extensions import evaluated
+from typing import Any, TextIO, BinaryIO, IO
+
+@evaluated
+def open(mode: str):
+    if mode == "r":
+        return TextIO
+    elif mode == "rb":
+        return BinaryIO
+    else:
+        return IO[Any]
diff --git a/pyanalyze/stubs/_pyanalyze_tests-stubs/typeddict.pyi b/pyanalyze/stubs/_pyanalyze_tests-stubs/typeddict.pyi
new file mode 100644
index 0000000..aaae30d
--- /dev/null
+++ b/pyanalyze/stubs/_pyanalyze_tests-stubs/typeddict.pyi
@@ -0,0 +1,16 @@
+from typing_extensions import TypedDict, NotRequired, Required
+
+class TD1(TypedDict):
+    a: int
+    b: str
+
+class TD2(TypedDict, total=False):
+    a: int
+    b: str
+
+class PEP655(TypedDict):
+    a: NotRequired[int]
+    b: Required[str]
+
+class Inherited(TD1):
+    c: float
diff --git a/pyanalyze/test_annotations.py b/pyanalyze/test_annotations.py
index f6dd769..230bfac 100644
--- a/pyanalyze/test_annotations.py
+++ b/pyanalyze/test_annotations.py
@@ -1676,3 +1676,20 @@ class TestCallable(TestNameCheckVisitorBase):
             func = with_request(takes_int_str)
             func(1, "A")
             func(1, 2)  # E: incompatible_argument
+
+
+class TestTypeAlias(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_runtime(self):
+        from typing_extensions import TypeAlias
+
+        X: TypeAlias = int
+        Y = X
+        Z: "TypeAlias" = int
+
+        def capybara(x: X, y: Y, x_quoted: "X", y_quoted: "Y", z: Z) -> None:
+            assert_is_value(x, TypedValue(int))
+            assert_is_value(y, TypedValue(int))
+            assert_is_value(x_quoted, TypedValue(int))
+            assert_is_value(y_quoted, TypedValue(int))
+            assert_is_value(z, TypedValue(int))
diff --git a/pyanalyze/test_arg_spec.py b/pyanalyze/test_arg_spec.py
index 74cd697..fad0102 100644
--- a/pyanalyze/test_arg_spec.py
+++ b/pyanalyze/test_arg_spec.py
@@ -100,6 +100,7 @@ def test_get_argspec():
 
     # test everything twice because calling qcore.get_original_fn has side effects
     for _ in range(2):
+        asc = ArgSpecCache(checker.options, checker.ts_finder)
 
         # there's special logic for this in signature_from_value; TODO move that into
         # ExtendedArgSpec
@@ -113,12 +114,10 @@ def test_get_argspec():
                 callable=ClassWithCall.normal_classmethod.__func__,
             ),
             Composite(KnownValue(ClassWithCall)),
-        ) == ArgSpecCache(checker.options).get_argspec(ClassWithCall.normal_classmethod)
+        ) == asc.get_argspec(ClassWithCall.normal_classmethod)
         assert Signature.make(
             [SigParameter("arg")], callable=ClassWithCall.normal_staticmethod
-        ) == ArgSpecCache(checker.options).get_argspec(
-            ClassWithCall.normal_staticmethod
-        )
+        ) == asc.get_argspec(ClassWithCall.normal_staticmethod)
 
         assert Signature.make(
             [
@@ -128,19 +127,19 @@ def test_get_argspec():
                 SigParameter("proechimys", ParameterKind.VAR_KEYWORD),
             ],
             callable=function,
-        ) == ArgSpecCache(checker.options).get_argspec(function)
+        ) == asc.get_argspec(function)
 
         assert Signature.make(
             [SigParameter("x"), SigParameter("y")],
             callable=async_function.fn,
             is_asynq=True,
-        ) == ArgSpecCache(checker.options).get_argspec(async_function)
+        ) == asc.get_argspec(async_function)
 
         assert Signature.make(
             [SigParameter("x"), SigParameter("y")],
             callable=async_function.fn,
             is_asynq=True,
-        ) == ArgSpecCache(checker.options).get_argspec(async_function.asynq)
+        ) == asc.get_argspec(async_function.asynq)
 
         instance = ClassWithCall(1)
 
@@ -151,7 +150,7 @@ def test_get_argspec():
                 is_asynq=True,
             ),
             Composite(KnownValue(instance)),
-        ) == ArgSpecCache(checker.options).get_argspec(instance.async_method)
+        ) == asc.get_argspec(instance.async_method)
 
         assert BoundMethodSignature(
             Signature.make(
@@ -160,21 +159,19 @@ def test_get_argspec():
                 is_asynq=True,
             ),
             Composite(KnownValue(instance)),
-        ) == ArgSpecCache(checker.options).get_argspec(instance.async_method.asynq)
+        ) == asc.get_argspec(instance.async_method.asynq)
 
         assert Signature.make(
             [SigParameter("y")],
             callable=ClassWithCall.async_staticmethod.fn,
             is_asynq=True,
-        ) == ArgSpecCache(checker.options).get_argspec(ClassWithCall.async_staticmethod)
+        ) == asc.get_argspec(ClassWithCall.async_staticmethod)
 
         assert Signature.make(
             [SigParameter("y")],
             callable=ClassWithCall.async_staticmethod.fn,
             is_asynq=True,
-        ) == ArgSpecCache(checker.options).get_argspec(
-            ClassWithCall.async_staticmethod.asynq
-        )
+        ) == asc.get_argspec(ClassWithCall.async_staticmethod.asynq)
 
         assert BoundMethodSignature(
             Signature.make(
@@ -183,7 +180,7 @@ def test_get_argspec():
                 is_asynq=True,
             ),
             Composite(KnownValue(ClassWithCall)),
-        ) == ArgSpecCache(checker.options).get_argspec(ClassWithCall.async_classmethod)
+        ) == asc.get_argspec(ClassWithCall.async_classmethod)
 
         assert BoundMethodSignature(
             Signature.make(
@@ -192,9 +189,7 @@ def test_get_argspec():
                 is_asynq=True,
             ),
             Composite(KnownValue(ClassWithCall)),
-        ) == ArgSpecCache(checker.options).get_argspec(
-            ClassWithCall.async_classmethod.asynq
-        )
+        ) == asc.get_argspec(ClassWithCall.async_classmethod.asynq)
 
         assert BoundMethodSignature(
             Signature.make(
@@ -202,9 +197,7 @@ def test_get_argspec():
                 callable=ClassWithCall.pure_async_classmethod.decorator.fn,
             ),
             Composite(KnownValue(ClassWithCall)),
-        ) == ArgSpecCache(checker.options).get_argspec(
-            ClassWithCall.pure_async_classmethod
-        )
+        ) == asc.get_argspec(ClassWithCall.pure_async_classmethod)
 
         # This behaves differently in 3.9 (decorator) than in 3.6-3.8 (__func__). Not
         # sure why.
@@ -220,9 +213,7 @@ def test_get_argspec():
                 is_asynq=True,
             ),
             Composite(KnownValue(ClassWithCall)),
-        ) == ArgSpecCache(checker.options).get_argspec(
-            ClassWithCall.classmethod_before_async
-        )
+        ) == asc.get_argspec(ClassWithCall.classmethod_before_async)
 
         assert Signature.make(
             [
@@ -231,7 +222,7 @@ def test_get_argspec():
             ],
             KnownValue(None),
             callable=wrapped,
-        ) == ArgSpecCache(checker.options).get_argspec(wrapped)
+        ) == asc.get_argspec(wrapped)
         decorated = decorator(wrapped)
         assert Signature.make(
             [
@@ -247,7 +238,7 @@ def test_get_argspec():
                 ),
             ],
             callable=decorated,
-        ) == ArgSpecCache(checker.options).get_argspec(decorated)
+        ) == asc.get_argspec(decorated)
         assert Signature.make(
             [
                 SigParameter(
@@ -256,7 +247,7 @@ def test_get_argspec():
             ],
             NewTypeValue(NT),
             callable=NT,
-        ) == ArgSpecCache(checker.options).get_argspec(NT)
+        ) == asc.get_argspec(NT)
 
 
 def test_positional_only():
diff --git a/pyanalyze/test_config.py b/pyanalyze/test_config.py
index 277f52b..cb22514 100644
--- a/pyanalyze/test_config.py
+++ b/pyanalyze/test_config.py
@@ -4,7 +4,10 @@ Configuration file specific to tests.
 
 """
 from typing import Dict, Optional
+from pathlib import Path
 
+from .options import Options
+from .typeshed import StubPath
 from .arg_spec import ArgSpecCache
 from .signature import (
     CallContext,
@@ -113,3 +116,7 @@ class TestConfig(Config):
         ):
             return cls.base
         return cls
+
+
+TEST_INSTANCES = [StubPath([Path(__file__).parent / "stubs"])]
+TEST_OPTIONS = Options.from_option_list(TEST_INSTANCES, TestConfig())
diff --git a/pyanalyze/test_name_check_visitor.py b/pyanalyze/test_name_check_visitor.py
index 7891810..481e8e7 100644
--- a/pyanalyze/test_name_check_visitor.py
+++ b/pyanalyze/test_name_check_visitor.py
@@ -4,7 +4,6 @@ from asynq import FutureBase, AsyncTask
 import collections
 import os
 import types
-
 from .analysis_lib import make_module
 from .checker import Checker
 from .name_check_visitor import (
@@ -15,7 +14,7 @@ from .name_check_visitor import (
 )
 from .implementation import assert_is_value, dump_value
 from .error_code import DISABLED_IN_TESTS, ErrorCode
-from .test_config import TestConfig
+from .test_config import TEST_INSTANCES, TestConfig
 from .value import (
     AnnotatedValue,
     AnySource,
@@ -84,7 +83,7 @@ class TestNameCheckVisitorBase(test_node_visitor.BaseNodeVisitorTester):
         verbosity = int(os.environ.get("ANS_TEST_SCOPE_VERBOSITY", 0))
         mod = _make_module(code_str)
         kwargs["settings"] = default_settings
-        kwargs = self.visitor_cls.prepare_constructor_kwargs(kwargs)
+        kwargs = self.visitor_cls.prepare_constructor_kwargs(kwargs, TEST_INSTANCES)
         new_code = ""
         with ClassAttributeChecker(
             self.visitor_cls.config,
diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py
index adec73b..753fe56 100644
--- a/pyanalyze/test_typeshed.py
+++ b/pyanalyze/test_typeshed.py
@@ -7,6 +7,7 @@ import io
 from pathlib import Path
 import sys
 import tempfile
+import textwrap
 import time
 from typeshed_client import Resolver, get_search_context
 import typing
@@ -14,7 +15,8 @@ from typing import Dict, Generic, List, TypeVar, NewType, Union
 from urllib.error import HTTPError
 import urllib.parse
 
-from .test_config import TestConfig
+from .extensions import evaluated
+from .test_config import TestConfig, TEST_OPTIONS
 from .test_name_check_visitor import TestNameCheckVisitorBase
 from .test_node_visitor import assert_passes
 from .options import Options
@@ -23,6 +25,8 @@ from .arg_spec import ArgSpecCache
 from .test_arg_spec import ClassWithCall
 from .typeshed import TypeshedFinder
 from .value import (
+    SubclassValue,
+    TypedDictValue,
     assert_is_value,
     AnySource,
     AnyValue,
@@ -79,15 +83,17 @@ class TestTypeshedClient(TestNameCheckVisitorBase):
             temp_dir = Path(temp_dir_str)
             (temp_dir / "typing.pyi").write_text("def NewType(a, b): pass\n")
             (temp_dir / "newt.pyi").write_text(
-                """
-from typing import NewType
+                textwrap.dedent(
+                    """
+                from typing import NewType
 
-NT = NewType("NT", int)
-Alias = int
+                NT = NewType("NT", int)
+                Alias = int
 
-def f(x: NT, y: Alias) -> None:
-    pass
-"""
+                def f(x: NT, y: Alias) -> None:
+                    pass
+                """
+                )
             )
             (temp_dir / "VERSIONS").write_text("newt: 3.5\ntyping: 3.5\n")
             (temp_dir / "@python2").mkdir()
@@ -149,6 +155,88 @@ def f(x: NT, y: Alias) -> None:
         )
 
 
+_EXPECTED_TYPED_DICTS = {
+    "TD1": TypedDictValue({"a": (True, TypedValue(int)), "b": (True, TypedValue(str))}),
+    "TD2": TypedDictValue(
+        {"a": (False, TypedValue(int)), "b": (False, TypedValue(str))}
+    ),
+    "PEP655": TypedDictValue(
+        {"a": (False, TypedValue(int)), "b": (True, TypedValue(str))}
+    ),
+    "Inherited": TypedDictValue(
+        {
+            "a": (True, TypedValue(int)),
+            "b": (True, TypedValue(str)),
+            "c": (True, TypedValue(float)),
+        }
+    ),
+}
+
+
+class TestBundledStubs(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_import_aliases(self):
+        def capybara():
+            from _pyanalyze_tests.aliases import (
+                constant,
+                aliased_constant,
+                explicitly_aliased_constant,
+                ExplicitAlias,
+            )
+
+            assert_is_value(ExplicitAlias, KnownValue(int))
+            assert_is_value(constant, TypedValue(int))
+            assert_is_value(aliased_constant, TypedValue(int))
+            assert_is_value(explicitly_aliased_constant, TypedValue(int))
+
+    def test_aliases(self):
+        tsf = TypeshedFinder.make(TEST_OPTIONS, verbose=True)
+        mod = "_pyanalyze_tests.aliases"
+        assert tsf.resolve_name(mod, "constant") == TypedValue(int)
+        assert tsf.resolve_name(mod, "aliased_constant") == TypedValue(int)
+        assert tsf.resolve_name(mod, "explicitly_aliased_constant") == TypedValue(int)
+
+    def test_typeddict(self):
+        tsf = TypeshedFinder.make(TEST_OPTIONS, verbose=True)
+        mod = "_pyanalyze_tests.typeddict"
+
+        for name, expected in _EXPECTED_TYPED_DICTS.items():
+            assert tsf.resolve_name(mod, name) == SubclassValue(expected, exactly=True)
+
+    @assert_passes()
+    def test_import_typeddicts(self):
+        def capybara():
+            from _pyanalyze_tests.typeddict import TD1, TD2, PEP655, Inherited
+            from pyanalyze.test_typeshed import _EXPECTED_TYPED_DICTS
+
+            def nested(td1: TD1, td2: TD2, pep655: PEP655, inherited: Inherited):
+                assert_is_value(td1, _EXPECTED_TYPED_DICTS["TD1"])
+                assert_is_value(td2, _EXPECTED_TYPED_DICTS["TD2"])
+                assert_is_value(pep655, _EXPECTED_TYPED_DICTS["PEP655"])
+                assert_is_value(inherited, _EXPECTED_TYPED_DICTS["Inherited"])
+
+    def test_evaluated(self):
+        tsf = TypeshedFinder.make(TEST_OPTIONS, verbose=True)
+        mod = "_pyanalyze_tests.evaluated"
+        assert tsf.resolve_name(mod, "evaluated") == KnownValue(evaluated)
+
+    @assert_passes()
+    def test_evaluated_import(self):
+        def capybara(unannotated):
+            from _pyanalyze_tests.evaluated import open
+            from typing import TextIO, BinaryIO, IO
+
+            assert_is_value(open("r"), TypedValue(TextIO))
+            assert_is_value(open("rb"), TypedValue(BinaryIO))
+            assert_is_value(
+                open(unannotated), GenericValue(IO, [AnyValue(AnySource.explicit)])
+            )
+            assert_is_value(
+                open("r" if unannotated else "rb"),
+                TypedValue(TextIO) | TypedValue(BinaryIO),
+            )
+
+
 class Parent(Generic[T]):
     pass
 
@@ -163,7 +251,9 @@ class GenericChild(Parent[T]):
 
 class TestGetGenericBases:
     def setup(self) -> None:
-        arg_spec_cache = ArgSpecCache(Options.from_option_list([], TestConfig()))
+        arg_spec_cache = ArgSpecCache(
+            Options.from_option_list([], TestConfig()), TypeshedFinder()
+        )
         self.get_generic_bases = arg_spec_cache.get_generic_bases
 
     def test_runtime(self):