Skip to content

Commit bb39499

Browse files
Fix various issues exposed by typeshed-client update (#615)
1 parent 5b4029e commit bb39499

File tree

14 files changed

+117
-19
lines changed

14 files changed

+117
-19
lines changed

docs/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
- Add support for `__new__` methods returning `typing.Self`, fixing
6+
various failures with the latest release of `typeshed-client` (#615)
7+
- Add support for importing stub-only modules in other stubs (#615)
8+
- Fix signature compatibility bug involving `**kwargs` and positional-only
9+
arguments (#615)
510
- Fix type narrowing with `in` on enum types in the negative case (#606)
611
- Fix crash when `getattr()` on a module object throws an error (#603)
712
- Fix handling of positional-only arguments using `/` syntax in stubs (#601)

pyanalyze/analysis_lib.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import ast
77
import linecache
88
import os
9+
from pathlib import Path
910
import secrets
1011
import sys
1112
import types
@@ -14,7 +15,7 @@
1415

1516

1617
def _all_files(
17-
root: str, filter_function: Optional[Callable[[str], bool]] = None
18+
root: Path, filter_function: Optional[Callable[[str], bool]] = None
1819
) -> Set[str]:
1920
"""Returns the set of all files at the given root.
2021
@@ -30,7 +31,7 @@ def _all_files(
3031
return all_files
3132

3233

33-
def files_with_extension_from_directory(extension: str, dirname: str) -> Set[str]:
34+
def files_with_extension_from_directory(extension: str, dirname: Path) -> Set[str]:
3435
"""Finds all files in a given directory with this extension."""
3536
return _all_files(dirname, filter_function=lambda fn: fn.endswith("." + extension))
3637

pyanalyze/arg_spec.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -644,12 +644,13 @@ def _uncached_get_argspec(
644644
signature = self._cached_get_argspec(
645645
_ENUM_CALL, impl, is_asynq, in_overload_resolution
646646
)
647-
bound_sig = make_bound_method(
648-
signature, Composite(SubclassValue(TypedValue(obj)))
649-
)
647+
self_value = SubclassValue(TypedValue(obj))
648+
bound_sig = make_bound_method(signature, Composite(self_value))
650649
if bound_sig is None:
651650
return None
652-
sig = bound_sig.get_signature(preserve_impl=True, ctx=self.ctx)
651+
sig = bound_sig.get_signature(
652+
preserve_impl=True, ctx=self.ctx, self_annotation_value=self_value
653+
)
653654
if sig is not None:
654655
return sig
655656
return bound_sig
@@ -739,6 +740,7 @@ def _uncached_get_argspec(
739740
if inspect.isclass(obj):
740741
obj = UnwrapClass.unwrap(obj, self.options)
741742
override = ConstructorHooks.get_constructor(obj, self.options)
743+
is_dunder_new = False
742744
if isinstance(override, Signature):
743745
signature = override
744746
else:
@@ -760,6 +762,7 @@ def _uncached_get_argspec(
760762
# doesn't have a useful signature.
761763
# In practice, we saw this make a difference with NamedTuples.
762764
elif isinstance(obj.__new__, FunctionType):
765+
is_dunder_new = True
763766
constructor = obj.__new__
764767
else:
765768
constructor = obj.__init__
@@ -783,7 +786,15 @@ def _uncached_get_argspec(
783786
bound_sig = make_bound_method(signature, Composite(TypedValue(obj)))
784787
if bound_sig is None:
785788
return None
786-
sig = bound_sig.get_signature(preserve_impl=True, ctx=self.ctx)
789+
if is_dunder_new:
790+
self_annotation_value = KnownValue(obj)
791+
else:
792+
self_annotation_value = TypedValue(obj)
793+
sig = bound_sig.get_signature(
794+
preserve_impl=True,
795+
ctx=self.ctx,
796+
self_annotation_value=self_annotation_value,
797+
)
787798
if sig is not None:
788799
return sig
789800
return bound_sig

pyanalyze/attributes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
KnownValue,
3131
KnownValueWithTypeVars,
3232
MultiValuedValue,
33+
SyntheticModuleValue,
3334
set_self,
3435
SubclassValue,
3536
TypedValue,
@@ -67,6 +68,9 @@ def record_attr_read(self, obj: Any) -> None:
6768
def get_property_type_from_argspec(self, obj: property) -> Value:
6869
return AnyValue(AnySource.inference)
6970

71+
def resolve_name_from_typeshed(self, module: str, name: str) -> Value:
72+
return UNINITIALIZED_VALUE
73+
7074
def get_attribute_from_typeshed(self, typ: type, *, on_class: bool) -> Value:
7175
return UNINITIALIZED_VALUE
7276

@@ -130,6 +134,9 @@ def get_attribute(ctx: AttrContext) -> Value:
130134
attribute_value = AnyValue(AnySource.from_another)
131135
elif isinstance(root_value, MultiValuedValue):
132136
raise TypeError("caller should unwrap MultiValuedValue")
137+
elif isinstance(root_value, SyntheticModuleValue):
138+
module = ".".join(root_value.module_path)
139+
attribute_value = ctx.resolve_name_from_typeshed(module, ctx.attr)
133140
else:
134141
attribute_value = UNINITIALIZED_VALUE
135142
if (

pyanalyze/checker.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ def display_value(self, value: Value) -> str:
239239
sig = self.arg_spec_cache.get_argspec(value.val)
240240
elif isinstance(value, UnboundMethodValue):
241241
sig = value.get_signature(self)
242+
elif isinstance(value, SubclassValue) and value.exactly:
243+
sig = self.signature_from_value(value)
242244
else:
243245
sig = None
244246
if sig is not None:
@@ -439,6 +441,9 @@ def _extract_protocol_members(typ: type) -> Set[str]:
439441
class CheckerAttrContext(AttrContext):
440442
checker: Checker
441443

444+
def resolve_name_from_typeshed(self, module: str, name: str) -> Value:
445+
return self.checker.ts_finder.resolve_name(module, name)
446+
442447
def get_attribute_from_typeshed(self, typ: type, *, on_class: bool) -> Value:
443448
return self.checker.ts_finder.get_attribute(typ, self.attr, on_class=on_class)
444449

pyanalyze/node_visitor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import logging
1515
import os
1616
import os.path
17+
from pathlib import Path
1718
import re
1819
import subprocess
1920
import sys
@@ -1116,7 +1117,7 @@ def _get_all_files(lst: Iterable[str]) -> Iterable[str]:
11161117
for entry in lst:
11171118
if os.path.isdir(entry):
11181119
yield from sorted(
1119-
analysis_lib.files_with_extension_from_directory("py", entry)
1120+
analysis_lib.files_with_extension_from_directory("py", Path(entry))
11201121
)
11211122
else:
11221123
yield entry

pyanalyze/options.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,9 @@ def parse_config_file(
362362
if path in seen_paths:
363363
raise InvalidConfigOption("Recursive config inclusion detected")
364364
with path.open("rb") as f:
365-
data = tomli.load(f)
365+
# tomli annotates the arg as BinaryIO, and we don't treat BufferedReader
366+
# as a BinaryIO
367+
data = tomli.load(f) # static analysis: ignore[incompatible_argument]
366368
data = data.get("tool", {}).get("pyanalyze", {})
367369
yield from _parse_config_section(
368370
data, path=path, priority=priority, seen_paths={path, *seen_paths}

pyanalyze/signature.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
)
6262
from .typevar import resolve_bounds_map
6363
from .value import (
64+
SelfT,
6465
annotate_value,
6566
AnnotatedValue,
6667
AnySource,
@@ -1453,6 +1454,7 @@ def can_assign(
14531454
if their_ellipsis is not None:
14541455
args_annotation = kwargs_annotation = AnyValue(AnySource.ellipsis_callable)
14551456
consumed_positional = set()
1457+
consumed_required_pos_only = set()
14561458
consumed_keyword = set()
14571459
consumed_paramspec = False
14581460
for i, my_param in enumerate(self.parameters.values()):
@@ -1479,6 +1481,8 @@ def can_assign(
14791481
)
14801482
tv_maps.append(tv_map)
14811483
consumed_positional.add(their_params[i].name)
1484+
if their_params[i].default is None:
1485+
consumed_required_pos_only.add(their_params[i].name)
14821486
elif args_annotation is not None:
14831487
new_tv_maps = can_assign_var_positional(
14841488
my_param, args_annotation, i - their_args_index, ctx
@@ -1608,6 +1612,7 @@ def can_assign(
16081612
if param.name not in consumed_keyword
16091613
and param.kind
16101614
in (ParameterKind.KEYWORD_ONLY, ParameterKind.POSITIONAL_OR_KEYWORD)
1615+
and param.name not in consumed_required_pos_only
16111616
]
16121617
for extra_param in extra_keyword:
16131618
tv_map = extra_param.get_annotation().can_assign(my_annotation, ctx)
@@ -1885,6 +1890,7 @@ def bind_self(
18851890
self,
18861891
*,
18871892
preserve_impl: bool = False,
1893+
self_annotation_value: Optional[Value] = None,
18881894
self_value: Optional[Value] = None,
18891895
ctx: CanAssignContext,
18901896
) -> Optional["Signature"]:
@@ -1910,12 +1916,14 @@ def bind_self(
19101916
self_annotation = params[0].annotation
19111917
else:
19121918
return None
1913-
if self_value is not None:
1914-
tv_map = get_tv_map(self_annotation, self_value, ctx)
1919+
if self_annotation_value is not None:
1920+
tv_map = get_tv_map(self_annotation, self_annotation_value, ctx)
19151921
if isinstance(tv_map, CanAssignError):
19161922
return None
19171923
else:
19181924
tv_map = {}
1925+
if self_value is not None:
1926+
tv_map = {**tv_map, SelfT: self_value}
19191927
if tv_map:
19201928
new_params = {
19211929
param.name: param.substitute_typevars(tv_map) for param in new_params
@@ -2414,10 +2422,16 @@ def bind_self(
24142422
*,
24152423
preserve_impl: bool = False,
24162424
self_value: Optional[Value] = None,
2425+
self_annotation_value: Optional[Value] = None,
24172426
ctx: CanAssignContext,
24182427
) -> Optional["ConcreteSignature"]:
24192428
bound_sigs = [
2420-
sig.bind_self(preserve_impl=preserve_impl, self_value=self_value, ctx=ctx)
2429+
sig.bind_self(
2430+
preserve_impl=preserve_impl,
2431+
self_value=self_value,
2432+
self_annotation_value=self_annotation_value,
2433+
ctx=ctx,
2434+
)
24212435
for sig in self.signatures
24222436
]
24232437
bound_sigs = [sig for sig in bound_sigs if isinstance(sig, Signature)]
@@ -2496,10 +2510,19 @@ def check_call(
24962510
return ret
24972511

24982512
def get_signature(
2499-
self, *, preserve_impl: bool = False, ctx: CanAssignContext
2513+
self,
2514+
*,
2515+
preserve_impl: bool = False,
2516+
ctx: CanAssignContext,
2517+
self_annotation_value: Optional[Value] = None,
25002518
) -> Optional[ConcreteSignature]:
2519+
if self_annotation_value is None:
2520+
self_annotation_value = self.self_composite.value
25012521
return self.signature.bind_self(
2502-
preserve_impl=preserve_impl, self_value=self.self_composite.value, ctx=ctx
2522+
preserve_impl=preserve_impl,
2523+
self_value=self.self_composite.value,
2524+
ctx=ctx,
2525+
self_annotation_value=self_annotation_value,
25032526
)
25042527

25052528
def has_return_value(self) -> bool:

pyanalyze/stubs/_pyanalyze_tests-stubs/self.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ class X:
44
def ret(self) -> Self: ...
55
@classmethod
66
def from_config(cls) -> Self: ...
7+
def __new__(cls) -> Self: ...
78

89
class Y(X): ...
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import _typeshed
2+
3+
class X:
4+
def __new__(cls: type[_typeshed.Self]) -> _typeshed.Self: ...
5+
def method(self) -> int: ...

0 commit comments

Comments
 (0)