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 some secondary method signatures #732

Merged
merged 1 commit into from
Feb 27, 2024
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
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Fix inference of signature for certain secondary methods (#732)

## Version 0.12.0 (February 25, 2024)

### New features
Expand Down
24 changes: 18 additions & 6 deletions pyanalyze/arg_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,14 +646,18 @@ def _uncached_get_argspec(
unbound, impl, is_asynq, in_overload_resolution
)
if sig is not None:
return make_bound_method(sig, Composite(KnownValue(obj.__self__)))
return make_bound_method(
sig, Composite(KnownValue(obj.__self__)), ctx=self.ctx
)

# for bound methods, see if we have an argspec for the unbound method
if inspect.ismethod(obj) and obj.__self__ is not None:
argspec = self._cached_get_argspec(
obj.__func__, impl, is_asynq, in_overload_resolution
)
return make_bound_method(argspec, Composite(KnownValue(obj.__self__)))
return make_bound_method(
argspec, Composite(KnownValue(obj.__self__)), ctx=self.ctx
)

# Must be after the check for bound methods, because otherwise we
# won't bind self correctly.
Expand Down Expand Up @@ -704,7 +708,9 @@ def _uncached_get_argspec(
_ENUM_CALL, impl, is_asynq, in_overload_resolution
)
self_value = SubclassValue(TypedValue(obj))
bound_sig = make_bound_method(signature, Composite(self_value))
bound_sig = make_bound_method(
signature, Composite(self_value), ctx=self.ctx
)
if bound_sig is None:
return None
sig = bound_sig.get_signature(
Expand Down Expand Up @@ -793,7 +799,9 @@ def _uncached_get_argspec(
)
# wrap if it's a bound method
if obj.instance is not None and argspec is not None:
return make_bound_method(argspec, Composite(KnownValue(obj.instance)))
return make_bound_method(
argspec, Composite(KnownValue(obj.instance)), ctx=self.ctx
)
return argspec

if inspect.isclass(obj):
Expand Down Expand Up @@ -856,7 +864,9 @@ def _uncached_get_argspec(
returns=return_type,
allow_call=allow_call,
)
bound_sig = make_bound_method(signature, Composite(TypedValue(obj)))
bound_sig = make_bound_method(
signature, Composite(TypedValue(obj)), ctx=self.ctx
)
if bound_sig is None:
return None
if is_dunder_new:
Expand Down Expand Up @@ -894,7 +904,9 @@ def _uncached_get_argspec(
argspec = self._cached_get_argspec(
method, impl, is_asynq, in_overload_resolution
)
return make_bound_method(argspec, Composite(KnownValue(obj.__self__)))
return make_bound_method(
argspec, Composite(KnownValue(obj.__self__)), ctx=self.ctx
)

if hasattr_static(obj, "__call__"):
# we could get an argspec here in some cases, but it's impossible to figure out
Expand Down
4 changes: 2 additions & 2 deletions pyanalyze/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,8 @@ def _get_attribute_from_known(obj: object, ctx: AttrContext) -> Value:
)
and isinstance(ctx.root_value, AnnotatedValue)
):
result = UnboundMethodValue(ctx.attr, ctx.root_composite)
if safe_isinstance(obj, type):
result = set_self(result, ctx.root_value)
elif safe_isinstance(obj, type):
result = set_self(result, TypedValue(obj))
if isinstance(obj, (types.ModuleType, type)):
ctx.record_usage(obj, result)
Expand Down
9 changes: 7 additions & 2 deletions pyanalyze/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,12 @@ def signature_from_value(
sig = self.arg_spec_cache.get_argspec(method)
if sig is None:
# TODO return None here and figure out when the signature is missing
# Probably because of cythonized methods
return ANY_SIGNATURE
return_override = get_return_override(sig)
bound = make_bound_method(sig, value.composite, return_override)
bound = make_bound_method(
sig, value.composite, return_override, ctx=self
)
if bound is not None and value.typevars is not None:
bound = bound.substitute_typevars(value.typevars)
return bound
Expand Down Expand Up @@ -352,7 +355,9 @@ def signature_from_value(
call_fn = typ.__call__
sig = self.arg_spec_cache.get_argspec(call_fn)
return_override = get_return_override(sig)
bound_method = make_bound_method(sig, Composite(value), return_override)
bound_method = make_bound_method(
sig, Composite(value), return_override, ctx=self
)
if bound_method is None:
return None
return bound_method.get_signature(ctx=self)
Expand Down
7 changes: 6 additions & 1 deletion pyanalyze/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -2570,6 +2570,8 @@ def make_bound_method(
argspec: MaybeSignature,
self_composite: Composite,
return_override: Optional[Value] = None,
*,
ctx: CanAssignContext,
) -> Optional[BoundMethodSignature]:
if argspec is None:
return None
Expand All @@ -2578,7 +2580,10 @@ def make_bound_method(
elif isinstance(argspec, BoundMethodSignature):
if return_override is None:
return_override = argspec.return_override
return BoundMethodSignature(argspec.signature, self_composite, return_override)
sig = argspec.get_signature(ctx=ctx)
if sig is None:
return None
return BoundMethodSignature(sig, self_composite, return_override)
else:
assert_never(argspec)

Expand Down
48 changes: 48 additions & 0 deletions pyanalyze/test_asynq.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,51 @@ def capybara(cond: bool) -> int:
if cond:
return 3
yield capybara.asynq(False)


class TestBindDecorator(TestNameCheckVisitorBase):
@assert_passes()
def test_l0(self):
import qcore
from asynq import asynq

class L0CacheDecoratorBinder(qcore.decorators.DecoratorBinder):
def dirty(self, *args, **kwargs):
pass

class L0AsyncCacheDecoratorBinder(L0CacheDecoratorBinder):
def asynq(self, *args, **kwargs):
pass

class L0CacheDecorator(qcore.decorators.DecoratorBase):
binder_cls = L0CacheDecoratorBinder

def dirty(self, *args, **kwargs):
pass

class L0AsyncCacheDecorator(L0CacheDecorator):
binder_cls = L0AsyncCacheDecoratorBinder

def __call__(self, *args, **kwargs):
pass

def asynq(self, *args, **kwargs):
pass

def is_pure_async_fn(self):
return False

def cached():
def decorate(fn):
return qcore.decorators.decorate(L0AsyncCacheDecorator)(fn)

return decorate

class A:
@cached()
@asynq()
def f1(self):
return 100

def f2(self):
self.f1.dirty() # should be ok
22 changes: 7 additions & 15 deletions pyanalyze/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
KnownValue,
MultiValuedValue,
TypedValue,
UnboundMethodValue,
assert_is_value,
)

Expand Down Expand Up @@ -208,29 +207,22 @@ def test(x: Union[Capybara, Paca]) -> None:

@assert_passes()
def test_annotated_known(self):
from typing import Any, cast
from unittest.mock import ANY

from typing_extensions import Annotated, Literal

from pyanalyze.extensions import LiteralOnly
from pyanalyze.stacked_scopes import Composite, VarnameWithOrigin
from pyanalyze.value import CustomCheckExtension

origin = VarnameWithOrigin("encoding", cast(Any, ANY))
from pyanalyze.value import CustomCheckExtension, KnownValueWithTypeVars, SelfT

def capybara():
encoding: Annotated[Literal["ascii"], LiteralOnly()] = "ascii"
assert_is_value(
encoding.encode,
UnboundMethodValue(
"encode",
Composite(
AnnotatedValue(
KnownValueWithTypeVars(
encoding.encode,
{
SelfT: AnnotatedValue(
KnownValue("ascii"), [CustomCheckExtension(LiteralOnly())]
),
origin,
),
)
},
),
)

Expand Down
8 changes: 6 additions & 2 deletions pyanalyze/typeshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ def get_argspec(
sig = self._get_sig_from_method_descriptor(method, allow_call)
if sig is None:
return None
bound = make_bound_method(sig, Composite(TypedValue(obj.__self__)))
bound = make_bound_method(
sig, Composite(TypedValue(obj.__self__)), ctx=self.ctx
)
if bound is None:
return None
return bound.get_signature(ctx=self.ctx)
Expand Down Expand Up @@ -806,7 +808,9 @@ def _get_signature_from_info(
self_annotation_value = self_val
else:
self_annotation_value = SubclassValue(self_val)
bound_sig = make_bound_method(sig, Composite(self_val))
bound_sig = make_bound_method(
sig, Composite(self_val), ctx=self.ctx
)
if bound_sig is None:
return None
sig = bound_sig.get_signature(
Expand Down
3 changes: 3 additions & 0 deletions pyanalyze/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@ class KnownValueWithTypeVars(KnownValue):
typevars: TypeVarMap = field(compare=False)
"""TypeVars substituted on this value."""

def __str__(self) -> str:
return super().__str__() + f" with typevars {self.typevars}"


@dataclass(frozen=True)
class SyntheticModuleValue(Value):
Expand Down
Loading