Compare commits

...

5 Commits

Author SHA1 Message Date
Micha Reiser
ace116342c Fix F811 false positive when a class field has the same name as an unused import 2024-10-26 11:05:27 +02:00
Micha Reiser
6aaf1d9446 [red-knot] Remove lint-phase (#13922)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-25 18:40:52 +00:00
Alex Waygood
5eb87aa56e [red-knot] Infer Todo, not Unknown, for PEP-604 unions in annotations (#13908) 2024-10-25 18:21:31 +00:00
David Peter
085a43a262 [red-knot] knot benchmark: fix --knot-path arg (#13923)
## Summary

Previously, this would fail with

```
AttributeError: 'str' object has no attribute 'is_file'
```

if I tried to use the `--knot-path` option. I wish we had a type checker
for Python*.

## Test Plan

```sh
uv run benchmark --knot-path ~/.cargo-target/release/red_knot
```

\* to be fair, this would probably require special handling for
`argparse` in the typechecker.
2024-10-25 11:43:39 +02:00
Micha Reiser
32b57b2ee4 Enable nursery rules: 'redundant_clone', 'debug_assert_with_mut_call', and 'unused_peekable' (#13920) 2024-10-25 09:46:30 +02:00
54 changed files with 449 additions and 421 deletions

View File

@@ -202,6 +202,10 @@ get_unwrap = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
rest_pat_in_fully_bound_structs = "warn"
# nursery rules
redundant_clone = "warn"
debug_assert_with_mut_call = "warn"
unused_peekable = "warn"
[profile.release]
# Note that we set these explicitly, and these values

View File

@@ -144,7 +144,7 @@ pub fn main() -> ExitStatus {
}
fn run() -> anyhow::Result<ExitStatus> {
let args = Args::parse_from(std::env::args().collect::<Vec<_>>());
let args = Args::parse_from(std::env::args());
if matches!(args.command, Some(Command::Server)) {
return run_server().map(|()| ExitStatus::Success);

View File

@@ -22,3 +22,13 @@ x: int = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` i
x: int
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
```
## PEP-604 annotations not yet supported
```py
def f() -> str | None:
return None
# TODO: should be `str | None` (but Todo is better than `Unknown`)
reveal_type(f()) # revealed: @Todo
```

View File

@@ -3,9 +3,12 @@
## Unbound
```py
x = foo
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
foo = 1
reveal_type(x) # revealed: Unbound
# error: [unresolved-reference]
# revealed: Unbound
reveal_type(x)
```
## Unbound class variable
@@ -13,6 +16,10 @@ reveal_type(x) # revealed: Unbound
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
class C:

View File

@@ -3,6 +3,11 @@
## Union of attributes
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
class C:
x = 1

View File

@@ -3,6 +3,11 @@
## Union of return types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
def f() -> int:
@@ -21,6 +26,11 @@ reveal_type(f()) # revealed: int | str
```py
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
def f() -> int:
@@ -34,6 +44,11 @@ reveal_type(f()) # revealed: Unknown | int
Calling a union with a non-callable element should emit a diagnostic.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
f = 1
else:
@@ -50,6 +65,11 @@ reveal_type(x) # revealed: Unknown | int
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
if flag:
f = 1
elif flag2:
@@ -69,6 +89,11 @@ reveal_type(f())
Calling a union with no callable elements can emit a simpler diagnostic.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
f = 1
else:

View File

@@ -5,6 +5,10 @@
Comparisons on union types need to consider all possible cases:
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
one_or_two = 1 if flag else 2
reveal_type(one_or_two <= 2) # revealed: Literal[True]
@@ -52,6 +56,10 @@ With unions on both sides, we need to consider the full cross product of
options when building the resulting (union) type:
```py
def bool_instance() -> bool:
return True
flag_s, flag_l = bool_instance(), bool_instance()
small = 1 if flag_s else 2
large = 2 if flag_l else 3
@@ -69,6 +77,10 @@ unsupported. For now, we fall back to `bool` for the result type instead of
trying to infer something more precise from the other (supported) variants:
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = [1, 2] if flag else 1
result = 1 in x # error: "Operator `in` is not supported"

View File

@@ -1,6 +1,9 @@
# Comparison: Unsupported operators
```py
def bool_instance() -> bool:
return True
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
@@ -15,6 +18,7 @@ d = 5 < object()
# TODO: should be `Unknown`
reveal_type(d) # revealed: bool
flag = bool_instance()
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
e = 42 in int_literal_or_str_literal

View File

@@ -3,6 +3,10 @@
## Simple if-expression
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else 2
reveal_type(x) # revealed: Literal[1, 2]
```
@@ -10,6 +14,10 @@ reveal_type(x) # revealed: Literal[1, 2]
## If-expression with walrus operator
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
y = 0
z = 0
x = (y := 1) if flag else (z := 2)
@@ -21,6 +29,10 @@ reveal_type(z) # revealed: Literal[0, 2]
## Nested if-expression
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
x = 1 if flag else 2 if flag2 else 3
reveal_type(x) # revealed: Literal[1, 2, 3]
```
@@ -28,6 +40,10 @@ reveal_type(x) # revealed: Literal[1, 2, 3]
## None
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else None
reveal_type(x) # revealed: Literal[1] | None
```

View File

@@ -3,6 +3,10 @@
## Simple if
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
y = 1
y = 2
@@ -15,6 +19,10 @@ reveal_type(y) # revealed: Literal[2, 3]
## Simple if-elif-else
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:
@@ -28,13 +36,24 @@ else:
x = y
reveal_type(x) # revealed: Literal[3, 4, 5]
reveal_type(r) # revealed: Unbound | Literal[2]
reveal_type(s) # revealed: Unbound | Literal[5]
# revealed: Unbound | Literal[2]
# error: [possibly-unresolved-reference]
reveal_type(r)
# revealed: Unbound | Literal[5]
# error: [possibly-unresolved-reference]
reveal_type(s)
```
## Single symbol across if-elif-else
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
if flag:
y = 1
elif flag2:
@@ -47,6 +66,10 @@ reveal_type(y) # revealed: Literal[1, 2, 3]
## if-elif-else without else assignment
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
@@ -60,6 +83,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
## if-elif-else with intervening assignment
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
@@ -74,6 +101,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2]
## Nested if statement
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
if flag2:
@@ -84,6 +115,10 @@ reveal_type(y) # revealed: Literal[0, 1]
## if-elif without else
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:

View File

@@ -21,7 +21,9 @@ match 0:
case 2:
y = 3
reveal_type(y) # revealed: Unbound | Literal[2, 3]
# revealed: Unbound | Literal[2, 3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
## Basic match

View File

@@ -10,6 +10,10 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
## Incompatible declarations
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: str
else:
@@ -20,6 +24,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
## Partial declarations
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
@@ -28,6 +36,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`:
## Incompatible declarations with bad assignment
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: str
else:

View File

@@ -6,7 +6,7 @@
import re
try:
x
help()
except NameError as e:
reveal_type(e) # revealed: NameError
except re.error as f:
@@ -19,7 +19,7 @@ except re.error as f:
from nonexistent_module import foo # error: [unresolved-import]
try:
x
help()
except foo as e:
reveal_type(foo) # revealed: Unknown
reveal_type(e) # revealed: Unknown
@@ -31,7 +31,7 @@ except foo as e:
EXCEPTIONS = (AttributeError, TypeError)
try:
x
help()
except (RuntimeError, OSError) as e:
reveal_type(e) # revealed: RuntimeError | OSError
except EXCEPTIONS as f:
@@ -43,7 +43,7 @@ except EXCEPTIONS as f:
```py
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
try:
w
help()
except x as e:
# TODO: should be `AttributeError`
reveal_type(e) # revealed: @Todo

View File

@@ -4,7 +4,7 @@
```py
try:
x
help()
except* BaseException as e:
reveal_type(e) # revealed: BaseExceptionGroup
```
@@ -13,7 +13,7 @@ except* BaseException as e:
```py
try:
x
help()
except* OSError as e:
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
reveal_type(e) # revealed: BaseExceptionGroup
@@ -23,7 +23,7 @@ except* OSError as e:
```py
try:
x
help()
except* (TypeError, AttributeError) as e:
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
reveal_type(e) # revealed: BaseExceptionGroup

View File

@@ -6,9 +6,13 @@ Basic PEP 695 generics
```py
class MyBox[T]:
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
data: T
box_model_number = 695
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
def __init__(self, data: T):
self.data = data
@@ -26,13 +30,19 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695]
```py
class MyBox[T]:
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
data: T
# TODO: `T` is defined here
# error: [unresolved-reference] "Name `T` used when not defined"
def __init__(self, data: T):
self.data = data
# TODO not error on the subscripting
class MySecureBox[T](MyBox[T]): ... # error: [non-subscriptable]
# TODO not error on the subscripting or the use of type param
# error: [unresolved-reference] "Name `T` used when not defined"
# error: [non-subscriptable]
class MySecureBox[T](MyBox[T]): ...
secure_box: MySecureBox[int] = MySecureBox(5)
reveal_type(secure_box) # revealed: MySecureBox

View File

@@ -3,11 +3,22 @@
## Maybe unbound
```py path=maybe_unbound.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
y = 3
x = y
reveal_type(x) # revealed: Unbound | Literal[3]
reveal_type(y) # revealed: Unbound | Literal[3]
x = y # error: [possibly-unresolved-reference]
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(x)
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
```py
@@ -20,11 +31,22 @@ reveal_type(y) # revealed: Literal[3]
## Maybe unbound annotated
```py path=maybe_unbound_annotated.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
y: int = 3
x = y
reveal_type(x) # revealed: Unbound | Literal[3]
reveal_type(y) # revealed: Unbound | Literal[3]
x = y # error: [possibly-unresolved-reference]
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(x)
# revealed: Unbound | Literal[3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
Importing an annotated name prefers the declared type over the inferred type:
@@ -43,6 +65,10 @@ def f(): ...
```
```py path=b.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
from c import f
else:
@@ -67,6 +93,10 @@ x: int
```
```py path=b.py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
from c import x
else:

View File

@@ -102,7 +102,7 @@ reveal_type(X) # revealed: Literal[42]
```
```py path=package/foo.py
x
x # error: [unresolved-reference]
```
```py path=package/bar.py

View File

@@ -17,8 +17,10 @@ async def foo():
async for x in Iterator():
pass
# TODO
reveal_type(x) # revealed: Unbound | @Todo
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
# revealed: Unbound | @Todo
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## Basic async for loop
@@ -37,5 +39,7 @@ async def foo():
async for x in IntAsyncIterable():
pass
reveal_type(x) # revealed: Unbound | @Todo
# error: [possibly-unresolved-reference]
# revealed: Unbound | @Todo
reveal_type(x)
```

View File

@@ -14,7 +14,9 @@ class IntIterable:
for x in IntIterable():
pass
reveal_type(x) # revealed: Unbound | int
# revealed: Unbound | int
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## With previous definition
@@ -85,7 +87,9 @@ class OldStyleIterable:
for x in OldStyleIterable():
pass
reveal_type(x) # revealed: Unbound | int
# revealed: Unbound | int
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## With heterogeneous tuple
@@ -94,12 +98,19 @@ reveal_type(x) # revealed: Unbound | int
for x in (1, "a", b"foo"):
pass
reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## With non-callable iterator
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
class NotIterable:
if flag:
__iter__ = 1
@@ -109,7 +120,9 @@ class NotIterable:
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
reveal_type(x) # revealed: Unbound | Unknown
# revealed: Unbound | Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## Invalid iterable

View File

@@ -3,6 +3,10 @@
## Basic While Loop
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
while flag:
x = 2
@@ -13,6 +17,10 @@ reveal_type(x) # revealed: Literal[1, 2]
## While with else (no break)
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
while flag:
x = 2
@@ -26,6 +34,10 @@ reveal_type(x) # revealed: Literal[3]
## While with Else (may break)
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
x = 1
y = 0
while flag:

View File

@@ -3,6 +3,10 @@
## `is None`
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
if x is None:
@@ -14,6 +18,11 @@ reveal_type(x) # revealed: None | Literal[1]
## `is` for other types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
class A: ...
x = A()
@@ -28,6 +37,10 @@ reveal_type(y) # revealed: A | None
## `is` in chained comparisons
```py
def bool_instance() -> bool:
return True
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False

View File

@@ -5,6 +5,10 @@
The type guard removes `None` from the union type:
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
if x is not None:
@@ -16,6 +20,10 @@ reveal_type(x) # revealed: None | Literal[1]
## `is not` for other singleton types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = True if flag else False
reveal_type(x) # revealed: bool
@@ -42,6 +50,10 @@ if x is not y:
The type guard removes `False` from the union type of the tested value only.
```py
def bool_instance() -> bool:
return True
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False

View File

@@ -16,6 +16,10 @@ if x != 1:
## Multiple negative contributions with simplification
```py
def bool_instance() -> bool:
return True
flag1, flag2 = bool_instance(), bool_instance()
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:

View File

@@ -3,6 +3,10 @@
## `x != None`
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
if x != None:
@@ -12,6 +16,10 @@ if x != None:
## `!=` for other singleton types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = True if flag else False
if x != False:
@@ -21,6 +29,10 @@ if x != False:
## `x != y` where `y` is of literal type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else 2
if x != 1:
@@ -30,6 +42,11 @@ if x != 1:
## `x != y` where `y` is a single-valued type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
class A: ...
class B: ...
@@ -44,8 +61,13 @@ if C != A:
Only single-valued types should narrow the type:
```py
def int_instance() -> int: ...
def bool_instance() -> bool:
return True
def int_instance() -> int:
return 42
flag = bool_instance()
x = int_instance() if flag else None
y = int_instance()

View File

@@ -5,6 +5,11 @@ Narrowing for `isinstance(object, classinfo)` expressions.
## `classinfo` is a single type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, int):
@@ -26,6 +31,11 @@ Note: `isinstance(x, (int, str))` should not be confused with
`isinstance(x, int | str)`:
```py
def bool_instance() -> bool:
return True
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
x = 1 if flag else "a"
if isinstance(x, (int, str)):
@@ -56,6 +66,11 @@ if isinstance(y, (str, bytes)):
## `classinfo` is a nested tuple of types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, (bool, (bytes, int))):
@@ -81,6 +96,11 @@ if isinstance(x, A):
## No narrowing for instances of `builtins.type`
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
t = type("t", (), {})
# This isn't testing what we want it to test if we infer anything more precise here:
@@ -94,6 +114,11 @@ if isinstance(x, t):
## Do not use custom `isinstance` for narrowing
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
def isinstance(x, t):
return True
@@ -105,6 +130,11 @@ if isinstance(x, int):
## Do support narrowing if `isinstance` is aliased
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
isinstance_alias = isinstance
x = 1 if flag else "a"
@@ -117,6 +147,10 @@ if isinstance_alias(x, int):
```py
from builtins import isinstance as imported_isinstance
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
if imported_isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
@@ -125,6 +159,10 @@ if imported_isinstance(x, int):
## Do not narrow if second argument is not a type
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic during
@@ -141,6 +179,10 @@ if isinstance(x, "int"):
## Do not narrow if there are keyword arguments
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic

View File

@@ -3,6 +3,11 @@
## Single `match` pattern
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = None if flag else 1
reveal_type(x) # revealed: None | Literal[1]

View File

@@ -3,6 +3,11 @@
## Shadow after incompatible declarations is OK
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
x: str
else:

View File

@@ -37,6 +37,11 @@ y = 1
## Union
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
p = 1
q = 3.3

View File

@@ -132,7 +132,7 @@ mod tests {
#[test]
fn inequality() {
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
let parsed = ParsedModule::new(parsed_raw.clone());
let parsed = ParsedModule::new(parsed_raw);
let stmt = &parsed.syntax().body[0];
let node = unsafe { AstNodeRef::new(parsed.clone(), stmt) };
@@ -150,7 +150,7 @@ mod tests {
#[allow(unsafe_code)]
fn debug() {
let parsed_raw = parse_unchecked_source("1 + 2", PySourceType::Python);
let parsed = ParsedModule::new(parsed_raw.clone());
let parsed = ParsedModule::new(parsed_raw);
let stmt = &parsed.syntax().body[0];

View File

@@ -1294,7 +1294,7 @@ mod tests {
search_paths: SearchPathSettings {
extra_paths: vec![],
src_root: src.clone(),
custom_typeshed: Some(custom_typeshed.clone()),
custom_typeshed: Some(custom_typeshed),
site_packages: SitePackages::Known(vec![site_packages]),
},
},
@@ -1445,7 +1445,7 @@ mod tests {
assert_function_query_was_not_run(
&db,
resolve_module_query,
ModuleNameIngredient::new(&db, functools_module_name.clone()),
ModuleNameIngredient::new(&db, functools_module_name),
&events,
);
assert_eq!(functools_module.search_path(), &stdlib);

View File

@@ -296,7 +296,7 @@ impl DefinitionNodeRef<'_> {
handler,
is_star,
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
handler: AstNodeRef::new(parsed.clone(), handler),
handler: AstNodeRef::new(parsed, handler),
is_star,
}),
}

View File

@@ -211,7 +211,13 @@ fn declarations_ty<'db>(
let declared_ty = if let Some(second) = all_types.next() {
let mut builder = UnionBuilder::new(db).add(first);
for other in [second].into_iter().chain(all_types) {
if !first.is_equivalent_to(db, other) {
// Make sure not to emit spurious errors relating to `Type::Todo`,
// since we only infer this type due to a limitation in our current model.
//
// `Unknown` is different here, since we might infer `Unknown`
// for one of these due to a variable being defined in one possible
// control-flow branch but not another one.
if !first.is_equivalent_to(db, other) && !first.is_todo() && !other.is_todo() {
conflicting.push(other);
}
builder = builder.add(other);
@@ -292,6 +298,10 @@ impl<'db> Type<'db> {
matches!(self, Type::Never)
}
pub const fn is_todo(&self) -> bool {
matches!(self, Type::Todo)
}
pub const fn into_class_literal_type(self) -> Option<ClassType<'db>> {
match self {
Type::ClassLiteral(class_type) => Some(class_type),

View File

@@ -320,9 +320,7 @@ impl<'db> TypeInferenceBuilder<'db> {
db,
index,
region,
file,
types: TypeInference::empty(scope),
}
}
@@ -2418,7 +2416,23 @@ impl<'db> TypeInferenceBuilder<'db> {
None
};
bindings_ty(self.db, definitions, unbound_ty)
let ty = bindings_ty(self.db, definitions, unbound_ty);
if ty.is_unbound() {
self.add_diagnostic(
name.into(),
"unresolved-reference",
format_args!("Name `{id}` used when not defined"),
);
} else if ty.may_be_unbound(self.db) {
self.add_diagnostic(
name.into(),
"possibly-unresolved-reference",
format_args!("Name `{id}` used when possibly not defined"),
);
}
ty
}
ExprContext::Store | ExprContext::Del => Type::None,
ExprContext::Invalid => Type::Unknown,
@@ -3471,6 +3485,17 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::Todo
}
// TODO PEP-604 unions
ast::Expr::BinOp(binary) => {
self.infer_binary_expression(binary);
match binary.op {
// PEP-604 unions are okay
ast::Operator::BitOr => Type::Todo,
// anything else is an invalid annotation:
_ => Type::Unknown,
}
}
// Forms which are invalid in the context of annotation expressions: we infer their
// nested expressions as normal expressions, but the type of the top-level expression is
// always `Type::Unknown` in these cases.
@@ -3482,10 +3507,6 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_named_expression(named);
Type::Unknown
}
ast::Expr::BinOp(binary) => {
self.infer_binary_expression(binary);
Type::Unknown
}
ast::Expr::UnaryOp(unary) => {
self.infer_unary_expression(unary);
Type::Unknown
@@ -3801,6 +3822,7 @@ mod tests {
Ok(db)
}
#[track_caller]
fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) {
let file = system_path_to_file(db, file_name).expect("file to exist");
@@ -3812,6 +3834,7 @@ mod tests {
);
}
#[track_caller]
fn assert_scope_ty(
db: &TestDb,
file_name: &str,
@@ -3837,6 +3860,7 @@ mod tests {
assert_eq!(ty.display(db).to_string(), expected);
}
#[track_caller]
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
let messages: Vec<&str> = diagnostics
.iter()
@@ -3845,6 +3869,7 @@ mod tests {
assert_eq!(&messages, expected);
}
#[track_caller]
fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) {
let file = system_path_to_file(db, filename).unwrap();
let diagnostics = check_types(db, file);
@@ -4432,7 +4457,7 @@ mod tests {
from typing_extensions import reveal_type
try:
x
print
except as e:
reveal_type(e)
",
@@ -4569,7 +4594,10 @@ mod tests {
assert_file_diagnostics(
&db,
"src/a.py",
&["Object of type `Unbound` is not iterable"],
&[
"Name `x` used when not defined",
"Object of type `Unbound` is not iterable",
],
);
Ok(())
@@ -4704,7 +4732,7 @@ mod tests {
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "z", "Unbound");
// (There is a diagnostic for invalid syntax that's emitted, but it's not listed by `assert_file_diagnostics`)
assert_file_diagnostics(&db, "src/a.py", &[]);
assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]);
Ok(())
}

View File

@@ -18,6 +18,7 @@ impl<I, T: DoubleEndedIterator<Item = I>> PythonSubscript for T {
}
#[cfg(test)]
#[allow(clippy::redundant_clone)]
mod tests {
use super::PythonSubscript;

View File

@@ -37,7 +37,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
params.cell_text_documents,
)
.with_failure_code(ErrorCode::InternalError)?;
session.open_notebook_document(params.notebook_document.uri.clone(), notebook);
session.open_notebook_document(params.notebook_document.uri, notebook);
match path {
AnySystemPath::System(path) => {

View File

@@ -110,14 +110,14 @@ impl Workspace {
pub fn check_file(&self, file_id: &FileHandle) -> Result<Vec<String>, Error> {
let result = self.db.check_file(file_id.file).map_err(into_error)?;
Ok(result.clone())
Ok(result)
}
/// Checks all open files
pub fn check(&self) -> Result<Vec<String>, Error> {
let result = self.db.check().map_err(into_error)?;
Ok(result.clone())
Ok(result)
}
/// Returns the parsed AST for `path`

View File

@@ -1,4 +1,3 @@
pub mod db;
pub mod lint;
pub mod watch;
pub mod workspace;

View File

@@ -1,318 +0,0 @@
use std::cell::RefCell;
use std::time::Duration;
use tracing::debug_span;
use red_knot_python_semantic::types::Type;
use red_knot_python_semantic::{HasTy, ModuleName, SemanticModel};
use ruff_db::files::File;
use ruff_db::parsed::{parsed_module, ParsedModule};
use ruff_db::source::{source_text, SourceText};
use ruff_python_ast as ast;
use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor};
use ruff_text_size::{Ranged, TextSize};
use crate::db::Db;
/// Workaround query to test for if the computation should be cancelled.
/// Ideally, push for Salsa to expose an API for testing if cancellation was requested.
#[salsa::tracked]
#[allow(unused_variables)]
pub(crate) fn unwind_if_cancelled(db: &dyn Db) {}
#[salsa::tracked(return_ref)]
pub(crate) fn lint_syntax(db: &dyn Db, file_id: File) -> Vec<String> {
#[allow(clippy::print_stdout)]
if std::env::var("RED_KNOT_SLOW_LINT").is_ok() {
for i in 0..10 {
unwind_if_cancelled(db);
println!("RED_KNOT_SLOW_LINT is set, sleeping for {i}/10 seconds");
std::thread::sleep(Duration::from_secs(1));
}
}
let mut diagnostics = Vec::new();
let source = source_text(db.upcast(), file_id);
lint_lines(&source, &mut diagnostics);
let parsed = parsed_module(db.upcast(), file_id);
if parsed.errors().is_empty() {
let ast = parsed.syntax();
let mut visitor = SyntaxLintVisitor {
diagnostics,
source: &source,
};
visitor.visit_body(&ast.body);
diagnostics = visitor.diagnostics;
}
diagnostics
}
fn lint_lines(source: &str, diagnostics: &mut Vec<String>) {
for (line_number, line) in source.lines().enumerate() {
if line.len() < 88 {
continue;
}
let char_count = line.chars().count();
if char_count > 88 {
diagnostics.push(format!(
"Line {} is too long ({} characters)",
line_number + 1,
char_count
));
}
}
}
#[allow(unreachable_pub)]
#[salsa::tracked(return_ref)]
pub fn lint_semantic(db: &dyn Db, file_id: File) -> Vec<String> {
let _span = debug_span!("lint_semantic", file=%file_id.path(db)).entered();
let source = source_text(db.upcast(), file_id);
let parsed = parsed_module(db.upcast(), file_id);
let semantic = SemanticModel::new(db.upcast(), file_id);
if !parsed.is_valid() {
return vec![];
}
let context = SemanticLintContext {
source,
parsed,
semantic,
diagnostics: RefCell::new(Vec::new()),
};
SemanticVisitor { context: &context }.visit_body(parsed.suite());
context.diagnostics.take()
}
fn format_diagnostic(context: &SemanticLintContext, message: &str, start: TextSize) -> String {
let source_location = context
.semantic
.line_index()
.source_location(start, context.source_text());
format!(
"{}:{}:{}: {}",
context.semantic.file_path(),
source_location.row,
source_location.column,
message,
)
}
fn lint_maybe_undefined(context: &SemanticLintContext, name: &ast::ExprName) {
if !matches!(name.ctx, ast::ExprContext::Load) {
return;
}
let semantic = &context.semantic;
let ty = name.ty(semantic);
if ty.is_unbound() {
context.push_diagnostic(format_diagnostic(
context,
&format!("Name `{}` used when not defined", &name.id),
name.start(),
));
} else if ty.may_be_unbound(semantic.db()) {
context.push_diagnostic(format_diagnostic(
context,
&format!("Name `{}` used when possibly not defined", &name.id),
name.start(),
));
}
}
fn lint_bad_override(context: &SemanticLintContext, class: &ast::StmtClassDef) {
let semantic = &context.semantic;
// TODO we should have a special marker on the real typing module (from typeshed) so if you
// have your own "typing" module in your project, we don't consider it THE typing module (and
// same for other stdlib modules that our lint rules care about)
let Some(typing) = semantic.resolve_module(&ModuleName::new("typing").unwrap()) else {
return;
};
let override_ty = semantic.global_symbol_ty(&typing, "override");
let Type::ClassLiteral(class_ty) = class.ty(semantic) else {
return;
};
for function in class
.body
.iter()
.filter_map(|stmt| stmt.as_function_def_stmt())
{
let Type::FunctionLiteral(ty) = function.ty(semantic) else {
return;
};
// TODO this shouldn't make direct use of the Db; see comment on SemanticModel::db
let db = semantic.db();
if ty.has_decorator(db, override_ty) {
let method_name = ty.name(db);
if class_ty
.inherited_class_member(db, method_name)
.is_unbound()
{
// TODO should have a qualname() method to support nested classes
context.push_diagnostic(
format!(
"Method {}.{} is decorated with `typing.override` but does not override any base class method",
class_ty.name(db),
method_name,
));
}
}
}
}
pub(crate) struct SemanticLintContext<'a> {
source: SourceText,
parsed: &'a ParsedModule,
semantic: SemanticModel<'a>,
diagnostics: RefCell<Vec<String>>,
}
impl<'db> SemanticLintContext<'db> {
#[allow(unused)]
pub(crate) fn source_text(&self) -> &str {
self.source.as_str()
}
#[allow(unused)]
pub(crate) fn ast(&self) -> &'db ast::ModModule {
self.parsed.syntax()
}
pub(crate) fn push_diagnostic(&self, diagnostic: String) {
self.diagnostics.borrow_mut().push(diagnostic);
}
#[allow(unused)]
pub(crate) fn extend_diagnostics(&mut self, diagnostics: impl IntoIterator<Item = String>) {
self.diagnostics.get_mut().extend(diagnostics);
}
}
#[derive(Debug)]
struct SyntaxLintVisitor<'a> {
diagnostics: Vec<String>,
source: &'a str,
}
impl Visitor<'_> for SyntaxLintVisitor<'_> {
fn visit_string_literal(&mut self, string_literal: &'_ ast::StringLiteral) {
// A very naive implementation of use double quotes
let text = &self.source[string_literal.range];
if text.starts_with('\'') {
self.diagnostics
.push("Use double quotes for strings".to_string());
}
}
}
struct SemanticVisitor<'a> {
context: &'a SemanticLintContext<'a>,
}
impl Visitor<'_> for SemanticVisitor<'_> {
fn visit_stmt(&mut self, stmt: &ast::Stmt) {
if let ast::Stmt::ClassDef(class) = stmt {
lint_bad_override(self.context, class);
}
walk_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &ast::Expr) {
match expr {
ast::Expr::Name(name) if matches!(name.ctx, ast::ExprContext::Load) => {
lint_maybe_undefined(self.context, name);
}
_ => {}
}
walk_expr(self, expr);
}
}
#[cfg(test)]
mod tests {
use red_knot_python_semantic::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use crate::db::tests::TestDb;
use super::lint_semantic;
fn setup_db() -> TestDb {
setup_db_with_root(SystemPathBuf::from("/src"))
}
fn setup_db_with_root(src_root: SystemPathBuf) -> TestDb {
let db = TestDb::new();
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
},
)
.expect("Valid program settings");
db
}
#[test]
fn undefined_variable() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
"
x = int
if flag:
y = x
y
",
)
.unwrap();
let file = system_path_to_file(&db, "/src/a.py").expect("file to exist");
let messages = lint_semantic(&db, file);
assert_ne!(messages, &[] as &[String], "expected some diagnostics");
assert_eq!(
*messages,
if cfg!(windows) {
vec![
"\\src\\a.py:3:4: Name `flag` used when not defined",
"\\src\\a.py:5:1: Name `y` used when possibly not defined",
]
} else {
vec![
"/src/a.py:3:4: Name `flag` used when not defined",
"/src/a.py:5:1: Name `y` used when possibly not defined",
]
}
);
}
}

View File

@@ -15,12 +15,9 @@ use ruff_db::{
use ruff_python_ast::{name::Name, PySourceType};
use ruff_text_size::Ranged;
use crate::db::Db;
use crate::db::RootDatabase;
use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
use crate::{
db::Db,
lint::{lint_semantic, lint_syntax},
};
mod files;
mod metadata;
@@ -423,8 +420,6 @@ pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<String> {
));
}
diagnostics.extend_from_slice(lint_syntax(db, file));
diagnostics.extend_from_slice(lint_semantic(db, file));
diagnostics
}
@@ -540,17 +535,17 @@ impl Iterator for WorkspaceFilesIter<'_> {
#[cfg(test)]
mod tests {
use red_knot_python_semantic::types::check_types;
use ruff_db::files::system_path_to_file;
use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, SystemPath};
use ruff_db::testing::assert_function_query_was_not_run;
use crate::db::tests::TestDb;
use crate::lint::lint_syntax;
use crate::workspace::check_file;
#[test]
fn check_file_skips_linting_when_file_cant_be_read() -> ruff_db::system::Result<()> {
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
let mut db = TestDb::new();
let path = SystemPath::new("test.py");
@@ -568,7 +563,7 @@ mod tests {
);
let events = db.take_salsa_events();
assert_function_query_was_not_run(&db, lint_syntax, file, &events);
assert_function_query_was_not_run(&db, check_types, file, &events);
// The user now creates a new file with an empty text. The source text
// content returned by `source_text` remains unchanged, but the diagnostics should get updated.

View File

@@ -28,30 +28,18 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
// We don't support `*` imports yet:
"/src/tomllib/_parser.py:7:29: Module `collections.abc` has no member `Iterable`",
// We don't support terminal statements in control flow yet:
"/src/tomllib/_parser.py:353:5: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
"/src/tomllib/_parser.py:455:9: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
// True positives!
"Line 69 is too long (89 characters)",
"Use double quotes for strings",
"Use double quotes for strings",
"Use double quotes for strings",
"Use double quotes for strings",
"Use double quotes for strings",
"Use double quotes for strings",
"Use double quotes for strings",
// We don't support terminal statements in control flow yet:
"/src/tomllib/_parser.py:66:18: Name `s` used when possibly not defined",
"/src/tomllib/_parser.py:98:12: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:101:12: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:126:12: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:348:20: Name `nest` used when possibly not defined",
"/src/tomllib/_parser.py:353:5: Name `nest` used when possibly not defined",
"/src/tomllib/_parser.py:353:5: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
"/src/tomllib/_parser.py:453:24: Name `nest` used when possibly not defined",
"/src/tomllib/_parser.py:455:9: Name `nest` used when possibly not defined",
"/src/tomllib/_parser.py:455:9: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`",
"/src/tomllib/_parser.py:482:16: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:566:12: Name `char` used when possibly not defined",
"/src/tomllib/_parser.py:573:12: Name `char` used when possibly not defined",

View File

@@ -0,0 +1,9 @@
"""Regression test for: https://github.com/astral-sh/ruff/issues/13930"""
from queue import Empty
class Types:
INVALID = 0
UINT = 1
HEX = 2
Empty = 3

View File

@@ -261,6 +261,14 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
) {
continue;
}
// If the re-definition is a class member, abort.
// from bar import foo
// class Test:
// bar = 10 # Okay
if scope.kind.is_class() {
continue;
}
}
// If the bindings are in different forks, abort.

View File

@@ -88,7 +88,7 @@ where
let line_end = locator.full_line_end(script_start.end());
let rest = locator.after(line_end);
let mut end_offset = None;
let mut lines = UniversalNewlineIterator::with_offset(rest, line_end).peekable();
let mut lines = UniversalNewlineIterator::with_offset(rest, line_end);
while let Some(line) = lines.next() {
let Some(content) = script_line_content(&line) else {

View File

@@ -1850,7 +1850,7 @@ static GOOGLE_ARGS_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*(\*?\*?\w+)\s*(\(.*?\))?\s*:(\r\n|\n)?\s*.+").unwrap());
fn args_section(context: &SectionContext) -> FxHashSet<String> {
let mut following_lines = context.following_lines().peekable();
let mut following_lines = context.following_lines();
let Some(first_line) = following_lines.next() else {
return FxHashSet::default();
};

View File

@@ -129,6 +129,7 @@ mod tests {
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_29.pyi"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_30.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_31.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_class_fields.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_0.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_1.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_2.py"))]

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---

View File

@@ -122,10 +122,7 @@ impl TestRule for StableTestRuleSafeFix {
} else {
Some(
Diagnostic::new(StableTestRuleSafeFix, ruff_text_size::TextRange::default())
.with_fix(Fix::safe_edit(Edit::insertion(
comment.to_string(),
TextSize::new(0),
))),
.with_fix(Fix::safe_edit(Edit::insertion(comment, TextSize::new(0)))),
)
}
}
@@ -169,10 +166,7 @@ impl TestRule for StableTestRuleUnsafeFix {
StableTestRuleUnsafeFix,
ruff_text_size::TextRange::default(),
)
.with_fix(Fix::unsafe_edit(Edit::insertion(
comment.to_string(),
TextSize::new(0),
))),
.with_fix(Fix::unsafe_edit(Edit::insertion(comment, TextSize::new(0)))),
)
}
}
@@ -217,7 +211,7 @@ impl TestRule for StableTestRuleDisplayOnlyFix {
ruff_text_size::TextRange::default(),
)
.with_fix(Fix::display_only_edit(Edit::insertion(
comment.to_string(),
comment,
TextSize::new(0),
))),
)

