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 crash on encountering certain decorators in stubs #734

Merged
merged 2 commits into from
Mar 1, 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
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

- Fix crash on encountering certain decorators in stubs (#734)
- Fix inference of signature for certain secondary methods (#732)

## Version 0.12.0 (February 25, 2024)
Expand Down
4 changes: 2 additions & 2 deletions pyanalyze/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -956,13 +956,13 @@ class _SubscriptedValue(Value):
members: Tuple[Value, ...]


@dataclass
@dataclass(frozen=True)
class TypeQualifierValue(Value):
qualifier: Literal["Required", "NotRequired", "ReadOnly"]
value: Value


@dataclass
@dataclass(frozen=True)
class DecoratorValue(Value):
decorator: object
args: Tuple[Value, ...]
Expand Down
9 changes: 9 additions & 0 deletions pyanalyze/test_typeshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,3 +819,12 @@ def __init__(self, arg: int) -> None:

def capybara() -> None:
Outer.Inner(1)


class TestDeprecated(TestNameCheckVisitorBase):
@assert_passes()
def test_utcnow(self):
import datetime

def capybara() -> None:
assert_is_value(datetime.datetime.utcnow(), TypedValue(datetime.datetime))
92 changes: 59 additions & 33 deletions pyanalyze/typeshed.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from .extensions import evaluated, overload, real_overload
from .node_visitor import Failure
from .options import Options, PathSequenceOption
from .safe import all_of_type, hasattr_static, is_typing_name, safe_isinstance
from .safe import hasattr_static, is_typing_name, safe_isinstance
from .signature import (
ConcreteSignature,
OverloadedSignature,
Expand All @@ -65,6 +65,7 @@
from .stacked_scopes import Composite, uniq_chain
from .value import (
UNINITIALIZED_VALUE,
AnnotatedValue,
AnySource,
AnyValue,
CallableValue,
Expand All @@ -83,6 +84,7 @@
annotate_value,
extract_typevars,
make_coro_type,
unannotate_value,
)

PROPERTY_LIKE = {KnownValue(property), KnownValue(types.DynamicClassAttribute)}
Expand Down Expand Up @@ -524,18 +526,25 @@ def _get_value_from_child_info(
if isinstance(node, ast.AnnAssign):
return self._parse_type(node.annotation, mod, is_typeddict=is_typeddict)
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
decorators = [
self._parse_expr(decorator, mod) for decorator in node.decorator_list
]
if node.returns and set(decorators) & PROPERTY_LIKE:
return self._parse_type(node.returns, mod)
sig = self._get_signature_from_func_def(
node, None, mod, autobind=not on_class
)
if sig is None:
return AnyValue(AnySource.inference)
is_property = False
for decorator_node in node.decorator_list:
decorator_value = self._parse_expr(decorator_node, mod)
if decorator_value in PROPERTY_LIKE:
is_property = True
if is_property:
if node.returns:
return self._parse_type(node.returns, mod)
else:
return AnyValue(AnySource.unannotated)
else:
return CallableValue(sig)
# TODO: apply decorators to the return value
sig = self._get_signature_from_func_def(
node, None, mod, autobind=not on_class
)
if sig is None:
return AnyValue(AnySource.inference)
else:
return CallableValue(sig)
elif isinstance(node, ast.ClassDef):
# should be
# SubclassValue(TypedValue(f"{mod}.{parent_name}.{node.name}"), exactly=True)
Expand All @@ -544,21 +553,20 @@ def _get_value_from_child_info(
elif isinstance(node, ast.Assign):
return UNINITIALIZED_VALUE
elif isinstance(node, typeshed_client.OverloadedName):
vals = [
self._get_value_from_child_info(
sigs = []
for subnode in node.definitions:
val = self._get_value_from_child_info(
subnode,
mod,
is_typeddict=is_typeddict,
on_class=on_class,
parent_name=parent_name,
)
for subnode in node.definitions
]
if all_of_type(vals, CallableValue):
sigs = [val.signature for val in vals]
if all_of_type(sigs, Signature):
return CallableValue(OverloadedSignature(sigs))
return AnyValue(AnySource.inference)
sig = self._sig_from_value(val)
if not isinstance(sig, Signature):
return AnyValue(AnySource.inference)
sigs.append(sig)
return CallableValue(OverloadedSignature(sigs))
assert False, repr(node)

def _get_child_info(
Expand Down Expand Up @@ -737,6 +745,18 @@ def _get_fq_name(self, obj: Any) -> Optional[str]:
self.log("Ignoring object without module or qualname", obj)
return None

def _sig_from_value(self, val: Value) -> Optional[ConcreteSignature]:
val, extensions = unannotate_value(val, DeprecatedExtension)
if isinstance(val, AnnotatedValue):
val = val.value
if not isinstance(val, CallableValue):
return None
sig = val.signature
if isinstance(sig, Signature):
for extension in extensions:
sig = replace(sig, deprecated=extension.deprecation_message)
return sig

def _get_signature_from_info(
self,
info: typeshed_client.resolver.ResolvedName,
Expand Down Expand Up @@ -773,17 +793,15 @@ def _get_signature_from_info(
new_value, provider = self.get_attribute_recursively(
fq_name, "__new__", on_class=True
)
sig = None
from_init = False
if new_value is UNINITIALIZED_VALUE or provider is object:
init_value, provider = self.get_attribute_recursively(
fq_name, "__init__", on_class=True
)
if isinstance(init_value, CallableValue):
sig = init_value.signature
if (sig := self._sig_from_value(init_value)) is not None:
from_init = True
elif isinstance(new_value, CallableValue):
sig = new_value.signature
else:
sig = self._sig_from_value(new_value)
if sig is not None:
if safe_isinstance(obj, type):
if allow_call:
Expand Down Expand Up @@ -1070,15 +1088,23 @@ def _extract_metadata(self, module: str, node: ast.ClassDef) -> Sequence[Extensi
metadata = []
for decorator in node.decorator_list:
decorator_val = self._parse_expr(decorator, module)
if (
isinstance(decorator_val, DecoratorValue)
and decorator_val.decorator is deprecated_decorator
):
arg = decorator_val.args[0]
if isinstance(arg, KnownValue) and isinstance(arg.val, str):
metadata.append(DeprecatedExtension(arg.val))
extension = self._extract_extension_from_decorator(decorator_val)
if extension is not None:
metadata.append(extension)
return metadata

def _extract_extension_from_decorator(
self, decorator_val: Value
) -> Optional[Extension]:
if (
isinstance(decorator_val, DecoratorValue)
and decorator_val.decorator is deprecated_decorator
):
arg = decorator_val.args[0]
if isinstance(arg, KnownValue) and isinstance(arg.val, str):
return DeprecatedExtension(arg.val)
return None

def make_synthetic_type(self, module: str, info: typeshed_client.NameInfo) -> Value:
fq_name = f"{module}.{info.name}"
bases = self._get_bases_from_info(info, module, fq_name)
Expand Down
Loading