Skip to content

Allow __getattr__ during semantic analysis #5295

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

Merged
merged 4 commits into from
Jun 29, 2018
Merged
Show file tree
Hide file tree
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
47 changes: 30 additions & 17 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1405,23 +1405,14 @@ def visit_import_from(self, imp: ImportFrom) -> None:
# If it is still not resolved, check for a module level __getattr__
if (module and not node and (module.is_stub or self.options.python_version >= (3, 7))
and '__getattr__' in module.names):
getattr_defn = module.names['__getattr__']
if isinstance(getattr_defn.node, (FuncDef, Var)):
if isinstance(getattr_defn.node.type, CallableType):
typ = getattr_defn.node.type.ret_type
else:
typ = AnyType(TypeOfAny.from_error)
if as_id:
name = as_id
else:
name = id
ast_node = Var(name, type=typ)
if self.type:
ast_node._fullname = self.type.fullname() + "." + name
else:
ast_node._fullname = self.qualified_name(name)
symbol = SymbolTableNode(GDEF, ast_node)
self.add_symbol(name, symbol, imp)
name = as_id if as_id else id
if self.type:
fullname = self.type.fullname() + "." + name
else:
fullname = self.qualified_name(name)
gvar = self.create_getattr_var(module.names['__getattr__'], name, fullname)
if gvar:
self.add_symbol(name, gvar, imp)
continue
if node and node.kind != UNBOUND_IMPORTED and not node.module_hidden:
if not node:
Expand Down Expand Up @@ -3087,6 +3078,12 @@ def lookup_qualified(self, name: str, ctx: Context,
n = names.get(parts[i], None)
if n and isinstance(n.node, ImportedName):
n = self.dereference_module_cross_ref(n)
elif '__getattr__' in names:
gvar = self.create_getattr_var(names['__getattr__'],
parts[i], parts[i])
if gvar:
names[name] = gvar
n = gvar
# TODO: What if node is Var or FuncDef?
# Currently, missing these cases results in controversial behavior, when
# lookup_qualified(x.y.z) returns Var(x).
Expand All @@ -3102,6 +3099,22 @@ def lookup_qualified(self, name: str, ctx: Context,
return n
return None

def create_getattr_var(self, getattr_defn: SymbolTableNode,
name: str, fullname: str) -> Optional[SymbolTableNode]:
"""Create a dummy global symbol using __getattr__ return type.

If not possible, return None.
"""
if isinstance(getattr_defn.node, (FuncDef, Var)):
if isinstance(getattr_defn.node.type, CallableType):
typ = getattr_defn.node.type.ret_type
else:
typ = AnyType(TypeOfAny.from_error)
v = Var(name, type=typ)
v._fullname = fullname
return SymbolTableNode(GDEF, v)
return None

def rebind_symbol_table_node(self, n: SymbolTableNode) -> Optional[SymbolTableNode]:
"""If node refers to old version of module, return reference to new version.

Expand Down
18 changes: 18 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4451,3 +4451,21 @@ class C(B):
def meth(cls, x: int) -> int: ...
[builtins fixtures/classmethod.pyi]
[out]

[case testGetAttrImportAnnotation]
import a
x: a.A
[file a.pyi]
from typing import Any
def __getattr__(attr: str) -> Any: ...
[builtins fixtures/module.pyi]
[out]

[case testGetAttrImportBaseClass]
import a
class B(a.A): ...
[file a.pyi]
from typing import Any
def __getattr__(attr: str) -> Any: ...
[builtins fixtures/module.pyi]
[out]