Skip to content

Commit 6ddfb4b

Browse files
Fix some secondary method signatures (#732)
1 parent d8d2e25 commit 6ddfb4b

9 files changed

+101
-28
lines changed

docs/changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Fix inference of signature for certain secondary methods (#732)
6+
37
## Version 0.12.0 (February 25, 2024)
48

59
### New features

pyanalyze/arg_spec.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -646,14 +646,18 @@ def _uncached_get_argspec(
646646
unbound, impl, is_asynq, in_overload_resolution
647647
)
648648
if sig is not None:
649-
return make_bound_method(sig, Composite(KnownValue(obj.__self__)))
649+
return make_bound_method(
650+
sig, Composite(KnownValue(obj.__self__)), ctx=self.ctx
651+
)
650652

651653
# for bound methods, see if we have an argspec for the unbound method
652654
if inspect.ismethod(obj) and obj.__self__ is not None:
653655
argspec = self._cached_get_argspec(
654656
obj.__func__, impl, is_asynq, in_overload_resolution
655657
)
656-
return make_bound_method(argspec, Composite(KnownValue(obj.__self__)))
658+
return make_bound_method(
659+
argspec, Composite(KnownValue(obj.__self__)), ctx=self.ctx
660+
)
657661

658662
# Must be after the check for bound methods, because otherwise we
659663
# won't bind self correctly.
@@ -704,7 +708,9 @@ def _uncached_get_argspec(
704708
_ENUM_CALL, impl, is_asynq, in_overload_resolution
705709
)
706710
self_value = SubclassValue(TypedValue(obj))
707-
bound_sig = make_bound_method(signature, Composite(self_value))
711+
bound_sig = make_bound_method(
712+
signature, Composite(self_value), ctx=self.ctx
713+
)
708714
if bound_sig is None:
709715
return None
710716
sig = bound_sig.get_signature(
@@ -793,7 +799,9 @@ def _uncached_get_argspec(
793799
)
794800
# wrap if it's a bound method
795801
if obj.instance is not None and argspec is not None:
796-
return make_bound_method(argspec, Composite(KnownValue(obj.instance)))
802+
return make_bound_method(
803+
argspec, Composite(KnownValue(obj.instance)), ctx=self.ctx
804+
)
797805
return argspec
798806

799807
if inspect.isclass(obj):
@@ -856,7 +864,9 @@ def _uncached_get_argspec(
856864
returns=return_type,
857865
allow_call=allow_call,
858866
)
859-
bound_sig = make_bound_method(signature, Composite(TypedValue(obj)))
867+
bound_sig = make_bound_method(
868+
signature, Composite(TypedValue(obj)), ctx=self.ctx
869+
)
860870
if bound_sig is None:
861871
return None
862872
if is_dunder_new:
@@ -894,7 +904,9 @@ def _uncached_get_argspec(
894904
argspec = self._cached_get_argspec(
895905
method, impl, is_asynq, in_overload_resolution
896906
)
897-
return make_bound_method(argspec, Composite(KnownValue(obj.__self__)))
907+
return make_bound_method(
908+
argspec, Composite(KnownValue(obj.__self__)), ctx=self.ctx
909+
)
898910

899911
if hasattr_static(obj, "__call__"):
900912
# we could get an argspec here in some cases, but it's impossible to figure out

pyanalyze/attributes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,8 @@ def _get_attribute_from_known(obj: object, ctx: AttrContext) -> Value:
448448
)
449449
and isinstance(ctx.root_value, AnnotatedValue)
450450
):
451-
result = UnboundMethodValue(ctx.attr, ctx.root_composite)
452-
if safe_isinstance(obj, type):
451+
result = set_self(result, ctx.root_value)
452+
elif safe_isinstance(obj, type):
453453
result = set_self(result, TypedValue(obj))
454454
if isinstance(obj, (types.ModuleType, type)):
455455
ctx.record_usage(obj, result)

pyanalyze/checker.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,12 @@ def signature_from_value(
320320
sig = self.arg_spec_cache.get_argspec(method)
321321
if sig is None:
322322
# TODO return None here and figure out when the signature is missing
323+
# Probably because of cythonized methods
323324
return ANY_SIGNATURE
324325
return_override = get_return_override(sig)
325-
bound = make_bound_method(sig, value.composite, return_override)
326+
bound = make_bound_method(
327+
sig, value.composite, return_override, ctx=self
328+
)
326329
if bound is not None and value.typevars is not None:
327330
bound = bound.substitute_typevars(value.typevars)
328331
return bound
@@ -352,7 +355,9 @@ def signature_from_value(
352355
call_fn = typ.__call__
353356
sig = self.arg_spec_cache.get_argspec(call_fn)
354357
return_override = get_return_override(sig)
355-
bound_method = make_bound_method(sig, Composite(value), return_override)
358+
bound_method = make_bound_method(
359+
sig, Composite(value), return_override, ctx=self
360+
)
356361
if bound_method is None:
357362
return None
358363
return bound_method.get_signature(ctx=self)

pyanalyze/signature.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -2570,6 +2570,8 @@ def make_bound_method(
25702570
argspec: MaybeSignature,
25712571
self_composite: Composite,
25722572
return_override: Optional[Value] = None,
2573+
*,
2574+
ctx: CanAssignContext,
25732575
) -> Optional[BoundMethodSignature]:
25742576
if argspec is None:
25752577
return None
@@ -2578,7 +2580,10 @@ def make_bound_method(
25782580
elif isinstance(argspec, BoundMethodSignature):
25792581
if return_override is None:
25802582
return_override = argspec.return_override
2581-
return BoundMethodSignature(argspec.signature, self_composite, return_override)
2583+
sig = argspec.get_signature(ctx=ctx)
2584+
if sig is None:
2585+
return None
2586+
return BoundMethodSignature(sig, self_composite, return_override)
25822587
else:
25832588
assert_never(argspec)
25842589

pyanalyze/test_asynq.py

+48
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,51 @@ def capybara(cond: bool) -> int:
225225
if cond:
226226
return 3
227227
yield capybara.asynq(False)
228+
229+
230+
class TestBindDecorator(TestNameCheckVisitorBase):
231+
@assert_passes()
232+
def test_l0(self):
233+
import qcore
234+
from asynq import asynq
235+
236+
class L0CacheDecoratorBinder(qcore.decorators.DecoratorBinder):
237+
def dirty(self, *args, **kwargs):
238+
pass
239+
240+
class L0AsyncCacheDecoratorBinder(L0CacheDecoratorBinder):
241+
def asynq(self, *args, **kwargs):
242+
pass
243+
244+
class L0CacheDecorator(qcore.decorators.DecoratorBase):
245+
binder_cls = L0CacheDecoratorBinder
246+
247+
def dirty(self, *args, **kwargs):
248+
pass
249+
250+
class L0AsyncCacheDecorator(L0CacheDecorator):
251+
binder_cls = L0AsyncCacheDecoratorBinder
252+
253+
def __call__(self, *args, **kwargs):
254+
pass
255+
256+
def asynq(self, *args, **kwargs):
257+
pass
258+
259+
def is_pure_async_fn(self):
260+
return False
261+
262+
def cached():
263+
def decorate(fn):
264+
return qcore.decorators.decorate(L0AsyncCacheDecorator)(fn)
265+
266+
return decorate
267+
268+
class A:
269+
@cached()
270+
@asynq()
271+
def f1(self):
272+
return 100
273+
274+
def f2(self):
275+
self.f1.dirty() # should be ok

pyanalyze/test_attributes.py

+7-15
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
KnownValue,
1212
MultiValuedValue,
1313
TypedValue,
14-
UnboundMethodValue,
1514
assert_is_value,
1615
)
1716

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

209208
@assert_passes()
210209
def test_annotated_known(self):
211-
from typing import Any, cast
212-
from unittest.mock import ANY
213-
214210
from typing_extensions import Annotated, Literal
215211

216212
from pyanalyze.extensions import LiteralOnly
217-
from pyanalyze.stacked_scopes import Composite, VarnameWithOrigin
218-
from pyanalyze.value import CustomCheckExtension
219-
220-
origin = VarnameWithOrigin("encoding", cast(Any, ANY))
213+
from pyanalyze.value import CustomCheckExtension, KnownValueWithTypeVars, SelfT
221214

222215
def capybara():
223216
encoding: Annotated[Literal["ascii"], LiteralOnly()] = "ascii"
224217
assert_is_value(
225218
encoding.encode,
226-
UnboundMethodValue(
227-
"encode",
228-
Composite(
229-
AnnotatedValue(
219+
KnownValueWithTypeVars(
220+
encoding.encode,
221+
{
222+
SelfT: AnnotatedValue(
230223
KnownValue("ascii"), [CustomCheckExtension(LiteralOnly())]
231-
),
232-
origin,
233-
),
224+
)
225+
},
234226
),
235227
)
236228

pyanalyze/typeshed.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ def get_argspec(
233233
sig = self._get_sig_from_method_descriptor(method, allow_call)
234234
if sig is None:
235235
return None
236-
bound = make_bound_method(sig, Composite(TypedValue(obj.__self__)))
236+
bound = make_bound_method(
237+
sig, Composite(TypedValue(obj.__self__)), ctx=self.ctx
238+
)
237239
if bound is None:
238240
return None
239241
return bound.get_signature(ctx=self.ctx)
@@ -806,7 +808,9 @@ def _get_signature_from_info(
806808
self_annotation_value = self_val
807809
else:
808810
self_annotation_value = SubclassValue(self_val)
809-
bound_sig = make_bound_method(sig, Composite(self_val))
811+
bound_sig = make_bound_method(
812+
sig, Composite(self_val), ctx=self.ctx
813+
)
810814
if bound_sig is None:
811815
return None
812816
sig = bound_sig.get_signature(

pyanalyze/value.py

+3
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,9 @@ class KnownValueWithTypeVars(KnownValue):
608608
typevars: TypeVarMap = field(compare=False)
609609
"""TypeVars substituted on this value."""
610610

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

612615
@dataclass(frozen=True)
613616
class SyntheticModuleValue(Value):

0 commit comments

Comments
 (0)