View File

@@ -60,15 +60,13 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
}
}
let docs: Vec<&Attribute> = struct_attributes
let docs = struct_attributes
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.collect();
.filter(|attr| attr.path().is_ident("doc"));
// Convert the list of `doc` attributes into a single string.
let doc = dedent(
&docs
.into_iter()
.map(parse_doc)
.collect::<syn::Result<Vec<_>>>()?
.join("\n"),

View File

@@ -30,7 +30,10 @@ pub fn find_only_token_in_range(
let token = tokens.next().expect("Expected a token");
debug_assert_eq!(token.kind(), token_kind);
let mut tokens = tokens.skip_while(|token| token.kind == SimpleTokenKind::LParen);
debug_assert_eq!(tokens.next(), None);
#[allow(clippy::debug_assert_with_mut_call)]
{
debug_assert_eq!(tokens.next(), None);
}
token
}

View File

@@ -298,7 +298,7 @@ fn super_resolution_overview() {
.unwrap();
}
let snapshot = session.take_snapshot(file_url.clone()).unwrap();
let snapshot = session.take_snapshot(file_url).unwrap();
insta::assert_snapshot!(
"changed_notebook",

View File

@@ -2029,11 +2029,11 @@ mod tests {
assert_override(
vec![
RuleSelection {
select: Some(vec![d417.clone()]),
select: Some(vec![d417]),
..RuleSelection::default()
},
RuleSelection {
extend_select: vec![d41.clone()],
extend_select: vec![d41],
..RuleSelection::default()
},
],

View File

@@ -154,12 +154,12 @@ class Venv:
self.path = path
@property
def name(self):
def name(self) -> str:
"""The name of the virtual environment directory."""
return self.path.name
@property
def python(self):
def python(self) -> Path:
"""Returns the path to the python executable"""
return self.script("python")
@@ -193,7 +193,7 @@ class Venv:
root = parent / "venv"
return Venv(root)
def install(self, dependencies: list[str]):
def install(self, dependencies: list[str]) -> None:
"""Installs the dependencies required to type check the project."""
logging.debug(f"Installing dependencies: {', '.join(dependencies)}")
@@ -202,7 +202,7 @@ class Venv:
"pip",
"install",
"--python",
self.python,
self.python.as_posix(),
"--quiet",
# We pass `--exclude-newer` to ensure that type-checking of one of
# our projects isn't unexpectedly broken by a change in the

View File

@@ -1,5 +1,4 @@
import logging
import os
import subprocess
import typing
from pathlib import Path
@@ -30,9 +29,9 @@ class Project(typing.NamedTuple):
mypy_arguments: list[str] | None = None
"""The arguments passed to mypy. Overrides `include` if set."""
def clone(self, checkout_dir: Path):
def clone(self, checkout_dir: Path) -> None:
# Skip cloning if the project has already been cloned (the script doesn't yet support updating)
if os.path.exists(os.path.join(checkout_dir, ".git")):
if (checkout_dir / ".git").exists():
return
logging.debug(f"Cloning {self.repository} to {checkout_dir}")

View File

@@ -16,7 +16,7 @@ if typing.TYPE_CHECKING:
from benchmark.cases import Tool
def main():
def main() -> None:
"""Run the benchmark."""
parser = argparse.ArgumentParser(
description="Benchmark knot against other packaging tools."
@@ -69,7 +69,7 @@ def main():
)
parser.add_argument(
"--knot-path",
type=str,
type=Path,
help="Path(s) to the red_knot binary to benchmark.",
action="append",
)
@@ -116,8 +116,8 @@ def main():
]
for project in projects:
with tempfile.TemporaryDirectory() as cwd:
cwd = Path(cwd)
with tempfile.TemporaryDirectory() as tempdir:
cwd = Path(tempdir)
project.clone(cwd)
venv = Venv.create(cwd)