Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix more crashes in class scoped imports #12199

Merged
merged 6 commits into from
Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
70 changes: 43 additions & 27 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
reduce memory use).
"""

import copy
from contextlib import contextmanager

from typing import (
Expand Down Expand Up @@ -4791,6 +4790,48 @@ def add_module_symbol(self,
module_hidden=module_hidden
)

def _get_node_for_class_scoped_import(
self, name: str, symbol_node: Optional[SymbolNode], context: Context
) -> Optional[SymbolNode]:
if symbol_node is None:
return None
# I promise this type checks; I'm just making mypyc issues go away.
# mypyc is absolutely convinced that `symbol_node` narrows to a Var in the following,
# when it can also be a FuncBase. Once fixed, `f` in the following can be removed.
# See also https://github.com/mypyc/mypyc/issues/892
f = cast(Any, lambda x: x)
if isinstance(f(symbol_node), (FuncBase, Var)):
# For imports in class scope, we construct a new node to represent the symbol and
# set its `info` attribute to `self.type`.
existing = self.current_symbol_table().get(name)
if (
# The redefinition checks in `add_symbol_table_node` don't work for our
# constructed Var / FuncBase, so check for possible redefinitions here.
existing is not None
and isinstance(f(existing.node), (FuncBase, Var))
and (
isinstance(get_proper_type(f(existing.type)), AnyType)
or f(existing.type) == f(symbol_node).type
)
):
return existing.node

# Construct the new node
if isinstance(f(symbol_node), FuncBase):
# In theory we could construct a new node here as well, but in practice
# it doesn't work well, see #12197
typ = AnyType(TypeOfAny.from_error)
self.fail('Unsupported class scoped import', context)
else:
typ = f(symbol_node).type
symbol_node = Var(name, typ)
symbol_node._fullname = self.qualified_name(name)
assert self.type is not None # guaranteed by is_class_scope
symbol_node.info = self.type
symbol_node.line = context.line
symbol_node.column = context.column
return symbol_node

def add_imported_symbol(self,
name: str,
node: SymbolTableNode,
Expand All @@ -4803,32 +4844,7 @@ def add_imported_symbol(self,
symbol_node: Optional[SymbolNode] = node.node

if self.is_class_scope():
# I promise this type checks; I'm just making mypyc issues go away.
# mypyc is absolutely convinced that `symbol_node` narrows to a Var in the following,
# when it can also be a FuncBase. Once fixed, `f` in the following can be removed.
# See also https://github.com/mypyc/mypyc/issues/892
f = cast(Any, lambda x: x)
if isinstance(f(symbol_node), (FuncBase, Var)):
# For imports in class scope, we construct a new node to represent the symbol and
# set its `info` attribute to `self.type`.
existing = self.current_symbol_table().get(name)
if (
# The redefinition checks in `add_symbol_table_node` don't work for our
# constructed Var / FuncBase, so check for possible redefinitions here.
existing is not None
and isinstance(f(existing.node), (FuncBase, Var))
and f(existing.type) == f(symbol_node).type
):
symbol_node = existing.node
else:
# Construct the new node
constructed_node = copy.copy(f(symbol_node))
assert self.type is not None # guaranteed by is_class_scope
constructed_node.line = context.line
constructed_node.column = context.column
constructed_node.info = self.type
constructed_node._fullname = self.qualified_name(name)
symbol_node = constructed_node
symbol_node = self._get_node_for_class_scoped_import(name, symbol_node, context)

symbol = SymbolTableNode(node.kind, symbol_node,
module_public=module_public,
Expand Down
115 changes: 78 additions & 37 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -7135,24 +7135,20 @@ class B(A): # E: Final class __main__.B has abstract attributes "foo"
class C:
class C1(XX): pass # E: Name "XX" is not defined

[case testClassScopeImportFunction]
[case testClassScopeImports]
class Foo:
from mod import foo
from mod import plain_function # E: Unsupported class scoped import
from mod import plain_var

reveal_type(Foo.foo) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"
reveal_type(Foo().foo) # E: Invalid self argument "Foo" to attribute function "foo" with type "Callable[[int, int], int]" \
# N: Revealed type is "def (y: builtins.int) -> builtins.int"
[file mod.py]
def foo(x: int, y: int) -> int: ...
reveal_type(Foo.plain_function) # N: Revealed type is "Any"
reveal_type(Foo().plain_function) # N: Revealed type is "Any"

[case testClassScopeImportVariable]
class Foo:
from mod import foo
reveal_type(Foo.plain_var) # N: Revealed type is "builtins.int"
reveal_type(Foo().plain_var) # N: Revealed type is "builtins.int"

reveal_type(Foo.foo) # N: Revealed type is "builtins.int"
reveal_type(Foo().foo) # N: Revealed type is "builtins.int"
[file mod.py]
foo: int
def plain_function(x: int, y: int) -> int: ...
plain_var: int

[case testClassScopeImportModule]
class Foo:
Expand All @@ -7163,42 +7159,64 @@ reveal_type(Foo.mod.foo) # N: Revealed type is "builtins.int"
[file mod.py]
foo: int

[case testClassScopeImportFunctionAlias]
[case testClassScopeImportAlias]
class Foo:
from mod import foo
bar = foo
from mod import function # E: Unsupported class scoped import
foo = function

from mod import const_foo
const_bar = const_foo
from mod import var1
bar = var1

from mod import var2
baz = var2

from mod import var3
qux = var3

reveal_type(Foo.foo) # N: Revealed type is "Any"
reveal_type(Foo.function) # N: Revealed type is "Any"

reveal_type(Foo.bar) # N: Revealed type is "builtins.int"
reveal_type(Foo.var1) # N: Revealed type is "builtins.int"

reveal_type(Foo.baz) # N: Revealed type is "mod.C"
reveal_type(Foo.var2) # N: Revealed type is "mod.C"

reveal_type(Foo.qux) # N: Revealed type is "builtins.int"
reveal_type(Foo.var3) # N: Revealed type is "builtins.int"

reveal_type(Foo.foo) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"
reveal_type(Foo.bar) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"
reveal_type(Foo.const_foo) # N: Revealed type is "builtins.int"
reveal_type(Foo.const_bar) # N: Revealed type is "builtins.int"
[file mod.py]
def foo(x: int, y: int) -> int: ...
const_foo: int
def function(x: int, y: int) -> int: ...
var1: int

class C: ...
var2: C

A = int
var3: A


[case testClassScopeImportModuleStar]
class Foo:
from mod import *
from mod import * # E: Unsupported class scoped import

reveal_type(Foo.foo) # N: Revealed type is "builtins.int"
reveal_type(Foo.bar) # N: Revealed type is "def (x: builtins.int) -> builtins.int"
reveal_type(Foo.bar) # N: Revealed type is "Any"
reveal_type(Foo.baz) # E: "Type[Foo]" has no attribute "baz" \
# N: Revealed type is "Any"

[file mod.py]
foo: int
def bar(x: int) -> int: ...

[case testClassScopeImportFunctionNested]
class Foo:
class Bar:
from mod import baz
from mod import baz # E: Unsupported class scoped import

reveal_type(Foo.Bar.baz) # N: Revealed type is "Any"
reveal_type(Foo.Bar().baz) # N: Revealed type is "Any"

reveal_type(Foo.Bar.baz) # N: Revealed type is "def (x: builtins.int) -> builtins.int"
reveal_type(Foo.Bar().baz) # E: Invalid self argument "Bar" to attribute function "baz" with type "Callable[[int], int]" \
# N: Revealed type is "def () -> builtins.int"
[file mod.py]
def baz(x: int) -> int: ...

Expand All @@ -7221,25 +7239,48 @@ def foo(x: int, y: int) -> int: ...

[case testClassScopeImportVarious]
class Foo:
from mod1 import foo
from mod2 import foo # E: Name "foo" already defined on line 2
from mod1 import foo # E: Unsupported class scoped import
from mod2 import foo

from mod1 import meth1
from mod1 import meth1 # E: Unsupported class scoped import
def meth1(self, a: str) -> str: ... # E: Name "meth1" already defined on line 5

def meth2(self, a: str) -> str: ...
from mod1 import meth2 # E: Name "meth2" already defined on line 8
from mod1 import meth2 # E: Unsupported class scoped import \
# E: Name "meth2" already defined on line 8

class Bar:
from mod1 import foo
from mod1 import foo # E: Unsupported class scoped import

import mod1
reveal_type(Foo.foo) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"
reveal_type(Bar.foo) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"
reveal_type(Foo.foo) # N: Revealed type is "Any"
reveal_type(Bar.foo) # N: Revealed type is "Any"
reveal_type(mod1.foo) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int"

[file mod1.py]
def foo(x: int, y: int) -> int: ...
def meth1(x: int) -> int: ...
def meth2(x: int) -> int: ...
[file mod2.py]
def foo(z: str) -> int: ...


[case testClassScopeImportWithError]
class Foo:
from mod import meth1 # E: Unsupported class scoped import
from mod import meth2 # E: Unsupported class scoped import
from mod import T

reveal_type(Foo.T) # E: Type variable "Foo.T" cannot be used as an expression \
# N: Revealed type is "Any"

[file mod.pyi]
from typing import Any, TypeVar, overload

@overload
def meth1(self: Any, y: int) -> int: ...
@overload
def meth1(self: Any, y: str) -> str: ...

T = TypeVar("T")
def meth2(self: Any, y: T) -> T: ...
5 changes: 3 additions & 2 deletions test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@ def f() -> None: pass
[case testImportWithinClassBody2]
import typing
class C:
from m import f # E: Method must have at least one argument
from m import f # E: Unsupported class scoped import
f()
f(C) # E: Too many arguments for "f" of "C"
# ideally, the following should error:
f(C)
[file m.py]
def f() -> None: pass
[out]
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-newsemanal.test
Original file line number Diff line number Diff line change
Expand Up @@ -2722,7 +2722,7 @@ import m

[file m.py]
class C:
from mm import f # E: Method must have at least one argument
from mm import f # E: Unsupported class scoped import
@dec(f)
def m(self): pass

Expand All @@ -2742,7 +2742,7 @@ import m

[file m/__init__.py]
class C:
from m.m import f # E: Method must have at least one argument
from m.m import f # E: Unsupported class scoped import
@dec(f)
def m(self): pass

Expand Down