diff --git a/docs/changelog.md b/docs/changelog.md index 0a3d6968..805dd9a2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ ## Unreleased +- Initial support for variable-length heterogeneous sequences + (required for PEP 646). More precise types are now inferred + for heterogeneous sequences containing variable-length + objects. (#515, #516) - Support `LiteralString` (PEP 675) (#514) - Add `unused_assignment` error code, separated out from `unused_variable`. Enable these error codes and diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py index 58b77fd8..67f28057 100644 --- a/pyanalyze/annotations.py +++ b/pyanalyze/annotations.py @@ -63,7 +63,13 @@ from .find_unused import used from .functions import FunctionDefNode from .node_visitor import ErrorContext -from .signature import ELLIPSIS_PARAM, SigParameter, Signature, ParameterKind +from .signature import ( + ELLIPSIS_PARAM, + InvalidSignature, + SigParameter, + Signature, + ParameterKind, +) from .safe import is_typing_name, is_instance_of_typing_name from . import type_evaluation from .value import ( @@ -85,7 +91,6 @@ SequenceValue, TypeGuardExtension, TypedValue, - SequenceIncompleteValue, annotate_value, unite_values, Value, @@ -385,7 +390,7 @@ def _type_from_runtime( origin = get_origin(val) args = get_args(val) if origin is tuple and not args: - return SequenceIncompleteValue(tuple, []) + return SequenceValue(tuple, []) return _value_of_origin_args( origin, args, val, ctx, unpack_allowed=origin is tuple ) @@ -404,24 +409,16 @@ def _type_from_runtime( if val is tuple or val is Tuple: return TypedValue(tuple) else: - return SequenceIncompleteValue(tuple, []) + return SequenceValue(tuple, []) elif len(args) == 2 and args[1] is Ellipsis: return GenericValue(tuple, [_type_from_runtime(args[0], ctx)]) elif len(args) == 1 and args[0] == (): - return SequenceIncompleteValue(tuple, []) # empty tuple + return SequenceValue(tuple, []) # empty tuple else: - args_vals = [ - _type_from_runtime(arg, ctx, unpack_allowed=True) for arg in args - ] - if any(isinstance(val, UnpackedValue) for val in args_vals): - members = [] - for val in args_vals: - if isinstance(val, UnpackedValue): - members += val.elements - else: - members.append((False, val)) - return SequenceValue(tuple, members) - return SequenceIncompleteValue(tuple, args_vals) + return _make_sequence_value( + tuple, + [_type_from_runtime(arg, ctx, unpack_allowed=True) for arg in args], + ) elif is_instance_of_typing_name(val, "_TypedDictMeta"): required_keys = getattr(val, "__required_keys__", None) # 3.8's typing.TypedDict doesn't have __required_keys__. With @@ -799,18 +796,12 @@ def _type_from_subscripted_value( if len(members) == 2 and members[1] == KnownValue(Ellipsis): return GenericValue(tuple, [_type_from_value(members[0], ctx)]) elif len(members) == 1 and members[0] == KnownValue(()): - return SequenceIncompleteValue(tuple, []) + return SequenceValue(tuple, []) else: - args = [_type_from_value(arg, ctx, unpack_allowed=True) for arg in members] - if any(isinstance(val, UnpackedValue) for val in args): - tuple_members = [] - for val in args: - if isinstance(val, UnpackedValue): - tuple_members += val.elements - else: - tuple_members.append((False, val)) - return SequenceValue(tuple, tuple_members) - return SequenceIncompleteValue(tuple, args) + return _make_sequence_value( + tuple, + [_type_from_value(arg, ctx, unpack_allowed=True) for arg in members], + ) elif root is typing.Optional: if len(members) != 1: ctx.show_error("Optional[] takes only one argument") @@ -982,9 +973,7 @@ def visit_Name(self, node: ast.Name) -> Value: def visit_Subscript(self, node: ast.Subscript) -> Value: value = self.visit(node.value) index = self.visit(node.slice) - if isinstance(index, SequenceIncompleteValue): - members = index.members - elif isinstance(index, SequenceValue): + if isinstance(index, SequenceValue): members = index.get_member_sequence() if members is None: # TODO support unpacking here @@ -1009,12 +998,12 @@ def visit_Attribute(self, node: ast.Attribute) -> Optional[Value]: return AnyValue(AnySource.error) def visit_Tuple(self, node: ast.Tuple) -> Value: - elts = [self.visit(elt) for elt in node.elts] - return SequenceIncompleteValue(tuple, elts) + elts = [(False, self.visit(elt)) for elt in node.elts] + return SequenceValue(tuple, elts) def visit_List(self, node: ast.List) -> Value: - elts = [self.visit(elt) for elt in node.elts] - return SequenceIncompleteValue(list, elts) + elts = [(False, self.visit(elt)) for elt in node.elts] + return SequenceValue(list, elts) def visit_Index(self, node: ast.Index) -> Value: # class is unused in 3.9 @@ -1159,12 +1148,12 @@ def _value_of_origin_args( elif len(args) == 2 and args[1] is Ellipsis: return GenericValue(tuple, [_type_from_runtime(args[0], ctx)]) elif len(args) == 1 and args[0] == (): - return SequenceIncompleteValue(tuple, []) + return SequenceValue(tuple, []) else: args_vals = [ _type_from_runtime(arg, ctx, unpack_allowed=True) for arg in args ] - return SequenceIncompleteValue(tuple, args_vals) + return _make_sequence_value(tuple, args_vals) elif origin is typing.Union: return unite_values(*[_type_from_runtime(arg, ctx) for arg in args]) elif origin is Callable or origin is typing.Callable: @@ -1254,11 +1243,19 @@ def _maybe_typed_value(val: Union[type, str]) -> Value: return TypedValue(val) +def _make_sequence_value(typ: type, members: Sequence[Value]) -> SequenceValue: + pairs = [] + for val in members: + if isinstance(val, UnpackedValue): + pairs += val.elements + else: + pairs.append((False, val)) + return SequenceValue(typ, pairs) + + def _make_unpacked_value(val: Value, ctx: Context) -> UnpackedValue: if isinstance(val, SequenceValue) and val.typ is tuple: return UnpackedValue(val.members) - elif isinstance(val, SequenceIncompleteValue) and val.typ is tuple: - return UnpackedValue([(False, elt) for elt in val.members]) elif isinstance(val, GenericValue) and val.typ is tuple: return UnpackedValue([(True, val.args[0])]) elif isinstance(val, TypedValue) and val.typ is tuple: @@ -1277,16 +1274,28 @@ def _make_callable_from_value( [ELLIPSIS_PARAM], return_annotation=return_annotation, is_asynq=is_asynq ) ) - elif isinstance(args, SequenceIncompleteValue): - params = [ - SigParameter( - f"__arg{i}", - kind=ParameterKind.POSITIONAL_ONLY, - annotation=_type_from_value(arg, ctx), - ) - for i, arg in enumerate(args.members) - ] - sig = Signature.make(params, return_annotation, is_asynq=is_asynq) + elif isinstance(args, SequenceValue): + params = [] + for i, (is_many, arg) in enumerate(args.members): + annotation = _type_from_value(arg, ctx) + if is_many: + param = SigParameter( + f"__arg{i}", + kind=ParameterKind.VAR_POSITIONAL, + annotation=GenericValue(tuple, [annotation]), + ) + else: + param = SigParameter( + f"__arg{i}", + kind=ParameterKind.POSITIONAL_ONLY, + annotation=annotation, + ) + params.append(param) + try: + sig = Signature.make(params, return_annotation, is_asynq=is_asynq) + except InvalidSignature as e: + ctx.show_error(str(e)) + return AnyValue(AnySource.error) return CallableValue(sig) elif isinstance(args, KnownValue) and is_instance_of_typing_name( args.val, "ParamSpec" diff --git a/pyanalyze/boolability.py b/pyanalyze/boolability.py index 1552f07e..90e3ca38 100644 --- a/pyanalyze/boolability.py +++ b/pyanalyze/boolability.py @@ -19,7 +19,6 @@ DictIncompleteValue, KnownValue, MultiValuedValue, - SequenceIncompleteValue, SequenceValue, SubclassValue, TypedDictValue, @@ -111,21 +110,6 @@ def _get_boolability_no_mvv(value: Value) -> Boolability: return Boolability.type_always_true else: return Boolability.boolable - elif isinstance(value, SequenceIncompleteValue): - if value.typ is tuple: - if value.members: - # We lie slightly here, since at the type level a tuple - # may be false. But tuples are a common source of boolability - # bugs and they're rarely mutated, so we put a stronger - # condition on them. - return Boolability.type_always_true - else: - return Boolability.value_always_false - else: - if value.members: - return Boolability.value_always_true_mutable - else: - return Boolability.value_always_false_mutable elif isinstance(value, SequenceValue): if not value.members: if value.typ is tuple: diff --git a/pyanalyze/format_strings.py b/pyanalyze/format_strings.py index 84c9d821..412b89bb 100644 --- a/pyanalyze/format_strings.py +++ b/pyanalyze/format_strings.py @@ -29,7 +29,6 @@ CanAssignContext, KnownValue, DictIncompleteValue, - SequenceIncompleteValue, SequenceValue, TypedValue, Value, @@ -369,9 +368,7 @@ def accept_tuple_args_no_mvv( if isinstance(args, AnnotatedValue): args = args.value args = replace_known_sequence_value(args) - if isinstance(args, SequenceIncompleteValue): - all_args = args.members - elif isinstance(args, SequenceValue): + if isinstance(args, SequenceValue): all_args = args.get_member_sequence() if all_args is None: return diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py index a88ed59d..bd1f9cf0 100644 --- a/pyanalyze/implementation.py +++ b/pyanalyze/implementation.py @@ -42,7 +42,6 @@ GenericValue, NewTypeValue, DictIncompleteValue, - SequenceIncompleteValue, TypedDictValue, KnownValue, MultiValuedValue, @@ -346,7 +345,8 @@ def inner(iterable: Value) -> Value: elif isinstance(cvi, Value): return GenericValue(typ, [cvi]) else: - return SequenceIncompleteValue.make_or_known(typ, cvi) + # TODO: Consider changing concrete_values_from_iterable to preserve unpacked bits + return SequenceValue.make_or_known(typ, [(False, elt) for elt in cvi]) return flatten_unions(inner, iterable) @@ -356,15 +356,7 @@ def _list_append_impl(ctx: CallContext) -> ImplReturn: element = ctx.vars["object"] varname = ctx.visitor.varname_for_self_constraint(ctx.node) if varname is not None: - if isinstance(lst, SequenceIncompleteValue): - no_return_unless = Constraint( - varname, - ConstraintType.is_value_object, - True, - SequenceIncompleteValue.make_or_known(list, (*lst.members, element)), - ) - return ImplReturn(KnownValue(None), no_return_unless=no_return_unless) - elif isinstance(lst, SequenceValue): + if isinstance(lst, SequenceValue): no_return_unless = Constraint( varname, ConstraintType.is_value_object, @@ -436,15 +428,6 @@ def inner(key: Value) -> Value: return member # fall back to the common type return self_value.args[0] - elif isinstance(self_value, SequenceIncompleteValue): - if -len(self_value.members) <= key.val < len(self_value.members): - return self_value.members[key.val] - elif typ is list: - # fall back to the common type - return self_value.args[0] - else: - ctx.show_error(f"Tuple index out of range: {key}") - return AnyValue(AnySource.error) else: return self_value.get_generic_arg_for_type(typ, ctx.visitor, 0) elif isinstance(key.val, slice): @@ -458,10 +441,6 @@ def inner(key: Value) -> Value: # If the value contains unpacked values, we don't attempt # to resolve the slice. return GenericValue(typ, self_value.args) - elif isinstance(self_value, SequenceIncompleteValue): - return SequenceIncompleteValue.make_or_known( - list, self_value.members[key.val] - ) elif self_value.typ in (list, tuple): # For generics of exactly list/tuple, return the self type. return self_value @@ -922,12 +901,6 @@ def inner(left: Value, right: Value) -> Value: right = replace_known_sequence_value(right) if isinstance(left, SequenceValue) and isinstance(right, SequenceValue): return SequenceValue.make_or_known(list, [*left.members, *right.members]) - elif isinstance(left, SequenceIncompleteValue) and isinstance( - right, SequenceIncompleteValue - ): - return SequenceIncompleteValue.make_or_known( - list, [*left.members, *right.members] - ) elif isinstance(left, TypedValue) and isinstance(right, TypedValue): left_arg = left.get_generic_arg_for_type(list, ctx.visitor, 0) right_arg = right.get_generic_arg_for_type(list, ctx.visitor, 0) @@ -946,36 +919,8 @@ def _list_extend_or_iadd_impl( def inner(lst: Value, iterable: Value) -> ImplReturn: cleaned_lst = replace_known_sequence_value(lst) iterable = replace_known_sequence_value(iterable) - if isinstance(cleaned_lst, SequenceIncompleteValue): - if isinstance( - iterable, SequenceIncompleteValue - ) and iterable.get_type_object(ctx.visitor).is_exactly((list, tuple)): - constrained_value = SequenceIncompleteValue.make_or_known( - list, (*cleaned_lst.members, *iterable.members) - ) - else: - if isinstance(iterable, TypedValue): - arg_type = iterable.get_generic_arg_for_type( - collections.abc.Iterable, ctx.visitor, 0 - ) - else: - arg_type = AnyValue(AnySource.generic_argument) - generic_arg = unite_values(*cleaned_lst.members, arg_type) - constrained_value = make_weak(GenericValue(list, [generic_arg])) - if return_container: - return ImplReturn(constrained_value) - if varname is not None: - no_return_unless = Constraint( - varname, ConstraintType.is_value_object, True, constrained_value - ) - return ImplReturn(KnownValue(None), no_return_unless=no_return_unless) - elif isinstance(cleaned_lst, SequenceValue): - if isinstance(iterable, SequenceIncompleteValue): - constrained_value = SequenceValue.make_or_known( - list, - (*cleaned_lst.members, *[(False, m) for m in iterable.members]), - ) - elif isinstance(iterable, SequenceValue): + if isinstance(cleaned_lst, SequenceValue): + if isinstance(iterable, SequenceValue): constrained_value = SequenceValue.make_or_known( list, (*cleaned_lst.members, *iterable.members) ) @@ -1073,17 +1018,7 @@ def _set_add_impl(ctx: CallContext) -> ImplReturn: element = ctx.vars["object"] varname = ctx.visitor.varname_for_self_constraint(ctx.node) if varname is not None: - if isinstance(set_value, SequenceIncompleteValue): - no_return_unless = Constraint( - varname, - ConstraintType.is_value_object, - True, - SequenceIncompleteValue.make_or_known( - set, (*set_value.members, element) - ), - ) - return ImplReturn(KnownValue(None), no_return_unless=no_return_unless) - elif isinstance(set_value, SequenceValue): + if isinstance(set_value, SequenceValue): no_return_unless = Constraint( varname, ConstraintType.is_value_object, @@ -1181,9 +1116,7 @@ def _str_format_impl(ctx: CallContext) -> Value: if not isinstance(self, KnownValue): return TypedValue(str) args_value = replace_known_sequence_value(ctx.vars["args"]) - if isinstance(args_value, SequenceIncompleteValue): - args = args_value.members - elif isinstance(args_value, SequenceValue): + if isinstance(args_value, SequenceValue): args = args_value.get_member_sequence() if args is None: return TypedValue(str) @@ -1313,12 +1246,6 @@ def _qcore_assert_impl( def len_of_value(val: Value) -> Value: - if ( - isinstance(val, SequenceIncompleteValue) - and isinstance(val.typ, type) - and not issubclass(val.typ, KNOWN_MUTABLE_TYPES) - ): - return KnownValue(len(val.members)) if ( isinstance(val, SequenceValue) and isinstance(val.typ, type) diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py index 96e0771c..d91e2873 100644 --- a/pyanalyze/name_check_visitor.py +++ b/pyanalyze/name_check_visitor.py @@ -87,13 +87,7 @@ from .predicates import EqualsPredicate from .shared_options import Paths, ImportPaths, EnforceNoUnused from .reexport import ImplicitReexportTracker -from .safe import ( - safe_getattr, - is_hashable, - all_of_type, - safe_issubclass, - is_dataclass_type, -) +from .safe import safe_getattr, is_hashable, safe_issubclass, is_dataclass_type from .stacked_scopes import ( EMPTY_ORIGIN, AbstractConstraint, @@ -185,7 +179,6 @@ ReferencingValue, SubclassValue, DictIncompleteValue, - SequenceIncompleteValue, AsyncTaskIncompleteValue, GenericValue, Value, @@ -2439,10 +2432,10 @@ def _visit_comprehension_inner( for val in iterable_type: self.visit_comprehension(generator, iterable_type=val) with qcore.override(self, "in_comprehension_body", True): - elts.append(self.visit(node.elt)) + elts.append((False, self.visit(node.elt))) finally: self.node_context.contexts.pop() - return SequenceIncompleteValue(typ, elts) + return SequenceValue(typ, elts) iterable_type = unite_and_simplify( *iterable_type, @@ -2713,7 +2706,6 @@ def _maybe_make_sequence( elt_nodes: Optional[Sequence[ast.AST]] = None, ) -> Value: values = [] - has_unknown_value = False for i, elt in enumerate(elts): if isinstance(elt, _StarredValue): vals = concrete_values_from_iterable(elt.value, self) @@ -2724,16 +2716,14 @@ def _maybe_make_sequence( ErrorCode.unsupported_operation, detail=str(vals), ) - new_vals = [AnyValue(AnySource.error)] - has_unknown_value = True + new_vals = [(True, AnyValue(AnySource.error))] elif isinstance(vals, Value): # single value - has_unknown_value = True - new_vals = [vals] + new_vals = [(True, vals)] else: - new_vals = vals + new_vals = [(False, val) for val in vals] if typ is set: - for val in new_vals: + for _, val in new_vals: hashability = check_hashability(val, self) if isinstance(hashability, CanAssignError): if elt_nodes: @@ -2762,22 +2752,9 @@ def _maybe_make_sequence( ErrorCode.unhashable_key, detail=str(hashability), ) - values.append(elt) - if has_unknown_value: - arg = unite_and_simplify( - *values, limit=self.options.get_value_for(UnionSimplificationLimit) - ) - return make_weak(GenericValue(typ, [arg])) - elif all_of_type(values, KnownValue): - vals = [elt.val for elt in values] - try: - obj = typ(vals) - except TypeError: - # probably an unhashable type being included in a set - return TypedValue(typ) - return KnownValue(obj) - else: - return SequenceIncompleteValue(typ, values) + values.append((False, elt)) + + return SequenceValue.make_or_known(typ, values) # Operations @@ -3213,14 +3190,7 @@ def _unwrap_yield_result(self, node: ast.AST, value: Value) -> Value: # https://github.com/quora/asynq/blob/b07682d8b11e53e4ee5c585020cc9033e239c7eb/asynq/async_task.py#L446 value.get_type_object().is_exactly({list, tuple}) ): - if isinstance(value, SequenceIncompleteValue) and isinstance( - value.typ, type - ): - values = [ - self._unwrap_yield_result(node, member) for member in value.members - ] - return self._maybe_make_sequence(value.typ, values, node) - elif isinstance(value, SequenceValue) and isinstance(value.typ, type): + if isinstance(value, SequenceValue) and isinstance(value.typ, type): values = [ (is_many, self._unwrap_yield_result(node, member)) for is_many, member in value.members @@ -4016,15 +3986,6 @@ def _composite_from_subscript_no_mvv( return_value = KnownValue(type[index.val]) else: return_value = AnyValue(AnySource.inference) - elif ( - isinstance(value, SequenceIncompleteValue) - and isinstance(index, KnownValue) - and isinstance(index.val, int) - and -len(value.members) <= index.val < len(value.members) - ): - # Don't error if it's out of range; the object may be mutated at runtime. - # TODO: handle slices; error for things that aren't ints or slices. - return_value = value.members[index.val] else: with self.catch_errors(): getitem = self._get_dunder(node.value, value, "__getitem__") diff --git a/pyanalyze/patma.py b/pyanalyze/patma.py index ef6a43f5..18905595 100644 --- a/pyanalyze/patma.py +++ b/pyanalyze/patma.py @@ -50,7 +50,6 @@ CustomCheckExtension, DictIncompleteValue, KVPair, - SequenceIncompleteValue, SequenceValue, SubclassValue, TypedValue, @@ -167,8 +166,8 @@ def __call__(self, value: Value, positive: bool) -> Optional[Value]: ): # Narrow Tuple[...] to a known length arg = cleaned.get_generic_arg_for_type(tuple, self.ctx, 0) - return SequenceIncompleteValue( - tuple, [arg for _ in range(self.expected_length)] + return SequenceValue( + tuple, [(False, arg) for _ in range(self.expected_length)] ) return value @@ -479,20 +478,7 @@ def get_match_args( if match_args_value is UNINITIALIZED_VALUE: return CanAssignError(f"{cls} has no attribute __match_args__") match_args_value = replace_known_sequence_value(match_args_value) - if isinstance(match_args_value, SequenceIncompleteValue): - if match_args_value.typ is not tuple: - return CanAssignError( - f"__match_args__ must be a literal tuple, not {match_args_value}" - ) - match_args = [] - for i, arg in enumerate(match_args_value.members): - if not isinstance(arg, KnownValue) or not isinstance(arg.val, str): - return CanAssignError( - f"__match_args__ element {i} is {arg}, not a string literal" - ) - match_args.append(arg.val) - return match_args - elif isinstance(match_args_value, SequenceValue): + if isinstance(match_args_value, SequenceValue): if match_args_value.typ is not tuple: return CanAssignError( f"__match_args__ must be a literal tuple, not {match_args_value}" diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py index c5e4bcc2..d27c4fe0 100644 --- a/pyanalyze/signature.py +++ b/pyanalyze/signature.py @@ -42,7 +42,6 @@ ParamSpecArgsValue, ParamSpecKwargsValue, ParameterTypeGuardExtension, - SequenceIncompleteValue, DictIncompleteValue, SequenceValue, TypeGuardExtension, @@ -924,10 +923,17 @@ def bind_arguments( tuple, [AnyValue(AnySource.ellipsis_callable)] ) elif actual_args.star_args is not None: - element_value = unite_values(*positionals, actual_args.star_args) - star_args_value = GenericValue(tuple, [element_value]) + star_args_value = SequenceValue( + tuple, + [ + *[(False, pos) for pos in positionals], + (True, actual_args.star_args), + ], + ) else: - star_args_value = SequenceIncompleteValue(tuple, positionals) + star_args_value = SequenceValue( + tuple, [(False, pos) for pos in positionals] + ) if not positionals: # no *args were actually provided position = DEFAULT @@ -2350,7 +2356,6 @@ def make_bound_method( def can_assign_var_positional( my_param: SigParameter, args_annotation: Value, idx: int, ctx: CanAssignContext ) -> Union[List[BoundsMap], CanAssignError]: - bounds_maps = [] my_annotation = my_param.get_annotation() if isinstance(args_annotation, SequenceValue): members = args_annotation.get_member_sequence() @@ -2369,41 +2374,20 @@ def can_assign_var_positional( " type is incompatible", [can_assign], ) - bounds_maps.append(can_assign) - return bounds_maps - - if isinstance(args_annotation, SequenceIncompleteValue): - length = len(args_annotation.members) - if idx >= length: - return CanAssignError( - f"parameter {my_param.name!r} is not accepted; {args_annotation} only" - f" accepts {length} values" - ) - their_annotation = args_annotation.members[idx] - can_assign = their_annotation.can_assign(my_annotation, ctx) - if isinstance(can_assign, CanAssignError): - return CanAssignError( - f"type of parameter {my_param.name!r} is incompatible: *args[{idx}]" - " type is incompatible", - [can_assign], - ) - bounds_maps.append(can_assign) - else: - tv_map = get_tv_map(IterableValue, args_annotation, ctx) - if isinstance(tv_map, CanAssignError): - return CanAssignError( - f"{args_annotation} is not an iterable type", [tv_map] - ) - iterable_arg = tv_map.get(T, AnyValue(AnySource.generic_argument)) - bounds_map = iterable_arg.can_assign(my_annotation, ctx) - if isinstance(bounds_map, CanAssignError): - return CanAssignError( - f"type of parameter {my_param.name!r} is incompatible: " - "*args type is incompatible", - [bounds_map], - ) - bounds_maps.append(bounds_map) - return bounds_maps + return [can_assign] + + tv_map = get_tv_map(IterableValue, args_annotation, ctx) + if isinstance(tv_map, CanAssignError): + return CanAssignError(f"{args_annotation} is not an iterable type", [tv_map]) + iterable_arg = tv_map.get(T, AnyValue(AnySource.generic_argument)) + bounds_map = iterable_arg.can_assign(my_annotation, ctx) + if isinstance(bounds_map, CanAssignError): + return CanAssignError( + f"type of parameter {my_param.name!r} is incompatible: " + "*args type is incompatible", + [bounds_map], + ) + return [bounds_map] def can_assign_var_keyword( diff --git a/pyanalyze/suggested_type.py b/pyanalyze/suggested_type.py index 35a72164..e3eced38 100644 --- a/pyanalyze/suggested_type.py +++ b/pyanalyze/suggested_type.py @@ -21,7 +21,6 @@ CanAssignError, GenericValue, KnownValue, - SequenceIncompleteValue, SequenceValue, SubclassValue, TypedDictValue, @@ -156,13 +155,6 @@ def prepare_type(value: Value) -> Value: """Simplify a type to turn it into a suggestion.""" if isinstance(value, AnnotatedValue): return prepare_type(value.value) - elif isinstance(value, SequenceIncompleteValue): - if value.typ is tuple: - return SequenceIncompleteValue( - tuple, [prepare_type(elt) for elt in value.members] - ) - else: - return GenericValue(value.typ, [prepare_type(arg) for arg in value.args]) elif isinstance(value, SequenceValue): if value.typ is tuple: members = value.get_member_sequence() diff --git a/pyanalyze/test.toml b/pyanalyze/test.toml index 1c60e583..a906a1fe 100644 --- a/pyanalyze/test.toml +++ b/pyanalyze/test.toml @@ -6,3 +6,4 @@ constructor_hooks = ["pyanalyze.test_config.get_constructor"] known_signatures = ["pyanalyze.test_config.get_known_signatures"] unwrap_class = ["pyanalyze.test_config.unwrap_class"] stub_path = ["./stubs"] +functions_safe_to_call = ["pyanalyze.tests.make_simple_sequence"] diff --git a/pyanalyze/test_annotations.py b/pyanalyze/test_annotations.py index 7d20736c..118fd493 100644 --- a/pyanalyze/test_annotations.py +++ b/pyanalyze/test_annotations.py @@ -11,7 +11,6 @@ KnownValue, MultiValuedValue, NewTypeValue, - SequenceIncompleteValue, SequenceValue, TypeVarValue, TypedDictValue, @@ -19,6 +18,7 @@ SubclassValue, GenericValue, ) +from .tests import make_simple_sequence class TestAnnotations(TestNameCheckVisitorBase): @@ -338,22 +338,11 @@ def capybara( empty: Tuple[()], ) -> None: assert_is_value(x, GenericValue(tuple, [TypedValue(int)])) - assert_is_value(y, SequenceIncompleteValue(tuple, [TypedValue(int)])) - assert_is_value( - z, SequenceIncompleteValue(tuple, [TypedValue(str), TypedValue(int)]) - ) - assert_is_value( - omega, - MultiValuedValue( - [ - SequenceIncompleteValue( - tuple, [TypedValue(str), TypedValue(int)] - ), - KnownValue(None), - ] - ), - ) - assert_is_value(empty, SequenceIncompleteValue(tuple, [])) + assert_is_value(y, make_simple_sequence(tuple, [TypedValue(int)])) + t_str_int = make_simple_sequence(tuple, [TypedValue(str), TypedValue(int)]) + assert_is_value(z, t_str_int) + assert_is_value(omega, t_str_int | KnownValue(None)) + assert_is_value(empty, SequenceValue(tuple, [])) @assert_passes() def test_stringified_tuples(self): @@ -367,22 +356,11 @@ def capybara( empty: "Tuple[()]", ) -> None: assert_is_value(x, GenericValue(tuple, [TypedValue(int)])) - assert_is_value(y, SequenceIncompleteValue(tuple, [TypedValue(int)])) - assert_is_value( - z, SequenceIncompleteValue(tuple, [TypedValue(str), TypedValue(int)]) - ) - assert_is_value( - omega, - MultiValuedValue( - [ - SequenceIncompleteValue( - tuple, [TypedValue(str), TypedValue(int)] - ), - KnownValue(None), - ] - ), - ) - assert_is_value(empty, SequenceIncompleteValue(tuple, [])) + assert_is_value(y, make_simple_sequence(tuple, [TypedValue(int)])) + t_str_int = make_simple_sequence(tuple, [TypedValue(str), TypedValue(int)]) + assert_is_value(z, t_str_int) + assert_is_value(omega, t_str_int | KnownValue(None)) + assert_is_value(empty, SequenceValue(tuple, [])) @skip_before((3, 9)) @assert_passes() @@ -397,22 +375,11 @@ def capybara( empty: tuple[()], ) -> None: assert_is_value(x, GenericValue(tuple, [TypedValue(int)])) - assert_is_value(y, SequenceIncompleteValue(tuple, [TypedValue(int)])) - assert_is_value( - z, SequenceIncompleteValue(tuple, [TypedValue(str), TypedValue(int)]) - ) - assert_is_value( - omega, - MultiValuedValue( - [ - SequenceIncompleteValue( - tuple, [TypedValue(str), TypedValue(int)] - ), - KnownValue(None), - ] - ), - ) - assert_is_value(empty, SequenceIncompleteValue(tuple, [])) + assert_is_value(y, make_simple_sequence(tuple, [TypedValue(int)])) + t_str_int = make_simple_sequence(tuple, [TypedValue(str), TypedValue(int)]) + assert_is_value(z, t_str_int) + assert_is_value(omega, t_str_int | KnownValue(None)) + assert_is_value(empty, SequenceValue(tuple, [])) @assert_passes() def test_invalid_annotation(self): @@ -532,7 +499,7 @@ def f(x: Queue[I]) -> None: def capybara(x: list[int], y: tuple[int, str], z: tuple[int, ...]) -> None: assert_is_value(x, GenericValue(list, [TypedValue(int)])) assert_is_value( - y, SequenceIncompleteValue(tuple, [TypedValue(int), TypedValue(str)]) + y, make_simple_sequence(tuple, [TypedValue(int), TypedValue(str)]) ) assert_is_value(z, GenericValue(tuple, [TypedValue(int)])) diff --git a/pyanalyze/test_async_await.py b/pyanalyze/test_async_await.py index 7a6017ec..15959105 100644 --- a/pyanalyze/test_async_await.py +++ b/pyanalyze/test_async_await.py @@ -1,13 +1,6 @@ # static analysis: ignore -from .value import ( - GenericValue, - KnownValue, - TypedValue, - make_weak, - AnyValue, - AnySource, - SequenceIncompleteValue, -) +from .tests import make_simple_sequence +from .value import GenericValue, KnownValue, TypedValue, make_weak, AnyValue, AnySource from .implementation import assert_is_value from .test_node_visitor import assert_passes, only_before from .test_name_check_visitor import TestNameCheckVisitorBase @@ -251,7 +244,7 @@ async def capybara(): GenericValue( Awaitable, [ - SequenceIncompleteValue( + make_simple_sequence( tuple, [TypedValue(StreamReader), TypedValue(StreamWriter)] ) ], diff --git a/pyanalyze/test_asynq.py b/pyanalyze/test_asynq.py index 6f63ee9e..6dd83671 100644 --- a/pyanalyze/test_asynq.py +++ b/pyanalyze/test_asynq.py @@ -1,4 +1,5 @@ # static analysis: ignore +from .tests import make_simple_sequence from .implementation import assert_is_value from .value import ( AnySource, @@ -9,7 +10,6 @@ TypedValue, GenericValue, DictIncompleteValue, - SequenceIncompleteValue, KVPair, ) from .test_name_check_visitor import TestNameCheckVisitorBase @@ -75,7 +75,7 @@ def caller(ints: Sequence[Literal[0, 1, 2]]): vals1 = yield [square.asynq(1), square.asynq(2), square.asynq(3)] assert_is_value( vals1, - SequenceIncompleteValue( + make_simple_sequence( list, [TypedValue(int), TypedValue(int), TypedValue(int)] ), ) diff --git a/pyanalyze/test_boolability.py b/pyanalyze/test_boolability.py index bcb4d2b4..cfe18b40 100644 --- a/pyanalyze/test_boolability.py +++ b/pyanalyze/test_boolability.py @@ -11,7 +11,6 @@ DictIncompleteValue, KVPair, KnownValue, - SequenceIncompleteValue, SequenceValue, TypedDictValue, UnboundMethodValue, @@ -50,18 +49,6 @@ def test_get_boolability() -> None: assert Boolability.boolable == get_boolability( TypedDictValue({"a": (False, TypedValue(int))}) ) - assert Boolability.type_always_true == get_boolability( - SequenceIncompleteValue(tuple, [KnownValue(1)]) - ) - assert Boolability.value_always_false == get_boolability( - SequenceIncompleteValue(tuple, []) - ) - assert Boolability.value_always_true_mutable == get_boolability( - SequenceIncompleteValue(list, [KnownValue(1)]) - ) - assert Boolability.value_always_false_mutable == get_boolability( - SequenceIncompleteValue(list, []) - ) assert Boolability.type_always_true == get_boolability( SequenceValue(tuple, [(False, KnownValue(1))]) ) diff --git a/pyanalyze/test_format_strings.py b/pyanalyze/test_format_strings.py index 42e36dc2..3fed165d 100644 --- a/pyanalyze/test_format_strings.py +++ b/pyanalyze/test_format_strings.py @@ -16,12 +16,12 @@ AnyValue, KnownValue, DictIncompleteValue, - SequenceIncompleteValue, TypedValue, ) from .test_node_visitor import assert_passes from .test_name_check_visitor import TestNameCheckVisitorBase from .test_value import CTX +from .tests import make_simple_sequence PERCENT_TESTCASES = [ @@ -328,12 +328,12 @@ def test_format_string_tuple(self): ) self.assert_errors( PercentFormatString.from_pattern("%s%s"), - SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]), + make_simple_sequence(list, [KnownValue(1), KnownValue(2)]), [too_few], ) self.assert_errors( PercentFormatString.from_pattern("%s%s"), - SequenceIncompleteValue(tuple, [KnownValue(1)]), + make_simple_sequence(tuple, [KnownValue(1)]), [too_few], ) @@ -343,7 +343,7 @@ def test_format_string_tuple(self): ) self.assert_errors( PercentFormatString.from_pattern("%s"), - SequenceIncompleteValue(tuple, [KnownValue(1), KnownValue(2)]), + make_simple_sequence(tuple, [KnownValue(1), KnownValue(2)]), [too_many], ) @@ -355,12 +355,12 @@ def test_format_string_tuple(self): self.assert_errors(PercentFormatString.from_pattern("%s%%"), KnownValue(1), []) self.assert_errors( PercentFormatString.from_pattern("%s"), - SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]), + make_simple_sequence(list, [KnownValue(1), KnownValue(2)]), [], ) self.assert_errors( PercentFormatString.from_pattern("%s"), - SequenceIncompleteValue(tuple, [KnownValue(1)]), + make_simple_sequence(tuple, [KnownValue(1)]), [], ) diff --git a/pyanalyze/test_implementation.py b/pyanalyze/test_implementation.py index 0a6a7ebd..b4f5f74c 100644 --- a/pyanalyze/test_implementation.py +++ b/pyanalyze/test_implementation.py @@ -1,17 +1,17 @@ # static analysis: ignore from .test_name_check_visitor import TestNameCheckVisitorBase from .test_node_visitor import assert_passes - +from .tests import make_simple_sequence from .value import ( NO_RETURN_VALUE, AnnotatedValue, AnySource, AnyValue, KVPair, + SequenceValue, assert_is_value, CallableValue, GenericValue, - SequenceIncompleteValue, KnownValue, TypedValue, DictIncompleteValue, @@ -197,9 +197,9 @@ def capybara(x, ints: Sequence[Literal[1, 2]]): assert_is_value(tuple(i for i in ints), GenericValue(tuple, [one_two])) assert_is_value(tuple({i: i for i in ints}), GenericValue(tuple, [one_two])) - # SequenceIncompleteValue + # SequenceValue assert_is_value( - tuple([int(x)]), SequenceIncompleteValue(tuple, [TypedValue(int)]) + tuple([int(x)]), make_simple_sequence(tuple, [TypedValue(int)]) ) # fallback @@ -389,10 +389,10 @@ def test_list_append(self): def capybara(x: int): lst = [x] - assert_is_value(lst, SequenceIncompleteValue(list, [TypedValue(int)])) + assert_is_value(lst, make_simple_sequence(list, [TypedValue(int)])) lst.append(1) assert_is_value( - lst, SequenceIncompleteValue(list, [TypedValue(int), KnownValue(1)]) + lst, make_simple_sequence(list, [TypedValue(int), KnownValue(1)]) ) lst2: List[str] = ["x"] @@ -427,10 +427,10 @@ def test_set_add(self): def capybara(x: int): s = {x} - assert_is_value(s, SequenceIncompleteValue(set, [TypedValue(int)])) + assert_is_value(s, make_simple_sequence(set, [TypedValue(int)])) s.add(1) assert_is_value( - s, SequenceIncompleteValue(set, [TypedValue(int), KnownValue(1)]) + s, make_simple_sequence(set, [TypedValue(int), KnownValue(1)]) ) s2: Set[str] = {"x"} @@ -445,11 +445,10 @@ def test_list_add(self): def capybara(x: int, y: str) -> None: assert_is_value( [x] + [y], - SequenceIncompleteValue(list, [TypedValue(int), TypedValue(str)]), + make_simple_sequence(list, [TypedValue(int), TypedValue(str)]), ) assert_is_value( - [x] + [1], - SequenceIncompleteValue(list, [TypedValue(int), KnownValue(1)]), + [x] + [1], make_simple_sequence(list, [TypedValue(int), KnownValue(1)]) ) left: List[int] = [] right: List[str] = [] @@ -487,27 +486,16 @@ def test_list_extend(self): def capybara(x: int, y: str) -> None: lst = [x] - assert_is_value(lst, SequenceIncompleteValue(list, [TypedValue(int)])) + assert_is_value(lst, make_simple_sequence(list, [TypedValue(int)])) lst.extend([y]) assert_is_value( - lst, SequenceIncompleteValue(list, [TypedValue(int), TypedValue(str)]) + lst, make_simple_sequence(list, [TypedValue(int), TypedValue(str)]) ) - # If we extend with a set, don't use a SequenceIncompleteValue any more, - # because we don't know how many values were added or in what order. - # (Technically we do know for a one-element set, but that doesn't seem worth - # writing a special case for.) lst.extend({float(1.0)}) assert_is_value( lst, - make_weak( - GenericValue( - list, - [ - MultiValuedValue( - [TypedValue(int), TypedValue(str), TypedValue(float)] - ) - ], - ) + make_simple_sequence( + list, [TypedValue(int), TypedValue(str), TypedValue(float)] ), ) @@ -522,27 +510,16 @@ def test_list_iadd(self): def capybara(x: int, y: str) -> None: lst = [x] - assert_is_value(lst, SequenceIncompleteValue(list, [TypedValue(int)])) + assert_is_value(lst, make_simple_sequence(list, [TypedValue(int)])) lst += [y] assert_is_value( - lst, SequenceIncompleteValue(list, [TypedValue(int), TypedValue(str)]) + lst, make_simple_sequence(list, [TypedValue(int), TypedValue(str)]) ) - # If we extend with a set, don't use a SequenceIncompleteValue any more, - # because we don't know how many values were added or in what order. - # (Technically we do know for a one-element set, but that doesn't seem worth - # writing a special case for.) lst += {float(1.0)} assert_is_value( lst, - make_weak( - GenericValue( - list, - [ - MultiValuedValue( - [TypedValue(int), TypedValue(str), TypedValue(float)] - ) - ], - ) + make_simple_sequence( + list, [TypedValue(int), TypedValue(str), TypedValue(float)] ), ) @@ -577,61 +554,40 @@ def capybara() -> None: lst.extend(func()) assert_is_value( lst, - make_weak( - GenericValue( - list, - [ - MultiValuedValue( - [ - KnownValue("a"), - KnownValue("b"), - KnownValue("c"), - KnownValue("d"), - ] - ) - ], - ) + SequenceValue( + list, + [ + (False, KnownValue("a")), + (False, KnownValue("b")), + (True, KnownValue("c") | KnownValue("d")), + ], ), ) lst.extend(["e"]) assert_is_value( lst, - make_weak( - GenericValue( - list, - [ - MultiValuedValue( - [ - KnownValue("a"), - KnownValue("b"), - KnownValue("c"), - KnownValue("d"), - KnownValue("e"), - ] - ) - ], - ) + SequenceValue( + list, + [ + (False, KnownValue("a")), + (False, KnownValue("b")), + (True, KnownValue("c") | KnownValue("d")), + (False, KnownValue("e")), + ], ), ) lst.append("f") assert_is_value( lst, - make_weak( - GenericValue( - list, - [ - MultiValuedValue( - [ - KnownValue("a"), - KnownValue("b"), - KnownValue("c"), - KnownValue("d"), - KnownValue("e"), - KnownValue("f"), - ] - ) - ], - ) + SequenceValue( + list, + [ + (False, KnownValue("a")), + (False, KnownValue("b")), + (True, KnownValue("c") | KnownValue("d")), + (False, KnownValue("e")), + (False, KnownValue("f")), + ], ), ) @@ -645,24 +601,20 @@ def capybara(arg) -> None: lst2 = [*lst1, "b"] assert_is_value( lst2, - make_weak( - GenericValue( - list, [MultiValuedValue([KnownValue("a"), KnownValue("b")])] - ) + SequenceValue( + list, [(True, KnownValue("a")), (False, KnownValue("b"))] ), ) lst2.append("c") assert_is_value( lst2, - make_weak( - GenericValue( - list, - [ - MultiValuedValue( - [KnownValue("a"), KnownValue("b"), KnownValue("c")] - ) - ], - ) + SequenceValue( + list, + [ + (True, KnownValue("a")), + (False, KnownValue("b")), + (False, KnownValue("c")), + ], ), ) @@ -902,7 +854,7 @@ def capybara(lst: List[int], i: int, s: slice, unannotated) -> None: assert_is_value(empty[0], AnyValue(AnySource.unreachable)) assert_is_value(empty[1:], KnownValue([])) assert_is_value(empty[i], AnyValue(AnySource.unreachable)) - assert_is_value(empty[s], SequenceIncompleteValue(list, [])) + assert_is_value(empty[s], SequenceValue(list, [])) assert_is_value(empty[unannotated], AnyValue(AnySource.from_another)) known = [1, 2] @@ -913,7 +865,7 @@ def capybara(lst: List[int], i: int, s: slice, unannotated) -> None: assert_is_value(known[::-1], KnownValue([2, 1])) assert_is_value(known[i], KnownValue(1) | KnownValue(2)) assert_is_value( - known[s], SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]) + known[s], make_simple_sequence(list, [KnownValue(1), KnownValue(2)]) ) assert_is_value(known[unannotated], AnyValue(AnySource.from_another)) @@ -934,7 +886,7 @@ def capybara(tpl: Tuple[int, ...], i: int, s: slice, unannotated) -> None: assert_is_value(empty[0], AnyValue(AnySource.error)) # E: incompatible_call assert_is_value(empty[1:], KnownValue(())) assert_is_value(empty[i], AnyValue(AnySource.unreachable)) - assert_is_value(empty[s], SequenceIncompleteValue(tuple, [])) + assert_is_value(empty[s], SequenceValue(tuple, [])) assert_is_value(empty[unannotated], AnyValue(AnySource.from_another)) known = (1, 2) @@ -947,7 +899,7 @@ def capybara(tpl: Tuple[int, ...], i: int, s: slice, unannotated) -> None: assert_is_value(known[::-1], KnownValue((2, 1))) assert_is_value(known[i], KnownValue(1) | KnownValue(2)) assert_is_value( - known[s], SequenceIncompleteValue(tuple, [KnownValue(1), KnownValue(2)]) + known[s], make_simple_sequence(tuple, [KnownValue(1), KnownValue(2)]) ) assert_is_value(known[unannotated], AnyValue(AnySource.from_another)) diff --git a/pyanalyze/test_name_check_visitor.py b/pyanalyze/test_name_check_visitor.py index 73e72b96..096f9ef9 100644 --- a/pyanalyze/test_name_check_visitor.py +++ b/pyanalyze/test_name_check_visitor.py @@ -27,7 +27,6 @@ MultiValuedValue, NewTypeValue, ReferencingValue, - SequenceIncompleteValue, SequenceValue, TypedValue, TypeVarValue, @@ -41,7 +40,13 @@ TypedDictValue, make_weak, ) -from .tests import proxied_fn, autogenerated, l0cached_async_fn, PropertyObject +from .tests import ( + proxied_fn, + autogenerated, + l0cached_async_fn, + PropertyObject, + make_simple_sequence, +) from . import test_node_visitor from .test_node_visitor import assert_passes, assert_fails, only_before, skip_before @@ -132,7 +137,6 @@ def _make_module(code_str: str) -> types.ModuleType: MultiValuedValue=MultiValuedValue, AnnotatedValue=AnnotatedValue, SequenceValue=SequenceValue, - SequenceIncompleteValue=SequenceIncompleteValue, TypedValue=TypedValue, UnboundMethodValue=UnboundMethodValue, AnySource=AnySource, @@ -146,6 +150,7 @@ def _make_module(code_str: str) -> types.ModuleType: TypeVarValue=TypeVarValue, dump_value=dump_value, make_weak=make_weak, + make_simple_sequence=make_simple_sequence, UNINITIALIZED_VALUE=UNINITIALIZED_VALUE, NO_RETURN_VALUE=NO_RETURN_VALUE, ) @@ -478,16 +483,14 @@ def test_display_type_inference(self): def capybara(a, b): x = [a, b] - assert_is_value( - x, SequenceIncompleteValue(list, [UNANNOTATED, UNANNOTATED]) - ) + assert_is_value(x, make_simple_sequence(list, [UNANNOTATED, UNANNOTATED])) y = a, 2 assert_is_value( - y, SequenceIncompleteValue(tuple, [UNANNOTATED, KnownValue(2)]) + y, make_simple_sequence(tuple, [UNANNOTATED, KnownValue(2)]) ) s = {a, b} - assert_is_value(s, SequenceIncompleteValue(set, [UNANNOTATED, UNANNOTATED])) + assert_is_value(s, make_simple_sequence(set, [UNANNOTATED, UNANNOTATED])) z = {a: b} assert_is_value( z, DictIncompleteValue(dict, [KVPair(UNANNOTATED, UNANNOTATED)]) @@ -1093,7 +1096,7 @@ def capybara(x): lst = [1, 2, int(x)] assert_is_value( lst, - SequenceIncompleteValue( + make_simple_sequence( list, [KnownValue(1), KnownValue(2), TypedValue(int)] ), ) @@ -1117,7 +1120,7 @@ def capybara(ints: Sequence[Literal[1, 2]]): lst2 = [x for x in (1, 2)] assert_is_value( - lst2, SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]) + lst2, make_simple_sequence(list, [KnownValue(1), KnownValue(2)]) ) lst3 = [i + j * 10 for i in range(2) for j in range(3)] @@ -1260,7 +1263,7 @@ def capybara(oid): UnboundMethodValue( "append", Composite( - SequenceIncompleteValue(list, [AnyValue(AnySource.unannotated)]) + make_simple_sequence(list, [AnyValue(AnySource.unannotated)]) ), ), ) @@ -1545,10 +1548,12 @@ def capybara(x): degu = (1, *x) assert_is_value( degu, - make_weak( - GenericValue( - tuple, [KnownValue(1) | AnyValue(AnySource.generic_argument)] - ) + SequenceValue( + tuple, + [ + (False, KnownValue(1)), + (True, AnyValue(AnySource.generic_argument)), + ], ), ) @@ -1613,7 +1618,7 @@ def run(lst: List[int], union: Union[Any, List[int], Tuple[str, float]]): *i, j, k = long_tuple assert_is_value( i, - SequenceIncompleteValue( + make_simple_sequence( list, [KnownValue(1), KnownValue(2), KnownValue(3), KnownValue(4)] ), ) @@ -1623,7 +1628,7 @@ def run(lst: List[int], union: Union[Any, List[int], Tuple[str, float]]): assert_is_value(l, KnownValue(1)) assert_is_value(m, KnownValue(2)) assert_is_value( - n, SequenceIncompleteValue(list, [KnownValue(3), KnownValue(4)]) + n, make_simple_sequence(list, [KnownValue(3), KnownValue(4)]) ) assert_is_value(o, KnownValue(5)) assert_is_value(p, KnownValue(6)) @@ -1631,7 +1636,7 @@ def run(lst: List[int], union: Union[Any, List[int], Tuple[str, float]]): q, r, *s = (1, 2) assert_is_value(q, KnownValue(1)) assert_is_value(r, KnownValue(2)) - assert_is_value(s, SequenceIncompleteValue(list, [])) + assert_is_value(s, SequenceValue(list, [])) for sprime in []: assert_is_value(sprime, NO_RETURN_VALUE) diff --git a/pyanalyze/test_patma.py b/pyanalyze/test_patma.py index a1c94f2d..d6ed663c 100644 --- a/pyanalyze/test_patma.py +++ b/pyanalyze/test_patma.py @@ -53,7 +53,7 @@ def capybara(seq: Tuple[int, ...], obj: object): case [1, 2, 3]: assert_is_value( seq, - SequenceIncompleteValue( + make_simple_sequence( tuple, [TypedValue(int), TypedValue(int), TypedValue(int)] ) diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py index d10dabd3..181f2c0a 100644 --- a/pyanalyze/test_signature.py +++ b/pyanalyze/test_signature.py @@ -1,6 +1,7 @@ # static analysis: ignore from collections.abc import Sequence + from .value import ( AnnotatedValue, AnySource, @@ -8,7 +9,6 @@ CanAssignError, GenericValue, KnownValue, - SequenceIncompleteValue, TypedDictValue, TypedValue, make_weak, @@ -25,6 +25,7 @@ ParameterKind as K, ) from .test_value import CTX +from .tests import make_simple_sequence TupleInt = GenericValue(tuple, [TypedValue(int)]) TupleBool = GenericValue(tuple, [TypedValue(bool)]) @@ -292,7 +293,7 @@ def test_advanced_var_positional(self) -> None: ) object_int = P( "args", - annotation=SequenceIncompleteValue( + annotation=make_simple_sequence( tuple, [TypedValue(object), TypedValue(int)] ), kind=K.VAR_POSITIONAL, diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py index 7daa37ba..67a801a6 100644 --- a/pyanalyze/test_typeshed.py +++ b/pyanalyze/test_typeshed.py @@ -23,6 +23,7 @@ from .test_node_visitor import assert_passes from .signature import OverloadedSignature, SigParameter, Signature from .test_arg_spec import ClassWithCall +from .tests import make_simple_sequence from .typeshed import TypeshedFinder from .value import ( CallableValue, @@ -38,7 +39,6 @@ make_weak, TypeVarValue, UNINITIALIZED_VALUE, - SequenceIncompleteValue, Value, ) @@ -386,7 +386,7 @@ def test_callable(self): def test_dict_items(self): TInt = TypedValue(int) TStr = TypedValue(str) - TTuple = SequenceIncompleteValue(tuple, [TInt, TStr]) + TTuple = make_simple_sequence(tuple, [TInt, TStr]) self.check( { "_collections_abc.dict_items": [TInt, TStr], @@ -445,7 +445,7 @@ def test_context_manager(self): def test_collections(self): int_tv = TypedValue(int) str_tv = TypedValue(str) - int_str_tuple = SequenceIncompleteValue(tuple, [int_tv, str_tv]) + int_str_tuple = make_simple_sequence(tuple, [int_tv, str_tv]) self.check( { collections.abc.ValuesView: [int_tv], diff --git a/pyanalyze/test_value.py b/pyanalyze/test_value.py index b878400d..61073543 100644 --- a/pyanalyze/test_value.py +++ b/pyanalyze/test_value.py @@ -30,7 +30,6 @@ TypedValue, MultiValuedValue, SubclassValue, - SequenceIncompleteValue, TypeVarMap, concrete_values_from_iterable, unite_and_simplify, @@ -221,22 +220,36 @@ def test_generic_value() -> None: ) -def test_sequence_incomplete_value() -> None: - val = value.SequenceIncompleteValue(tuple, [TypedValue(int), TypedValue(str)]) +def test_sequence_value() -> None: + val = value.SequenceValue( + tuple, [(False, TypedValue(int)), (False, TypedValue(str))] + ) assert_can_assign(val, TypedValue(tuple)) assert_can_assign(val, GenericValue(tuple, [TypedValue(int) | TypedValue(str)])) assert_cannot_assign(val, GenericValue(tuple, [TypedValue(int) | TypedValue(list)])) assert_can_assign(val, val) - assert_cannot_assign(val, value.SequenceIncompleteValue(tuple, [TypedValue(int)])) + assert_cannot_assign(val, value.SequenceValue(tuple, [(False, TypedValue(int))])) assert_can_assign( - val, value.SequenceIncompleteValue(tuple, [TypedValue(bool), TypedValue(str)]) + val, + value.SequenceValue( + tuple, [(False, TypedValue(bool)), (False, TypedValue(str))] + ), ) - assert "tuple[int, str]" == str(val) - assert "tuple[int]" == str(value.SequenceIncompleteValue(tuple, [TypedValue(int)])) - assert "" == str( - value.SequenceIncompleteValue(list, [TypedValue(int)]) + assert str(val) == "tuple[int, str]" + assert str(value.SequenceValue(tuple, [(False, TypedValue(int))])) == "tuple[int]" + assert ( + str( + value.SequenceValue( + tuple, [(False, TypedValue(int)), (True, TypedValue(str))] + ) + ) + == "tuple[int, *tuple[str, ...]]" + ) + assert ( + str(value.SequenceValue(list, [(False, TypedValue(int))])) + == "" ) @@ -503,13 +516,14 @@ def test_io() -> None: def test_concrete_values_from_iterable() -> None: assert isinstance(concrete_values_from_iterable(KnownValue(1), CTX), CanAssignError) - assert () == concrete_values_from_iterable(KnownValue(()), CTX) - assert (KnownValue(1), KnownValue(2)) == concrete_values_from_iterable( - KnownValue((1, 2)), CTX - ) - assert (KnownValue(1), KnownValue(2)) == concrete_values_from_iterable( - SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]), CTX - ) + assert concrete_values_from_iterable(KnownValue(()), CTX) == [] + assert concrete_values_from_iterable(KnownValue((1, 2)), CTX) == [ + KnownValue(1), + KnownValue(2), + ] + assert concrete_values_from_iterable( + tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]), CTX + ) == [KnownValue(1), KnownValue(2)] assert TypedValue(int) == concrete_values_from_iterable( GenericValue(list, [TypedValue(int)]), CTX ) @@ -519,7 +533,7 @@ def test_concrete_values_from_iterable() -> None: ] == concrete_values_from_iterable( MultiValuedValue( [ - SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]), + tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]), KnownValue((3, 4)), ] ), @@ -530,7 +544,7 @@ def test_concrete_values_from_iterable() -> None: ) == concrete_values_from_iterable( MultiValuedValue( [ - SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]), + tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]), GenericValue(list, [TypedValue(int)]), ] ), @@ -541,7 +555,7 @@ def test_concrete_values_from_iterable() -> None: ) == concrete_values_from_iterable( MultiValuedValue( [ - SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]), + tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]), KnownValue((3,)), ] ), diff --git a/pyanalyze/tests.py b/pyanalyze/tests.py index fafab14b..92a4f110 100644 --- a/pyanalyze/tests.py +++ b/pyanalyze/tests.py @@ -5,12 +5,12 @@ """ -from typing import ClassVar, Union, overload, NoReturn +from typing import ClassVar, Sequence, Union, overload, NoReturn from asynq import asynq, async_proxy, AsyncTask, ConstFuture, get_async_fn, result from asynq.decorators import AsyncDecorator import qcore -from .value import VariableNameValue +from .value import SequenceValue, VariableNameValue, Value ASYNQ_METHOD_NAME = "asynq" ASYNQ_METHOD_NAMES = ("asynq",) @@ -235,3 +235,7 @@ def overloaded(*args: str) -> Union[int, str]: def assert_never(arg: NoReturn) -> NoReturn: raise RuntimeError("no way") + + +def make_simple_sequence(typ: type, vals: Sequence[Value]) -> SequenceValue: + return SequenceValue(typ, [(False, val) for val in vals]) diff --git a/pyanalyze/type_evaluation.py b/pyanalyze/type_evaluation.py index 5461c0cb..6fc698be 100644 --- a/pyanalyze/type_evaluation.py +++ b/pyanalyze/type_evaluation.py @@ -33,7 +33,6 @@ VarnameWithOrigin, constrain_value, ) -from .safe import all_of_type from .value import ( NO_RETURN_VALUE, BoundsMap, @@ -42,7 +41,6 @@ CanAssignError, KnownValue, MultiValuedValue, - SequenceIncompleteValue, SequenceValue, Value, flatten_values, @@ -602,12 +600,6 @@ def visit_BoolOp(self, node: ast.BoolOp) -> ConditionReturn: def evaluate_literal(self, node: ast.expr) -> Optional[KnownValue]: val = self.evaluator.evaluate_value(node) - if ( - isinstance(val, SequenceIncompleteValue) - and isinstance(val.typ, type) - and all_of_type(val.members, KnownValue) - ): - val = KnownValue(val.typ(elt.val for elt in val.members)) if isinstance(val, SequenceValue): val = val.make_known_value() if isinstance(val, KnownValue): diff --git a/pyanalyze/value.py b/pyanalyze/value.py index fc8c9937..775589a6 100644 --- a/pyanalyze/value.py +++ b/pyanalyze/value.py @@ -51,6 +51,7 @@ def function(x: int, y: list[int], z: Any): import pyanalyze from pyanalyze.extensions import CustomCheck +from .find_unused import used from .safe import all_of_type, safe_equals, safe_issubclass, safe_isinstance T = TypeVar("T") @@ -848,7 +849,7 @@ def maybe_specify_error( can_assign = expected.can_assign(value, ctx) if isinstance(can_assign, CanAssignError): return CanAssignError(f"In TypedDict key {key!r}", [can_assign]) - elif isinstance(other, SequenceIncompleteValue) and self.typ in { + elif isinstance(other, SequenceValue) and self.typ in { list, set, tuple, @@ -858,7 +859,7 @@ def maybe_specify_error( collections.abc.Container, collections.abc.Collection, }: - for i, key in enumerate(other.members): + for i, (_, key) in enumerate(other.members): can_assign = expected.can_assign(key, ctx) if isinstance(can_assign, CanAssignError): return CanAssignError(f"In element {i}", [can_assign]) @@ -938,43 +939,14 @@ def make_or_known( if is_many or not isinstance(member, KnownValue): return SequenceValue(typ, members) known_members.append(member.val) - return KnownValue(typ(known_members)) + try: + return KnownValue(typ(known_members)) + except TypeError: + # Probably an unhashable object in a set. + return SequenceValue(typ, members) def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign: - if isinstance(other, SequenceIncompleteValue): - can_assign = self.get_type_object(ctx).can_assign(self, other, ctx) - if isinstance(can_assign, CanAssignError): - return CanAssignError( - f"Cannot assign {stringify_object(other.typ)} to" - f" {stringify_object(self.typ)}" - ) - my_len = len(self.members) - their_len = len(other.members) - if my_len != their_len: - type_str = stringify_object(self.typ) - return CanAssignError( - f"Cannot assign {type_str} of length {their_len} to {type_str} of" - f" length {my_len}" - ) - if my_len == 0: - return {} # they're both empty - bounds_maps = [can_assign] - for i, ((is_many, my_member), their_member) in enumerate( - zip(self.members, other.members) - ): - if is_many: - return CanAssignError( - f"Member {i} is an unpacked type, but a non-unpacked type is" - " provided" - ) - can_assign = my_member.can_assign(their_member, ctx) - if isinstance(can_assign, CanAssignError): - return CanAssignError( - f"Types for member {i} are incompatible", [can_assign] - ) - bounds_maps.append(can_assign) - return unify_bounds_maps(bounds_maps) - elif isinstance(other, SequenceValue): + if isinstance(other, SequenceValue): can_assign = self.get_type_object(ctx).can_assign(self, other, ctx) if isinstance(can_assign, CanAssignError): return CanAssignError( @@ -1052,6 +1024,7 @@ def simplify(self) -> GenericValue: # TODO(jelle): Replace with SequenceValue +@used # for compatibility for now @dataclass(unsafe_hash=True, init=False) class SequenceIncompleteValue(GenericValue): """A :class:`TypedValue` subclass representing a sequence of known type and length. @@ -2429,8 +2402,6 @@ def concrete_values_from_iterable( if not value_subvals and len(set(map(len, seq_subvals))) == 1: return [unite_values(*vals) for vals in zip(*seq_subvals)] return unite_values(*value_subvals, *chain.from_iterable(seq_subvals)) - if isinstance(value, SequenceIncompleteValue): - return value.members if isinstance(value, SequenceValue): members = value.get_member_sequence() if members is None: @@ -2631,18 +2602,7 @@ def unpack_values( # iterable approach. We experimented both with treating lists # like tuples and with always falling back, and both approaches # led to false positives. - if isinstance(value, SequenceIncompleteValue): - if value.typ is tuple: - return _unpack_value_sequence( - value, value.members, target_length, post_starred_length - ) - elif value.typ is list: - vals = _unpack_value_sequence( - value, value.members, target_length, post_starred_length - ) - if not isinstance(vals, CanAssignError): - return vals - elif isinstance(value, SequenceValue): + if isinstance(value, SequenceValue): if value.typ is tuple: return _unpack_sequence_value(value, target_length, post_starred_length) elif value.typ is list: @@ -2710,7 +2670,7 @@ def _unpack_sequence_value( return CanAssignError(f"{value} must have exactly {target_length} elements") middle_length = remaining_target_length - len(tail) fallback_value = unite_values(*[val for _, val in remaining_members]) - return [*head, *[fallback_value for _ in range(middle_length)], *tail] + return [*head, *[fallback_value for _ in range(middle_length)], *reversed(tail)] else: while len(tail) < post_starred_length: if len(tail) >= len(value.members) - len(head): @@ -2741,40 +2701,10 @@ def _unpack_sequence_value( *[fallback_value for _ in range(remaining_target_length)], GenericValue(list, [fallback_value]), *[fallback_value for _ in range(remaining_post_starred_length)], - *tail, + *reversed(tail), ] else: - return [*head, SequenceValue(list, remaining_members), *tail] - - -def _unpack_value_sequence( - value: Value, - members: Sequence[Value], - target_length: int, - post_starred_length: Optional[int], -) -> Union[Sequence[Value], CanAssignError]: - actual_length = len(members) - if post_starred_length is None: - if actual_length != target_length: - return CanAssignError( - f"{value} is of length {actual_length} (expected {target_length})" - ) - return members - if actual_length < target_length + post_starred_length: - return CanAssignError( - f"{value} is of length {actual_length} (expected at least" - f" {target_length + post_starred_length})" - ) - head = members[:target_length] - if post_starred_length > 0: - body = SequenceIncompleteValue( - list, members[target_length:-post_starred_length] - ) - tail = members[-post_starred_length:] - else: - body = SequenceIncompleteValue(list, members[target_length:]) - tail = [] - return [*head, body, *tail] + return [*head, SequenceValue(list, remaining_members), *reversed(tail)] def replace_known_sequence_value(value: Value) -> Value: @@ -2785,7 +2715,7 @@ def replace_known_sequence_value(value: Value) -> Value: - Replace AnnotatedValue with its inner type - Replace TypeVarValue with its fallback type - Replace KnownValues representing list, tuples, sets, or dicts with - SequenceIncompleteValue or DictIncompleteValue. + SequenceValue or DictIncompleteValue. """ if isinstance(value, AnnotatedValue): @@ -2794,8 +2724,8 @@ def replace_known_sequence_value(value: Value) -> Value: return replace_known_sequence_value(value.get_fallback_value()) if isinstance(value, KnownValue): if isinstance(value.val, (list, tuple, set)): - return SequenceIncompleteValue( - type(value.val), [KnownValue(elt) for elt in value.val] + return SequenceValue( + type(value.val), [(False, KnownValue(elt)) for elt in value.val] ) elif isinstance(value.val, dict): return DictIncompleteValue(