Skip to content

Commit 701cfd1

Browse files
committed
Merge branch 'main' into dcreager/infer-function-calls
* main: [red-knot] Specialize `str.startswith` for string literals (#17351) [syntax-errors] `yield`, `yield from`, and `await` outside functions (#17298) [red-knot] Refresh diagnostics when changing related files (#17350) Add `Checker::import_from_typing` (#17340) Don't add chaperone space after escaped quote in triple quote (#17216) [red-knot] Silence errors in unreachable type annotations / class bases (#17342)
2 parents 0744540 + 47956db commit 701cfd1

35 files changed

+1119
-264
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# `str.startswith`
2+
3+
We special-case `str.startswith` to allow inference of precise Boolean literal types, because those
4+
are used in [`sys.platform` checks].
5+
6+
```py
7+
reveal_type("abc".startswith("")) # revealed: Literal[True]
8+
reveal_type("abc".startswith("a")) # revealed: Literal[True]
9+
reveal_type("abc".startswith("ab")) # revealed: Literal[True]
10+
reveal_type("abc".startswith("abc")) # revealed: Literal[True]
11+
12+
reveal_type("abc".startswith("abcd")) # revealed: Literal[False]
13+
reveal_type("abc".startswith("bc")) # revealed: Literal[False]
14+
15+
reveal_type("AbC".startswith("")) # revealed: Literal[True]
16+
reveal_type("AbC".startswith("A")) # revealed: Literal[True]
17+
reveal_type("AbC".startswith("Ab")) # revealed: Literal[True]
18+
reveal_type("AbC".startswith("AbC")) # revealed: Literal[True]
19+
20+
reveal_type("AbC".startswith("a")) # revealed: Literal[False]
21+
reveal_type("AbC".startswith("aB")) # revealed: Literal[False]
22+
23+
reveal_type("".startswith("")) # revealed: Literal[True]
24+
25+
reveal_type("".startswith(" ")) # revealed: Literal[False]
26+
```
27+
28+
Make sure that we fall back to `bool` for more complex cases:
29+
30+
```py
31+
reveal_type("abc".startswith("b", 1)) # revealed: bool
32+
reveal_type("abc".startswith("bc", 1, 3)) # revealed: bool
33+
34+
reveal_type("abc".startswith(("a", "x"))) # revealed: bool
35+
```
36+
37+
And similiarly, we should still infer `bool` if the instance or the prefix are not string literals:
38+
39+
```py
40+
from typing_extensions import LiteralString
41+
42+
def _(string_instance: str, literalstring: LiteralString):
43+
reveal_type(string_instance.startswith("a")) # revealed: bool
44+
reveal_type(literalstring.startswith("a")) # revealed: bool
45+
46+
reveal_type("a".startswith(string_instance)) # revealed: bool
47+
reveal_type("a".startswith(literalstring)) # revealed: bool
48+
```
49+
50+
[`sys.platform` checks]: https://docs.python.org/3/library/sys.html#sys.platform

crates/red_knot_python_semantic/resources/mdtest/sys_platform.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ reveal_type(sys.platform) # revealed: Literal["linux"]
3131

3232
## Testing for a specific platform
3333

34-
### Exact comparison
35-
3634
```toml
3735
[environment]
3836
python-platform = "freebsd8"
3937
```
4038

39+
### Exact comparison
40+
4141
```py
4242
import sys
4343

@@ -48,11 +48,11 @@ reveal_type(sys.platform == "linux") # revealed: Literal[False]
4848
### Substring comparison
4949

5050
It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to use
51-
`sys.platform.startswith(...)` for platform checks. This is not yet supported in type inference:
51+
`sys.platform.startswith(...)` for platform checks:
5252

5353
```py
5454
import sys
5555

56-
reveal_type(sys.platform.startswith("freebsd")) # revealed: bool
57-
reveal_type(sys.platform.startswith("linux")) # revealed: bool
56+
reveal_type(sys.platform.startswith("freebsd")) # revealed: Literal[True]
57+
reveal_type(sys.platform.startswith("linux")) # revealed: Literal[False]
5858
```

crates/red_knot_python_semantic/resources/mdtest/unreachable.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -447,15 +447,16 @@ We should not show any diagnostics in type annotations inside unreachable sectio
447447

448448
```py
449449
def _():
450-
class C: ...
450+
class C:
451+
class Inner: ...
452+
451453
return
452454

453-
# TODO
454-
# error: [invalid-type-form] "Variable of type `Never` is not allowed in a type expression"
455-
c: C = C()
455+
c1: C = C()
456+
c2: C.Inner = C.Inner()
457+
c3: tuple[C, C] = (C(), C())
458+
c4: tuple[C.Inner, C.Inner] = (C.Inner(), C.Inner())
456459

457-
# TODO
458-
# error: [invalid-base] "Invalid class base with type `Never` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
459460
class Sub(C): ...
460461
```
461462

crates/red_knot_python_semantic/src/types.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2508,6 +2508,10 @@ impl<'db> Type<'db> {
25082508
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)),
25092509
)
25102510
.into(),
2511+
Type::StringLiteral(literal) if name == "startswith" => Symbol::bound(
2512+
Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)),
2513+
)
2514+
.into(),
25112515

25122516
Type::ClassLiteral(class)
25132517
if name == "__get__" && class.is_known(db, KnownClass::FunctionType) =>
@@ -3112,6 +3116,34 @@ impl<'db> Type<'db> {
31123116
))
31133117
}
31143118

3119+
Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => {
3120+
Signatures::single(CallableSignature::single(
3121+
self,
3122+
Signature::new(
3123+
Parameters::new([
3124+
Parameter::positional_only(Some(Name::new_static("prefix")))
3125+
.with_annotated_type(UnionType::from_elements(
3126+
db,
3127+
[
3128+
KnownClass::Str.to_instance(db),
3129+
// TODO: tuple[str, ...]
3130+
KnownClass::Tuple.to_instance(db),
3131+
],
3132+
)),
3133+
Parameter::positional_only(Some(Name::new_static("start")))
3134+
// TODO: SupportsIndex | None
3135+
.with_annotated_type(Type::object(db))
3136+
.with_default_type(Type::none(db)),
3137+
Parameter::positional_only(Some(Name::new_static("end")))
3138+
// TODO: SupportsIndex | None
3139+
.with_annotated_type(Type::object(db))
3140+
.with_default_type(Type::none(db)),
3141+
]),
3142+
Some(KnownClass::Bool.to_instance(db)),
3143+
),
3144+
))
3145+
}
3146+
31153147
Type::FunctionLiteral(function_type) => match function_type.known(db) {
31163148
Some(
31173149
KnownFunction::IsEquivalentTo
@@ -4293,6 +4325,7 @@ impl<'db> Type<'db> {
42934325
| Type::AlwaysTruthy
42944326
| Type::AlwaysFalsy
42954327
| Type::WrapperDescriptor(_)
4328+
| Type::MethodWrapper(MethodWrapperKind::StrStartswith(_))
42964329
| Type::ModuleLiteral(_)
42974330
// A non-generic class never needs to be specialized. A generic class is specialized
42984331
// explicitly (via a subscript expression) or implicitly (via a call), and not because
@@ -4530,17 +4563,24 @@ pub struct InvalidTypeExpressionError<'db> {
45304563
}
45314564

45324565
impl<'db> InvalidTypeExpressionError<'db> {
4533-
fn into_fallback_type(self, context: &InferContext, node: &ast::Expr) -> Type<'db> {
4566+
fn into_fallback_type(
4567+
self,
4568+
context: &InferContext,
4569+
node: &ast::Expr,
4570+
is_reachable: bool,
4571+
) -> Type<'db> {
45344572
let InvalidTypeExpressionError {
45354573
fallback_type,
45364574
invalid_expressions,
45374575
} = self;
4538-
for error in invalid_expressions {
4539-
context.report_lint_old(
4540-
&INVALID_TYPE_FORM,
4541-
node,
4542-
format_args!("{}", error.reason(context.db())),
4543-
);
4576+
if is_reachable {
4577+
for error in invalid_expressions {
4578+
context.report_lint_old(
4579+
&INVALID_TYPE_FORM,
4580+
node,
4581+
format_args!("{}", error.reason(context.db())),
4582+
);
4583+
}
45444584
}
45454585
fallback_type
45464586
}
@@ -6219,6 +6259,8 @@ pub enum MethodWrapperKind<'db> {
62196259
PropertyDunderGet(PropertyInstanceType<'db>),
62206260
/// Method wrapper for `some_property.__set__`
62216261
PropertyDunderSet(PropertyInstanceType<'db>),
6262+
/// Method wrapper for `str.startswith`
6263+
StrStartswith(StringLiteralType<'db>),
62226264
}
62236265

62246266
/// Represents a specific instance of `types.WrapperDescriptorType`

crates/red_knot_python_semantic/src/types/call/bind.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,16 @@ impl<'db> Bindings<'db> {
403403
}
404404
}
405405

406+
Type::MethodWrapper(MethodWrapperKind::StrStartswith(literal)) => {
407+
if let [Some(Type::StringLiteral(prefix)), None, None] =
408+
overload.parameter_types()
409+
{
410+
overload.set_return_type(Type::BooleanLiteral(
411+
literal.value(db).starts_with(&**prefix.value(db)),
412+
));
413+
}
414+
}
415+
406416
Type::BoundMethod(bound_method)
407417
if bound_method.self_instance(db).is_property_instance() =>
408418
{

crates/red_knot_python_semantic/src/types/display.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ impl Display for DisplayRepresentation<'_> {
145145
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => {
146146
write!(f, "<method-wrapper `__set__` of `property` object>",)
147147
}
148+
Type::MethodWrapper(MethodWrapperKind::StrStartswith(_)) => {
149+
write!(f, "<method-wrapper `startswith` of `str` object>",)
150+
}
148151
Type::WrapperDescriptor(kind) => {
149152
let (method, object) = match kind {
150153
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),

0 commit comments

Comments
 (0)