Skip to content

Commit dd51df7

Browse files
committed
[red-knot] Support custom typeshed Markdown tests
1 parent 05abd64 commit dd51df7

File tree

9 files changed

+242
-141
lines changed

9 files changed

+242
-141
lines changed
Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,62 @@
1-
# Importing builtin module
1+
# Builtins
2+
3+
## Importing builtin module
24

35
```py
46
import builtins
57

6-
x = builtins.chr
7-
reveal_type(x) # revealed: Literal[chr]
8+
reveal_type(builtins.chr) # revealed: Literal[chr]
9+
```
10+
11+
## Implicit use of builtin
12+
13+
```py
14+
reveal_type(chr) # revealed: Literal[chr]
15+
```
16+
17+
## `str` builtin
18+
19+
```py
20+
reveal_type(str) # revealed: Literal[str]
21+
```
22+
23+
## Builtin symbol from custom typeshed
24+
25+
```toml
26+
[environment]
27+
typeshed = "/typeshed"
28+
```
29+
30+
```pyi path=/typeshed/stdlib/builtins.pyi
31+
class Custom: ...
32+
33+
custom_builtin: Custom
34+
```
35+
36+
```pyi path=/typeshed/stdlib/typing_extensions.pyi
37+
def reveal_type(obj, /): ...
38+
```
39+
40+
```py
41+
reveal_type(custom_builtin) # revealed: Custom
42+
```
43+
44+
## Unknown builtin (later defined)
45+
46+
```toml
47+
[environment]
48+
typeshed = "/typeshed"
49+
```
50+
51+
```pyi path=/typeshed/stdlib/builtins.pyi
52+
foo = bar
53+
bar = 1
54+
```
55+
56+
```pyi path=/typeshed/stdlib/typing_extensions.pyi
57+
def reveal_type(obj, /): ...
58+
```
59+
60+
```py
61+
reveal_type(foo) # revealed: Unknown
862
```
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Custom typeshed
2+
3+
The `environment.typeshed` configuration option can be used to specify a custom typeshed directory
4+
for Markdown-based tests. Custom typeshed stubs can then be placed in the specified directory using
5+
fenced code blocks with language `pyi`, and will be used instead of the vendored copy of typeshed.
6+
7+
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
8+
custom typeshed root. If no such file is created explicitly, it will be automatically created with
9+
entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python versions.
10+
11+
## Basic example (auto-generated `VERSIONS` file)
12+
13+
First, we specify `/typeshed` as the custom typeshed directory:
14+
15+
```toml
16+
[environment]
17+
typeshed = "/typeshed"
18+
```
19+
20+
We can then place custom stub files in `/typeshed/stdlib`, for example:
21+
22+
```pyi path=/typeshed/stdlib/builtins.pyi
23+
class BuiltinClass: ...
24+
25+
builtin_symbol: BuiltinClass
26+
```
27+
28+
And finally write a normal Python code block that makes use of the custom stubs:
29+
30+
```py
31+
b: BuiltinClass = builtin_symbol
32+
33+
class OtherClass: ...
34+
35+
o: OtherClass = builtin_symbol # error: [invalid-assignment]
36+
```
37+
38+
## Custom `VERSIONS` file
39+
40+
If we want to specify a custom `VERSIONS` file, we can do so by creating a fenced code block with
41+
language `text`:
42+
43+
```toml
44+
[environment]
45+
python-version = "3.10"
46+
typeshed = "/typeshed"
47+
```
48+
49+
```pyi path=/typeshed/stdlib/old_module.pyi
50+
class OldClass: ...
51+
```
52+
53+
```pyi path=/typeshed/stdlib/new_module.pyi
54+
class NewClass: ...
55+
```
56+
57+
```text path=/typeshed/stdlib/VERSIONS
58+
old_module: 3.0-
59+
new_module: 3.11-
60+
```
61+
62+
```py
63+
from old_module import OldClass
64+
65+
# error: [unresolved-import] "Cannot resolve import `new_module`"
66+
from new_module import NewClass
67+
```
68+
69+
## Using `reveal_type` with a custom typeshed
70+
71+
When providing a custom typeshed directory, basic things like `reveal_type` will stop working
72+
because we rely on being able to import it from `typing_extensions`. The actual definition of
73+
`reveal_type` in typeshed is slightly involved (depends on generics, `TypeVar`, etc.), but a very
74+
simple untyped definition is enough to make `reveal_type` work in tests:
75+
76+
```toml
77+
[environment]
78+
typeshed = "/typeshed"
79+
```
80+
81+
```pyi path=/typeshed/stdlib/typing_extensions.pyi
82+
def reveal_type(obj, /): ...
83+
```
84+
85+
```py
86+
reveal_type(()) # revealed: tuple[()]
87+
```

