-
- Notifications
You must be signed in to change notification settings - Fork 3.1k
Implement TypeIs (PEP 742) #16898
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
Implement TypeIs (PEP 742) #16898
Changes from 21 commits
8a30073 58e8403 c8d2af8 f205910 75c9dec 4666486 25a9c79 faa4a07 c0e0210 f107e5b 34700bb 065ec92 aef3036 4b19c77 6b0e749 c9e53e6 909e53c eb88371 1b1e368 84c69d2 ae294bf 7fedbcf dbc229d 8b2fb0b 816fd1a d6fcc35 ef825ce d32956d a36a16a b32ba80 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -1209,6 +1209,22 @@ def check_func_def( | |
| # visible from *inside* of this function/method. | ||
| ref_type: Type | None = self.scope.active_self_type() | ||
| | ||
| if typ.type_is: | ||
| arg_index = 0 | ||
| # For methods and classmethods, we want the second parameter | ||
| if ref_type is not None and (not defn.is_static or defn.name == "__new__"): | ||
| arg_index = 1 | ||
| if arg_index < len(typ.arg_types) and not is_subtype( | ||
| typ.type_is, typ.arg_types[arg_index] | ||
| ): | ||
| self.fail( | ||
| message_registry.TYPE_NARROWER_NOT_SUBTYPE.format( | ||
| format_type(typ.type_is, self.options), | ||
| format_type(typ.arg_types[arg_index], self.options), | ||
| ), | ||
| item, | ||
| ) | ||
| | ||
| # Store argument types. | ||
| for i in range(len(typ.arg_types)): | ||
| arg_type = typ.arg_types[i] | ||
| | @@ -2177,6 +2193,8 @@ def check_override( | |
| elif isinstance(original, CallableType) and isinstance(override, CallableType): | ||
| if original.type_guard is not None and override.type_guard is None: | ||
| fail = True | ||
| if original.type_is is not None and override.type_is is None: | ||
| fail = True | ||
| | ||
| if is_private(name): | ||
| fail = False | ||
| | @@ -5629,7 +5647,7 @@ def combine_maps(list_maps: list[TypeMap]) -> TypeMap: | |
| def find_isinstance_check(self, node: Expression) -> tuple[TypeMap, TypeMap]: | ||
| """Find any isinstance checks (within a chain of ands). Includes | ||
| implicit and explicit checks for None and calls to callable. | ||
| Also includes TypeGuard functions. | ||
| Also includes TypeGuard and TypeIs functions. | ||
| | ||
| Return value is a map of variables to their types if the condition | ||
| is true and a map of variables to their types if the condition is false. | ||
| | @@ -5681,7 +5699,7 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM | |
| if literal(expr) == LITERAL_TYPE and attr and len(attr) == 1: | ||
| return self.hasattr_type_maps(expr, self.lookup_type(expr), attr[0]) | ||
| elif isinstance(node.callee, RefExpr): | ||
| if node.callee.type_guard is not None: | ||
| if node.callee.type_guard is not None or node.callee.type_is is not None: | ||
| # TODO: Follow *args, **kwargs | ||
| if node.arg_kinds[0] != nodes.ARG_POS: | ||
| # the first argument might be used as a kwarg | ||
| | @@ -5707,15 +5725,31 @@ def find_isinstance_check_helper(self, node: Expression) -> tuple[TypeMap, TypeM | |
| # we want the idx-th variable to be narrowed | ||
| expr = collapse_walrus(node.args[idx]) | ||
| else: | ||
| self.fail(message_registry.TYPE_GUARD_POS_ARG_REQUIRED, node) | ||
| kind = ( | ||
| "guard" if node.callee.type_guard is not None else "narrower" | ||
| ) | ||
| self.fail( | ||
| message_registry.TYPE_GUARD_POS_ARG_REQUIRED.format(kind), node | ||
| ) | ||
| return {}, {} | ||
| if literal(expr) == LITERAL_TYPE: | ||
| # Note: we wrap the target type, so that we can special case later. | ||
| # Namely, for isinstance() we use a normal meet, while TypeGuard is | ||
| # considered "always right" (i.e. even if the types are not overlapping). | ||
| # Also note that a care must be taken to unwrap this back at read places | ||
| # where we use this to narrow down declared type. | ||
| return {expr: TypeGuardedType(node.callee.type_guard)}, {} | ||
| if node.callee.type_guard is not None: | ||
| return {expr: TypeGuardedType(node.callee.type_guard)}, {} | ||
| else: | ||
| assert node.callee.type_is is not None | ||
| return conditional_types_to_typemaps( | ||
| Member Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the crucial part of the change, which implements the new type narrowing behavior. It's the same as | ||
| expr, | ||
| *self.conditional_types_with_intersection( | ||
| self.lookup_type(expr), | ||
| [TypeRange(node.callee.type_is, is_upper_bound=False)], | ||
| expr, | ||
| ), | ||
| ) | ||
| elif isinstance(node, ComparisonExpr): | ||
| # Step 1: Obtain the types of each operand and whether or not we can | ||
| # narrow their types. (For example, we shouldn't try narrowing the | ||
| | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -1018,10 +1018,22 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]: | |
| param_spec = template.param_spec() | ||
| | ||
| template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type | ||
| if template.type_guard is not None: | ||
| if template.type_guard is not None and cactual.type_guard is not None: | ||
| Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, thanks! I think this should fix a thing I ran into while testing #16939 | ||
| template_ret_type = template.type_guard | ||
| if cactual.type_guard is not None: | ||
| cactual_ret_type = cactual.type_guard | ||
| elif template.type_guard is not None: | ||
| template_ret_type = AnyType(TypeOfAny.special_form) | ||
| elif cactual.type_guard is not None: | ||
| cactual_ret_type = AnyType(TypeOfAny.special_form) | ||
| | ||
| if template.type_is is not None and cactual.type_is is not None: | ||
| template_ret_type = template.type_is | ||
| cactual_ret_type = cactual.type_is | ||
| elif template.type_is is not None: | ||
| template_ret_type = AnyType(TypeOfAny.special_form) | ||
| elif cactual.type_is is not None: | ||
| cactual_ret_type = AnyType(TypeOfAny.special_form) | ||
| | ||
| res.extend(infer_constraints(template_ret_type, cactual_ret_type, self.direction)) | ||
| | ||
| if param_spec is None: | ||
| | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -281,5 +281,11 @@ def __hash__(self) -> int: | |
| sub_code_of=MISC, | ||
| ) | ||
| | ||
| TYPE_NARROWER_NOT_SUBTYPE: Final[ErrorCode] = ErrorCode( | ||
| "type-is-not-subtype", | ||
| ||
| "Warn if a TypeIs function's narrowed type is not a subtype of the original type", | ||
| "General", | ||
| ) | ||
| | ||
| # This copy will not include any error codes defined later in the plugins. | ||
| mypy_error_codes = error_codes.copy() | ||
Uh oh!
There was an error while loading. Please reload this page.