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

type check function decorators #428

Merged
merged 3 commits into from
Jan 17, 2022
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
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Type check function decorators (#428)
- Handle `NoReturn` in `async def` functions (#427)
- Support PEP 673 (`typing_extensions.Self`) (#423)
- Updates for compatibility with recent changes in typeshed (#421):
Expand Down
18 changes: 8 additions & 10 deletions pyanalyze/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ class FunctionInfo:
is_overload: bool # typing.overload or pyanalyze.extensions.overload
is_evaluated: bool # @pyanalyze.extensions.evaluated
is_abstractmethod: bool # has @abstractmethod
# a list of pairs of (decorator function, applied decorator function). These are different
# for decorators that take arguments, like @asynq(): the first element will be the asynq
# function and the second will be the result of calling asynq().
decorators: List[Tuple[Value, Value]]
# a list of tuples of (decorator function, applied decorator function, AST node). These are
# different for decorators that take arguments, like @asynq(): the first element will be the
# asynq function and the second will be the result of calling asynq().
decorators: List[Tuple[Value, Value, ast.AST]]
node: FunctionNode
params: Sequence[ParamInfo]
return_annotation: Optional[Value]
Expand Down Expand Up @@ -162,7 +162,7 @@ def compute_function_info(
# @async_proxy(pure=True) is a noop, so don't treat it specially
if not any(kw.arg == "pure" for kw in decorator.keywords):
async_kind = AsyncFunctionKind.async_proxy
decorators.append((callee, decorator_value))
decorators.append((callee, decorator_value, decorator))
else:
decorator_value = ctx.visit_expression(decorator)
if decorator_value == KnownValue(classmethod):
Expand All @@ -179,7 +179,7 @@ def compute_function_info(
is_abstractmethod = True
elif decorator_value == KnownValue(evaluated):
is_evaluated = True
decorators.append((decorator_value, decorator_value))
decorators.append((decorator_value, decorator_value, decorator))
params = compute_parameters(
node,
enclosing_class,
Expand Down Expand Up @@ -323,7 +323,7 @@ def compute_value_of_function(
has_return_annotation=info.return_annotation is not None,
)
val = CallableValue(sig, types.FunctionType)
for unapplied, decorator in reversed(info.decorators):
for unapplied, decorator, node in reversed(info.decorators):
# Special case asynq.asynq until we can create the type automatically
if unapplied == KnownValue(asynq.asynq) and isinstance(val, CallableValue):
sig = replace(val.signature, is_asynq=True)
Expand All @@ -332,7 +332,5 @@ def compute_value_of_function(
allow_call = isinstance(
unapplied, KnownValue
) and SafeDecoratorsForNestedFunctions.contains(unapplied.val, ctx.options)
val = ctx.check_call(
info.node, decorator, [Composite(val)], allow_call=allow_call
)
val = ctx.check_call(node, decorator, [Composite(val)], allow_call=allow_call)
return val
3 changes: 2 additions & 1 deletion pyanalyze/name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1578,8 +1578,9 @@ def visit_FunctionDef(self, node: FunctionDefNode) -> Value:
node, error_code=ErrorCode.missing_return_annotation
)

computed_function = compute_value_of_function(info, self)
if potential_function is None:
val = compute_value_of_function(info, self)
val = computed_function
else:
val = KnownValue(potential_function)
if not info.is_overload and not info.is_evaluated:
Expand Down
6 changes: 3 additions & 3 deletions pyanalyze/stacked_scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,7 +1109,7 @@ def get_origin(
return self._resolve_origin(definers)

@contextlib.contextmanager
def subscope(self) -> Iterable[SubScope]:
def subscope(self) -> Iterator[SubScope]:
"""Create a new subscope, to be used for conditional branches."""
# Ignore LEAVES_SCOPE if it's already there, so that we type check code after the
# assert False correctly. Without this, test_after_assert_false fails.
Expand All @@ -1127,7 +1127,7 @@ def subscope(self) -> Iterable[SubScope]:
yield new_name_to_nodes

@contextlib.contextmanager
def loop_scope(self) -> Iterable[SubScope]:
def loop_scope(self) -> Iterator[SubScope]:
loop_scopes = []
with self.subscope() as main_scope:
loop_scopes.append(main_scope)
Expand Down Expand Up @@ -1276,7 +1276,7 @@ def add_scope(
scope_type: ScopeType,
scope_node: Node,
scope_object: Optional[object] = None,
) -> Iterable[None]:
) -> Iterator[None]:
"""Context manager that adds a scope of this type to the top of the stack."""
if scope_type is ScopeType.function_scope:
scope = FunctionScope(
Expand Down
11 changes: 11 additions & 0 deletions pyanalyze/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,14 @@ def capybara():

fun4 = lambda a, b, c: a if c else b
assert_is_value(fun4(1, 2, 3), KnownValue(1) | KnownValue(2))


class TestDecorators(TestNameCheckVisitorBase):
@assert_passes()
def test_applied(self) -> None:
def bad_deco(x: int) -> str:
return "x"

@bad_deco # E: incompatible_argument
def capybara():
pass