Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 106 additions & 61 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6625,8 +6625,59 @@ def create_getattr_var(
return v
return None

def _lookup_fully_qualified(
self, fullname: str, *, allow_missing: bool
) -> SymbolTableNode | None:
"""Implementation detail shared by fully qualified lookup helpers.

This is not part of the public or plugin APIs. It only exists to centralize
the traversal logic so the public helpers remain thin wrappers without
changing any semantic rules governed by the analyzer.
"""
if "." not in fullname:
raise ValueError(
f"_lookup_fully_qualified requires a qualified name with at least one dot, "
f"got: {fullname!r}"
)

module: str | None = None
filenode: MypyFile | None = None
parts = fullname.split(".")
names: list[str] = []

while parts:
candidate = ".".join(parts)
filenode = self.modules.get(candidate)
if filenode is not None:
module = candidate
break
names.append(parts.pop())

if filenode is None or module is None or not names:
return None

names.reverse()
result = filenode.names.get(names[0])

if result is None and self.is_incomplete_namespace(module):
# TODO: More explicit handling of incomplete refs?
self.record_incomplete_ref()
# When the namespace is incomplete and we don't have a result,
# return None immediately. If allow_missing=False, the caller
# will assert this is not None (preserving original behavior).
# If allow_missing=True, returning None is the expected outcome.
return None

for part in names[1:]:
if result is not None and isinstance(result.node, TypeInfo):
result = result.node.names.get(part)
else:
return None

return result

def lookup_fully_qualified(self, fullname: str) -> SymbolTableNode:
ret = self.lookup_fully_qualified_or_none(fullname)
ret = self._lookup_fully_qualified(fullname, allow_missing=False)
assert ret is not None, fullname
return ret

Expand All @@ -6639,47 +6690,7 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non

Note that this can't be used for names nested in class namespaces.
"""
# TODO: unify/clean-up/simplify lookup methods, see #4157.
module, name = fullname.rsplit(".", maxsplit=1)

if module in self.modules:
# If the module exists, look up the name in the module.
# This is the common case.
filenode = self.modules[module]
result = filenode.names.get(name)
if result is None and self.is_incomplete_namespace(module):
# TODO: More explicit handling of incomplete refs?
self.record_incomplete_ref()
return result
else:
# Else, try to find the longest prefix of the module name that is in the modules dictionary.
splitted_modules = fullname.split(".")
names = []

while splitted_modules and ".".join(splitted_modules) not in self.modules:
names.append(splitted_modules.pop())

if not splitted_modules or not names:
# If no module or name is found, return None.
return None

# Reverse the names list to get the correct order of names.
names.reverse()

module = ".".join(splitted_modules)
filenode = self.modules[module]
result = filenode.names.get(names[0])

if result is None and self.is_incomplete_namespace(module):
# TODO: More explicit handling of incomplete refs?
self.record_incomplete_ref()

for part in names[1:]:
if result is not None and isinstance(result.node, TypeInfo):
result = result.node.names.get(part)
else:
return None
return result
return self._lookup_fully_qualified(fullname, allow_missing=True)

def object_type(self) -> Instance:
if self._object_type is None:
Expand All @@ -6696,33 +6707,67 @@ def function_type(self) -> Instance:
self._function_type = self.named_type("builtins.function")
return self._function_type

def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance:
sym = self.lookup_fully_qualified(fullname)
assert sym, "Internal error: attempted to construct unknown type"
node = sym.node
assert isinstance(node, TypeInfo), node
if args:
# TODO: assert len(args) == len(node.defn.type_vars)
return Instance(node, args)
return Instance(node, [AnyType(TypeOfAny.special_form)] * len(node.defn.type_vars))
def _build_named_instance(
self,
fullname: str,
args: list[Type] | None,
*,
allow_missing: bool,
any_flavor: int,
unwrap_alias: bool,
) -> Instance | None:
"""Internal helper to construct Instances for fully-qualified names.

This exists solely to remove duplication between the public helpers. It
doesn't change user-visible behavior and is not part of the public or
plugin APIs.
"""
if allow_missing:
sym = self.lookup_fully_qualified_or_none(fullname)
if not sym or isinstance(sym.node, PlaceholderNode):
return None
else:
sym = self.lookup_fully_qualified(fullname)

def named_type_or_none(self, fullname: str, args: list[Type] | None = None) -> Instance | None:
sym = self.lookup_fully_qualified_or_none(fullname)
if not sym or isinstance(sym.node, PlaceholderNode):
return None
node = sym.node
if isinstance(node, TypeAlias):
if unwrap_alias and isinstance(node, TypeAlias):
assert isinstance(node.target, Instance) # type: ignore[misc]
node = node.target.type
assert isinstance(node, TypeInfo), node
if args is not None:
# TODO: assert len(args) == len(node.defn.type_vars)
return Instance(node, args)
return Instance(node, [AnyType(TypeOfAny.unannotated)] * len(node.defn.type_vars))

inst_args = args
if inst_args is None:
inst_args = [AnyType(any_flavor)] * len(node.defn.type_vars)
# TODO: assert len(inst_args) == len(node.defn.type_vars)
return Instance(node, inst_args)

def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance:
inst = self._build_named_instance(
fullname,
args,
allow_missing=False,
any_flavor=TypeOfAny.special_form,
unwrap_alias=False,
)
assert inst is not None, "Internal error: attempted to construct unknown type"
return inst

def named_type_or_none(self, fullname: str, args: list[Type] | None = None) -> Instance | None:
return self._build_named_instance(
fullname, args, allow_missing=True, any_flavor=TypeOfAny.unannotated, unwrap_alias=True
)

def builtin_type(self, fully_qualified_name: str) -> Instance:
"""Legacy function -- use named_type() instead."""
return self.named_type(fully_qualified_name)
inst = self._build_named_instance(
fully_qualified_name,
None,
allow_missing=False,
any_flavor=TypeOfAny.special_form,
unwrap_alias=False,
)
assert inst is not None
return inst

def lookup_current_scope(self, name: str) -> SymbolTableNode | None:
if self.locals[-1] is not None:
Expand Down