Skip to content

Commit

Permalink
Refactor isinstance() support (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Jan 25, 2022
1 parent 4c4adfa commit e207d0a
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 7 deletions.
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

- Refactor `isinstance()` support (#440)
- Exclude `Any[unreachable]` from unified values (#439)
- Add support for `reveal_locals()` (#436)
- Add support for `assert_error()` (#435)
Expand Down
19 changes: 15 additions & 4 deletions pyanalyze/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,23 @@ def _issubclass_impl(ctx: CallContext) -> Value:
return annotate_with_constraint(TypedValue(bool), constraint)


def _isinstance_impl(ctx: CallContext) -> ImplReturn:
def _isinstance_impl(ctx: CallContext) -> Value:
class_or_tuple = ctx.vars["class_or_tuple"]
varname = ctx.varname_for_arg("obj")
return ImplReturn(
TypedValue(bool), _constraint_from_isinstance(varname, class_or_tuple)
)
if varname is None or not isinstance(class_or_tuple, KnownValue):
return TypedValue(bool)
if isinstance(class_or_tuple.val, type):
narrowed_type = TypedValue(class_or_tuple.val)
elif isinstance(class_or_tuple.val, tuple) and all(
isinstance(elt, type) for elt in class_or_tuple.val
):
vals = [TypedValue(elt) for elt in class_or_tuple.val]
narrowed_type = unite_values(*vals)
else:
return TypedValue(bool)
predicate = IsAssignablePredicate(narrowed_type, ctx.visitor, positive_only=False)
constraint = Constraint(varname, ConstraintType.predicate, True, predicate)
return annotate_with_constraint(TypedValue(bool), constraint)


def _constraint_from_isinstance(
Expand Down
35 changes: 32 additions & 3 deletions pyanalyze/predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@

from .safe import safe_issubclass
from .value import (
NO_RETURN_VALUE,
AnnotatedValue,
AnyValue,
KnownValue,
MultiValuedValue,
SubclassValue,
TypedValue,
Value,
unite_values,
Expand All @@ -20,9 +25,27 @@
)


def is_universally_assignable(value: Value, target_value: Value) -> bool:
if value is NO_RETURN_VALUE or isinstance(value, AnyValue):
return True
elif value == TypedValue(type) and isinstance(target_value, SubclassValue):
return True
elif isinstance(value, AnnotatedValue):
return is_universally_assignable(value.value, target_value)
elif isinstance(value, MultiValuedValue):
return all(
is_universally_assignable(subval, target_value) for subval in value.vals
)
return False


@dataclass
class IsAssignablePredicate:
"""Predicate that filters out values that are not assignable to pattern_value."""
"""Predicate that filters out values that are not assignable to pattern_value.
This only works reliably for simple pattern_values, such as TypedValue.
"""

pattern_value: Value
ctx: CanAssignContext
Expand All @@ -33,10 +56,16 @@ def __call__(self, value: Value, positive: bool) -> Optional[Value]:
if positive:
if not compatible:
return None
if value.is_assignable(self.pattern_value, self.ctx):
if self.pattern_value.is_assignable(value, self.ctx):
if is_universally_assignable(value, unannotate(self.pattern_value)):
return self.pattern_value
return value
else:
return self.pattern_value
elif not self.positive_only:
if compatible:
if self.pattern_value.is_assignable(
value, self.ctx
) and not is_universally_assignable(value, unannotate(self.pattern_value)):
return None
return value

Expand Down

0 comments on commit e207d0a

Please sign in to comment.