diff --git a/docs/changelog.md b/docs/changelog.md index d58896b9..a1cc3dc0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,7 @@ ## Unreleased +- Infer the signature for built-in static methods, such as `dict.fromkeys` (#619) - Fix type inference for subscripting on `Sequence` (#618) - Improve support for Cythonized methods (#617) - Add support for the PEP 698 `@override` decorator (#614) diff --git a/pyanalyze/arg_spec.py b/pyanalyze/arg_spec.py index 7a8ef7fc..d5315b70 100644 --- a/pyanalyze/arg_spec.py +++ b/pyanalyze/arg_spec.py @@ -800,7 +800,14 @@ def _uncached_get_argspec( return bound_sig if inspect.isbuiltin(obj): - if not isinstance(obj.__self__, ModuleType): + if isinstance(obj.__self__, ModuleType): + inspect_sig = self._safe_get_signature(obj) + if inspect_sig is not None: + return self.from_signature( + inspect_sig, function_object=obj, callable_object=obj + ) + return self._make_any_sig(obj) + else: cls = type(obj.__self__) try: method = getattr(cls, obj.__name__) @@ -812,12 +819,6 @@ def _uncached_get_argspec( method, impl, is_asynq, in_overload_resolution ) return make_bound_method(argspec, Composite(KnownValue(obj.__self__))) - inspect_sig = self._safe_get_signature(obj) - if inspect_sig is not None: - return self.from_signature( - inspect_sig, function_object=obj, callable_object=obj - ) - return self._make_any_sig(obj) if hasattr_static(obj, "__call__"): # we could get an argspec here in some cases, but it's impossible to figure out diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py index 10e1b1b0..9ff65b9a 100644 --- a/pyanalyze/implementation.py +++ b/pyanalyze/implementation.py @@ -1380,22 +1380,25 @@ def get_default_argspecs() -> Dict[object, Signature]: annotation=TypedValue(bool), ), ], - KnownValue(None), + return_annotation=KnownValue(None), impl=_assert_is_value_impl, callable=assert_is_value, ), Signature.make( [SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))], - TypeVarValue(T), + return_annotation=TypeVarValue(T), impl=_reveal_type_impl, callable=reveal_type, ), Signature.make( - [], KnownValue(None), impl=_reveal_locals_impl, callable=reveal_locals + [], + return_annotation=KnownValue(None), + impl=_reveal_locals_impl, + callable=reveal_locals, ), Signature.make( [SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))], - TypeVarValue(T), + return_annotation=TypeVarValue(T), impl=_dump_value_impl, callable=dump_value, ), @@ -1404,11 +1407,13 @@ def get_default_argspecs() -> Dict[object, Signature]: [SigParameter("self", _POS_ONLY)], callable=type.__subclasses__, impl=_subclasses_impl, + return_annotation=GenericValue(list, [TypedValue(type)]), ), Signature.make( [SigParameter("obj", _POS_ONLY), SigParameter("class_or_tuple", _POS_ONLY)], impl=_isinstance_impl, callable=isinstance, + return_annotation=TypedValue(bool), ), Signature.make( [ @@ -1423,11 +1428,13 @@ def get_default_argspecs() -> Dict[object, Signature]: ], impl=_issubclass_impl, callable=issubclass, + return_annotation=TypedValue(bool), ), Signature.make( [SigParameter("obj"), SigParameter("class_or_tuple")], impl=_isinstance_impl, callable=safe_isinstance, + return_annotation=TypedValue(bool), ), Signature.make( [ @@ -1442,6 +1449,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], impl=_issubclass_impl, callable=safe_issubclass, + return_annotation=TypedValue(bool), ), Signature.make( [ @@ -1449,7 +1457,7 @@ def get_default_argspecs() -> Dict[object, Signature]: SigParameter("name", _POS_ONLY, annotation=TypedValue(str)), SigParameter("default", _POS_ONLY, default=_NO_ARG_SENTINEL), ], - AnyValue(AnySource.inference), + return_annotation=AnyValue(AnySource.inference), callable=getattr, ), Signature.make( @@ -1459,6 +1467,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], impl=_hasattr_impl, callable=hasattr, + return_annotation=TypedValue(bool), ), Signature.make( [ @@ -1467,6 +1476,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], impl=_hasattr_impl, callable=hasattr_static, + return_annotation=TypedValue(bool), ), Signature.make( [ @@ -1476,6 +1486,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], impl=_setattr_impl, callable=setattr, + return_annotation=KnownValue(None), ), Signature.make( [ @@ -1484,26 +1495,25 @@ def get_default_argspecs() -> Dict[object, Signature]: ], impl=_super_impl, callable=super, + return_annotation=TypedValue(super), ), Signature.make( [SigParameter("iterable", _POS_ONLY, default=_NO_ARG_SENTINEL)], impl=_tuple_impl, callable=tuple, - ), - Signature.make( - [SigParameter("iterable", _POS_ONLY, default=_NO_ARG_SENTINEL)], - impl=_tuple_impl, - callable=tuple, + return_annotation=TypedValue(tuple), ), Signature.make( [SigParameter("iterable", _POS_ONLY, default=_NO_ARG_SENTINEL)], impl=_list_impl, callable=list, + return_annotation=TypedValue(list), ), Signature.make( [SigParameter("iterable", _POS_ONLY, default=_NO_ARG_SENTINEL)], impl=_set_impl, callable=set, + return_annotation=TypedValue(set), ), Signature.make( [ @@ -1512,6 +1522,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=list.append, impl=_list_append_impl, + return_annotation=KnownValue(None), ), Signature.make( [ @@ -1520,6 +1531,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=list.__add__, impl=_list_add_impl, + return_annotation=TypedValue(list), ), Signature.make( [ @@ -1530,6 +1542,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=list.__iadd__, impl=_list_iadd_impl, + return_annotation=TypedValue(list), ), Signature.make( [ @@ -1542,6 +1555,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=list.extend, impl=_list_extend_impl, + return_annotation=KnownValue(None), ), Signature.make( [ @@ -1550,6 +1564,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=list.__getitem__, impl=_list_getitem_impl, + return_annotation=AnyValue(AnySource.inference), ), Signature.make( [ @@ -1558,6 +1573,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=tuple.__getitem__, impl=_tuple_getitem_impl, + return_annotation=AnyValue(AnySource.inference), ), Signature.make( [ @@ -1568,6 +1584,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=collections.abc.Sequence.__getitem__, impl=_sequence_getitem_impl, + return_annotation=AnyValue(AnySource.inference), ), Signature.make( [ @@ -1576,6 +1593,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=set.add, impl=_set_add_impl, + return_annotation=KnownValue(None), ), Signature.make( [ @@ -1585,14 +1603,20 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=dict.__setitem__, impl=_dict_setitem_impl, + return_annotation=KnownValue(None), ), Signature.make( [ - SigParameter("self", _POS_ONLY, annotation=TypedValue(dict)), + SigParameter( + "self", + _POS_ONLY, + annotation=GenericValue(dict, [TypeVarValue(K), TypeVarValue(V)]), + ), SigParameter("k", _POS_ONLY), ], callable=dict.__getitem__, impl=_dict_getitem_impl, + return_annotation=TypeVarValue(V), ), Signature.make( [ @@ -1602,6 +1626,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=dict.get, impl=_dict_get_impl, + return_annotation=AnyValue(AnySource.inference), ), Signature.make( [ @@ -1611,6 +1636,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=dict.setdefault, impl=_dict_setdefault_impl, + return_annotation=AnyValue(AnySource.inference), ), Signature.make( [ @@ -1620,6 +1646,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=dict.pop, impl=_dict_pop_impl, + return_annotation=AnyValue(AnySource.inference), ), Signature.make( [ @@ -1627,7 +1654,7 @@ def get_default_argspecs() -> Dict[object, Signature]: SigParameter("m", _POS_ONLY, default=_NO_ARG_SENTINEL), SigParameter("kwargs", ParameterKind.VAR_KEYWORD), ], - KnownValue(None), + return_annotation=KnownValue(None), callable=dict.update, impl=_dict_update_impl, ), @@ -1639,7 +1666,7 @@ def get_default_argspecs() -> Dict[object, Signature]: annotation=GenericValue(dict, [TypeVarValue(K), TypeVarValue(V)]), ) ], - DictIncompleteValue( + return_annotation=DictIncompleteValue( dict, [KVPair(TypeVarValue(K), TypeVarValue(V), is_many=True)] ), callable=dict.copy, @@ -1685,7 +1712,7 @@ def get_default_argspecs() -> Dict[object, Signature]: "errors", annotation=TypedValue(str), default=KnownValue("") ), ], - TypedValue(str), + return_annotation=TypedValue(str), callable=bytes.decode, allow_call=True, ), @@ -1697,7 +1724,7 @@ def get_default_argspecs() -> Dict[object, Signature]: "errors", annotation=TypedValue(str), default=KnownValue("") ), ], - TypedValue(bytes), + return_annotation=TypedValue(bytes), callable=str.encode, allow_call=True, ), @@ -1709,6 +1736,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], impl=_str_format_impl, callable=str.format, + return_annotation=TypedValue(str), ), Signature.make( [SigParameter("typ", _POS_ONLY), SigParameter("val", _POS_ONLY)], @@ -1720,7 +1748,7 @@ def get_default_argspecs() -> Dict[object, Signature]: SigParameter("val", _POS_ONLY, annotation=TypeVarValue(T)), SigParameter("typ", _POS_ONLY), ], - TypeVarValue(T), + return_annotation=TypeVarValue(T), callable=assert_type, impl=_assert_type_impl, ), @@ -1733,6 +1761,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=qcore.asserts.assert_is, impl=_assert_is_impl, + return_annotation=KnownValue(None), ), Signature.make( [ @@ -1743,6 +1772,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=qcore.asserts.assert_is_not, impl=_assert_is_not_impl, + return_annotation=KnownValue(None), ), Signature.make( [ @@ -1753,6 +1783,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=qcore.asserts.assert_is_instance, impl=_assert_is_instance_impl, + return_annotation=KnownValue(None), ), # Need to override this because the type for the tp parameter in typeshed is too strict Signature.make( @@ -1769,6 +1800,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=len, impl=_len_impl, + return_annotation=TypedValue(int), ), Signature.make( [ @@ -1778,6 +1810,7 @@ def get_default_argspecs() -> Dict[object, Signature]: ], callable=bool, impl=_bool_impl, + return_annotation=TypedValue(bool), ), # Typeshed has it as TypeGuard[Callable[..., object]], which causes some # false positives. @@ -1803,7 +1836,7 @@ def get_default_argspecs() -> Dict[object, Signature]: # Anticipating https://bugs.python.org/issue46414 sig = Signature.make( [SigParameter("value", _POS_ONLY, annotation=TypeVarValue(T))], - TypeVarValue(T), + return_annotation=TypeVarValue(T), impl=_reveal_type_impl, callable=reveal_type_func, ) @@ -1819,7 +1852,7 @@ def get_default_argspecs() -> Dict[object, Signature]: SigParameter("val", _POS_ONLY, annotation=TypeVarValue(T)), SigParameter("typ", _POS_ONLY), ], - TypeVarValue(T), + return_annotation=TypeVarValue(T), callable=assert_type_func, impl=_assert_type_impl, ) diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py index 4a0fd307..135528f9 100644 --- a/pyanalyze/test_typeshed.py +++ b/pyanalyze/test_typeshed.py @@ -143,6 +143,17 @@ def test_str_count(self): def capybara(s: str) -> None: assert_is_value(s.count("x"), TypedValue(int)) + @assert_passes() + def test_dict_fromkeys(self): + def capybara(i: int) -> None: + assert_is_value( + dict.fromkeys([i]), + GenericValue( + dict, + [TypedValue(int), AnyValue(AnySource.explicit) | KnownValue(None)], + ), + ) + def test_has_stubs(self) -> None: tsf = TypeshedFinder(Checker(), verbose=True) assert tsf.has_stubs(object) diff --git a/pyanalyze/typeshed.py b/pyanalyze/typeshed.py index 13d08d21..68fd000b 100644 --- a/pyanalyze/typeshed.py +++ b/pyanalyze/typeshed.py @@ -14,7 +14,7 @@ from collections.abc import Collection, MutableMapping, Set as AbstractSet, Sized from dataclasses import dataclass, field, replace from enum import Enum, EnumMeta -from types import GeneratorType, ModuleType +from types import GeneratorType, MethodDescriptorType, ModuleType from typing import ( Any, Callable, @@ -191,6 +191,19 @@ def log(self, message: str, obj: object) -> None: return print(f"{message}: {obj!r}") + def _get_sig_from_method_descriptor( + self, obj: MethodDescriptorType, allow_call: bool + ) -> Optional[ConcreteSignature]: + objclass = obj.__objclass__ + fq_name = self._get_fq_name(objclass) + if fq_name is None: + return None + info = self._get_info_for_name(fq_name) + sig = self._get_method_signature_from_info( + info, obj, fq_name, objclass.__module__, objclass, allow_call=allow_call + ) + return sig + def get_argspec( self, obj: object, @@ -204,15 +217,24 @@ def get_argspec( obj, obj, type_params=type_params ) if inspect.ismethoddescriptor(obj) and hasattr_static(obj, "__objclass__"): - objclass = obj.__objclass__ - fq_name = self._get_fq_name(objclass) - if fq_name is None: - return None - info = self._get_info_for_name(fq_name) - sig = self._get_method_signature_from_info( - info, obj, fq_name, objclass.__module__, objclass, allow_call=allow_call - ) - return sig + return self._get_sig_from_method_descriptor(obj, allow_call) + if inspect.isbuiltin(obj) and isinstance(obj.__self__, type): + # This covers cases like dict.fromkeys and type.__subclasses__. We + # want to make sure we get the underlying method descriptor object, + # which we can apparently only get out of the __dict__. + method = obj.__self__.__dict__.get(obj.__name__) + if ( + method is not None + and inspect.ismethoddescriptor(method) + and hasattr_static(method, "__objclass__") + ): + sig = self._get_sig_from_method_descriptor(method, allow_call) + if sig is None: + return None + bound = make_bound_method(sig, Composite(KnownValue(obj.__self__))) + if bound is None: + return None + return bound.get_signature(ctx=self.ctx) if inspect.ismethod(obj): self.log("Ignoring method", obj) diff --git a/pyanalyze/value.py b/pyanalyze/value.py index 7f786d6a..7fee146f 100644 --- a/pyanalyze/value.py +++ b/pyanalyze/value.py @@ -50,7 +50,7 @@ def function(x: int, y: list[int], z: Any): import pyanalyze from pyanalyze.error_code import ErrorCode -from pyanalyze.extensions import CustomCheck +from pyanalyze.extensions import CustomCheck, ExternalType from .safe import all_of_type, safe_equals, safe_isinstance, safe_issubclass @@ -60,9 +60,11 @@ def function(x: int, y: list[int], z: Any): KNOWN_MUTABLE_TYPES = (list, set, dict, deque) ITERATION_LIMIT = 1000 -TypeVarLike = Union["TypeVar", "ParamSpec"] -TypeVarMap = Mapping[TypeVarLike, "Value"] -BoundsMap = Mapping[TypeVarLike, Sequence["Bound"]] +TypeVarLike = Union[ + ExternalType["typing.TypeVar"], ExternalType["typing_extensions.ParamSpec"] +] +TypeVarMap = Mapping[TypeVarLike, ExternalType["pyanalyze.value.Value"]] +BoundsMap = Mapping[TypeVarLike, Sequence[ExternalType["pyanalyze.value.Bound"]]] GenericBases = Mapping[Union[type, str], TypeVarMap] diff --git a/pyproject.toml b/pyproject.toml index 5be8e13d..4ac6f55c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,11 @@ value_always_true = true suggested_parameter_type = true suggested_return_type = true incompatible_override = true + +[[tool.pyanalyze.overrides]] +module = "pyanalyze.typevar" +implicit_any = true + +[[tool.pyanalyze.overrides]] +module = "pyanalyze.yield_checker" +implicit_any = true