Skip to content
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
29 changes: 22 additions & 7 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ def is_implicit_any(t: Type) -> bool:
if name in nodes.reverse_op_method_set:
self.check_reverse_op_method(item, typ, name)
elif name in ('__getattr__', '__getattribute__'):
self.check_getattr_method(typ, defn)
self.check_getattr_method(typ, defn, name)
elif name == '__setattr__':
self.check_setattr_method(typ, defn)
# Refuse contravariant return type variable
Expand Down Expand Up @@ -927,12 +927,27 @@ def check_inplace_operator_method(self, defn: FuncBase) -> None:
if fail:
self.msg.signatures_incompatible(method, other_method, defn)

def check_getattr_method(self, typ: CallableType, context: Context) -> None:
method_type = CallableType([AnyType(), self.named_type('builtins.str')],
[nodes.ARG_POS, nodes.ARG_POS],
[None, None],
AnyType(),
self.named_type('builtins.function'))
def check_getattr_method(self, typ: CallableType, context: Context, name: str) -> None:
if len(self.scope.stack) == 1:
# module-level __getattr__
if name == '__getattribute__':
self.msg.fail('__getattribute__ is not valid at the module level', context)
return
elif name == '__getattr__' and not self.is_stub:
self.msg.fail('__getattr__ is not valid at the module level outside a stub file',
context)
return
method_type = CallableType([self.named_type('builtins.str')],
[nodes.ARG_POS],
[None],
AnyType(),
self.named_type('builtins.function'))
else:
method_type = CallableType([AnyType(), self.named_type('builtins.str')],
[nodes.ARG_POS, nodes.ARG_POS],
[None, None],
AnyType(),
self.named_type('builtins.function'))
if not is_subtype(typ, method_type):
self.msg.invalid_signature(typ, context)

Expand Down
12 changes: 12 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3117,6 +3117,18 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
expr.kind = n.kind
expr.fullname = n.fullname
expr.node = n.node
elif file is not None and file.is_stub and '__getattr__' in file.names:
# If there is a module-level __getattr__, then any attribute on the module is valid
# per PEP 484.
getattr_defn = file.names['__getattr__']
if isinstance(getattr_defn.node, FuncDef):
if isinstance(getattr_defn.node.type, CallableType):
typ = getattr_defn.node.type.ret_type
else:
typ = AnyType()
expr.kind = MDEF
expr.fullname = '{}.{}'.format(file.fullname(), expr.name)
expr.node = Var(expr.name, type=typ)
else:
# We only catch some errors here; the rest will be
# caught during type checking.
Expand Down
74 changes: 74 additions & 0 deletions test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -1640,3 +1640,77 @@ m = n # E: Cannot assign multiple modules to name 'm' without explicit 'types.M
[file n.py]

[builtins fixtures/module.pyi]

[case testModuleLevelGetattr]
import has_getattr

reveal_type(has_getattr.any_attribute) # E: Revealed type is 'Any'

[file has_getattr.pyi]
from typing import Any

def __getattr__(name: str) -> Any: ...

[builtins fixtures/module.pyi]

[case testModuleLevelGetattrReturnType]
import has_getattr

reveal_type(has_getattr.any_attribute) # E: Revealed type is 'builtins.str'

[file has_getattr.pyi]
def __getattr__(name: str) -> str: ...

[builtins fixtures/module.pyi]

[case testModuleLevelGetattrInvalidSignature]
import has_getattr

reveal_type(has_getattr.any_attribute)

[file has_getattr.pyi]
def __getattr__(x: int, y: str) -> str: ...

[out]
tmp/has_getattr.pyi:1: error: Invalid signature "def (builtins.int, builtins.str) -> builtins.str"
main:3: error: Revealed type is 'builtins.str'

[builtins fixtures/module.pyi]

[case testModuleLevelGetattrNotCallable]
import has_getattr

reveal_type(has_getattr.any_attribute) # E: Revealed type is 'Any' # E: Module has no attribute "any_attribute"

[file has_getattr.pyi]
__getattr__ = 3

[builtins fixtures/module.pyi]

[case testModuleLevelGetattrUntyped]
import has_getattr
reveal_type(has_getattr.any_attribute) # E: Revealed type is 'Any'

[file has_getattr.pyi]
def __getattr__(name): ...

[builtins fixtures/module.pyi]

[case testModuleLevelGetattrNotStub]

import has_getattr
reveal_type(has_getattr.any_attribute)

[file has_getattr.py]
def __getattr__(name): ...

[out]
tmp/has_getattr.py:1: error: __getattr__ is not valid at the module level outside a stub file
main:3: error: Revealed type is 'Any'
main:3: error: Module has no attribute "any_attribute"

[builtins fixtures/module.pyi]

[case testModuleLevelGetattribute]

def __getattribute__(): ... # E: __getattribute__ is not valid at the module level
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another potential test: an untyped __getattr__, and if the implementation is changed to refuse it in Python files, a test for that of course.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the first one.