Compare commits
5 Commits
micha/has_
...
zb/virtual
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c6e2e26a | ||
|
|
52849a5e68 | ||
|
|
2a61fe2353 | ||
|
|
764ad8b29b | ||
|
|
85af715880 |
@@ -1,5 +1,6 @@
|
||||
use glob::PatternError;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::panic::RefUnwindSafe;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -20,18 +21,47 @@ use super::walk_directory::WalkDirectoryBuilder;
|
||||
///
|
||||
/// ## Warning
|
||||
/// Don't use this system for production code. It's intended for testing only.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct TestSystem {
|
||||
inner: Arc<dyn WritableSystem + RefUnwindSafe + Send + Sync>,
|
||||
/// Environment variable overrides. If a key is present here, it takes precedence
|
||||
/// over the inner system's environment variables.
|
||||
env_overrides: Arc<Mutex<FxHashMap<String, Option<String>>>>,
|
||||
}
|
||||
|
||||
impl Clone for TestSystem {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
env_overrides: self.env_overrides.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestSystem {
|
||||
pub fn new(inner: impl WritableSystem + RefUnwindSafe + Send + Sync + 'static) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(inner),
|
||||
env_overrides: Arc::new(Mutex::new(FxHashMap::default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets an environment variable override. This takes precedence over the inner system.
|
||||
pub fn set_env_var(&self, name: impl Into<String>, value: impl Into<String>) {
|
||||
self.env_overrides
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(name.into(), Some(value.into()));
|
||||
}
|
||||
|
||||
/// Removes an environment variable override, making it appear as not set.
|
||||
pub fn remove_env_var(&self, name: impl Into<String>) {
|
||||
self.env_overrides
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(name.into(), None);
|
||||
}
|
||||
|
||||
/// Returns the [`InMemorySystem`].
|
||||
///
|
||||
/// ## Panics
|
||||
@@ -147,6 +177,18 @@ impl System for TestSystem {
|
||||
self.system().case_sensitivity()
|
||||
}
|
||||
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
// Check overrides first
|
||||
if let Some(override_value) = self.env_overrides.lock().unwrap().get(name) {
|
||||
return match override_value {
|
||||
Some(value) => Ok(value.clone()),
|
||||
None => Err(std::env::VarError::NotPresent),
|
||||
};
|
||||
}
|
||||
// Fall back to inner system
|
||||
self.system().env_var(name)
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn System> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
@@ -156,6 +198,7 @@ impl Default for TestSystem {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(InMemorySystem::default()),
|
||||
env_overrides: Arc::new(Mutex::new(FxHashMap::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py
vendored
Normal file
38
crates/ruff_linter/resources/test/fixtures/semantic_errors/annotated_global.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
a: int = 1
|
||||
def f1():
|
||||
global a
|
||||
a: str = "foo" # error
|
||||
|
||||
b: int = 1
|
||||
def outer():
|
||||
def inner():
|
||||
global b
|
||||
b: str = "nested" # error
|
||||
|
||||
c: int = 1
|
||||
def f2():
|
||||
global c
|
||||
c: list[str] = [] # error
|
||||
|
||||
d: int = 1
|
||||
def f3():
|
||||
global d
|
||||
d: str # error
|
||||
|
||||
e: int = 1
|
||||
def f4():
|
||||
e: str = "happy" # okay
|
||||
|
||||
global f
|
||||
f: int = 1 # okay
|
||||
|
||||
g: int = 1
|
||||
global g # error
|
||||
|
||||
class C:
|
||||
x: str
|
||||
global x # error
|
||||
|
||||
class D:
|
||||
global x # error
|
||||
x: str
|
||||
@@ -1001,6 +1001,7 @@ mod tests {
|
||||
#[test_case(Path::new("write_to_debug.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("invalid_expression.py"), PythonVersion::PY312)]
|
||||
#[test_case(Path::new("global_parameter.py"), PythonVersion::PY310)]
|
||||
#[test_case(Path::new("annotated_global.py"), PythonVersion::PY314)]
|
||||
fn test_semantic_errors(path: &Path, python_version: PythonVersion) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"semantic_syntax_error_{}_{}",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/linter.rs
|
||||
---
|
||||
invalid-syntax: annotated name `a` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:4:5
|
||||
|
|
||||
2 | def f1():
|
||||
3 | global a
|
||||
4 | a: str = "foo" # error
|
||||
| ^
|
||||
5 |
|
||||
6 | b: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `b` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:10:9
|
||||
|
|
||||
8 | def inner():
|
||||
9 | global b
|
||||
10 | b: str = "nested" # error
|
||||
| ^
|
||||
11 |
|
||||
12 | c: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `c` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:15:5
|
||||
|
|
||||
13 | def f2():
|
||||
14 | global c
|
||||
15 | c: list[str] = [] # error
|
||||
| ^
|
||||
16 |
|
||||
17 | d: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `d` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:20:5
|
||||
|
|
||||
18 | def f3():
|
||||
19 | global d
|
||||
20 | d: str # error
|
||||
| ^
|
||||
21 |
|
||||
22 | e: int = 1
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `g` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:29:1
|
||||
|
|
||||
27 | f: int = 1 # okay
|
||||
28 |
|
||||
29 | g: int = 1
|
||||
| ^
|
||||
30 | global g # error
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `x` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:33:5
|
||||
|
|
||||
32 | class C:
|
||||
33 | x: str
|
||||
| ^
|
||||
34 | global x # error
|
||||
|
|
||||
|
||||
invalid-syntax: annotated name `x` can't be global
|
||||
--> resources/test/fixtures/semantic_errors/annotated_global.py:38:5
|
||||
|
|
||||
36 | class D:
|
||||
37 | global x # error
|
||||
38 | x: str
|
||||
| ^
|
||||
|
|
||||
@@ -272,7 +272,9 @@ impl SemanticSyntaxChecker {
|
||||
|
||||
fn check_annotation<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
|
||||
match stmt {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) => {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
target, annotation, ..
|
||||
}) => {
|
||||
if ctx.python_version() > PythonVersion::PY313 {
|
||||
// test_ok valid_annotation_py313
|
||||
// # parse_options: {"target-version": "3.13"}
|
||||
@@ -297,6 +299,18 @@ impl SemanticSyntaxChecker {
|
||||
};
|
||||
visitor.visit_expr(annotation);
|
||||
}
|
||||
if let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() {
|
||||
if let Some(global_stmt) = ctx.global(id.as_str()) {
|
||||
let global_start = global_stmt.start();
|
||||
if !ctx.in_module_scope() || target.start() < global_start {
|
||||
Self::add_error(
|
||||
ctx,
|
||||
SemanticSyntaxErrorKind::AnnotatedGlobal(id.to_string()),
|
||||
target.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::FunctionDef(ast::StmtFunctionDef {
|
||||
type_params,
|
||||
|
||||
@@ -2703,3 +2703,51 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test behavior when `VIRTUAL_ENV` is set but points to a non-existent path.
|
||||
#[test]
|
||||
fn missing_virtual_env() -> anyhow::Result<()> {
|
||||
let working_venv_package1_path = if cfg!(windows) {
|
||||
"project/.venv/Lib/site-packages/package1/__init__.py"
|
||||
} else {
|
||||
"project/.venv/lib/python3.13/site-packages/package1/__init__.py"
|
||||
};
|
||||
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"project/test.py",
|
||||
r#"
|
||||
from package1 import WorkingVenv
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"project/.venv/pyvenv.cfg",
|
||||
r#"
|
||||
home = ./
|
||||
|
||||
"#,
|
||||
),
|
||||
(
|
||||
working_venv_package1_path,
|
||||
r#"
|
||||
class WorkingVenv: ...
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command()
|
||||
.current_dir(case.root().join("project"))
|
||||
.env("VIRTUAL_ENV", case.root().join("nonexistent-venv")), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ty failed
|
||||
Cause: Failed to discover local Python environment
|
||||
Cause: Invalid `VIRTUAL_ENV` environment variable `<temp_dir>/nonexistent-venv`: does not point to a directory on disk
|
||||
Cause: No such file or directory (os error 2)
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1208,7 +1208,7 @@ def _(flag: bool):
|
||||
reveal_type(C1.y) # revealed: int | str
|
||||
|
||||
C1.y = 100
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'C1'> | <class 'C1'>`"
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
|
||||
@@ -643,6 +643,91 @@ reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None
|
||||
Person(name="Alice")
|
||||
```
|
||||
|
||||
### Field specifiers using `**kwargs`
|
||||
|
||||
Some field specifiers may use `**kwargs` to pass through standard parameters like `default`,
|
||||
`default_factory`, `init`, `kw_only`, and `alias`. This section tests that all these parameters work
|
||||
correctly when passed via `**kwargs` for all three kinds of transformers.
|
||||
|
||||
#### Function-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
def create_model[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
@create_model
|
||||
class Person:
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
#### Metaclass-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
class ModelMeta(type): ...
|
||||
|
||||
class ModelBase(metaclass=ModelMeta): ...
|
||||
|
||||
class Person(ModelBase):
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
#### Base-class-based transformer
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
def field(**kwargs: Any) -> Any: ...
|
||||
@dataclass_transform(field_specifiers=(field,))
|
||||
class ModelBase: ...
|
||||
|
||||
class Person(ModelBase):
|
||||
id: int = field(init=False)
|
||||
name: str
|
||||
age: int = field(default=0)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
email: str = field(kw_only=True)
|
||||
internal_notes: str = field(alias="notes")
|
||||
|
||||
# revealed: (self: Person, name: str, age: int = ..., tags: list[str] = ..., notes: str, *, email: str) -> None
|
||||
reveal_type(Person.__init__)
|
||||
|
||||
Person("Alice", 30, [], "some notes", email="alice@example.com")
|
||||
Person("Bob", email="bob@example.com", notes="other notes")
|
||||
```
|
||||
|
||||
### Support for `alias`
|
||||
|
||||
The `alias` parameter in field specifiers allows providing an alternative name for the parameter in
|
||||
@@ -868,4 +953,83 @@ reveal_type(t.key) # revealed: int
|
||||
reveal_type(t.name) # revealed: str
|
||||
```
|
||||
|
||||
## `__dataclass_fields__` and `DataclassInstance` protocol
|
||||
|
||||
Classes created via `dataclass_transform` should have `__dataclass_fields__` and
|
||||
`__dataclass_params__` attributes, allowing them to satisfy the `DataclassInstance` protocol. This
|
||||
enables use of `dataclasses.fields`, `dataclasses.asdict`, `dataclasses.replace`, etc.
|
||||
|
||||
### Function-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
def create_model[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
@create_model
|
||||
class Person:
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
### Metaclass-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelMeta(type): ...
|
||||
|
||||
class ModelBase(metaclass=ModelMeta): ...
|
||||
|
||||
class Person(ModelBase):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
### Base-class-based transformer
|
||||
|
||||
```py
|
||||
from dataclasses import fields, asdict, replace, Field
|
||||
from typing import dataclass_transform, Any
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelBase: ...
|
||||
|
||||
class Person(ModelBase):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
p = Person("Alice", 30)
|
||||
|
||||
reveal_type(Person.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
reveal_type(p.__dataclass_fields__) # revealed: dict[str, Field[Any]]
|
||||
|
||||
reveal_type(fields(Person)) # revealed: tuple[Field[Any], ...]
|
||||
reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
|
||||
@@ -195,3 +195,52 @@ class C:
|
||||
c = C()
|
||||
c.square("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Types with the same name but from different files
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import needs_a_foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
needs_a_foo(Foo()) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## TypeVars with bounds that have the same name but are from different files
|
||||
|
||||
In this case, using fully qualified names is *not* necessary.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from module import needs_a_foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def f[T: Foo](x: T) -> T:
|
||||
needs_a_foo(x) # error: [invalid-argument-type]
|
||||
return x
|
||||
```
|
||||
|
||||
@@ -393,7 +393,7 @@ else:
|
||||
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
reveal_mro(B)
|
||||
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'B'> | <class 'B'>`"
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28'>`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)
|
||||
|
||||
@@ -37,7 +37,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'C1'> | <class 'C1'>`
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - TypeVars with bounds that have the same name but are from different files
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## module.py
|
||||
|
||||
```
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from module import needs_a_foo
|
||||
2 |
|
||||
3 | class Foo: ...
|
||||
4 |
|
||||
5 | def f[T: Foo](x: T) -> T:
|
||||
6 | needs_a_foo(x) # error: [invalid-argument-type]
|
||||
7 | return x
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect
|
||||
--> src/main.py:6:17
|
||||
|
|
||||
5 | def f[T: Foo](x: T) -> T:
|
||||
6 | needs_a_foo(x) # error: [invalid-argument-type]
|
||||
| ^ Expected `Foo`, found `T@f`
|
||||
7 | return x
|
||||
|
|
||||
info: Function defined here
|
||||
--> src/module.py:3:5
|
||||
|
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
| ^^^^^^^^^^^ ------ Parameter declared here
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Types with the same name but from different files
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## module.py
|
||||
|
||||
```
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from module import needs_a_foo
|
||||
2 |
|
||||
3 | class Foo: ...
|
||||
4 |
|
||||
5 | needs_a_foo(Foo()) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect
|
||||
--> src/main.py:5:13
|
||||
|
|
||||
3 | class Foo: ...
|
||||
4 |
|
||||
5 | needs_a_foo(Foo()) # error: [invalid-argument-type]
|
||||
| ^^^^^ Expected `module.Foo`, found `main.Foo`
|
||||
|
|
||||
info: Function defined here
|
||||
--> src/module.py:3:5
|
||||
|
|
||||
1 | class Foo: ...
|
||||
2 |
|
||||
3 | def needs_a_foo(x: Foo): ...
|
||||
| ^^^^^^^^^^^ ------ Parameter declared here
|
||||
|
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
@@ -21,7 +21,6 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use smallvec::{SmallVec, smallvec, smallvec_inline};
|
||||
|
||||
use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type};
|
||||
use crate::Program;
|
||||
use crate::db::Db;
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::module_resolver::KnownModule;
|
||||
@@ -52,6 +51,7 @@ use crate::types::{
|
||||
enums, list_members, todo_type,
|
||||
};
|
||||
use crate::unpack::EvaluationMode;
|
||||
use crate::{DisplaySettings, Program};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
|
||||
|
||||
@@ -214,7 +214,7 @@ impl<'db> Bindings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
self.evaluate_known_cases(db, dataclass_field_specifiers);
|
||||
self.evaluate_known_cases(db, argument_types, dataclass_field_specifiers);
|
||||
|
||||
// In order of precedence:
|
||||
//
|
||||
@@ -337,7 +337,12 @@ impl<'db> Bindings<'db> {
|
||||
|
||||
/// Evaluates the return type of certain known callables, where we have special-case logic to
|
||||
/// determine the return type in a way that isn't directly expressible in the type system.
|
||||
fn evaluate_known_cases(&mut self, db: &'db dyn Db, dataclass_field_specifiers: &[Type<'db>]) {
|
||||
fn evaluate_known_cases(
|
||||
&mut self,
|
||||
db: &'db dyn Db,
|
||||
argument_types: &CallArguments<'_, 'db>,
|
||||
dataclass_field_specifiers: &[Type<'db>],
|
||||
) {
|
||||
let to_bool = |ty: &Option<Type<'_>>, default: bool| -> bool {
|
||||
if let Some(Type::BooleanLiteral(value)) = ty {
|
||||
*value
|
||||
@@ -666,25 +671,32 @@ impl<'db> Bindings<'db> {
|
||||
if dataclass_field_specifiers.contains(&function)
|
||||
|| function_type.is_known(db, KnownFunction::Field) =>
|
||||
{
|
||||
let has_default_value = overload
|
||||
.parameter_type_by_name("default", false)
|
||||
.is_ok_and(|ty| ty.is_some())
|
||||
|| overload
|
||||
.parameter_type_by_name("default_factory", false)
|
||||
.is_ok_and(|ty| ty.is_some())
|
||||
|| overload
|
||||
.parameter_type_by_name("factory", false)
|
||||
.is_ok_and(|ty| ty.is_some());
|
||||
// Helper to get the type of a keyword argument by name. We first try to get it from
|
||||
// the parameter binding (for explicit parameters), and then fall back to checking the
|
||||
// call site arguments (for field-specifier functions that use a `**kwargs` parameter,
|
||||
// instead of specifying `init`, `default` etc. explicitly).
|
||||
let get_argument_type = |name, fallback_to_default| -> Option<Type<'db>> {
|
||||
if let Ok(ty) =
|
||||
overload.parameter_type_by_name(name, fallback_to_default)
|
||||
{
|
||||
return ty;
|
||||
}
|
||||
argument_types.iter().find_map(|(arg, ty)| {
|
||||
if matches!(arg, Argument::Keyword(arg_name) if arg_name == name) {
|
||||
ty
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let init = overload
|
||||
.parameter_type_by_name("init", true)
|
||||
.unwrap_or(None);
|
||||
let kw_only = overload
|
||||
.parameter_type_by_name("kw_only", true)
|
||||
.unwrap_or(None);
|
||||
let alias = overload
|
||||
.parameter_type_by_name("alias", true)
|
||||
.unwrap_or(None);
|
||||
let has_default_value = get_argument_type("default", false).is_some()
|
||||
|| get_argument_type("default_factory", false).is_some()
|
||||
|| get_argument_type("factory", false).is_some();
|
||||
|
||||
let init = get_argument_type("init", true);
|
||||
let kw_only = get_argument_type("kw_only", true);
|
||||
let alias = get_argument_type("alias", true);
|
||||
|
||||
// `dataclasses.field` and field-specifier functions of commonly used
|
||||
// libraries like `pydantic`, `attrs`, and `SQLAlchemy` all return
|
||||
@@ -4156,8 +4168,13 @@ impl<'db> BindingError<'db> {
|
||||
return;
|
||||
};
|
||||
|
||||
let provided_ty_display = provided_ty.display(context.db());
|
||||
let expected_ty_display = expected_ty.display(context.db());
|
||||
let display_settings = DisplaySettings::from_possibly_ambiguous_types(
|
||||
context.db(),
|
||||
[provided_ty, expected_ty],
|
||||
);
|
||||
let provided_ty_display =
|
||||
provided_ty.display_with(context.db(), display_settings.clone());
|
||||
let expected_ty_display = expected_ty.display_with(context.db(), display_settings);
|
||||
|
||||
let mut diag = builder.into_diagnostic(format_args!(
|
||||
"Argument{} is incorrect",
|
||||
|
||||
@@ -2277,7 +2277,11 @@ impl<'db> ClassLiteral<'db> {
|
||||
specialization: Option<Specialization<'db>>,
|
||||
name: &str,
|
||||
) -> Member<'db> {
|
||||
if self.dataclass_params(db).is_some() {
|
||||
// Check if this class is dataclass-like (either via @dataclass or via dataclass_transform)
|
||||
if matches!(
|
||||
CodeGeneratorKind::from_class(db, self, specialization),
|
||||
Some(CodeGeneratorKind::DataclassLike(_))
|
||||
) {
|
||||
if name == "__dataclass_fields__" {
|
||||
// Make this class look like a subclass of the `DataClassInstance` protocol
|
||||
return Member {
|
||||
|
||||
@@ -76,14 +76,15 @@ impl<'db> DisplaySettings<'db> {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_possibly_ambiguous_types(
|
||||
db: &'db dyn Db,
|
||||
types: impl IntoIterator<Item = Type<'db>>,
|
||||
) -> Self {
|
||||
pub fn from_possibly_ambiguous_types<I, T>(db: &'db dyn Db, types: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<Type<'db>>,
|
||||
{
|
||||
let collector = AmbiguousClassCollector::default();
|
||||
|
||||
for ty in types {
|
||||
collector.visit_type(db, ty);
|
||||
collector.visit_type(db, ty.into());
|
||||
}
|
||||
|
||||
Self {
|
||||
@@ -422,6 +423,8 @@ impl<'db> super::visitor::TypeVisitor<'db> for AmbiguousClassCollector<'db> {
|
||||
inner: Protocol::FromClass(class),
|
||||
..
|
||||
}) => return self.visit_type(db, Type::from(class)),
|
||||
// no need to recurse into TypeVar bounds/constraints
|
||||
Type::TypeVar(_) => return,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -439,7 +442,7 @@ impl<'db> Type<'db> {
|
||||
pub fn display(self, db: &'db dyn Db) -> DisplayType<'db> {
|
||||
DisplayType {
|
||||
ty: self,
|
||||
settings: DisplaySettings::default(),
|
||||
settings: DisplaySettings::from_possibly_ambiguous_types(db, [self]),
|
||||
db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use lsp_types::{Position, notification::ShowMessage, request::RegisterCapability};
|
||||
use lsp_types::notification::ShowMessage;
|
||||
use lsp_types::{Position, request::RegisterCapability};
|
||||
use ruff_db::system::SystemPath;
|
||||
use serde_json::Value;
|
||||
use ty_server::{ClientOptions, DiagnosticMode};
|
||||
@@ -474,3 +475,20 @@ fn register_multiple_capabilities() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that the server doesn't panic when `VIRTUAL_ENV` points to a non-existent directory.
|
||||
///
|
||||
/// See: <https://github.com/astral-sh/ty/issues/2031>
|
||||
#[test]
|
||||
fn missing_virtual_env_does_not_panic() -> Result<()> {
|
||||
let workspace_root = SystemPath::new("project");
|
||||
|
||||
// This should not panic even though VIRTUAL_ENV points to a non-existent path
|
||||
let _server = TestServerBuilder::new()?
|
||||
.with_workspace(workspace_root, None)?
|
||||
.with_env_var("VIRTUAL_ENV", "/nonexistent/virtual/env/path")
|
||||
.build()
|
||||
.wait_until_workspaces_are_initialized();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ impl TestServer {
|
||||
test_context: TestContext,
|
||||
capabilities: ClientCapabilities,
|
||||
initialization_options: Option<ClientOptions>,
|
||||
env_vars: Vec<(String, String)>,
|
||||
) -> Self {
|
||||
setup_tracing();
|
||||
|
||||
@@ -219,11 +220,16 @@ impl TestServer {
|
||||
// Create OS system with the test directory as cwd
|
||||
let os_system = OsSystem::new(test_context.root());
|
||||
|
||||
// Create test system and set environment variable overrides
|
||||
let test_system = Arc::new(TestSystem::new(os_system));
|
||||
for (name, value) in env_vars {
|
||||
test_system.set_env_var(name, value);
|
||||
}
|
||||
|
||||
// Start the server in a separate thread
|
||||
let server_thread = std::thread::spawn(move || {
|
||||
// TODO: This should probably be configurable to test concurrency issues
|
||||
let worker_threads = NonZeroUsize::new(1).unwrap();
|
||||
let test_system = Arc::new(TestSystem::new(os_system));
|
||||
|
||||
match Server::new(worker_threads, server_connection, test_system, true) {
|
||||
Ok(server) => {
|
||||
@@ -1052,6 +1058,7 @@ pub(crate) struct TestServerBuilder {
|
||||
workspaces: Vec<(WorkspaceFolder, Option<ClientOptions>)>,
|
||||
initialization_options: Option<ClientOptions>,
|
||||
client_capabilities: ClientCapabilities,
|
||||
env_vars: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl TestServerBuilder {
|
||||
@@ -1082,6 +1089,7 @@ impl TestServerBuilder {
|
||||
test_context: TestContext::new()?,
|
||||
initialization_options: None,
|
||||
client_capabilities,
|
||||
env_vars: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1091,6 +1099,16 @@ impl TestServerBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an environment variable for the test server's system.
|
||||
pub(crate) fn with_env_var(
|
||||
mut self,
|
||||
name: impl Into<String>,
|
||||
value: impl Into<String>,
|
||||
) -> Self {
|
||||
self.env_vars.push((name.into(), value.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a workspace to the test server with the given root path and options.
|
||||
///
|
||||
/// This option will be used to respond to the `workspace/configuration` request that the
|
||||
@@ -1237,6 +1255,7 @@ impl TestServerBuilder {
|
||||
self.test_context,
|
||||
self.client_capabilities,
|
||||
self.initialization_options,
|
||||
self.env_vars,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,11 @@ export default function Chrome() {
|
||||
|
||||
const [theme, setTheme] = useTheme();
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
const handleShare = useCallback(async () => {
|
||||
if (settings == null || pythonSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
persist(settings, pythonSource).catch((error) =>
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to share playground: ${error}`),
|
||||
);
|
||||
await persist(settings, pythonSource);
|
||||
}, [pythonSource, settings]);
|
||||
|
||||
if (initPromise.current == null) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Header({
|
||||
version: string | null;
|
||||
onChangeTheme: (theme: Theme) => void;
|
||||
onReset?(): void;
|
||||
onShare: () => void;
|
||||
onShare: () => Promise<void>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import AstralButton from "./AstralButton";
|
||||
|
||||
export default function ShareButton({ onShare }: { onShare: () => void }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
type ShareStatus = "initial" | "copying" | "copied";
|
||||
|
||||
export default function ShareButton({
|
||||
onShare,
|
||||
}: {
|
||||
onShare: () => Promise<void>;
|
||||
}) {
|
||||
const [status, setStatus] = useState<ShareStatus>("initial");
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
const timeout = setTimeout(() => setCopied(false), 2000);
|
||||
if (status === "copied") {
|
||||
const timeout = setTimeout(() => setStatus("initial"), 2000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [copied]);
|
||||
}, [status]);
|
||||
|
||||
return copied ? (
|
||||
return status === "copied" ? (
|
||||
<AstralButton
|
||||
type="button"
|
||||
className="relative flex-none leading-6 py-1.5 px-3 cursor-auto dark:shadow-copied"
|
||||
@@ -28,10 +34,17 @@ export default function ShareButton({ onShare }: { onShare: () => void }) {
|
||||
<AstralButton
|
||||
type="button"
|
||||
className="relative flex-none leading-6 py-1.5 px-3 shadow-xs disabled:opacity-50"
|
||||
disabled={copied}
|
||||
onClick={() => {
|
||||
setCopied(true);
|
||||
onShare();
|
||||
disabled={status === "copying"}
|
||||
onClick={async () => {
|
||||
setStatus("copying");
|
||||
try {
|
||||
await onShare();
|
||||
setStatus("copied");
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to share playground", error);
|
||||
setStatus("initial");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -48,14 +48,11 @@ export default function Playground() {
|
||||
|
||||
usePersistLocally(files);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
const handleShare = useCallback(async () => {
|
||||
const serialized = serializeFiles(files);
|
||||
|
||||
if (serialized != null) {
|
||||
persist(serialized).catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to share playground", error);
|
||||
});
|
||||
await persist(serialized);
|
||||
}
|
||||
}, [files]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user