crates/red_knot_python_semantic/src/db.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ pub(crate) mod tests {
136136
/// Target Python platform
137137
python_platform: PythonPlatform,
138138
/// Path to a custom typeshed directory
139-
custom_typeshed: Option<SystemPathBuf>,
139+
typeshed: Option<SystemPathBuf>,
140140
/// Path and content pairs for files that should be present
141141
files: Vec<(&'a str, &'a str)>,
142142
}
@@ -146,7 +146,7 @@ pub(crate) mod tests {
146146
Self {
147147
python_version: PythonVersion::default(),
148148
python_platform: PythonPlatform::default(),
149-
custom_typeshed: None,
149+
typeshed: None,
150150
files: vec![],
151151
}
152152
}
@@ -156,11 +156,6 @@ pub(crate) mod tests {
156156
self
157157
}
158158

159-
pub(crate) fn with_custom_typeshed(mut self, path: &str) -> Self {
160-
self.custom_typeshed = Some(SystemPathBuf::from(path));
161-
self
162-
}
163-
164159
pub(crate) fn with_file(mut self, path: &'a str, content: &'a str) -> Self {
165160
self.files.push((path, content));
166161
self
@@ -176,7 +171,7 @@ pub(crate) mod tests {
176171
.context("Failed to write test files")?;
177172

178173
let mut search_paths = SearchPathSettings::new(vec![src_root]);
179-
search_paths.typeshed = self.custom_typeshed;
174+
search_paths.typeshed = self.typeshed;
180175

181176
Program::from_settings(
182177
&db,

crates/red_knot_python_semantic/src/module_resolver/testing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ pub(crate) struct UnspecifiedTypeshed;
7373
///
7474
/// For tests checking that standard-library module resolution is working
7575
/// correctly, you should usually create a [`MockedTypeshed`] instance
76-
/// and pass it to the [`TestCaseBuilder::with_custom_typeshed`] method.
76+
/// and pass it to the [`TestCaseBuilder::with_mocked_typeshed`] method.
7777
/// If you need to check something that involves the vendored typeshed stubs
7878
/// we include as part of the binary, you can instead use the
7979
/// [`TestCaseBuilder::with_vendored_typeshed`] method.

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6003,14 +6003,13 @@ fn perform_membership_test_comparison<'db>(
60036003

60046004
#[cfg(test)]
60056005
mod tests {
6006-
use crate::db::tests::{setup_db, TestDb, TestDbBuilder};
6006+
use crate::db::tests::{setup_db, TestDb};
60076007
use crate::semantic_index::definition::Definition;
60086008
use crate::semantic_index::symbol::FileScopeId;
60096009
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
60106010
use crate::types::check_types;
60116011
use crate::{HasType, SemanticModel};
60126012
use ruff_db::files::{system_path_to_file, File};
6013-
use ruff_db::parsed::parsed_module;
60146013
use ruff_db::system::DbWithTestSystem;
60156014
use ruff_db::testing::assert_function_query_was_not_run;
60166015

@@ -6281,56 +6280,6 @@ mod tests {
62816280
Ok(())
62826281
}
62836282

6284-
#[test]
6285-
fn builtin_symbol_vendored_stdlib() -> anyhow::Result<()> {
6286-
let mut db = setup_db();
6287-
6288-
db.write_file("/src/a.py", "c = chr")?;
6289-
6290-
assert_public_type(&db, "/src/a.py", "c", "Literal[chr]");
6291-
6292-
Ok(())
6293-
}
6294-
6295-
#[test]
6296-
fn builtin_symbol_custom_stdlib() -> anyhow::Result<()> {
6297-
let db = TestDbBuilder::new()
6298-
.with_custom_typeshed("/typeshed")
6299-
.with_file("/src/a.py", "c = copyright")
6300-
.with_file(
6301-
"/typeshed/stdlib/builtins.pyi",
6302-
"def copyright() -> None: ...",
6303-
)
6304-
.with_file("/typeshed/stdlib/VERSIONS", "builtins: 3.8-")
6305-
.build()?;
6306-
6307-
assert_public_type(&db, "/src/a.py", "c", "Literal[copyright]");
6308-
6309-
Ok(())
6310-
}
6311-
6312-
#[test]
6313-
fn unknown_builtin_later_defined() -> anyhow::Result<()> {
6314-
let db = TestDbBuilder::new()
6315-
.with_custom_typeshed("/typeshed")
6316-
.with_file("/src/a.py", "x = foo")
6317-
.with_file("/typeshed/stdlib/builtins.pyi", "foo = bar; bar = 1")
6318-
.with_file("/typeshed/stdlib/VERSIONS", "builtins: 3.8-")
6319-
.build()?;
6320-
6321-
assert_public_type(&db, "/src/a.py", "x", "Unknown");
6322-
6323-
Ok(())
6324-
}
6325-
6326-
#[test]
6327-
fn str_builtin() -> anyhow::Result<()> {
6328-
let mut db = setup_db();
6329-
db.write_file("/src/a.py", "x = str")?;
6330-
assert_public_type(&db, "/src/a.py", "x", "Literal[str]");
6331-
Ok(())
6332-
}
6333-
63346283
#[test]
63356284
fn deferred_annotation_builtin() -> anyhow::Result<()> {
63366285
let mut db = setup_db();

crates/red_knot_test/README.md

Lines changed: 16 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ under a certain directory as test suites.
88

99
A Markdown test suite can contain any number of tests. A test consists of one or more embedded
1010
"files", each defined by a triple-backticks fenced code block. The code block must have a tag string
11-
specifying its language; currently only `py` (Python files) and `pyi` (type stub files) are
12-
supported.
11+
specifying its language. We currently support `py` (Python files) and `pyi` (type stub files), as
12+
well as typeshed `VERSIONS` files and `toml` for configuration.
1313

1414
The simplest possible test suite consists of just a single test, with a single embedded file:
1515

@@ -243,6 +243,20 @@ section. Nested sections can override configurations from their parent sections.
243243

244244
See [`MarkdownTestConfig`](https://github.com/astral-sh/ruff/blob/main/crates/red_knot_test/src/config.rs) for the full list of supported configuration options.
245245

246+
### Specifying a custom typeshed
247+
248+
Some tests will need to override the default typeshed with custom files. The `[environment]`
249+
configuration option `typeshed` can be used to do this:
250+
251+
````markdown
252+
```toml
253+
[environment]
254+
typeshed = "/typeshed"
255+
```
256+
````
257+
258+
For more details, take a look at the `mdtest_custom_typeshed.md` test.
259+
246260
## Documentation of tests
247261

248262
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
@@ -294,36 +308,6 @@ The column assertion `6` on the ending line should be optional.
294308
In cases of overlapping such assertions, resolve ambiguity using more angle brackets: `<<<<` begins
295309
an assertion ended by `>>>>`, etc.
296310

297-
### Non-Python files
298-
299-
Some tests may need to specify non-Python embedded files: typeshed `stdlib/VERSIONS`, `pth` files,
300-
`py.typed` files, `pyvenv.cfg` files...
301-
302-
We will allow specifying any of these using the `text` language in the code block tag string:
303-
304-
````markdown
305-
```text path=/third-party/foo/py.typed
306-
partial
307-
```
308-
````
309-
310-
We may want to also support testing Jupyter notebooks as embedded files; exact syntax for this is
311-
yet to be determined.
312-
313-
Of course, red-knot is only run directly on `py` and `pyi` files, and assertion comments are only
314-
possible in these files.
315-
316-
A fenced code block with no language will always be an error.
317-
318-
### Running just a single test from a suite
319-
320-
Having each test in a suite always run as a distinct Rust test would require writing our own test
321-
runner or code-generating tests in a build script; neither of these is planned.
322-
323-
We could still allow running just a single test from a suite, for debugging purposes, either via
324-
some "focus" syntax that could be easily temporarily added to a test, or via an environment
325-
variable.
326-
327311
### Configuring search paths and kinds
328312

329313
The red-knot TOML configuration format hasn't been finalized, and we may want to implement
@@ -346,38 +330,6 @@ Paths for `workspace-root` and `third-party-root` must be absolute.
346330
Relative embedded-file paths are relative to the workspace root, even if it is explicitly set to a
347331
non-default value using the `workspace-root` config.
348332

349-
### Specifying a custom typeshed
350-
351-
Some tests will need to override the default typeshed with custom files. The `[environment]`
352-
configuration option `typeshed-path` can be used to do this:
353-
354-
````markdown
355-
```toml
356-
[environment]
357-
typeshed-path = "/typeshed"
358-
```
359-
360-
This file is importable as part of our custom typeshed, because it is within `/typeshed`, which we
361-
configured above as our custom typeshed root:
362-
363-
```py path=/typeshed/stdlib/builtins.pyi
364-
I_AM_THE_ONLY_BUILTIN = 1
365-
```
366-
367-
This file is written to `/src/test.py`, because the default workspace root is `/src/ and the default
368-
file path is `test.py`:
369-
370-
```py
371-
reveal_type(I_AM_THE_ONLY_BUILTIN) # revealed: Literal[1]
372-
```
373-
374-
````
375-
376-
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
377-
custom typeshed root. If no such file is created explicitly, one should be created implicitly
378-
including entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python
379-
versions.
380-
381333
### I/O errors
382334

383335
We could use an `error=` configuration option in the tag string to make an embedded file cause an

0 commit comments

Comments
 (0)