Compare commits

...

72 Commits

Author SHA1 Message Date
Andrew Gallant
6d8acee9db [ty] Demonstrate SIGSEGV via salsa
This is a demonstration of what I believe must imply unsoundness
somewhere inside of Salsa. That is, I don't use any `unsafe`, but I get
an "invalid memory reference" error.

Here is the backtrace I get from `gdb`:

```
(gdb) bt
 #0  ty_python_semantic::module_resolver::path::{impl#33}::eq (self=<error reading variable: Cannot access memory at address 0x7ff80efb0bd0>, other=0x7ffff106fcb0) at crates/ty_python_semantic/src/module_resolver/path.rs:452
 #1  0x0000555555ccc9fb in alloc::sync::{impl#50}::eq<ty_python_semantic::module_resolver::path::SearchPathInner, alloc::alloc::Global> (self=0x7ffff0110470, other=0x7ffff1033660)
     at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/sync.rs:3358
 #2  0x0000555555ccc6a4 in alloc::sync::{impl#51}::eq<ty_python_semantic::module_resolver::path::SearchPathInner, alloc::alloc::Global> (self=0x7ffff0110470, other=0x7ffff1033660)
     at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/sync.rs:3388
 #3  0x0000555555b92744 in ty_python_semantic::module_resolver::path::{impl#40}::eq (self=0x7ffff0110470, other=0x7ffff1033660) at crates/ty_python_semantic/src/module_resolver/path.rs:491
 #4  0x00005555562a4239 in core::cmp::impls::{impl#9}::eq<ty_python_semantic::module_resolver::path::SearchPath, ty_python_semantic::module_resolver::path::SearchPath> (self=0x7ffff0e88f50, other=0x7ffff7c755c8)
     at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cmp.rs:2027
 #5  0x00005555562a4436 in core::tuple::{impl#10}::eq<(), &ty_python_semantic::module_resolver::path::SearchPath> (self=0x7ffff0e88f50, other=0x7ffff7c755c8)
     at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/tuple.rs:32
 #6  0x00005555562a44f2 in core::cmp::impls::{impl#9}::eq<((), &ty_python_semantic::module_resolver::path::SearchPath), ((), &ty_python_semantic::module_resolver::path::SearchPath)> (self=0x7ffff7c75178, other=0x7ffff7c75180)
     at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cmp.rs:2027
 #7  salsa::interned::{impl#14}::eq<((), &ty_python_semantic::module_resolver::path::SearchPath)> (self=0x7ffff0e88f50, data=0x7ffff7c755c8) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/interned.rs:1161
 #8  0x0000555555c172e7 in salsa::interned::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_>::value_eq<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_, ((), &ty_python_semantic::module_resolver::path::SearchPath)> (id=..., key=0x7ffff7c755c8, zalsa=0x7ffff000daa0, found_value=0x7ffff7c755e8)
     at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/interned.rs:763
 #9  0x0000555555ca12b7 in salsa::interned::{impl#7}::intern_id::{closure#0}<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_, ((), &ty_python_semantic::module_resolver::path::SearchPath), ty_python_semantic::module_resolver::list::list_modules_in::{closure#0}::{closure_env#0}> (id=0x7ffff0e3a588) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/interned.rs:374
 #10 0x00005555565a907e in hashbrown::raw::{impl#8}::find::{closure#0}<salsa::id::Id, allocator_api2::stable::alloc::global::Global, salsa::interned::{impl#7}::intern_id::{closure_env#0}<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_, ((), &ty_python_semantic::module_resolver::path::SearchPath), ty_python_semantic::module_resolver::list::list_modules_in::{closure#0}::{closure_env#0}>> (index=0)
     at /home/andrew/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:1197
 #11 0x00005555565821ec in hashbrown::raw::RawTableInner::find_inner (self=0x7ffff0098e08, hash=16887370829962692076, eq=...) at /home/andrew/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:1921
 #12 hashbrown::raw::RawTable<salsa::id::Id, allocator_api2::stable::alloc::global::Global>::find<salsa::id::Id, allocator_api2::stable::alloc::global::Global, salsa::interned::{impl#7}::intern_id::{closure_env#0}<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_, ((), &ty_python_semantic::module_resolver::path::SearchPath), ty_python_semantic::module_resolver::list::list_modules_in::{closure#0}::{closure_env#0}>> (
     self=0x7ffff0098e08, hash=16887370829962692076, eq=...) at /home/andrew/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:1197
 #13 0x0000555555c4f7f7 in hashbrown::raw::RawTable<salsa::id::Id, allocator_api2::stable::alloc::global::Global>::get<salsa::id::Id, allocator_api2::stable::alloc::global::Global, salsa::interned::{impl#7}::intern_id::{closure_env#0}<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_, ((), &ty_python_semantic::module_resolver::path::SearchPath), ty_python_semantic::module_resolver::list::list_modules_in::{closure#0}::{closure_env#0}>> (self=0x7ffff0098e08, hash=16887370829962692076, eq=...) at /home/andrew/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/raw/mod.rs:1212
 #14 hashbrown::table::HashTable<salsa::id::Id, allocator_api2::stable::alloc::global::Global>::find<salsa::id::Id, allocator_api2::stable::alloc::global::Global, salsa::interned::{impl#7}::intern_id::{closure_env#0}<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_, ((), &ty_python_semantic::module_resolver::path::SearchPath), ty_python_semantic::module_resolver::list::list_modules_in::{closure#0}::{closure_env#0}>> (
     self=0x7ffff0098e08, hash=16887370829962692076, eq=...) at /home/andrew/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hashbrown-0.15.5/src/table.rs:224
 #15 salsa::interned::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_>::intern_id<ty_python_semantic::module_resolver::list::list_modules_in::list_modules_in_Configuration_, ((), &ty_python_semantic::module_resolver::path::SearchPath), ty_python_semantic::module_resolver::list::list_modules_in::{closure#0}::{closure_env#0}> (self=0x7ffff00982c0, zalsa=0x7ffff000daa0, zalsa_local=0x7ffff7c785a8,
     key=..., assemble=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/interned.rs:377
 #16 0x0000555555a24e68 in ty_python_semantic::module_resolver::list::list_modules_in::{closure#0} () at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/components/salsa-macro-rules/src/setup_tracked_fn.rs:473
 #17 0x000055555647410c in salsa::attach::Attached::attach<dyn ty_python_semantic::db::Db, alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, ty_python_semantic::module_resolver::list::list_modules_in::{closure_env#0}> (self=0x7ffff7c7a618, db=..., op=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/attach.rs:79
 #18 0x0000555556470f92 in salsa::attach::attach::{closure#0}<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules_in::{closure_env#0}> (a=0x7ffff7c7a618) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/attach.rs:103
 #19 0x0000555556304feb in std::thread::local::LocalKey<salsa::attach::Attached>::try_with<salsa::attach::Attached, salsa::attach::attach::{closure_env#0}<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules_in::{closure_env#0}>, alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>> (
     self=0x555557db8810, f=...) at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:315
 #20 0x00005555562e7c8d in std::thread::local::LocalKey<salsa::attach::Attached>::with<salsa::attach::Attached, salsa::attach::attach::{closure_env#0}<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules_in::{closure_env#0}>, alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>> (
     self=0x555557db8810, f=<error reading variable: Cannot access memory at address 0x0>) at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:279
 #21 0x000055555646f5e4 in salsa::attach::attach<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules_in::{closure_env#0}> (db=..., op=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/attach.rs:101
 #22 0x0000555555cc9f94 in ty_python_semantic::module_resolver::list::list_modules_in (db=..., search_path=0x7ffff1033660)
     at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/components/salsa-macro-rules/src/setup_tracked_fn.rs:468
 #23 0x0000555555cc95b5 in ty_python_semantic::module_resolver::list::list_modules::{impl#2}::execute::inner_ (db=...) at crates/ty_python_semantic/src/module_resolver/list.rs:20
 #24 0x0000555555cc946b in ty_python_semantic::module_resolver::list::list_modules::{impl#2}::execute (db=...)
     at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/components/salsa-macro-rules/src/setup_tracked_fn.rs:302
 #25 0x00005555561c575a in salsa::function::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>::execute_query<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_> (db=..., zalsa=0x7ffff000daa0, active_query=..., opt_old_memo=..., id=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/function/execute.rs:311
 #26 0x0000555556211b9d in salsa::function::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>::execute<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_> (self=0x7ffff0098010, db=..., active_query=..., opt_old_memo=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/function/execute.rs:46
 #27 0x0000555556143974 in salsa::function::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>::fetch_cold<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_> (self=0x7ffff0098010, zalsa=0x7ffff000daa0, zalsa_local=0x7ffff7c785a8, db=..., id=..., memo_ingredient_index=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/function/fetch.rs:235
 #28 0x00005555561775d0 in salsa::function::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>::fetch_cold_with_retry<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_> (self=0x7ffff0098010, zalsa=0x7ffff000daa0, zalsa_local=0x7ffff7c785a8, db=..., id=..., memo_ingredient_index=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/function/fetch.rs:107
 #29 0x0000555556754069 in salsa::function::fetch::{impl#0}::refresh_memo::{closure#0}<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_> ()
     at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/function/fetch.rs:64
 #30 0x00005555562d0891 in core::option::Option<&salsa::function::memo::Memo<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>>::or_else<&salsa::function::memo::Memo<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>, salsa::function::fetch::{impl#0}::refresh_memo::{closure_env#0}<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>> (self=..., f=...)
 --Type <RET> for more, q to quit, c to continue without paging--
     at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:1609
 #31 0x0000555556183e04 in salsa::function::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>::refresh_memo<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_> (self=0x7ffff0098010, db=..., zalsa=0x7ffff000daa0, zalsa_local=0x7ffff7c785a8, id=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/function/fetch.rs:63
 #32 salsa::function::IngredientImpl<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_>::fetch<ty_python_semantic::module_resolver::list::list_modules::list_modules_Configuration_> (
     self=0x7ffff0098010, db=..., zalsa=0x7ffff000daa0, zalsa_local=0x7ffff7c785a8, id=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/function/fetch.rs:30
 #33 0x0000555555a24d85 in ty_python_semantic::module_resolver::list::list_modules::{closure#0} () at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/components/salsa-macro-rules/src/setup_tracked_fn.rs:474
 #34 0x0000555556474dd4 in salsa::attach::Attached::attach<dyn ty_python_semantic::db::Db, alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, ty_python_semantic::module_resolver::list::list_modules::{closure_env#0}> (self=0x7ffff7c7a618, db=..., op=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/attach.rs:79
 #35 0x0000555556470491 in salsa::attach::attach::{closure#0}<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules::{closure_env#0}> (a=0x7ffff7c7a618) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/attach.rs:103
 #36 0x00005555563064e6 in std::thread::local::LocalKey<salsa::attach::Attached>::try_with<salsa::attach::Attached, salsa::attach::attach::{closure_env#0}<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules::{closure_env#0}>, alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>> (
     self=0x555557db8810, f=...) at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:315
 #37 0x00005555562e897d in std::thread::local::LocalKey<salsa::attach::Attached>::with<salsa::attach::Attached, salsa::attach::attach::{closure_env#0}<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules::{closure_env#0}>, alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>> (self=0x555557db8810,
     f=<error reading variable: Cannot access memory at address 0x0>) at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:279
 #38 0x000055555646f265 in salsa::attach::attach<alloc::vec::Vec<ty_python_semantic::module_resolver::module::Module, alloc::alloc::Global>, dyn ty_python_semantic::db::Db, ty_python_semantic::module_resolver::list::list_modules::{closure_env#0}> (db=..., op=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/src/attach.rs:101
 #39 0x0000555555cc93c7 in ty_python_semantic::module_resolver::list::list_modules (db=...) at /home/andrew/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/a3ffa22/components/salsa-macro-rules/src/setup_tracked_fn.rs:468
 #40 0x00005555557e2186 in ty_test::run_module_resolution_consistency_test (db=0x7ffff7c78598) at crates/ty_test/src/lib.rs:450
 #41 0x00005555557defec in ty_test::run (absolute_fixture_path=..., relative_fixture_path=..., snapshot_path=..., short_title=..., test_name=..., output_format=ty_test::OutputFormat::Cli) at crates/ty_test/src/lib.rs:73
 #42 0x0000555555793c06 in mdtest::mdtest (fixture=...) at crates/ty_python_semantic/tests/mdtest.rs:29
 #43 0x00005555557959c1 in mdtest::mdtest__generics_pep695_classes () at crates/ty_python_semantic/tests/mdtest.rs:7
 #44 0x00005555557903a7 in mdtest::mdtest__generics_pep695_classes::{closure#0} () at crates/ty_python_semantic/tests/mdtest.rs:10
 #45 0x000055555579a1b6 in core::ops::function::FnOnce::call_once<mdtest::mdtest__generics_pep695_classes::{closure_env#0}, ()> ()
     at /home/andrew/.rustup/toolchains/1.89-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250
 #46 0x00005555557d98eb in core::ops::function::FnOnce::call_once<fn() -> core::result::Result<(), alloc::string::String>, ()> () at library/core/src/ops/function.rs:250
 #47 test::__rust_begin_short_backtrace<core::result::Result<(), alloc::string::String>, fn() -> core::result::Result<(), alloc::string::String>> () at library/test/src/lib.rs:648
 #48 0x00005555557d8b6e in test::run_test_in_process::{closure#0} () at library/test/src/lib.rs:671
 #49 core::panic::unwind_safe::{impl#23}::call_once<core::result::Result<(), alloc::string::String>, test::run_test_in_process::{closure_env#0}> () at library/core/src/panic/unwind_safe.rs:272
 #50 std::panicking::catch_unwind::do_call<core::panic::unwind_safe::AssertUnwindSafe<test::run_test_in_process::{closure_env#0}>, core::result::Result<(), alloc::string::String>> () at library/std/src/panicking.rs:589
 #51 std::panicking::catch_unwind<core::result::Result<(), alloc::string::String>, core::panic::unwind_safe::AssertUnwindSafe<test::run_test_in_process::{closure_env#0}>> () at library/std/src/panicking.rs:552
 #52 std::panic::catch_unwind<core::panic::unwind_safe::AssertUnwindSafe<test::run_test_in_process::{closure_env#0}>, core::result::Result<(), alloc::string::String>> () at library/std/src/panic.rs:359
 #53 test::run_test_in_process () at library/test/src/lib.rs:671
 #54 test::run_test::{closure#0} () at library/test/src/lib.rs:592
 #55 0x000055555579d944 in test::run_test::{closure#1} () at library/test/src/lib.rs:622
 #56 std::sys::backtrace::__rust_begin_short_backtrace<test::run_test::{closure_env#1}, ()> () at library/std/src/sys/backtrace.rs:152
 #57 0x00005555557a10ca in std::thread::{impl#0}::spawn_unchecked_::{closure#1}::{closure#0}<test::run_test::{closure_env#1}, ()> () at library/std/src/thread/mod.rs:559
 #58 core::panic::unwind_safe::{impl#23}::call_once<(), std::thread::{impl#0}::spawn_unchecked_::{closure#1}::{closure_env#0}<test::run_test::{closure_env#1}, ()>> () at library/core/src/panic/unwind_safe.rs:272
 #59 std::panicking::catch_unwind::do_call<core::panic::unwind_safe::AssertUnwindSafe<std::thread::{impl#0}::spawn_unchecked_::{closure#1}::{closure_env#0}<test::run_test::{closure_env#1}, ()>>, ()> ()
     at library/std/src/panicking.rs:589
 #60 std::panicking::catch_unwind<(), core::panic::unwind_safe::AssertUnwindSafe<std::thread::{impl#0}::spawn_unchecked_::{closure#1}::{closure_env#0}<test::run_test::{closure_env#1}, ()>>> () at library/std/src/panicking.rs:552
 #61 std::panic::catch_unwind<core::panic::unwind_safe::AssertUnwindSafe<std::thread::{impl#0}::spawn_unchecked_::{closure#1}::{closure_env#0}<test::run_test::{closure_env#1}, ()>>, ()> () at library/std/src/panic.rs:359
 #62 std::thread::{impl#0}::spawn_unchecked_::{closure#1}<test::run_test::{closure_env#1}, ()> () at library/std/src/thread/mod.rs:557
 #63 core::ops::function::FnOnce::call_once<std::thread::{impl#0}::spawn_unchecked_::{closure_env#1}<test::run_test::{closure_env#1}, ()>, ()> () at library/core/src/ops/function.rs:250
 #64 0x000055555705088f in alloc::boxed::{impl#28}::call_once<(), dyn core::ops::function::FnOnce<(), Output=()>, alloc::alloc::Global> () at library/alloc/src/boxed.rs:1966
 #65 std::sys::pal::unix::thread::{impl#2}::new::thread_start () at library/std/src/sys/pal/unix/thread.rs:107
 #66 0x00007ffff7d127eb in ?? () from /usr/lib/libc.so.6
 #67 0x00007ffff7d9618c in ?? () from /usr/lib/libc.so.6
```
2025-08-20 10:46:45 -04:00
Andrew Gallant
ddd4bab67c [ty] Re-arrange "list modules" implementation for Salsa caching
This basically splits `list_modules` into a higher level "aggregation"
routine and a lower level "get modules for one search path" routine.
This permits Salsa to cache the lower level components, e.g., many
search paths refer to directories that rarely change. This saves us
interaction with the system.

This did require a fair bit of surgery in terms of being careful about
adding file roots. Namely, now that we rely even more on file roots
existing for correct handling of cache invalidation, there were several
spots in our code that needed to be updated to add roots (that we
weren't previously doing). This feels Not Great, and it would be better
if we had some kind of abstraction that handled this for us. But it
isn't clear to me at this time what that looks like.
2025-08-20 10:41:47 -04:00
Andrew Gallant
468eb37d75 [ty] Test "list modules" versus "resolve module" in every mdtest
This ensures there is some level of consistency between the APIs.

This did require exposing a couple more things on `Module` for good
error messages. This also motivated a switch to an interned struct
instead of a tracked struct. This ensures that `list_modules` and
`resolve_modules` reuse the same `Module` values when the inputs are the
same.

Ref https://github.com/astral-sh/ruff/pull/19883#discussion_r2272520194
2025-08-20 10:27:54 -04:00
Andrew Gallant
2e9c241d7e [ty] Wire up "list modules" API to make module completions work
This makes `import <CURSOR>` and `from <CURSOR>` completions work.

This also makes `import os.<CURSOR>` and `from os.<CURSOR>`
completions work. In this case, we are careful to only offer
submodule completions.
2025-08-20 10:27:54 -04:00
Andrew Gallant
05478d5cc7 [ty] Tweak some completion tests
These tests were added as a regression check that a panic
didn't occur. So we were asserting a bit more than necessary.
In particular, these will soon return completions for modules,
which creates large snapshots that we don't need.

So modify these to just check there is sensible output that
doesn't panic.
2025-08-20 10:27:54 -04:00
Andrew Gallant
4db20f459c [ty] Add "list modules" implementation
The actual implementation wasn't too bad. It's not long
but pretty fiddly. I copied over the tests from the existing
module resolver and adapted them to work with this API. Then
I added a number of my own tests as well.
2025-08-20 10:27:54 -04:00
Andrew Gallant
ec7c2efef9 [ty] Lightly expose FileModule and NamespacePackage fields
This will make it easier to emit this info into snapshots for
testing.
2025-08-20 10:27:54 -04:00
Andrew Gallant
79b2754215 [ty] Add some more helper routines to ModulePath 2025-08-20 10:27:54 -04:00
Andrew Gallant
a0ddf1f7c4 [ty] Fix a bug when converting ModulePath to ModuleName
Previously, if the module was just `foo-stubs`, we'd skip over
stripping the `-stubs` suffix which would lead to us returning
`None`.

This function is now a little convoluted and could be simpler
if we did an intermediate allocation. But I kept the iterative
approach and added a special case to handle `foo-stubs`.
2025-08-20 10:27:54 -04:00
Andrew Gallant
5b00ec981b [ty] Split out another constructor for ModuleName
This makes it a little more flexible to call. For example,
we might have a `StmtImport` and not a `StmtImportFrom`.
2025-08-20 10:27:54 -04:00
Andrew Gallant
306ef3bb02 [ty] Add stub-file tests to existing module resolver
These tests capture existing behavior.

I added these when I stumbled upon what I thought was an
oddity: we prioritize `foo.pyi` over `foo.py`, but
prioritize `foo/__init__.py` over `foo.pyi`.

(I plan to investigate this more closely in follow-up
work. Particularly, to look at other type checkers. It
seems like we may want to change this to always prioritize
stubs.)
2025-08-20 10:27:54 -04:00
Andrew Gallant
a4cd13c6e2 [ty] Expose some routines in the module resolver
We'll want to use these when implementing the
"list modules" API.
2025-08-20 10:27:54 -04:00
Andrew Gallant
e0c98874e2 [ty] Add more path helper functions
This makes it easier to do exhaustive case analysis
on a `SearchPath` depending on whether it is a vendored
or system path.
2025-08-20 10:27:54 -04:00
Andrey
f4be05a83b [flake8-annotations] Remove unused import in example (ANN401) (#20000)
## Summary

Remove unused import in the  "Use instead" example.

## Test Plan

It's just a text description, no test needed
2025-08-20 09:19:18 -04:00
Aria Desires
1d2128f918 [ty] distinguish base conda from child conda (#19990)
This is a port of the logic in https://github.com/astral-sh/uv/pull/7691

The basic idea is we use CONDA_DEFAULT_ENV as a signal for whether
CONDA_PREFIX is just the ambient system conda install, or the user has
explicitly activated a custom one. If the former, then the conda is
treated like a system install (having lowest priority). If the latter,
the conda is treated like an activated venv (having priority over
everything but an Actual activated venv).

Fixes https://github.com/astral-sh/ty/issues/611
2025-08-20 09:07:42 -04:00
Micha Reiser
276405b44e [ty] Fix server hang (#19991) 2025-08-20 10:28:30 +02:00
Dhruv Manilawala
f019cfd15f [ty] Use specialized parameter type for overload filter (#19964)
## Summary

Closes: https://github.com/astral-sh/ty/issues/669

(This turned out to be simpler that I thought :))

## Test Plan

Update existing test cases.

### Ecosystem report

Most of them are basically because ty has now started inferring more
precise types for the return type to an overloaded call and a lot of the
types are defined using type aliases, here's some examples:

<details><summary>Details</summary>
<p>

> attrs (https://github.com/python-attrs/attrs)
> + tests/test_make.py:146:14: error[unresolved-attribute] Type
`Literal[42]` has no attribute `default`
> - Found 555 diagnostics
> + Found 556 diagnostics

This is accurate now that we infer the type as `Literal[42]` instead of
`Unknown` (Pyright infers it as `int`)

> optuna (https://github.com/optuna/optuna)
> + optuna/_gp/search_space.py:181:53: error[invalid-argument-type]
Argument to function `_round_one_normalized_param` is incorrect:
Expected `tuple[int | float, int | float]`, found `tuple[Unknown |
ndarray[Unknown, <class 'float'>], Unknown | ndarray[Unknown, <class
'float'>]]`
> + optuna/_gp/search_space.py:181:83: error[invalid-argument-type]
Argument to function `_round_one_normalized_param` is incorrect:
Expected `int | float`, found `Unknown | ndarray[Unknown, <class
'float'>]`
> + tests/gp_tests/test_search_space.py:109:13:
error[invalid-argument-type] Argument to function
`_unnormalize_one_param` is incorrect: Expected `tuple[int | float, int
| float]`, found `Unknown | ndarray[Unknown, <class 'float'>]`
> + tests/gp_tests/test_search_space.py:110:13:
error[invalid-argument-type] Argument to function
`_unnormalize_one_param` is incorrect: Expected `int | float`, found
`Unknown | ndarray[Unknown, <class 'float'>]`
> - Found 559 diagnostics
> + Found 563 diagnostics

Same as above where ty is now inferring a more precise type like
`Unknown | ndarray[tuple[int, int], <class 'float'>]` instead of just
`Unknown` as before

> jinja (https://github.com/pallets/jinja)
> + src/jinja2/bccache.py:298:39: error[invalid-argument-type] Argument
to bound method `write_bytecode` is incorrect: Expected `IO[bytes]`,
found `_TemporaryFileWrapper[str]`
> - Found 186 diagnostics
> + Found 187 diagnostics

This requires support for type aliases to match the correct overload.

> hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
> + src/hydra_zen/wrapper/_implementations.py:945:16:
error[invalid-return-type] Return type does not match returned value:
expected `DataClass_ | type[@Todo(type[T] for protocols)] | ListConfig |
DictConfig`, found `@Todo(unsupported type[X] special form) | (((...) ->
Any) & dict[Unknown, Unknown]) | (DataClass_ & dict[Unknown, Unknown]) |
dict[Any, Any] | (ListConfig & dict[Unknown, Unknown]) | (DictConfig &
dict[Unknown, Unknown]) | (((...) -> Any) & list[Unknown]) | (DataClass_
& list[Unknown]) | list[Any] | (ListConfig & list[Unknown]) |
(DictConfig & list[Unknown])`
> + tests/annotations/behaviors.py:60:28: error[call-non-callable]
Object of type `Path` is not callable
> + tests/annotations/behaviors.py:64:21: error[call-non-callable]
Object of type `Path` is not callable
> + tests/annotations/declarations.py:167:17: error[call-non-callable]
Object of type `Path` is not callable
> + tests/annotations/declarations.py:524:17:
error[unresolved-attribute] Type `<class 'int'>` has no attribute
`_target_`
> - Found 561 diagnostics
> + Found 566 diagnostics

Same as above, this requires support for type aliases to match the
correct overload.

> paasta (https://github.com/yelp/paasta)
> + paasta_tools/utils.py:4188:19: warning[redundant-cast] Value is
already of type `list[str]`
> - Found 888 diagnostics
> + Found 889 diagnostics

This is correct.

> colour (https://github.com/colour-science/colour)
> + colour/plotting/diagrams.py:448:13: error[invalid-argument-type]
Argument to bound method `__init__` is incorrect: Expected
`Sequence[@Todo(Support for `typing.TypeAlias`)]`, found
`ndarray[tuple[int, int, int], dtype[Unknown]]`
> + colour/plotting/diagrams.py:462:13: error[invalid-argument-type]
Argument to bound method `__init__` is incorrect: Expected
`Sequence[@Todo(Support for `typing.TypeAlias`)]`, found
`ndarray[tuple[int, int, int], dtype[Unknown]]`
> + colour/plotting/models.py:419:13: error[invalid-argument-type]
Argument to bound method `__init__` is incorrect: Expected
`Sequence[@Todo(Support for `typing.TypeAlias`)]`, found
`ndarray[tuple[int, int, int], dtype[Unknown]]`
> + colour/plotting/temperature.py:230:9: error[invalid-argument-type]
Argument to bound method `__init__` is incorrect: Expected
`Sequence[@Todo(Support for `typing.TypeAlias`)]`, found
`ndarray[tuple[int, int, int], dtype[Unknown]]`
> + colour/plotting/temperature.py:474:13: error[invalid-argument-type]
Argument to bound method `__init__` is incorrect: Expected
`Sequence[@Todo(Support for `typing.TypeAlias`)]`, found
`ndarray[tuple[int, int, int], dtype[Unknown]]`
> + colour/plotting/temperature.py:495:17: error[invalid-argument-type]
Argument to bound method `__init__` is incorrect: Expected
`Sequence[@Todo(Support for `typing.TypeAlias`)]`, found
`ndarray[tuple[int, int, int], dtype[Unknown]]`
> + colour/plotting/temperature.py:513:13: error[invalid-argument-type]
Argument to bound method `text` is incorrect: Expected `int | float`,
found `ndarray[@Todo(Support for `typing.TypeAlias`), dtype[Unknown]]`
> + colour/plotting/temperature.py:514:13: error[invalid-argument-type]
Argument to bound method `text` is incorrect: Expected `int | float`,
found `ndarray[@Todo(Support for `typing.TypeAlias`), dtype[Unknown]]`
> - Found 480 diagnostics
> + Found 488 diagnostics

Most of them are correct except for the last two diagnostics which I'm
not sure
what's happening, it's trying to index into an `np.ndarray` type (which
is
inferred correctly) but I think it might be picking up an incorrect
overload
for the `__getitem__` method.

Scipy's diagnostics also requires support for type alises to pick the
correct overload.

</p>
</details>
2025-08-20 09:39:05 +05:30
Eric Mark Martin
33030b34cd [ty] linear variance inference for PEP-695 type parameters (#18713)
## Summary

Implement linear-time variance inference for type variables
(https://github.com/astral-sh/ty/issues/488).

Inspired by Martin Huschenbett's [PyCon 2025
Talk](https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s).

## Test Plan

update tests, add new tests, including for mutually recursive classes

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-08-19 17:54:09 -07:00
Alex Waygood
656fc335f2 [ty] Strict validation of protocol members (#17750) 2025-08-19 22:45:41 +00:00
Dan Parizher
e0f4cec7a1 [pyupgrade] Handle nested Optionals (UP045) (#19770)
## Summary

Fixes #19746

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-08-19 18:12:15 -04:00
Alex Waygood
662d18bd05 [ty] Add precise inference for unpacking a TypeVar if the TypeVar has an upper bound with a precise tuple spec (#19985) 2025-08-19 22:11:30 +01:00
Aria Desires
c82e255ca8 [ty] Fix namespace packages that behave like partial stubs (#19994)
In implementing partial stubs I had observed that this continue in the
namespace package code seemed erroneous since the same continue for
partial stubs didn't work. Unfortunately I wasn't confident enough to
push on that hunch. Fortunately I remembered that hunch to make this an
easy fix.

The issue with the continue is that it bails out of the current
search-path without testing any .py files. This breaks when for example
`google` and `google-stubs`/`types-google` are both in the same
site-packages dir -- failing to find a module in `types-google` has us
completely skip over `google`!

Fixes https://github.com/astral-sh/ty/issues/520
2025-08-19 16:34:39 -04:00
Eric Jolibois
58efd19f11 [ty] apply KW_ONLY sentinel only to local fields (#19986)
fix https://github.com/astral-sh/ty/issues/1047

## Summary

This PR fixes how `KW_ONLY` is applied in dataclasses. Previously, the
sentinel leaked into subclasses and incorrectly marked their fields as
keyword-only; now it only affects fields declared in the same class.

```py
from dataclasses import dataclass, KW_ONLY

@dataclass
class D:
    x: int
    _: KW_ONLY
    y: str

@dataclass
class E(D):
    z: bytes

# This should work: x=1 (positional), z=b"foo" (positional), y="foo" (keyword-only)
E(1, b"foo", y="foo")

reveal_type(E.__init__)  # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
```

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
mdtests
2025-08-19 11:01:35 -07:00
Aria Desires
c6dcfe36d0 [ty] introduce multiline pretty printer (#19979)
Requires some iteration, but this includes the most tedious part --
threading a new concept of DisplaySettings through every type display
impl. Currently it only holds a boolean for multiline, but in the future
it could also take other things like "render to markdown" or "here's
your base indent if you make a newline".

For types which have exposed display functions I've left the old
signature as a compatibility polyfill to avoid having to audit
everywhere that prints types right off the bat (notably I originally
tried doing multiline functions unconditionally and a ton of things
churned that clearly weren't ready for multi-line (diagnostics).

The only real use of this API in this PR is to multiline render function
types in hovers, which is the highest impact (see snapshot changes).

Fixes https://github.com/astral-sh/ty/issues/1000
2025-08-19 17:31:44 +00:00
Avasam
59b078b1bf Update outdated links to https://typing.python.org/en/latest/source/stubs.html (#19992) 2025-08-19 18:12:08 +01:00
Andrew Gallant
5e943d3539 [ty] Ask the LSP client to watch all project search paths
This change rejiggers how we register globs for file watching with the
LSP client. Previously, we registered a few globs like `**/*.py`,
`**/pyproject.toml` and more. There were two problems with this
approach.

Firstly, it only watches files within the project root. Search paths may
be outside the project root. Such as virtualenv directory.

Secondly, there is variation on how tools interact with virtual
environments. In the case of uv, depending on its link mode, we might
not get any file change notifications after running `uv add foo` or
`uv remove foo`.

To remedy this, we instead just list for file change notifications on
all files for all search paths. This simplifies the globs we use, but
does potentially increase the number of notifications we'll get.
However, given the somewhat simplistic interface supported by the LSP
protocol, I think this is unavoidable (unless we used our own file
watcher, which has its own considerably downsides). Moreover, this is
seemingly consistent with how `ty check --watch` works.

This also required moving file watcher registration to *after*
workspaces are initialized, or else we don't know what the right search
paths are.

This change is in service of #19883, which in order for cache
invalidation to work right, the LSP client needs to send notifications
whenever a dependency is added or removed. This change should make that
possible.

I tried this patch with #19883 in addition to my work to activate Salsa
caching, and everything seems to work as I'd expect. That is,
completions no longer show stale results after a dependency is added or
removed.
2025-08-19 10:57:07 -04:00
renovate[bot]
0967e7e088 Update Rust crate glob to v0.3.3 (#19959)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [glob](https://redirect.github.com/rust-lang/glob) |
workspace.dependencies | patch | `0.3.2` -> `0.3.3` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>rust-lang/glob (glob)</summary>

###
[`v0.3.3`](https://redirect.github.com/rust-lang/glob/blob/HEAD/CHANGELOG.md#033---2025-08-11)

[Compare
Source](https://redirect.github.com/rust-lang/glob/compare/v0.3.2...v0.3.3)

- Optimize memory allocations
([#&#8203;147](https://redirect.github.com/rust-lang/glob/pull/147))
- Bump the MSRV to 1.63
([#&#8203;172](https://redirect.github.com/rust-lang/glob/pull/172))
- Fix spelling in pattern documentation
([#&#8203;164](https://redirect.github.com/rust-lang/glob/pull/164))
- Fix version numbers and some formatting
([#&#8203;157](https://redirect.github.com/rust-lang/glob/pull/157))
- Style fixes
([#&#8203;137](https://redirect.github.com/rust-lang/glob/pull/137))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 4am on Monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/astral-sh/ruff).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS43MS4xIiwidXBkYXRlZEluVmVyIjoiNDEuNzEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-08-19 10:39:23 -04:00
Alex Waygood
600245478c [ty] Look for site-packages directories in <sys.prefix>/lib64/ as well as <sys.prefix>/lib/ on non-Windows systems (#19978) 2025-08-19 11:53:06 +00:00
Alex Waygood
e5c091b850 [ty] Fix protocol interface inference for stub protocols and subprotocols (#19950) 2025-08-19 10:31:11 +00:00
David Peter
10301f6190 [ty] Enable virtual terminal on Windows (#19984)
## Summary

Should hopefully fix https://github.com/astral-sh/ty/issues/1045
2025-08-19 09:13:03 +00:00
Alex Waygood
4242905b36 [ty] Detect NamedTuple classes where fields without default values follow fields with default values (#19945) 2025-08-19 08:56:08 +00:00
Aria Desires
c20d906503 [ty] improve goto/hover for definitions (#19976)
By computing the actual Definition for, well, definitions, we unlock a
bunch of richer machinery in the goto/hover subsystems for free.

Fixes https://github.com/astral-sh/ty/issues/1001
Fixes https://github.com/astral-sh/ty/issues/1004
2025-08-18 21:42:53 -04:00
Carl Meyer
a04375173c [ty] fix unpacking a type alias with detailed tuple spec (#19981)
## Summary

Fixes https://github.com/astral-sh/ty/issues/1046

We special-case iteration of certain types because they may have a more
detailed tuple-spec. Now that type aliases are a distinct type variant,
we need to handle them as well.

I don't love that `Type::TypeAlias` means we have to remember to add a
case for it basically anywhere we are special-casing a certain kind of
type, but at the moment I don't have a better plan. It's another
argument for avoiding fallback cases in `Type` matches, which we usually
prefer; I've updated this match statement to be comprehensive.

## Test Plan

Added mdtest.
2025-08-18 17:54:05 -07:00
Alex Waygood
e6dcdd29f2 [ty] Add a Todo-type branch for type[P] where P is a protocol class (#19947) 2025-08-18 20:38:19 +00:00
Matthew Mckee
24f6d2dc13 [ty] Infer the correct type of Enum __eq__ and __ne__ comparisions (#19666)
## Summary

Resolves https://github.com/astral-sh/ty/issues/920

## Test Plan

Update `enums.md`

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-08-18 19:45:44 +02:00
Alex Waygood
3314cf90ed [ty] Add more regression tests for tuple (#19974) 2025-08-18 18:30:05 +01:00
Aria Desires
0cb1abc1fc [ty] Implement partial stubs (#19931)
Fixes https://github.com/astral-sh/ty/issues/184
2025-08-18 13:14:13 -04:00
Brent Westbrook
f6491cacd1 Add full output format changes to the changelog (#19968)
Summary
--

I thought this might warrant a small blog-style writeup, especially
since we already got a question about it (#19966), but I'm happy to
switch back to a one-liner under `### Other changes` if preferred.

I'll copy whatever we add here to the release notes too.

Do we need a note at the top about the late addition?
2025-08-18 11:46:16 -04:00
Alex Waygood
e4f1b587cc Upgrade mypy_primer pin (#19967) 2025-08-18 13:27:54 +01:00
Alex Waygood
fbf24be8ae [ty] Detect illegal multiple inheritance with NamedTuple (#19943) 2025-08-18 12:03:01 +00:00
Micha Reiser
5e4fa9e442 [ty] Speedup tracing checks (#19965) 2025-08-18 12:56:06 +02:00
Micha Reiser
67529edad6 [ty] Short-circuit inlayhints request if disabled in settings (#19963) 2025-08-18 10:35:40 +00:00
Alex Waygood
4ac2b2c222 [ty] Have SemanticIndex::place_table() and SemanticIndex::use_def_map return references (#19944) 2025-08-18 11:30:52 +01:00
renovate[bot]
083bb85d9d Update actions/checkout to v5.0.0 (#19952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-08-18 07:31:07 +00:00
Micha Reiser
c7af595fc1 [ty] Use debug builds for conformance tests and run them single threaded (#19938) 2025-08-18 07:20:49 +00:00
Micha Reiser
7d8f7c20da [ty] Log server version at info level (#19961) 2025-08-18 07:16:53 +00:00
renovate[bot]
76c933d10e Update dependency ruff to v0.12.9 (#19954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:54:23 +02:00
renovate[bot]
d423191d94 Update Rust crate bitflags to v2.9.2 (#19957)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:54:09 +02:00
renovate[bot]
c8d155b2b9 Update Rust crate clap to v4.5.45 (#19958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:53:51 +02:00
renovate[bot]
a5339a52c3 Update Rust crate libc to v0.2.175 (#19960)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:53:31 +02:00
renovate[bot]
48772c04d7 Update Rust crate anyhow to v1.0.99 (#19956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:53:10 +02:00
renovate[bot]
510a07dee2 Update PyO3/maturin-action action to v1.49.4 (#19955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:44:00 +02:00
gkowzan
47d44e5f7b Fix description of global config file discovery strategy (#19143) (#19188)
Contrary to docs, ruff uses etcetera's base strategy rather than the
native strategy.
2025-08-17 18:35:37 -05:00
Alex Waygood
ec3163781c [ty] Remove unused code (#19949) 2025-08-17 18:54:24 +01:00
Douglas Creager
b892e4548e [ty] Track when type variables are inferable or not (#19786)
`Type::TypeVar` now distinguishes whether the typevar in question is
inferable or not.

A typevar is _not inferable_ inside the body of the generic class or
function that binds it:

```py
def f[T](t: T) -> T:
    return t
```

The infered type of `t` in the function body is `TypeVar(T,
NotInferable)`. This represents how e.g. assignability checks need to be
valid for all possible specializations of the typevar. Most of the
existing assignability/etc logic only applies to non-inferable typevars.

Outside of the function body, the typevar is _inferable_:

```py
f(4)
```

Here, the parameter type of `f` is `TypeVar(T, Inferable)`. This
represents how e.g. assignability doesn't need to hold for _all_
specializations; instead, we need to find the constraints under which
this specific assignability check holds.

This is in support of starting to perform specialization inference _as
part of_ performing the assignability check at the call site.

In the [[POPL2015][]] paper, this concept is called _monomorphic_ /
_polymorphic_, but I thought _non-inferable_ / _inferable_ would be
clearer for us.

Depends on #19784 

[POPL2015]: https://doi.org/10.1145/2676726.2676991

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-08-16 18:25:03 -04:00
Alex Waygood
9ac39cee98 [ty] Ban protocols from inheriting from non-protocol generic classes (#19941) 2025-08-16 19:38:43 +01:00
Alex Waygood
f4d8826428 [ty] Fix error message for invalidly providing type arguments to NamedTuple when it occurs in a type expression (#19940) 2025-08-16 17:45:15 +00:00
Micha Reiser
527a690a73 [ty] Fix example in environment docs (#19937) 2025-08-16 14:37:28 +00:00
Dan Parizher
f0e9c1d8f9 [isort] Handle multiple continuation lines after module docstring (I002) (#19818)
## Summary

Fixes #19815

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-08-15 17:17:50 -04:00
Frazer McLean
2e1d6623cd [flake8-simplify] Implement fix for maxsplit without separator (SIM905) (#19851)
**Stacked on top of #19849; diff will include that PR until it is
merged.**

---

<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

As part of #19849, I noticed this fix could be implemented.

## Test Plan

Tests added based on CPython behaviour.
2025-08-15 15:18:06 -04:00
Dan Parizher
2dc2f68b0f [pycodestyle] Make E731 fix unsafe instead of display-only for class assignments (#19700)
## Summary

Fixes #19650

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
2025-08-15 19:09:55 +00:00
Alex Waygood
26d6c3831f [ty] Represent NamedTuple as an opaque special form, not a class (#19915) 2025-08-15 18:20:14 +01:00
Alex Waygood
9ced219ffc [ty] Remove incorrect type narrowing for if type(x) is C[int] (#19926) 2025-08-15 17:52:14 +01:00
Micha Reiser
f344dda82c Bump Rust MSRV to 1.87 (#19924) 2025-08-15 17:55:38 +02:00
Alex Waygood
6de84ed56e Add else-branch narrowing for if type(a) is A when A is @final (#19925) 2025-08-15 14:52:30 +01:00
github-actions[bot]
bd4506aac5 [ty] Sync vendored typeshed stubs (#19923)
Close and reopen this PR to trigger CI

---------

Co-authored-by: typeshedbot <>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-08-14 18:09:35 -07:00
Shunsuke Shibayama
0e5577ab56 [ty] fix lazy snapshot sweeping in nested scopes (#19908)
## Summary

This PR closes astral-sh/ty#955.

## Test Plan

New test cases in `narrowing/conditionals/nested.md`.
2025-08-14 17:52:52 -07:00
Andrii Turov
957320c0f1 [ty] Add diagnostics for invalid await expressions (#19711)
## Summary

This PR adds a new lint, `invalid-await`, for all sorts of reasons why
an object may not be `await`able, as discussed in astral-sh/ty#919.
Precisely, `__await__` is guarded against being missing, possibly
unbound, or improperly defined (expects additional arguments or doesn't
return an iterator).

Of course, diagnostics need to be fine-tuned. If `__await__` cannot be
called with no extra arguments, it indicates an error (or a quirk?) in
the method signature, not at the call site. Without any doubt, such an
object is not `Awaitable`, but I feel like talking about arguments for
an *implicit* call is a bit leaky.
I didn't reference any actual diagnostic messages in the lint
definition, because I want to hear feedback first.

Also, there's no mention of the actual required method signature for
`__await__` anywhere in the docs. The only reference I had is the
`typing` stub. I basically ended up linking `[Awaitable]` to ["must
implement
`__await__`"](https://docs.python.org/3/library/collections.abc.html#collections.abc.Awaitable),
which is insufficient on its own.

## Test Plan

The following code was tested:
```python
import asyncio
import typing


class Awaitable:
    def __await__(self) -> typing.Generator[typing.Any, None, int]:
        yield None
        return 5


class NoDunderMethod:
    pass


class InvalidAwaitArgs:
    def __await__(self, value: int) -> int:
        return value


class InvalidAwaitReturn:
    def __await__(self) -> int:
        return 5


class InvalidAwaitReturnImplicit:
    def __await__(self):
        pass


async def main() -> None:
    result = await Awaitable()  # valid
    result = await NoDunderMethod()  # `__await__` is missing
    result = await InvalidAwaitReturn()  # `__await__` returns `int`, which is not a valid iterator 
    result = await InvalidAwaitArgs()  # `__await__` expects additional arguments and cannot be called implicitly
    result = await InvalidAwaitReturnImplicit()  # `__await__` returns `Unknown`, which is not a valid iterator


asyncio.run(main())
```

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-08-14 14:38:33 -07:00
Alex Waygood
f6093452ed [ty] Synthesize read-only properties for all declared members on NamedTuple classes (#19899) 2025-08-14 21:25:45 +00:00
Alex Waygood
82350a398e [ty] Remove use of ClassBase::try_from_type from super() machinery (#19902) 2025-08-14 22:14:31 +01:00
Micha Reiser
ce938fe205 [ty] Speedup project file discovery (#19913) 2025-08-14 19:38:39 +01:00
Brent Westbrook
7f8f1ab2c1 [pyflakes] Add secondary annotation showing previous definition (F811) (#19900)
## Summary

This is a second attempt at a first use of a new diagnostic feature
after #19886. I'll blame rustc for this one because it also has a
similar diagnostic:

<img width="735" height="335" alt="image"
src="https://github.com/user-attachments/assets/572fe1c3-1742-4ce4-b575-1d9196ff0932"
/>

We end up with a very similar diagnostic:

<img width="764" height="401" alt="image"
src="https://github.com/user-attachments/assets/01eaf0c7-2567-467b-a5d8-a27206b2c74c"
/>

## Test Plan

New snapshots and manual tests above
2025-08-14 13:23:43 -04:00
257 changed files with 13627 additions and 2892 deletions

View File

@@ -49,7 +49,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build sdist"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
command: sdist
args: --out dist
@@ -79,7 +79,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: x86_64
args: --release --locked --out dist
@@ -121,7 +121,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - aarch64"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: aarch64
args: --release --locked --out dist
@@ -177,7 +177,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.platform.target }}
args: --release --locked --out dist
@@ -230,7 +230,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -306,7 +306,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.platform.target }}
manylinux: auto
@@ -372,7 +372,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
@@ -437,7 +437,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2

View File

@@ -715,7 +715,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
with:
args: --out dist
- name: "Test wheel"

View File

@@ -11,6 +11,7 @@ on:
- "crates/ruff_python_parser"
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"
- "scripts/mypy_primer.sh"
- "Cargo.lock"
- "!**.md"

View File

@@ -61,7 +61,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
submodules: recursive
@@ -124,7 +124,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
submodules: recursive
@@ -175,7 +175,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
submodules: recursive
@@ -251,7 +251,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
submodules: recursive

View File

@@ -54,6 +54,9 @@ jobs:
- name: Compute diagnostic diff
shell: bash
env:
# TODO: Remove this once we fixed the remaining panics in the conformance suite.
TY_MAX_PARALLELISM: 1
run: |
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
@@ -63,15 +66,15 @@ jobs:
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
cargo build --release --bin ty
mv target/release/ty ty-new
cargo build --bin ty
mv target/debug/ty ty-new
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
echo "old commit (merge base)"
git rev-list --format=%s --max-count=1 old_commit
cargo build --release --bin ty
mv target/release/ty ty-old
cargo build --bin ty
mv target/debug/ty ty-old
)
(

View File

@@ -24,8 +24,31 @@
### Other changes
- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
```diff
-unused.py:8:19: F401 [*] `pathlib` imported but unused
+F401 [*] `pathlib` imported but unused
+ --> unused.py:8:19
|
7 | # Unused, _not_ marked as required (due to the alias).
8 | import pathlib as non_alias
- | ^^^^^^^^^ F401
+ | ^^^^^^^^^
9 |
10 | # Unused, marked as required.
|
- = help: Remove unused import: `pathlib`
+help: Remove unused import: `pathlib`
```
For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
## 0.12.8
### Preview features

74
Cargo.lock generated
View File

@@ -128,9 +128,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "approx"
@@ -257,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.1"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
[[package]]
name = "bitvec"
@@ -408,9 +408,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.43"
version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.43"
version = "4.5.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
dependencies = [
"anstream",
"anstyle",
@@ -461,9 +461,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.41"
version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
dependencies = [
"heck",
"proc-macro2",
@@ -1218,9 +1218,9 @@ dependencies = [
[[package]]
name = "glob"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "globset"
@@ -1241,7 +1241,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"ignore",
"walkdir",
]
@@ -1521,7 +1521,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"inotify-sys",
"libc",
]
@@ -1764,9 +1764,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libcst"
@@ -1809,7 +1809,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"libc",
"redox_syscall",
]
@@ -2014,7 +2014,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2026,7 +2026,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2054,7 +2054,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"fsevent-sys",
"inotify",
"kqueue",
@@ -2666,7 +2666,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
]
[[package]]
@@ -2749,7 +2749,7 @@ dependencies = [
"argfile",
"assert_fs",
"bincode 2.0.1",
"bitflags 2.9.1",
"bitflags 2.9.2",
"cachedir",
"clap",
"clap_complete_command",
@@ -3000,7 +3000,7 @@ version = "0.12.9"
dependencies = [
"aho-corasick",
"anyhow",
"bitflags 2.9.1",
"bitflags 2.9.2",
"clap",
"colored 3.0.0",
"fern",
@@ -3106,7 +3106,7 @@ name = "ruff_python_ast"
version = "0.0.0"
dependencies = [
"aho-corasick",
"bitflags 2.9.1",
"bitflags 2.9.2",
"compact_str",
"get-size2",
"is-macro",
@@ -3194,7 +3194,7 @@ dependencies = [
name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"itertools 0.14.0",
"ruff_python_ast",
"unic-ucd-category",
@@ -3205,7 +3205,7 @@ name = "ruff_python_parser"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"bitflags 2.9.2",
"bstr",
"compact_str",
"get-size2",
@@ -3230,7 +3230,7 @@ dependencies = [
name = "ruff_python_semantic"
version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"insta",
"is-macro",
"ruff_cache",
@@ -3251,7 +3251,7 @@ dependencies = [
name = "ruff_python_stdlib"
version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"unicode-ident",
]
@@ -3428,7 +3428,7 @@ version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"errno",
"libc",
"linux-raw-sys",
@@ -3450,7 +3450,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=918d35d873b2b73a0237536144ef4d22e8d57f27#918d35d873b2b73a0237536144ef4d22e8d57f27"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
dependencies = [
"boxcar",
"compact_str",
@@ -3474,12 +3474,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=918d35d873b2b73a0237536144ef4d22e8d57f27#918d35d873b2b73a0237536144ef4d22e8d57f27"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=918d35d873b2b73a0237536144ef4d22e8d57f27#918d35d873b2b73a0237536144ef4d22e8d57f27"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
dependencies = [
"proc-macro2",
"quote",
@@ -4238,7 +4238,7 @@ dependencies = [
name = "ty_ide"
version = "0.0.0"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
"insta",
"itertools 0.14.0",
"regex",
@@ -4297,7 +4297,7 @@ name = "ty_python_semantic"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"bitflags 2.9.2",
"bitvec",
"camino",
"colored 3.0.0",
@@ -4350,7 +4350,7 @@ name = "ty_server"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"bitflags 2.9.2",
"crossbeam",
"dunce",
"insta",
@@ -4393,7 +4393,7 @@ name = "ty_test"
version = "0.0.0"
dependencies = [
"anyhow",
"bitflags 2.9.1",
"bitflags 2.9.2",
"camino",
"colored 3.0.0",
"insta",
@@ -5143,7 +5143,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.1",
"bitflags 2.9.2",
]
[[package]]

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
rust-version = "1.86"
rust-version = "1.87"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -143,7 +143,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "918d35d873b2b73a0237536144ef4d22e8d57f27", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -5588,15 +5588,15 @@ fn cookiecutter_globbing() -> Result<()> {
.args(STDIN_BASE_OPTIONS)
.arg("--select=F811")
.current_dir(tempdir.path()), @r"
success: false
exit_code: 1
----- stdout -----
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1
Found 1 error.
[*] 1 fixable with the `--fix` option.
success: false
exit_code: 1
----- stdout -----
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1: `foo` redefined here
Found 1 error.
[*] 1 fixable with the `--fix` option.
----- stderr -----
");
----- stderr -----
");
});
Ok(())
@@ -5801,3 +5801,32 @@ fn future_annotations_preview_warning() {
",
);
}
#[test]
fn up045_nested_optional_flatten_all() {
let contents = "\
from typing import Optional
nested_optional: Optional[Optional[Optional[str]]] = None
";
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--select", "UP045", "--diff", "--target-version", "py312"])
.arg("-")
.pass_stdin(contents),
@r"
success: false
exit_code: 1
----- stdout -----
@@ -1,2 +1,2 @@
from typing import Optional
-nested_optional: Optional[Optional[Optional[str]]] = None
+nested_optional: str | None = None
----- stderr -----
Would fix 1 error.
",
);
}

View File

@@ -254,6 +254,11 @@ impl Diagnostic {
.find(|ann| ann.is_primary)
}
/// Returns a mutable borrow of all annotations of this diagnostic.
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
Arc::make_mut(&mut self.inner).annotations.iter_mut()
}
/// Returns the "primary" span of this diagnostic if one exists.
///
/// When there are multiple primary spans, then the first one that was
@@ -310,6 +315,11 @@ impl Diagnostic {
&self.inner.subs
}
/// Returns a mutable borrow of the sub-diagnostics of this diagnostic.
pub fn sub_diagnostics_mut(&mut self) -> impl Iterator<Item = &mut SubDiagnostic> {
Arc::make_mut(&mut self.inner).subs.iter_mut()
}
/// Returns the fix for this diagnostic if it exists.
pub fn fix(&self) -> Option<&Fix> {
self.inner.fix.as_ref()
@@ -621,6 +631,11 @@ impl SubDiagnostic {
&self.inner.annotations
}
/// Returns a mutable borrow of the annotations of this sub-diagnostic.
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
self.inner.annotations.iter_mut()
}
/// Returns a shared borrow of the "primary" annotation of this diagnostic
/// if one exists.
///

View File

@@ -264,7 +264,12 @@ impl<'a> ResolvedDiagnostic<'a> {
.annotations
.iter()
.filter_map(|ann| {
let path = ann.span.file.path(resolver);
let path = ann
.span
.file
.relative_path(resolver)
.to_str()
.unwrap_or_else(|| ann.span.file.path(resolver));
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
})

View File

@@ -87,11 +87,12 @@ impl Files {
.system_by_path
.entry(absolute.clone())
.or_insert_with(|| {
tracing::trace!("Adding file '{path}'");
let metadata = db.system().path_metadata(path);
tracing::trace!("Adding file '{absolute}'");
let durability = self
.root(db, path)
.root(db, &absolute)
.map_or(Durability::default(), |root| root.durability(db));
let builder = File::builder(FilePath::System(absolute))

View File

@@ -166,3 +166,7 @@ r"""first
print("S\x1cP\x1dL\x1eI\x1fT".split())
print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
# leading/trailing whitespace should not count towards maxsplit
" a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
" a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]

View File

@@ -0,0 +1,4 @@
"""Hello, world!"""\
\
x = 1; y = 2

View File

@@ -69,3 +69,10 @@ a7: OptionalTE[typing.NamedTuple] = None
a8: typing_extensions.Optional[typing.NamedTuple] = None
a9: "Optional[NamedTuple]" = None
a10: Optional[NamedTupleTE] = None
# Test for: https://github.com/astral-sh/ruff/issues/19746
# Nested Optional types should be flattened
nested_optional: Optional[Optional[str]] = None
nested_optional_typing: typing.Optional[Optional[int]] = None
triple_nested_optional: Optional[Optional[Optional[str]]] = None

View File

@@ -28,7 +28,7 @@ use itertools::Itertools;
use log::debug;
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::diagnostic::{Annotation, Diagnostic, IntoDiagnosticMessage, Span};
use ruff_diagnostics::{Applicability, Fix, IsolationLevel};
use ruff_notebook::{CellOffsets, NotebookIndex};
use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path};
@@ -3305,6 +3305,17 @@ impl DiagnosticGuard<'_, '_> {
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
}
}
/// Add a secondary annotation with the given message and range.
pub(crate) fn secondary_annotation<'a>(
&mut self,
message: impl IntoDiagnosticMessage + 'a,
range: impl Ranged,
) {
let span = Span::from(self.context.source_file.clone()).with_range(range.range());
let ann = Annotation::secondary(span).message(message);
self.diagnostic.as_mut().unwrap().annotate(ann);
}
}
impl std::ops::Deref for DiagnosticGuard<'_, '_> {

View File

@@ -63,9 +63,9 @@ impl<'a> Insertion<'a> {
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
}
// If the first token after the docstring is a continuation character (i.e. "\"), advance
// an additional row to prevent inserting in the same logical line.
if match_continuation(locator.after(location)).is_some() {
// While the first token after the docstring is a continuation character (i.e. "\"), advance
// additional rows to prevent inserting in the same logical line.
while match_continuation(locator.after(location)).is_some() {
location = locator.full_line_end(location);
}
@@ -379,6 +379,17 @@ mod tests {
Insertion::own_line("", TextSize::from(22), "\n")
);
let contents = r#"
"""Hello, world!"""\
\
"#
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(24), "\n")
);
let contents = r"
x = 1
"

View File

@@ -230,3 +230,8 @@ pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterS
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19851
pub(crate) const fn is_maxsplit_without_separator_fix_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -485,9 +485,6 @@ impl Violation for MissingReturnTypeClassMethod {
/// Use instead:
///
/// ```python
/// from typing import Any
///
///
/// def foo(x: int): ...
/// ```
///

View File

@@ -16,7 +16,7 @@ use crate::{checkers::ast::Checker, fix};
/// statement has no effect and should be omitted.
///
/// ## References
/// - [Static Typing with Python: Type Stubs](https://typing.python.org/en/latest/source/stubs.html)
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#language-features)
#[derive(ViolationMetadata)]
pub(crate) struct FutureAnnotationsInStub;

View File

@@ -60,6 +60,7 @@ mod tests {
}
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"))]
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -9,6 +9,8 @@ use ruff_python_ast::{
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_maxsplit_without_separator_fix_enabled;
use crate::settings::LinterSettings;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## What it does
@@ -84,7 +86,9 @@ pub(crate) fn split_static_string(
let sep_arg = arguments.find_argument_value("sep", 0);
let split_replacement = if let Some(sep) = sep_arg {
match sep {
Expr::NoneLiteral(_) => split_default(str_value, maxsplit_value, direction),
Expr::NoneLiteral(_) => {
split_default(str_value, maxsplit_value, direction, checker.settings())
}
Expr::StringLiteral(sep_value) => {
let sep_value_str = sep_value.value.to_str();
Some(split_sep(
@@ -100,7 +104,7 @@ pub(crate) fn split_static_string(
}
}
} else {
split_default(str_value, maxsplit_value, direction)
split_default(str_value, maxsplit_value, direction, checker.settings())
};
let mut diagnostic = checker.report_diagnostic(SplitStaticString, call.range());
@@ -174,6 +178,7 @@ fn split_default(
str_value: &StringLiteralValue,
max_split: i32,
direction: Direction,
settings: &LinterSettings,
) -> Option<Expr> {
// From the Python documentation:
// > If sep is not specified or is None, a different splitting algorithm is applied: runs of
@@ -185,10 +190,31 @@ fn split_default(
let string_val = str_value.to_str();
match max_split.cmp(&0) {
Ordering::Greater => {
// Autofix for `maxsplit` without separator not yet implemented, as
// `split_whitespace().remainder()` is not stable:
// https://doc.rust-lang.org/std/str/struct.SplitWhitespace.html#method.remainder
None
if !is_maxsplit_without_separator_fix_enabled(settings) {
return None;
}
let Ok(max_split) = usize::try_from(max_split) else {
return None;
};
let list_items: Vec<&str> = if direction == Direction::Left {
string_val
.trim_start_matches(py_unicode_is_whitespace)
.splitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect()
} else {
let mut items: Vec<&str> = string_val
.trim_end_matches(py_unicode_is_whitespace)
.rsplitn(max_split + 1, py_unicode_is_whitespace)
.filter(|s| !s.is_empty())
.collect();
items.reverse();
items
};
Some(construct_replacement(
&list_items,
str_value.first_literal_flags(),
))
}
Ordering::Equal => {
// Behavior for maxsplit = 0 when sep is None:

View File

@@ -1439,6 +1439,7 @@ help: Replace with list literal
166 |+print(["S", "P", "L", "I", "T"])
167 167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
168 168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
169 169 |
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:167:7
@@ -1458,6 +1459,8 @@ help: Replace with list literal
167 |-print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
167 |+print([">"])
168 168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
169 169 |
170 170 | # leading/trailing whitespace should not count towards maxsplit
SIM905 [*] Consider using a list literal instead of `str.split`
--> SIM905.py:168:7
@@ -1466,6 +1469,8 @@ SIM905 [*] Consider using a list literal instead of `str.split`
167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
168 | print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
169 |
170 | # leading/trailing whitespace should not count towards maxsplit
|
help: Replace with list literal
@@ -1475,3 +1480,26 @@ help: Replace with list literal
167 167 | print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
168 |-print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
168 |+print(["<"])
169 169 |
170 170 | # leading/trailing whitespace should not count towards maxsplit
171 171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:171:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
|
help: Replace with list literal
SIM905 Consider using a list literal instead of `str.split`
--> SIM905.py:172:1
|
170 | # leading/trailing whitespace should not count towards maxsplit
171 | " a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
172 | " a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with list literal

View File

@@ -797,6 +797,7 @@ mod tests {
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("docstring_with_continuation.py"))]
#[test_case(Path::new("docstring_with_multiple_continuations.py"))]
#[test_case(Path::new("docstring_with_semicolon.py"))]
#[test_case(Path::new("empty.py"))]
#[test_case(Path::new("existing_import.py"))]
@@ -832,6 +833,7 @@ mod tests {
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("docstring_with_continuation.py"))]
#[test_case(Path::new("docstring_with_multiple_continuations.py"))]
#[test_case(Path::new("docstring_with_semicolon.py"))]
#[test_case(Path::new("empty.py"))]
#[test_case(Path::new("existing_import.py"))]

View File

@@ -0,0 +1,13 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I002 [*] Missing required import: `from __future__ import annotations`
--> docstring_with_multiple_continuations.py:1:1
help: Insert required import: `from __future__ import annotations`
Safe fix
1 1 | """Hello, world!"""\
2 2 | \
3 3 |
4 |+from __future__ import annotations
4 5 | x = 1; y = 2

View File

@@ -0,0 +1,13 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I002 [*] Missing required import: `from __future__ import annotations as _annotations`
--> docstring_with_multiple_continuations.py:1:1
help: Insert required import: `from __future__ import annotations as _annotations`
Safe fix
1 1 | """Hello, world!"""\
2 2 | \
3 3 |
4 |+from __future__ import annotations as _annotations
4 5 | x = 1; y = 2

View File

@@ -60,7 +60,7 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1;
/// ## References
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
#[derive(ViolationMetadata)]
pub(crate) struct BlankLineBetweenMethods;
@@ -113,7 +113,7 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods {
/// ## References
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
#[derive(ViolationMetadata)]
pub(crate) struct BlankLinesTopLevel {
actual_blank_lines: u32,
@@ -180,7 +180,7 @@ impl AlwaysFixableViolation for BlankLinesTopLevel {
/// ## References
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
#[derive(ViolationMetadata)]
pub(crate) struct TooManyBlankLines {
actual_blank_lines: u32,
@@ -277,7 +277,7 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator {
/// ## References
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
#[derive(ViolationMetadata)]
pub(crate) struct BlankLinesAfterFunctionOrClass {
actual_blank_lines: u32,
@@ -331,7 +331,7 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
/// ## References
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
/// - [Typing Style Guide](https://typing.python.org/en/latest/source/stubs.html#blank-lines)
/// - [Typing Style Guide](https://typing.python.org/en/latest/guides/writing_stubs.html#blank-lines)
#[derive(ViolationMetadata)]
pub(crate) struct BlankLinesBeforeNestedDefinition;

View File

@@ -10,7 +10,7 @@ use ruff_source_file::UniversalNewlines;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for lambda expressions which are assigned to a variable.
@@ -105,29 +105,24 @@ pub(crate) fn lambda_assignment(
}
}
// Otherwise, if the assignment is in a class body, flag it, but use a display-only fix.
// Rewriting safely would require making this a static method.
//
// Similarly, if the lambda is shadowing a variable in the current scope,
// If the lambda is shadowing a variable in the current scope,
// rewriting it as a function declaration may break type-checking.
// See: https://github.com/astral-sh/ruff/issues/5421
if checker.semantic().current_scope().kind.is_class()
|| checker
.semantic()
.current_scope()
.get_all(id)
.any(|binding_id| checker.semantic().binding(binding_id).kind.is_annotation())
let applicability = if checker
.semantic()
.current_scope()
.get_all(id)
.any(|binding_id| checker.semantic().binding(binding_id).kind.is_annotation())
{
diagnostic.set_fix(Fix::display_only_edit(Edit::range_replacement(
indented,
stmt.range(),
)));
Applicability::DisplayOnly
} else {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
indented,
stmt.range(),
)));
}
Applicability::Unsafe
};
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(indented, stmt.range()),
applicability,
));
}
}

View File

@@ -105,7 +105,7 @@ help: Rewrite `f` as a `def`
26 27 |
27 28 | def scope():
E731 Do not assign a `lambda` expression, use a `def`
E731 [*] Do not assign a `lambda` expression, use a `def`
--> E731.py:57:5
|
55 | class Scope:
@@ -115,7 +115,7 @@ E731 Do not assign a `lambda` expression, use a `def`
|
help: Rewrite `f` as a `def`
Display-only fix
Unsafe fix
54 54 |
55 55 | class Scope:
56 56 | # E731
@@ -318,7 +318,7 @@ help: Rewrite `f` as a `def`
137 138 |
138 139 | class TemperatureScales(Enum):
E731 Do not assign a `lambda` expression, use a `def`
E731 [*] Do not assign a `lambda` expression, use a `def`
--> E731.py:139:5
|
138 | class TemperatureScales(Enum):
@@ -328,7 +328,7 @@ E731 Do not assign a `lambda` expression, use a `def`
|
help: Rewrite `CELSIUS` as a `def`
Display-only fix
Unsafe fix
136 136 |
137 137 |
138 138 | class TemperatureScales(Enum):
@@ -339,7 +339,7 @@ help: Rewrite `CELSIUS` as a `def`
141 142 |
142 143 |
E731 Do not assign a `lambda` expression, use a `def`
E731 [*] Do not assign a `lambda` expression, use a `def`
--> E731.py:140:5
|
138 | class TemperatureScales(Enum):
@@ -349,7 +349,7 @@ E731 Do not assign a `lambda` expression, use a `def`
|
help: Rewrite `FAHRENHEIT` as a `def`
Display-only fix
Unsafe fix
137 137 |
138 138 | class TemperatureScales(Enum):
139 139 | CELSIUS = (lambda deg_c: deg_c)

View File

@@ -183,14 +183,24 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope
// Create diagnostics for each statement.
for (source, entries) in &redefinitions {
for (shadowed, binding) in entries {
let name = binding.name(checker.source());
let mut diagnostic = checker.report_diagnostic(
RedefinedWhileUnused {
name: binding.name(checker.source()).to_string(),
name: name.to_string(),
row: checker.compute_source_row(shadowed.start()),
},
binding.range(),
);
diagnostic.secondary_annotation(
format_args!("previous definition of `{name}` here"),
shadowed,
);
if let Some(ann) = diagnostic.primary_annotation_mut() {
ann.set_message(format_args!("`{name}` redefined here"));
}
if let Some(range) = binding.parent_range(checker.semantic()) {
diagnostic.set_parent(range.start());
}

View File

@@ -5,7 +5,14 @@ F811 Redefinition of unused `bar` from line 6
--> F811_0.py:10:5
|
10 | def bar():
| ^^^
| ^^^ `bar` redefined here
11 | pass
|
::: F811_0.py:6:5
|
5 | @foo
6 | def bar():
| --- previous definition of `bar` here
7 | pass
|
help: Remove definition: `bar`

View File

@@ -2,9 +2,11 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `FU` from line 1
--> F811_1.py:1:25
--> F811_1.py:1:14
|
1 | import fu as FU, bar as FU
| ^^
| -- ^^ `FU` redefined here
| |
| previous definition of `FU` here
|
help: Remove definition: `FU`

View File

@@ -2,12 +2,16 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `mixer` from line 2
--> F811_12.py:6:20
--> F811_12.py:2:20
|
1 | try:
2 | from aa import mixer
| ----- previous definition of `mixer` here
3 | except ImportError:
4 | pass
5 | else:
6 | from bb import mixer
| ^^^^^
| ^^^^^ `mixer` redefined here
7 | mixer(123)
|
help: Remove definition: `mixer`

View File

@@ -5,7 +5,12 @@ F811 Redefinition of unused `fu` from line 1
--> F811_15.py:4:5
|
4 | def fu():
| ^^
| ^^ `fu` redefined here
5 | pass
|
::: F811_15.py:1:8
|
1 | import fu
| -- previous definition of `fu` here
|
help: Remove definition: `fu`

View File

@@ -7,7 +7,14 @@ F811 Redefinition of unused `fu` from line 3
6 | def bar():
7 | def baz():
8 | def fu():
| ^^
| ^^ `fu` redefined here
9 | pass
|
::: F811_16.py:3:8
|
1 | """Test that shadowing a global with a nested function generates a warning."""
2 |
3 | import fu
| -- previous definition of `fu` here
|
help: Remove definition: `fu`

View File

@@ -6,10 +6,16 @@ F811 [*] Redefinition of unused `fu` from line 2
|
5 | def bar():
6 | import fu
| ^^
| ^^ `fu` redefined here
7 |
8 | def baz():
|
::: F811_17.py:2:8
|
1 | """Test that shadowing a global name with a nested function generates a warning."""
2 | import fu
| -- previous definition of `fu` here
|
help: Remove definition: `fu`
Safe fix
@@ -22,11 +28,15 @@ help: Remove definition: `fu`
9 8 | def fu():
F811 Redefinition of unused `fu` from line 6
--> F811_17.py:9:13
--> F811_17.py:6:12
|
5 | def bar():
6 | import fu
| -- previous definition of `fu` here
7 |
8 | def baz():
9 | def fu():
| ^^
| ^^ `fu` redefined here
10 | pass
|
help: Remove definition: `fu`

View File

@@ -2,9 +2,11 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `FU` from line 1
--> F811_2.py:1:34
--> F811_2.py:1:23
|
1 | from moo import fu as FU, bar as FU
| ^^
| -- ^^ `FU` redefined here
| |
| previous definition of `FU` here
|
help: Remove definition: `FU`

View File

@@ -7,9 +7,17 @@ F811 [*] Redefinition of unused `Sequence` from line 26
30 | from typing import (
31 | List, # noqa: F811
32 | Sequence,
| ^^^^^^^^
| ^^^^^^^^ `Sequence` redefined here
33 | )
|
::: F811_21.py:26:5
|
24 | from typing import (
25 | List, # noqa
26 | Sequence, # noqa
| -------- previous definition of `Sequence` here
27 | )
|
help: Remove definition: `Sequence`
Safe fix

View File

@@ -2,10 +2,13 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `foo` from line 3
--> F811_23.py:4:15
--> F811_23.py:3:15
|
1 | """Test that shadowing an explicit re-export produces a warning."""
2 |
3 | import foo as foo
| --- previous definition of `foo` here
4 | import bar as foo
| ^^^
| ^^^ `foo` redefined here
|
help: Remove definition: `foo`

View File

@@ -2,12 +2,15 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `func` from line 2
--> F811_26.py:5:9
--> F811_26.py:2:9
|
1 | class Class:
2 | def func(self):
| ---- previous definition of `func` here
3 | pass
4 |
5 | def func(self):
| ^^^^
| ^^^^ `func` redefined here
6 | pass
|
help: Remove definition: `func`

View File

@@ -2,11 +2,14 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `datetime` from line 3
--> F811_28.py:4:22
--> F811_28.py:3:8
|
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10384"""
2 |
3 | import datetime
| -------- previous definition of `datetime` here
4 | from datetime import datetime
| ^^^^^^^^
| ^^^^^^^^ `datetime` redefined here
5 |
6 | datetime(1, 2, 3)
|

View File

@@ -2,11 +2,17 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `Bar` from line 3
--> F811_29.pyi:8:1
--> F811_29.pyi:3:24
|
1 | """Regression test for: https://github.com/astral-sh/ruff/issues/10509"""
2 |
3 | from foo import Bar as Bar
| --- previous definition of `Bar` here
4 |
5 | class Eggs:
6 | Bar: int # OK
7 |
8 | Bar = 1 # F811
| ^^^
| ^^^ `Bar` redefined here
|
help: Remove definition: `Bar`

View File

@@ -2,9 +2,11 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `fu` from line 1
--> F811_3.py:1:12
--> F811_3.py:1:8
|
1 | import fu; fu = 3
| ^^
| -- ^^ `fu` redefined here
| |
| previous definition of `fu` here
|
help: Remove definition: `fu`

View File

@@ -2,32 +2,43 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `bar` from line 10
--> F811_30.py:12:9
--> F811_30.py:10:5
|
8 | """Foo."""
9 |
10 | bar = foo
| --- previous definition of `bar` here
11 |
12 | def bar(self) -> None:
| ^^^
| ^^^ `bar` redefined here
13 | """Bar."""
|
help: Remove definition: `bar`
F811 Redefinition of unused `baz` from line 18
--> F811_30.py:21:5
--> F811_30.py:18:9
|
16 | class B:
17 | """B."""
18 | def baz(self) -> None:
| --- previous definition of `baz` here
19 | """Baz."""
20 |
21 | baz = 1
| ^^^
| ^^^ `baz` redefined here
|
help: Remove definition: `baz`
F811 Redefinition of unused `foo` from line 26
--> F811_30.py:29:12
--> F811_30.py:26:9
|
24 | class C:
25 | """C."""
26 | def foo(self) -> None:
| --- previous definition of `foo` here
27 | """Foo."""
28 |
29 | bar = (foo := 1)
| ^^^
| ^^^ `foo` redefined here
|
help: Remove definition: `foo`

View File

@@ -2,12 +2,14 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `baz` from line 17
--> F811_31.py:19:29
--> F811_31.py:17:5
|
16 | try:
17 | baz = None
| --- previous definition of `baz` here
18 |
19 | from some_module import baz
| ^^^
| ^^^ `baz` redefined here
20 | except ImportError:
21 | pass
|

View File

@@ -2,12 +2,13 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 [*] Redefinition of unused `List` from line 4
--> F811_32.py:5:5
--> F811_32.py:4:5
|
3 | from typing import (
4 | List,
| ---- previous definition of `List` here
5 | List,
| ^^^^
| ^^^^ `List` redefined here
6 | )
|
help: Remove definition: `List`

View File

@@ -2,9 +2,11 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `fu` from line 1
--> F811_4.py:1:12
--> F811_4.py:1:8
|
1 | import fu; fu, bar = 3
| ^^
| -- ^^ `fu` redefined here
| |
| previous definition of `fu` here
|
help: Remove definition: `fu`

View File

@@ -2,9 +2,11 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 Redefinition of unused `fu` from line 1
--> F811_5.py:1:13
--> F811_5.py:1:8
|
1 | import fu; [fu, bar] = 3
| ^^
| -- ^^ `fu` redefined here
| |
| previous definition of `fu` here
|
help: Remove definition: `fu`

View File

@@ -2,12 +2,14 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 [*] Redefinition of unused `os` from line 5
--> F811_6.py:6:12
--> F811_6.py:5:12
|
3 | i = 2
4 | if i == 1:
5 | import os
| -- previous definition of `os` here
6 | import os
| ^^
| ^^ `os` redefined here
7 | os.path
|
help: Remove definition: `os`

View File

@@ -2,12 +2,13 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 [*] Redefinition of unused `os` from line 4
--> F811_8.py:5:12
--> F811_8.py:4:12
|
3 | try:
4 | import os
| -- previous definition of `os` here
5 | import os
| ^^
| ^^ `os` redefined here
6 | except:
7 | pass
|

View File

@@ -19,11 +19,14 @@ help: Remove unused import: `os`
5 4 | import os
F811 [*] Redefinition of unused `os` from line 2
--> <filename>:5:12
--> <filename>:2:8
|
2 | import os
| -- previous definition of `os` here
3 |
4 | def f():
5 | import os
| ^^
| ^^ `os` redefined here
6 |
7 | # Despite this `del`, `import os` in `f` should still be flagged as shadowing an unused
|

View File

@@ -19,11 +19,14 @@ help: Remove unused import: `os`
5 4 | os = 1
F811 Redefinition of unused `os` from line 2
--> <filename>:5:5
--> <filename>:2:8
|
2 | import os
| -- previous definition of `os` here
3 |
4 | def f():
5 | os = 1
| ^^
| ^^ `os` redefined here
6 | print(os)
|
help: Remove definition: `os`

View File

@@ -2,12 +2,13 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F811 [*] Redefinition of unused `os` from line 3
--> <filename>:4:12
--> <filename>:3:12
|
2 | def f():
3 | import os
| -- previous definition of `os` here
4 | import os
| ^^
| ^^ `os` redefined here
5 |
6 | # Despite this `del`, `import os` should still be flagged as shadowing an unused
|

View File

@@ -2,7 +2,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::PythonVersion;
use ruff_python_ast::helpers::{pep_604_optional, pep_604_union};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::Pep604Operator;
use ruff_python_semantic::analyze::typing::{Pep604Operator, to_pep604_operator};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -171,10 +171,22 @@ pub(crate) fn non_pep604_annotation(
// Invalid type annotation.
}
_ => {
// Unwrap all nested Optional[...] and wrap once as `X | None`.
let mut inner = slice;
while let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = inner {
if let Some(Pep604Operator::Optional) =
to_pep604_operator(value, slice, checker.semantic())
{
inner = slice;
} else {
break;
}
}
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_replacement(
pad(
checker.generator().expr(&pep_604_optional(slice)),
checker.generator().expr(&pep_604_optional(inner)),
expr.range(),
checker.locator(),
),

View File

@@ -171,3 +171,134 @@ UP045 Use `X | None` for type annotations
| ^^^^^^^^^^^^^^
|
help: Convert to `X | None`
UP045 [*] Use `X | None` for type annotations
--> UP045.py:76:18
|
74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
75 | # Nested Optional types should be flattened
76 | nested_optional: Optional[Optional[str]] = None
| ^^^^^^^^^^^^^^^^^^^^^^^
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
help: Convert to `X | None`
Safe fix
73 73 |
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
75 75 | # Nested Optional types should be flattened
76 |-nested_optional: Optional[Optional[str]] = None
76 |+nested_optional: str | None = None
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
UP045 [*] Use `X | None` for type annotations
--> UP045.py:76:27
|
74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
75 | # Nested Optional types should be flattened
76 | nested_optional: Optional[Optional[str]] = None
| ^^^^^^^^^^^^^
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
help: Convert to `X | None`
Safe fix
73 73 |
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
75 75 | # Nested Optional types should be flattened
76 |-nested_optional: Optional[Optional[str]] = None
76 |+nested_optional: Optional[str | None] = None
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
UP045 [*] Use `X | None` for type annotations
--> UP045.py:77:25
|
75 | # Nested Optional types should be flattened
76 | nested_optional: Optional[Optional[str]] = None
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
help: Convert to `X | None`
Safe fix
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
75 75 | # Nested Optional types should be flattened
76 76 | nested_optional: Optional[Optional[str]] = None
77 |-nested_optional_typing: typing.Optional[Optional[int]] = None
77 |+nested_optional_typing: int | None = None
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
UP045 [*] Use `X | None` for type annotations
--> UP045.py:77:41
|
75 | # Nested Optional types should be flattened
76 | nested_optional: Optional[Optional[str]] = None
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
| ^^^^^^^^^^^^^
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
help: Convert to `X | None`
Safe fix
74 74 | # Test for: https://github.com/astral-sh/ruff/issues/19746
75 75 | # Nested Optional types should be flattened
76 76 | nested_optional: Optional[Optional[str]] = None
77 |-nested_optional_typing: typing.Optional[Optional[int]] = None
77 |+nested_optional_typing: typing.Optional[int | None] = None
78 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
UP045 [*] Use `X | None` for type annotations
--> UP045.py:78:25
|
76 | nested_optional: Optional[Optional[str]] = None
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to `X | None`
Safe fix
75 75 | # Nested Optional types should be flattened
76 76 | nested_optional: Optional[Optional[str]] = None
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 |-triple_nested_optional: Optional[Optional[Optional[str]]] = None
78 |+triple_nested_optional: str | None = None
UP045 [*] Use `X | None` for type annotations
--> UP045.py:78:34
|
76 | nested_optional: Optional[Optional[str]] = None
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
| ^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to `X | None`
Safe fix
75 75 | # Nested Optional types should be flattened
76 76 | nested_optional: Optional[Optional[str]] = None
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 |-triple_nested_optional: Optional[Optional[Optional[str]]] = None
78 |+triple_nested_optional: Optional[str | None] = None
UP045 [*] Use `X | None` for type annotations
--> UP045.py:78:43
|
76 | nested_optional: Optional[Optional[str]] = None
77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None
| ^^^^^^^^^^^^^
|
help: Convert to `X | None`
Safe fix
75 75 | # Nested Optional types should be flattened
76 76 | nested_optional: Optional[Optional[str]] = None
77 77 | nested_optional_typing: typing.Optional[Optional[int]] = None
78 |-triple_nested_optional: Optional[Optional[Optional[str]]] = None
78 |+triple_nested_optional: Optional[Optional[str | None]] = None

View File

@@ -9,7 +9,7 @@ use anyhow::Result;
use itertools::Itertools;
use rustc_hash::FxHashMap;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::diagnostic::{Diagnostic, Span};
use ruff_notebook::Notebook;
#[cfg(not(fuzzing))]
use ruff_notebook::NotebookError;
@@ -281,10 +281,16 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
// noqa offset and the source file
let range = diagnostic.expect_range();
diagnostic.set_noqa_offset(directives.noqa_line_for.resolve(range.start()));
if let Some(annotation) = diagnostic.primary_annotation_mut() {
annotation.set_span(
ruff_db::diagnostic::Span::from(source_code.clone()).with_range(range),
);
// This part actually is necessary to avoid long relative paths in snapshots.
for annotation in diagnostic.annotations_mut() {
let range = annotation.get_span().range().unwrap();
annotation.set_span(Span::from(source_code.clone()).with_range(range));
}
for sub in diagnostic.sub_diagnostics_mut() {
for annotation in sub.annotations_mut() {
let range = annotation.get_span().range().unwrap();
annotation.set_span(Span::from(source_code.clone()).with_range(range));
}
}
diagnostic

View File

@@ -44,7 +44,7 @@ or pyright's `stubPath` configuration setting.
```toml
[tool.ty.environment]
extra-paths = ["~/shared/my-search-path"]
extra-paths = ["./shared/my-search-path"]
```
---

225
crates/ty/docs/rules.md generated
View File

@@ -36,7 +36,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L101)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L109)
</small>
**What it does**
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L145)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L153)
</small>
**What it does**
@@ -88,7 +88,7 @@ f(int) # error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L171)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L179)
</small>
**What it does**
@@ -117,7 +117,7 @@ a = 1
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L196)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204)
</small>
**What it does**
@@ -147,7 +147,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L222)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L230)
</small>
**What it does**
@@ -177,7 +177,7 @@ class B(A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L287)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L295)
</small>
**What it does**
@@ -202,7 +202,7 @@ class B(A, A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L308)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L316)
</small>
**What it does**
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L450)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L519)
</small>
**What it does**
@@ -334,7 +334,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L474)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L543)
</small>
**What it does**
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L340)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L348)
</small>
**What it does**
@@ -445,7 +445,7 @@ an atypical memory layout.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L519)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L588)
</small>
**What it does**
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L559)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L628)
</small>
**What it does**
@@ -496,7 +496,7 @@ a: int = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1563)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1662)
</small>
**What it does**
@@ -523,12 +523,46 @@ C().instance_var = 3 # okay
C.instance_var = 3 # error: Cannot assign to instance variable
```
## `invalid-await`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L650)
</small>
**What it does**
Checks for `await` being used with types that are not [Awaitable].
**Why is this bad?**
Such expressions will lead to `TypeError` being raised at runtime.
**Examples**
```python
import asyncio
class InvalidAwait:
def __await__(self) -> int:
return 5
async def main() -> None:
await InvalidAwait() # error: [invalid-await]
await 42 # error: [invalid-await]
asyncio.run(main())
```
[Awaitable]: https://docs.python.org/3/library/collections.abc.html#collections.abc.Awaitable
## `invalid-base`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L581)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L680)
</small>
**What it does**
@@ -550,7 +584,7 @@ class A(42): ... # error: [invalid-base]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L731)
</small>
**What it does**
@@ -575,7 +609,7 @@ with 1:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L653)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L752)
</small>
**What it does**
@@ -602,7 +636,7 @@ a: str
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L676)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L775)
</small>
**What it does**
@@ -644,7 +678,7 @@ except ZeroDivisionError:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L712)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811)
</small>
**What it does**
@@ -675,7 +709,7 @@ class C[U](Generic[T]): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L494)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L563)
</small>
**What it does**
@@ -704,7 +738,7 @@ alice["height"] # KeyError: 'height'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L738)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L837)
</small>
**What it does**
@@ -737,7 +771,7 @@ def f(t: TypeVar("U")): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L787)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L886)
</small>
**What it does**
@@ -764,12 +798,42 @@ class B(metaclass=f): ...
- [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
## `invalid-named-tuple`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L493)
</small>
**What it does**
Checks for invalidly defined `NamedTuple` classes.
**Why is this bad?**
An invalidly defined `NamedTuple` class may lead to the type checker
drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
**Examples**
A class definition cannot combine `NamedTuple` with other base classes
in multiple inheritance; doing so raises a `TypeError` at runtime. The sole
exception to this rule is `Generic[]`, which can be used alongside `NamedTuple`
in a class's bases list.
```pycon
>>> from typing import NamedTuple
>>> class Foo(NamedTuple, object): ...
TypeError: can only inherit from a NamedTuple type and Generic
```
## `invalid-overload`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L814)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L913)
</small>
**What it does**
@@ -817,7 +881,7 @@ def foo(x: int) -> int: ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L857)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L956)
</small>
**What it does**
@@ -841,12 +905,12 @@ def f(a: int = ''): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L422)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L430)
</small>
**What it does**
Checks for invalidly defined protocol classes.
Checks for protocol classes that will raise `TypeError` at runtime.
**Why is this bad?**
@@ -873,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L877)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L976)
</small>
Checks for `raise` statements that raise non-exceptions or use invalid
@@ -920,7 +984,7 @@ def g():
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L540)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L609)
</small>
**What it does**
@@ -943,7 +1007,7 @@ def func() -> int:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L920)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1019)
</small>
**What it does**
@@ -997,7 +1061,7 @@ TODO #14889
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L766)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L865)
</small>
**What it does**
@@ -1022,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L959)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1058)
</small>
**What it does**
@@ -1050,7 +1114,7 @@ TYPE_CHECKING = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L983)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1082)
</small>
**What it does**
@@ -1078,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1035)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1134)
</small>
**What it does**
@@ -1110,7 +1174,7 @@ f(10) # Error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1007)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1106)
</small>
**What it does**
@@ -1142,7 +1206,7 @@ class C:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1063)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1162)
</small>
**What it does**
@@ -1175,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1092)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1191)
</small>
**What it does**
@@ -1198,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1111)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1210)
</small>
**What it does**
@@ -1225,7 +1289,7 @@ func("string") # error: [no-matching-overload]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1134)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1233)
</small>
**What it does**
@@ -1247,7 +1311,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1152)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1251)
</small>
**What it does**
@@ -1271,7 +1335,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1203)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1302)
</small>
**What it does**
@@ -1325,7 +1389,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1539)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1638)
</small>
**What it does**
@@ -1353,7 +1417,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1294)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1393)
</small>
**What it does**
@@ -1380,7 +1444,7 @@ class B(A): ... # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1339)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1438)
</small>
**What it does**
@@ -1405,7 +1469,7 @@ f("foo") # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1317)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1416)
</small>
**What it does**
@@ -1431,7 +1495,7 @@ def _(x: int):
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1360)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1459)
</small>
**What it does**
@@ -1475,7 +1539,7 @@ class A:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1516)
</small>
**What it does**
@@ -1500,7 +1564,7 @@ f(x=1, y=2) # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1438)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1537)
</small>
**What it does**
@@ -1526,7 +1590,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1460)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1559)
</small>
**What it does**
@@ -1549,7 +1613,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1479)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1578)
</small>
**What it does**
@@ -1572,7 +1636,7 @@ print(x) # NameError: name 'x' is not defined
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1172)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1271)
</small>
**What it does**
@@ -1607,7 +1671,7 @@ b1 < b2 < b1 # exception raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1498)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1597)
</small>
**What it does**
@@ -1633,7 +1697,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1520)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1619)
</small>
**What it does**
@@ -1651,12 +1715,51 @@ l = list(range(10))
l[1:10:0] # ValueError: slice step cannot be zero
```
## `ambiguous-protocol-member`
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L458)
</small>
**What it does**
Checks for protocol classes with members that will lead to ambiguous interfaces.
**Why is this bad?**
Assigning to an undeclared variable in a protocol class leads to an ambiguous
interface which may lead to the type checker inferring unexpected things. It's
recommended to ensure that all members of a protocol class are explicitly declared.
**Examples**
```py
from typing import Protocol
class BaseProto(Protocol):
a: int # fine (explicitly declared as `int`)
def method_member(self) -> int: ... # fine: a method definition using `def` is considered a declaration
c = "some variable" # error: no explicit declaration, leading to ambiguity
b = method_member # error: no explicit declaration, leading to ambiguity
# error: this creates implicit assignments of `d` and `e` in the protocol class body.
# Were they really meant to be considered protocol members?
for d, e in enumerate(range(42)):
pass
class SubProto(BaseProto, Protocol):
a = 42 # fine (declared in superclass)
```
## `deprecated`
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L266)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L274)
</small>
**What it does**
@@ -1709,7 +1812,7 @@ a = 20 / 0 # type: ignore
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1224)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323)
</small>
**What it does**
@@ -1735,7 +1838,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L119)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L127)
</small>
**What it does**
@@ -1765,7 +1868,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1246)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1345)
</small>
**What it does**
@@ -1795,7 +1898,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1591)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1690)
</small>
**What it does**
@@ -1820,7 +1923,7 @@ cast(int, f()) # Redundant
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1399)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1498)
</small>
**What it does**
@@ -1871,7 +1974,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1612)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1711)
</small>
**What it does**
@@ -1925,7 +2028,7 @@ def g():
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L599)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L698)
</small>
**What it does**
@@ -1962,7 +2065,7 @@ class D(C): ... # error: [unsupported-base]
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L248)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256)
</small>
**What it does**
@@ -1984,7 +2087,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1272)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371)
</small>
**What it does**

View File

@@ -62,6 +62,10 @@ pub(crate) fn version() -> Result<()> {
}
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
// Enabled ANSI colors on Windows 10.
#[cfg(windows)]
assert!(colored::control::set_virtual_terminal(true).is_ok());
set_colored_override(args.color);
let verbosity = args.verbosity.level();

View File

@@ -345,6 +345,51 @@ import bar",
Ok(())
}
/// On Unix systems, a virtual environment can come with multiple `site-packages` directories:
/// one at `<sys.prefix>/lib/pythonX.Y/site-packages` and one at
/// `<sys.prefix>/lib64/pythonX.Y/site-packages`. According to [the stdlib docs], the `lib64`
/// is not *meant* to have any Python files in it (only C extensions and similar). Empirically,
/// however, it sometimes does indeed have Python files in it: popular tools such as poetry
/// appear to sometimes install Python packages into the `lib64` site-packages directory even
/// though they probably shouldn't. We therefore check for both a `lib64` and a `lib` directory,
/// and add them both as search paths if they both exist.
///
/// See:
/// - <https://github.com/astral-sh/ty/issues/1043>
/// - <https://github.com/astral-sh/ty/issues/257>.
///
/// [the stdlib docs]: https://docs.python.org/3/library/sys.html#sys.platlibdir
#[cfg(unix)]
#[test]
fn lib64_site_packages_directory_on_unix() -> anyhow::Result<()> {
let case = CliTest::with_files([
(".venv/lib/python3.13/site-packages/foo.py", ""),
(".venv/lib64/python3.13/site-packages/bar.py", ""),
("test.py", "import foo, bar, baz"),
])?;
assert_cmd_snapshot!(case.command().arg("--python").arg(".venv"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> test.py:1:18
|
1 | import foo, bar, baz
| ^^^
|
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
#[test]
fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
let case = CliTest::with_files([
@@ -762,7 +807,7 @@ fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
ty failed
Cause: Failed to discover the site-packages directory
Cause: Failed to iterate over the contents of the `lib` directory of the Python installation
Cause: Failed to iterate over the contents of the `lib`/`lib64` directories of the Python installation
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
@@ -771,8 +816,6 @@ fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
3 | python = "directory-but-no-site-packages"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
Cause: No such file or directory (os error 2)
"#);
Ok(())
@@ -857,59 +900,165 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
/// The `site-packages` directory is used by ty for external import.
/// Ty does the following checks to discover the `site-packages` directory in the order:
/// 1) If `VIRTUAL_ENV` environment variable is set
/// 2) If `CONDA_PREFIX` environment variable is set
/// 2) If `CONDA_PREFIX` environment variable is set (and .filename != `CONDA_DEFAULT_ENV`)
/// 3) If a `.venv` directory exists at the project root
/// 4) If `CONDA_PREFIX` environment variable is set (and .filename == `CONDA_DEFAULT_ENV`)
///
/// This test is aiming at validating the logic around `CONDA_PREFIX`.
/// This test (and the next one) is aiming at validating the logic around these cases.
///
/// A conda-like environment file structure is used
/// We test by first not setting the `CONDA_PREFIX` and expect a fail.
/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass.
/// To do this we create a program that has these 4 imports:
///
/// ```python
/// from package1 import ActiveVenv
/// from package1 import ChildConda
/// from package1 import WorkingVenv
/// from package1 import BaseConda
/// ```
///
/// We then create 4 different copies of package1. Each copy defines all of these
/// classes... except the one that describes it. Therefore we know we got e.g.
/// the working venv if we get a diagnostic like this:
///
/// ```text
/// Unresolved import
/// 4 | from package1 import WorkingVenv
/// | ^^^^^^^^^^^
/// ```
///
/// This test uses a directory structure as follows:
///
/// ├── project
/// │ ── test.py
/// └── conda-env
/// ── lib
/// └── python3.13
/// └── site-packages
/// └── package1
/// └── __init__.py
/// │ ── test.py
/// │ └── .venv
/// ── pyvenv.cfg
/// └── lib
/// └── python3.13
/// └── site-packages
/// └── package1
/// │ └── __init__.py
/// ├── myvenv
/// │ ├── pyvenv.cfg
/// │ └── lib
/// │ └── python3.13
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// ├── conda-env
/// │ └── lib
/// │ └── python3.13
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// └── conda
/// └── envs
/// └── base
/// └── lib
/// └── python3.13
/// └── site-packages
/// └── package1
/// └── __init__.py
///
/// test.py imports package1
/// And the command is run in the `child` directory.
#[test]
fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
let conda_package1_path = if cfg!(windows) {
fn check_venv_resolution_with_working_venv() -> anyhow::Result<()> {
let child_conda_package1_path = if cfg!(windows) {
"conda-env/Lib/site-packages/package1/__init__.py"
} else {
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
};
let base_conda_package1_path = if cfg!(windows) {
"conda/envs/base/Lib/site-packages/package1/__init__.py"
} else {
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
};
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 active_venv_package1_path = if cfg!(windows) {
"myvenv/Lib/site-packages/package1/__init__.py"
} else {
"myvenv/lib/python3.13/site-packages/package1/__init__.py"
};
let case = CliTest::with_files([
(
"project/test.py",
r#"
import package1
from package1 import ActiveVenv
from package1 import ChildConda
from package1 import WorkingVenv
from package1 import BaseConda
"#,
),
(
conda_package1_path,
"project/.venv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
"myvenv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
active_venv_package1_path,
r#"
class ChildConda: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
child_conda_package1_path,
r#"
class ActiveVenv: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
working_venv_package1_path,
r#"
class ActiveVenv: ...
class ChildConda: ...
class BaseConda: ...
"#,
),
(
base_conda_package1_path,
r#"
class ActiveVenv: ...
class ChildConda: ...
class WorkingVenv: ...
"#,
),
])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r"
// Run with nothing set, should find the working venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:2:8
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:4:22
|
2 | import package1
| ^^^^^^^^
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
| ^^^^^^^^^^^
5 | from package1 import BaseConda
|
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
@@ -918,12 +1067,373 @@ fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// do command : CONDA_PREFIX=<temp_dir>/conda_env
assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r"
success: true
exit_code: 0
// Run with VIRTUAL_ENV set, should find the active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
success: false
exit_code: 1
----- stdout -----
All checks passed!
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX set, should find the child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
// should find child active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_DEFAULT_ENV", "base")
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find working venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:4:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
| ^^^^^^^^^^^
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// The exact same test as above, but without a working venv
///
/// In this case the Base Conda should be a possible outcome.
#[test]
fn check_venv_resolution_without_working_venv() -> anyhow::Result<()> {
let child_conda_package1_path = if cfg!(windows) {
"conda-env/Lib/site-packages/package1/__init__.py"
} else {
"conda-env/lib/python3.13/site-packages/package1/__init__.py"
};
let base_conda_package1_path = if cfg!(windows) {
"conda/envs/base/Lib/site-packages/package1/__init__.py"
} else {
"conda/envs/base/lib/python3.13/site-packages/package1/__init__.py"
};
let active_venv_package1_path = if cfg!(windows) {
"myvenv/Lib/site-packages/package1/__init__.py"
} else {
"myvenv/lib/python3.13/site-packages/package1/__init__.py"
};
let case = CliTest::with_files([
(
"project/test.py",
r#"
from package1 import ActiveVenv
from package1 import ChildConda
from package1 import WorkingVenv
from package1 import BaseConda
"#,
),
(
"myvenv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
active_venv_package1_path,
r#"
class ChildConda: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
child_conda_package1_path,
r#"
class ActiveVenv: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
base_conda_package1_path,
r#"
class ActiveVenv: ...
class ChildConda: ...
class WorkingVenv: ...
"#,
),
])?;
// Run with nothing set, should fail to find anything
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:2:6
|
2 | from package1 import ActiveVenv
| ^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:3:6
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:4:6
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
| ^^^^^^^^
5 | from package1 import BaseConda
|
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:5:6
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^
|
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
Found 4 diagnostics
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// Run with VIRTUAL_ENV set, should find the active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX set, should find the child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
// should find child active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda-env"))
.env("CONDA_DEFAULT_ENV", "base")
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find base conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/base"))
.env("CONDA_DEFAULT_ENV", "base"), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.

View File

@@ -23,7 +23,18 @@ pub fn completion(db: &dyn Db, file: File, offset: TextSize) -> Vec<Completion<'
let model = SemanticModel::new(db, file);
let mut completions = match target {
CompletionTargetAst::ObjectDot { expr } => model.attribute_completions(expr),
CompletionTargetAst::ImportFrom { import, name } => model.import_completions(import, name),
CompletionTargetAst::ObjectDotInImport { import, name } => {
model.import_submodule_completions(import, name)
}
CompletionTargetAst::ObjectDotInImportFrom { import } => {
model.from_import_submodule_completions(import)
}
CompletionTargetAst::ImportFrom { import, name } => {
model.from_import_completions(import, name)
}
CompletionTargetAst::Import { .. } | CompletionTargetAst::ImportViaFrom { .. } => {
model.import_completions()
}
CompletionTargetAst::Scoped { node } => model.scoped_completions(node),
};
completions.sort_by(compare_suggestions);
@@ -50,11 +61,11 @@ enum CompletionTargetTokens<'t> {
object: &'t Token,
/// The token, if non-empty, following the dot.
///
/// This is currently unused, but we should use this
/// eventually to remove completions that aren't a
/// prefix of what has already been typed. (We are
/// currently relying on the LSP client to do this.)
#[expect(dead_code)]
/// For right now, this is only used to determine which
/// module in an `import` statement to return submodule
/// completions for. But we could use it for other things,
/// like only returning completions that start with a prefix
/// corresponding to this token.
attribute: Option<&'t Token>,
},
/// A `from module import attribute` token form was found, where
@@ -63,6 +74,20 @@ enum CompletionTargetTokens<'t> {
/// The module being imported from.
module: &'t Token,
},
/// A `import module` token form was found, where `module` may be
/// empty.
Import {
/// The token corresponding to the `import` keyword.
import: &'t Token,
/// The token closest to the cursor.
///
/// This is currently unused, but we should use this
/// eventually to remove completions that aren't a
/// prefix of what has already been typed. (We are
/// currently relying on the LSP client to do this.)
#[expect(dead_code)]
module: &'t Token,
},
/// A token was found under the cursor, but it didn't
/// match any of our anticipated token patterns.
Generic { token: &'t Token },
@@ -105,6 +130,8 @@ impl<'t> CompletionTargetTokens<'t> {
}
} else if let Some(module) = import_from_tokens(before) {
CompletionTargetTokens::ImportFrom { module }
} else if let Some((import, module)) = import_tokens(before) {
CompletionTargetTokens::Import { import, module }
} else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Float]) {
// If we're writing a `float`, then we should
// specifically not offer completions. This wouldn't
@@ -140,19 +167,47 @@ impl<'t> CompletionTargetTokens<'t> {
offset: TextSize,
) -> Option<CompletionTargetAst<'t>> {
match *self {
CompletionTargetTokens::PossibleObjectDot { object, .. } => {
CompletionTargetTokens::PossibleObjectDot { object, attribute } => {
let covering_node = covering_node(parsed.syntax().into(), object.range())
// We require that the end of the node range not
// exceed the cursor offset. This avoids selecting
// a node "too high" in the AST in cases where
// completions are requested in the middle of an
// expression. e.g., `foo.<CURSOR>.bar`.
.find_last(|node| node.is_expr_attribute() && node.range().end() <= offset)
.find_last(|node| {
// We require that the end of the node range not
// exceed the cursor offset. This avoids selecting
// a node "too high" in the AST in cases where
// completions are requested in the middle of an
// expression. e.g., `foo.<CURSOR>.bar`.
if node.is_expr_attribute() {
return node.range().end() <= offset;
}
// For import statements though, they can't be
// nested, so we don't care as much about the
// cursor being strictly after the statement.
// And indeed, sometimes it won't be! e.g.,
//
// import re, os.p<CURSOR>, zlib
//
// So just return once we find an import.
node.is_stmt_import() || node.is_stmt_import_from()
})
.ok()?;
match covering_node.node() {
ast::AnyNodeRef::ExprAttribute(expr) => {
Some(CompletionTargetAst::ObjectDot { expr })
}
ast::AnyNodeRef::StmtImport(import) => {
let range = attribute
.map(Ranged::range)
.unwrap_or_else(|| object.range());
// Find the name that overlaps with the
// token we identified for the attribute.
let name = import
.names
.iter()
.position(|alias| alias.range().contains_range(range))?;
Some(CompletionTargetAst::ObjectDotInImport { import, name })
}
ast::AnyNodeRef::StmtImportFrom(import) => {
Some(CompletionTargetAst::ObjectDotInImportFrom { import })
}
_ => None,
}
}
@@ -165,6 +220,20 @@ impl<'t> CompletionTargetTokens<'t> {
};
Some(CompletionTargetAst::ImportFrom { import, name: None })
}
CompletionTargetTokens::Import { import, .. } => {
let covering_node = covering_node(parsed.syntax().into(), import.range())
.find_first(|node| node.is_stmt_import() || node.is_stmt_import_from())
.ok()?;
match covering_node.node() {
ast::AnyNodeRef::StmtImport(import) => {
Some(CompletionTargetAst::Import { import, name: None })
}
ast::AnyNodeRef::StmtImportFrom(import) => {
Some(CompletionTargetAst::ImportViaFrom { import })
}
_ => None,
}
}
CompletionTargetTokens::Generic { token } => {
let covering_node = covering_node(parsed.syntax().into(), token.range());
Some(CompletionTargetAst::Scoped {
@@ -188,6 +257,18 @@ enum CompletionTargetAst<'t> {
/// A `object.attribute` scenario, where we want to
/// list attributes on `object` for completions.
ObjectDot { expr: &'t ast::ExprAttribute },
/// A `import module.submodule` scenario, where we only want to
/// list submodules for completions.
ObjectDotInImport {
/// The import statement.
import: &'t ast::StmtImport,
/// An index into `import.names`. The index is guaranteed to be
/// valid.
name: usize,
},
/// A `from module.submodule` scenario, where we only want to list
/// submodules for completions.
ObjectDotInImportFrom { import: &'t ast::StmtImportFrom },
/// A `from module import attribute` scenario, where we want to
/// list attributes on `module` for completions.
ImportFrom {
@@ -197,6 +278,24 @@ enum CompletionTargetAst<'t> {
/// set, the index is guaranteed to be valid.
name: Option<usize>,
},
/// A `import module` scenario, where we want to
/// list available modules for completions.
Import {
/// The import statement.
#[expect(dead_code)]
import: &'t ast::StmtImport,
/// An index into `import.names` if relevant. When this is
/// set, the index is guaranteed to be valid.
#[expect(dead_code)]
name: Option<usize>,
},
/// A `from module` scenario, where we want to
/// list available modules for completions.
ImportViaFrom {
/// The import statement.
#[expect(dead_code)]
import: &'t ast::StmtImportFrom,
},
/// A scoped scenario, where we want to list all items available in
/// the most narrow scope containing the giving AST node.
Scoped { node: ast::AnyNodeRef<'t> },
@@ -317,6 +416,52 @@ fn import_from_tokens(tokens: &[Token]) -> Option<&Token> {
None
}
/// Looks for the start of a `import <CURSOR>` statement.
///
/// This also handles cases like `import foo, c<CURSOR>, bar`.
///
/// If found, a token corresponding to the `import` or `from` keyword
/// and the the closest point of the `<CURSOR>` is returned.
///
/// It is assumed that callers will call `from_import_tokens` first to
/// try and recognize a `from ... import ...` statement before using
/// this.
fn import_tokens(tokens: &[Token]) -> Option<(&Token, &Token)> {
use TokenKind as TK;
/// A look-back limit, in order to bound work.
///
/// See `LIMIT` in `import_from_tokens` for more context.
const LIMIT: usize = 1_000;
/// A state used to "parse" the tokens preceding the user's cursor,
/// in reverse, to detect a `import` statement.
enum S {
Start,
Names,
}
let mut state = S::Start;
let module_token = tokens.last()?;
// Move backward through the tokens until we get to
// the `import` token.
for token in tokens.iter().rev().take(LIMIT) {
state = match (state, token.kind()) {
// It's okay to pop off a newline token here initially,
// since it may occur when the name being imported is
// empty.
(S::Start, TK::Newline) => S::Names,
// Munch through tokens that can make up an alias.
(S::Start | S::Names, TK::Name | TK::Comma | TK::As | TK::Unknown) => S::Names,
(S::Start | S::Names, TK::Import | TK::From) => {
return Some((token, module_token));
}
_ => return None,
};
}
None
}
/// Order completions lexicographically, with these exceptions:
///
/// 1) A `_[^_]` prefix sorts last and
@@ -1247,7 +1392,7 @@ quux.<CURSOR>
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Self@__new__
__new__ :: bound method Quux.__new__() -> Quux
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method Quux.__repr__() -> str
@@ -1292,7 +1437,7 @@ quux.b<CURSOR>
__init_subclass__ :: bound method Quux.__init_subclass__() -> None
__module__ :: str
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
__new__ :: bound method Quux.__new__() -> Self@__new__
__new__ :: bound method Quux.__new__() -> Quux
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
__repr__ :: bound method Quux.__repr__() -> str
@@ -2262,7 +2407,11 @@ import fo<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
// This snapshot would generate a big list of modules,
// which is kind of annoying. So just assert that it
// runs without panicking and produces some non-empty
// output.
assert!(!test.completions_without_builtins().is_empty());
}
// Ref: https://github.com/astral-sh/ty/issues/572
@@ -2274,7 +2423,11 @@ import foo as ba<CURSOR>
",
);
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
// This snapshot would generate a big list of modules,
// which is kind of annoying. So just assert that it
// runs without panicking and produces some non-empty
// output.
assert!(!test.completions_without_builtins().is_empty());
}
// Ref: https://github.com/astral-sh/ty/issues/572
@@ -2286,7 +2439,11 @@ from fo<CURSOR> import wat
",
);
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
// This snapshot would generate a big list of modules,
// which is kind of annoying. So just assert that it
// runs without panicking and produces some non-empty
// output.
assert!(!test.completions_without_builtins().is_empty());
}
// Ref: https://github.com/astral-sh/ty/issues/572
@@ -2697,6 +2854,143 @@ importlib.<CURSOR>
test.assert_completions_include("resources");
}
#[test]
fn import_with_leading_character() {
let test = cursor_test(
"\
import c<CURSOR>
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_without_leading_character() {
let test = cursor_test(
"\
import <CURSOR>
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_multiple() {
let test = cursor_test(
"\
import re, c<CURSOR>, sys
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_with_aliases() {
let test = cursor_test(
"\
import re as regexp, c<CURSOR>, sys as system
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_over_multiple_lines() {
let test = cursor_test(
"\
import re as regexp, \\
c<CURSOR>, \\
sys as system
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_unknown_in_module() {
let test = cursor_test(
"\
import ?, <CURSOR>
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_via_from_with_leading_character() {
let test = cursor_test(
"\
from c<CURSOR>
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_via_from_without_leading_character() {
let test = cursor_test(
"\
from <CURSOR>
",
);
test.assert_completions_include("collections");
}
#[test]
fn import_statement_with_submodule_with_leading_character() {
let test = cursor_test(
"\
import os.p<CURSOR>
",
);
test.assert_completions_include("path");
test.assert_completions_do_not_include("abspath");
}
#[test]
fn import_statement_with_submodule_multiple() {
let test = cursor_test(
"\
import re, os.p<CURSOR>, zlib
",
);
test.assert_completions_include("path");
test.assert_completions_do_not_include("abspath");
}
#[test]
fn import_statement_with_submodule_without_leading_character() {
let test = cursor_test(
"\
import os.<CURSOR>
",
);
test.assert_completions_include("path");
test.assert_completions_do_not_include("abspath");
}
#[test]
fn import_via_from_with_submodule_with_leading_character() {
let test = cursor_test(
"\
from os.p<CURSOR>
",
);
test.assert_completions_include("path");
test.assert_completions_do_not_include("abspath");
}
#[test]
fn import_via_from_with_submodule_without_leading_character() {
let test = cursor_test(
"\
from os.<CURSOR>
",
);
test.assert_completions_include("path");
test.assert_completions_do_not_include("abspath");
}
#[test]
fn regression_test_issue_642() {
// Regression test for https://github.com/astral-sh/ty/issues/642

View File

@@ -11,6 +11,7 @@ use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::HasDefinition;
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
@@ -285,36 +286,24 @@ impl GotoTarget<'_> {
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => {
let range = function.name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
focus_range: range,
full_range: function.range(),
}),
))
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(function.definition(&model)),
]))
}
GotoTarget::ClassDef(class) => {
let range = class.name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
focus_range: range,
full_range: class.range(),
}),
))
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(class.definition(&model)),
]))
}
GotoTarget::Parameter(parameter) => {
let range = parameter.name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
focus_range: range,
full_range: parameter.range(),
}),
))
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(parameter.definition(&model)),
]))
}
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
@@ -376,14 +365,10 @@ impl GotoTarget<'_> {
// For exception variables, they are their own definitions (like parameters)
GotoTarget::ExceptVariable(except_handler) => {
if let Some(name) = &except_handler.name {
let range = name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
))
} else {
None
}
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(except_handler.definition(&model)),
]))
}
// For pattern match rest variables, they are their own definitions

View File

@@ -181,6 +181,49 @@ def other_function(): ...
"#);
}
/// goto-definition on a function definition in a .pyi should go to the .py
#[test]
fn goto_definition_stub_map_function_def() {
let test = CursorTest::builder()
.source(
"mymodule.py",
r#"
def my_function():
return "hello"
def other_function():
return "other"
"#,
)
.source(
"mymodule.pyi",
r#"
def my_fun<CURSOR>ction(): ...
def other_function(): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:2:5
|
2 | def my_function():
| ^^^^^^^^^^^
3 | return "hello"
|
info: Source
--> mymodule.pyi:2:5
|
2 | def my_function(): ...
| ^^^^^^^^^^^
3 |
4 | def other_function(): ...
|
"#);
}
/// goto-definition on a function that's redefined many times in the impl .py
///
/// Currently this yields all instances. There's an argument for only yielding
@@ -328,6 +371,53 @@ class MyOtherClass:
");
}
/// goto-definition on a class def in a .pyi should go to the .py
#[test]
fn goto_definition_stub_map_class_def() {
let test = CursorTest::builder()
.source(
"mymodule.py",
r#"
class MyClass:
def __init__(self, val):
self.val = val
class MyOtherClass:
def __init__(self, val):
self.val = val + 1
"#,
)
.source(
"mymodule.pyi",
r#"
class MyCl<CURSOR>ass:
def __init__(self, val: bool): ...
class MyOtherClass:
def __init__(self, val: bool): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> mymodule.py:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self, val):
4 | self.val = val
|
info: Source
--> mymodule.pyi:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self, val: bool): ...
|
");
}
/// goto-definition on a class init should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_class_init() {

View File

@@ -405,9 +405,7 @@ except ValueError as err:
",
);
// Note: Currently only finds the declaration, not the usages
// This is because semantic analysis for except handler variables isn't fully implemented
assert_snapshot!(test.references(), @r###"
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:4:29
|
@@ -418,7 +416,37 @@ except ValueError as err:
5 | print(f'Error: {err}')
6 | return err
|
"###);
info[references]: Reference 2
--> main.py:5:21
|
3 | x = 1 / 0
4 | except ZeroDivisionError as err:
5 | print(f'Error: {err}')
| ^^^
6 | return err
|
info[references]: Reference 3
--> main.py:6:12
|
4 | except ZeroDivisionError as err:
5 | print(f'Error: {err}')
6 | return err
| ^^^
7 |
8 | try:
|
info[references]: Reference 4
--> main.py:11:31
|
9 | y = 2 / 0
10 | except ValueError as err:
11 | print(f'Different error: {err}')
| ^^^
|
");
}
#[test]

View File

@@ -197,16 +197,16 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:901:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:13
@@ -216,7 +216,7 @@ mod tests {
4 | a
| ^
|
"#);
"###);
}
#[test]
fn goto_type_of_expression_with_literal_node() {
@@ -226,16 +226,16 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:901:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:2:22
@@ -243,7 +243,7 @@ mod tests {
2 | a: str = "test"
| ^^^^^^
|
"#);
"###);
}
#[test]
@@ -342,16 +342,16 @@ mod tests {
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:901:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:18
@@ -361,7 +361,7 @@ mod tests {
4 | test(a= "123")
| ^
|
"#);
"###);
}
#[test]
@@ -411,16 +411,16 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:2890:7
--> stdlib/builtins.pyi:2901:7
|
2888 | """See PEP 585"""
2889 |
2890 | class dict(MutableMapping[_KT, _VT]):
2899 | """See PEP 585"""
2900 |
2901 | class dict(MutableMapping[_KT, _VT]):
| ^^^^
2891 | """dict() -> new empty dictionary
2892 | dict(mapping) -> new dictionary initialized from a mapping object's
2902 | """dict() -> new empty dictionary
2903 | dict(mapping) -> new dictionary initialized from a mapping object's
|
info: Source
--> main.py:6:5
@@ -430,7 +430,7 @@ f(**kwargs<CURSOR>)
6 | f(**kwargs)
| ^^^^^^
|
"#);
"###);
}
#[test]
@@ -442,16 +442,16 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:901:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:17
@@ -460,7 +460,7 @@ f(**kwargs<CURSOR>)
3 | a
| ^
|
"#);
"###);
}
#[test]
@@ -535,16 +535,16 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:901:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:27
@@ -554,7 +554,7 @@ f(**kwargs<CURSOR>)
4 | print(a)
| ^
|
"#);
"###);
}
#[test]
@@ -566,7 +566,7 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:922:11
|
@@ -585,14 +585,14 @@ f(**kwargs<CURSOR>)
|
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:901:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:17
@@ -601,7 +601,7 @@ f(**kwargs<CURSOR>)
3 | a
| ^
|
"#);
"###);
}
impl CursorTest {

View File

@@ -6,8 +6,8 @@ use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use std::fmt;
use std::fmt::Formatter;
use ty_python_semantic::SemanticModel;
use ty_python_semantic::types::Type;
use ty_python_semantic::{DisplaySettings, SemanticModel};
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover<'_>>> {
let parsed = parsed_module(db, file).load(db);
@@ -135,7 +135,10 @@ impl fmt::Display for DisplayHoverContent<'_, '_> {
match self.content {
HoverContent::Type(ty) => self
.kind
.fenced_code_block(ty.display(self.db), "python")
.fenced_code_block(
ty.display_with(self.db, DisplaySettings::default().multiline()),
"python",
)
.fmt(f),
HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f),
}
@@ -201,7 +204,10 @@ mod tests {
);
assert_snapshot!(test.hover(), @r"
def my_func(a, b) -> Unknown
def my_func(
a,
b
) -> Unknown
---------------------------------------------
This is such a great func!!
@@ -211,7 +217,10 @@ mod tests {
---------------------------------------------
```python
def my_func(a, b) -> Unknown
def my_func(
a,
b
) -> Unknown
```
---
```text
@@ -237,6 +246,63 @@ mod tests {
");
}
#[test]
fn hover_function_def() {
let test = cursor_test(
r#"
def my_fu<CURSOR>nc(a, b):
'''This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
'''
return 0
"#,
);
assert_snapshot!(test.hover(), @r"
def my_func(
a,
b
) -> Unknown
---------------------------------------------
This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
---------------------------------------------
```python
def my_func(
a,
b
) -> Unknown
```
---
```text
This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:13
|
2 | def my_func(a, b):
| ^^^^^-^
| | |
| | Cursor offset
| source
3 | '''This is such a great func!!
|
");
}
#[test]
fn hover_class() {
let test = cursor_test(
@@ -304,6 +370,71 @@ mod tests {
");
}
#[test]
fn hover_class_def() {
let test = cursor_test(
r#"
class MyCla<CURSOR>ss:
'''
This is such a great class!!
Don't you know?
Everyone loves my class!!
'''
def __init__(self, val):
"""initializes MyClass (perfectly)"""
self.val = val
def my_method(self, a, b):
'''This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
'''
return 0
"#,
);
assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
This is such a great class!!
Don't you know?
Everyone loves my class!!
---------------------------------------------
```python
<class 'MyClass'>
```
---
```text
This is such a great class!!
Don't you know?
Everyone loves my class!!
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:15
|
2 | class MyClass:
| ^^^^^-^
| | |
| | Cursor offset
| source
3 | '''
4 | This is such a great class!!
|
");
}
#[test]
fn hover_class_init() {
let test = cursor_test(
@@ -403,7 +534,10 @@ mod tests {
);
assert_snapshot!(test.hover(), @r"
bound method MyClass.my_method(a, b) -> Unknown
bound method MyClass.my_method(
a,
b
) -> Unknown
---------------------------------------------
This is such a great func!!
@@ -413,7 +547,10 @@ mod tests {
---------------------------------------------
```python
bound method MyClass.my_method(a, b) -> Unknown
bound method MyClass.my_method(
a,
b
) -> Unknown
```
---
```text
@@ -485,10 +622,16 @@ mod tests {
);
assert_snapshot!(test.hover(), @r"
def foo(a, b) -> Unknown
def foo(
a,
b
) -> Unknown
---------------------------------------------
```python
def foo(a, b) -> Unknown
def foo(
a,
b
) -> Unknown
```
---------------------------------------------
info[hover]: Hovered content is
@@ -571,6 +714,40 @@ mod tests {
");
}
#[test]
fn hover_keyword_parameter_def() {
let test = cursor_test(
r#"
def test(a<CURSOR>b: int):
"""my cool test
Args:
ab: a nice little integer
"""
return 0
"#,
);
assert_snapshot!(test.hover(), @r#"
int
---------------------------------------------
```python
int
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:22
|
2 | def test(ab: int):
| ^-
| ||
| |Cursor offset
| source
3 | """my cool test
|
"#);
}
#[test]
fn hover_union() {
let test = cursor_test(
@@ -613,6 +790,128 @@ mod tests {
");
}
#[test]
fn hover_overload() {
let test = cursor_test(
r#"
from typing import overload
@overload
def foo(a: int, b):
"""The first overload"""
return 0
@overload
def foo(a: str, b):
"""The second overload"""
return 1
if random.choice([True, False]):
a = 1
else:
a = "hello"
foo<CURSOR>(a, 2)
"#,
);
assert_snapshot!(test.hover(), @r#"
(
a: int,
b
) -> Unknown
(
a: str,
b
) -> Unknown
---------------------------------------------
The first overload
---------------------------------------------
```python
(
a: int,
b
) -> Unknown
(
a: str,
b
) -> Unknown
```
---
```text
The first overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:19:13
|
17 | a = "hello"
18 |
19 | foo(a, 2)
| ^^^- Cursor offset
| |
| source
|
"#);
}
#[test]
fn hover_overload_compact() {
let test = cursor_test(
r#"
from typing import overload
@overload
def foo(a: int):
"""The first overload"""
return 0
@overload
def foo(a: str):
"""The second overload"""
return 1
if random.choice([True, False]):
a = 1
else:
a = "hello"
foo<CURSOR>(a)
"#,
);
assert_snapshot!(test.hover(), @r#"
(a: int) -> Unknown
(a: str) -> Unknown
---------------------------------------------
The first overload
---------------------------------------------
```python
(a: int) -> Unknown
(a: str) -> Unknown
```
---
```text
The first overload
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:19:13
|
17 | a = "hello"
18 |
19 | foo(a)
| ^^^- Cursor offset
| |
| source
|
"#);
}
#[test]
fn hover_module() {
let mut test = cursor_test(
@@ -1081,6 +1380,110 @@ mod tests {
assert_snapshot!(test.hover(), @"Hover provided no content");
}
#[test]
fn hover_complex_type1() {
let test = cursor_test(
r#"
from typing import Callable, Any, List
def ab(x: int, y: Callable[[int, int], Any], z: List[int]) -> int: ...
a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r"
def ab(
x: int,
y: (int, int, /) -> Any,
z: list[int]
) -> int
---------------------------------------------
```python
def ab(
x: int,
y: (int, int, /) -> Any,
z: list[int]
) -> int
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:9
|
3 | def ab(x: int, y: Callable[[int, int], Any], z: List[int]) -> int: ...
4 |
5 | ab
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_complex_type2() {
let test = cursor_test(
r#"
from typing import Callable, Tuple, Any
ab: Tuple[Any, int, Callable[[int, int], Any]] = ...
a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r"
tuple[Any, int, (int, int, /) -> Any]
---------------------------------------------
```python
tuple[Any, int, (int, int, /) -> Any]
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:9
|
3 | ab: Tuple[Any, int, Callable[[int, int], Any]] = ...
4 |
5 | ab
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_complex_type3() {
let test = cursor_test(
r#"
from typing import Callable, Any
ab: Callable[[int, int], Any] | None = ...
a<CURSOR>b
"#,
);
assert_snapshot!(test.hover(), @r"
((int, int, /) -> Any) | None
---------------------------------------------
```python
((int, int, /) -> Any) | None
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:5:9
|
3 | ab: Callable[[int, int], Any] | None = ...
4 |
5 | ab
| ^-
| ||
| |Cursor offset
| source
|
");
}
#[test]
fn hover_docstring() {
let test = cursor_test(

View File

@@ -85,6 +85,13 @@ pub struct InlayHintSettings {
/// foo("x="1)
/// ```
pub call_argument_names: bool,
// Add any new setting that enables additional inlays to `any_enabled`.
}
impl InlayHintSettings {
pub fn any_enabled(&self) -> bool {
self.variable_types || self.call_argument_names
}
}
impl Default for InlayHintSettings {

View File

@@ -235,7 +235,8 @@ impl HasNavigationTargets for Type<'_> {
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
match self {
Type::Union(union) => union
.iter(db)
.elements(db)
.iter()
.flat_map(|target| target.navigation_targets(db))
.collect(),

View File

@@ -337,7 +337,9 @@ impl<'db> SemanticTokenVisitor<'db> {
match ty {
Type::ClassLiteral(_) => (SemanticTokenType::Class, modifiers),
Type::TypeVar(_) => (SemanticTokenType::TypeParameter, modifiers),
Type::NonInferableTypeVar(_) | Type::TypeVar(_) => {
(SemanticTokenType::TypeParameter, modifiers)
}
Type::FunctionLiteral(_) => {
// Check if this is a method based on current scope
if self.in_class_scope {

View File

@@ -21,6 +21,8 @@ mod changes;
#[salsa::db]
pub trait Db: SemanticDb {
fn project(&self) -> Project;
fn dyn_clone(&self) -> Box<dyn Db>;
}
#[salsa::db]
@@ -484,6 +486,10 @@ impl Db for ProjectDatabase {
fn project(&self) -> Project {
self.project.unwrap()
}
fn dyn_clone(&self) -> Box<dyn Db> {
Box::new(self.clone())
}
}
#[cfg(feature = "format")]
@@ -611,6 +617,10 @@ pub(crate) mod tests {
fn project(&self) -> Project {
self.project.unwrap()
}
fn dyn_clone(&self) -> Box<dyn Db> {
Box::new(self.clone())
}
}
#[salsa::db]

View File

@@ -498,7 +498,7 @@ pub struct EnvironmentOptions {
default = r#"[]"#,
value_type = "list[str]",
example = r#"
extra-paths = ["~/shared/my-search-path"]
extra-paths = ["./shared/my-search-path"]
"#
)]
pub extra_paths: Option<Vec<RelativePathBuf>>,

View File

@@ -4,7 +4,7 @@ use ruff_db::files::{File, system_path_to_file};
use ruff_db::system::walk_directory::{ErrorKind, WalkDirectoryBuilder, WalkState};
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_python_ast::PySourceType;
use rustc_hash::{FxBuildHasher, FxHashSet};
use rustc_hash::FxHashSet;
use std::path::PathBuf;
use thiserror::Error;
@@ -163,20 +163,24 @@ impl<'a> ProjectFilesWalker<'a> {
/// Walks the project paths and collects the paths of all files that
/// are included in the project.
pub(crate) fn walk_paths(self) -> (Vec<SystemPathBuf>, Vec<IOErrorDiagnostic>) {
let paths = std::sync::Mutex::new(Vec::new());
pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec<File>, Vec<IOErrorDiagnostic>) {
let files = std::sync::Mutex::new(Vec::new());
let diagnostics = std::sync::Mutex::new(Vec::new());
self.walker.run(|| {
Box::new(|entry| {
let db = db.dyn_clone();
let filter = &self.filter;
let files = &files;
let diagnostics = &diagnostics;
Box::new(move |entry| {
match entry {
Ok(entry) => {
// Skip excluded directories unless they were explicitly passed to the walker
// (which is the case passed to `ty check <paths>`).
if entry.file_type().is_directory() {
if entry.depth() > 0 {
let directory_included = self
.filter
let directory_included = filter
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
return match directory_included {
IncludeResult::Included => WalkState::Continue,
@@ -210,8 +214,7 @@ impl<'a> ProjectFilesWalker<'a> {
// For all files, except the ones that were explicitly passed to the walker (CLI),
// check if they're included in the project.
if entry.depth() > 0 {
match self
.filter
match filter
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
{
IncludeResult::Included => {},
@@ -232,8 +235,11 @@ impl<'a> ProjectFilesWalker<'a> {
}
}
let mut paths = paths.lock().unwrap();
paths.push(entry.into_path());
// If this returns `Err`, then the file was deleted between now and when the walk callback was called.
// We can ignore this.
if let Ok(file) = system_path_to_file(&*db, entry.path()) {
files.lock().unwrap().push(file);
}
}
}
Err(error) => match error.kind() {
@@ -274,39 +280,14 @@ impl<'a> ProjectFilesWalker<'a> {
});
(
paths.into_inner().unwrap(),
files.into_inner().unwrap(),
diagnostics.into_inner().unwrap(),
)
}
pub(crate) fn collect_vec(self, db: &dyn Db) -> (Vec<File>, Vec<IOErrorDiagnostic>) {
let (paths, diagnostics) = self.walk_paths();
(
paths
.into_iter()
.filter_map(move |path| {
// If this returns `None`, then the file was deleted between the `walk_directory` call and now.
// We can ignore this.
system_path_to_file(db, &path).ok()
})
.collect(),
diagnostics,
)
}
pub(crate) fn collect_set(self, db: &dyn Db) -> (FxHashSet<File>, Vec<IOErrorDiagnostic>) {
let (paths, diagnostics) = self.walk_paths();
let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher);
for path in paths {
if let Ok(file) = system_path_to_file(db, &path) {
files.insert(file);
}
}
(files, diagnostics)
let (files, diagnostics) = self.collect_vec(db);
(files.into_iter().collect(), diagnostics)
}
}

View File

@@ -1,16 +1,16 @@
# Self
```toml
[environment]
python-version = "3.11"
```
`Self` is treated as if it were a `TypeVar` bound to the class it's being used on.
`typing.Self` is only available in Python 3.11 and later.
## Methods
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Self
@@ -74,11 +74,6 @@ reveal_type(C().method()) # revealed: C
## Class Methods
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Self, TypeVar
@@ -101,11 +96,6 @@ reveal_type(Shape.bar()) # revealed: Unknown
## Attributes
```toml
[environment]
python-version = "3.11"
```
TODO: The use of `Self` to annotate the `next_node` attribute should be
[modeled as a property][self attribute], using `Self` in its parameter and return type.
@@ -127,11 +117,6 @@ reveal_type(LinkedList().next()) # revealed: LinkedList
## Generic Classes
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Self, Generic, TypeVar
@@ -153,11 +138,6 @@ TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
## Annotations
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Self
@@ -171,11 +151,6 @@ class Shape:
`Self` cannot be used in the signature of a function or variable.
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Self, Generic, TypeVar
@@ -218,4 +193,33 @@ class MyMetaclass(type):
return super().__new__(cls)
```
## Binding a method fixes `Self`
When a method is bound, any instances of `Self` in its signature are "fixed", since we now know the
specific type of the bound parameter.
```py
from typing import Self
class C:
def instance_method(self, other: Self) -> Self:
return self
@classmethod
def class_method(cls) -> Self:
return cls()
# revealed: bound method C.instance_method(other: C) -> C
reveal_type(C().instance_method)
# revealed: bound method <class 'C'>.class_method() -> C
reveal_type(C.class_method)
class D(C): ...
# revealed: bound method D.instance_method(other: D) -> D
reveal_type(D().instance_method)
# revealed: bound method <class 'D'>.class_method() -> D
reveal_type(D.class_method)
```
[self attribute]: https://typing.python.org/en/latest/spec/generics.html#use-in-attribute-annotations

View File

@@ -198,7 +198,7 @@ python-version = "3.12"
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any, /) -> _SpecialForm
```
## Method calls on types not disjoint from `None`

View File

@@ -876,8 +876,7 @@ def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any):
# TODO: revealed: A
reveal_type(f(*(list_int,))) # revealed: Unknown
# TODO: Should be `str`
reveal_type(f(list_str)) # revealed: Unknown
reveal_type(f(list_str)) # revealed: str
# TODO: Should be `str`
reveal_type(f(*(list_str,))) # revealed: Unknown

View File

@@ -331,6 +331,9 @@ instance or a subclass of the first. If either condition is violated, a `TypeErr
runtime.
```py
import typing
import collections
def f(x: int):
# error: [invalid-super-argument] "`int` is not a valid class"
super(x, x)
@@ -367,6 +370,19 @@ reveal_type(super(B, A))
reveal_type(super(B, object))
super(object, object()).__class__
# Not all objects valid in a class's bases list are valid as the first argument to `super()`.
# For example, it's valid to inherit from `typing.ChainMap`, but it's not valid as the first argument to `super()`.
#
# error: [invalid-super-argument] "`typing.ChainMap` is not a valid class"
reveal_type(super(typing.ChainMap, collections.ChainMap())) # revealed: Unknown
# Meanwhile, it's not valid to inherit from unsubscripted `typing.Generic`,
# but it *is* valid as the first argument to `super()`.
reveal_type(super(typing.Generic, typing.SupportsInt)) # revealed: <super: typing.Generic, <class 'SupportsInt'>>
def _(x: type[typing.Any], y: typing.Any):
reveal_type(super(x, y)) # revealed: <super: Any, Any>
```
### Instance Member Access via `super`

View File

@@ -988,6 +988,28 @@ class D: # error: [duplicate-kw-only]
z: float
```
`KW_ONLY` should only affect fields declared after it within the same class, not fields in
subclasses:
```py
from dataclasses import dataclass, KW_ONLY
@dataclass
class D:
x: int
_: KW_ONLY
y: str
@dataclass
class E(D):
z: bytes
# This should work: x=1 (positional), z=b"foo" (positional), y="foo" (keyword-only)
E(1, b"foo", y="foo")
reveal_type(E.__init__) # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
```
## Other special cases
### `dataclasses.dataclass`

View File

@@ -0,0 +1,105 @@
# Invalid await diagnostics
<!-- snapshot-diagnostics -->
## Basic
This is a test showcasing a primitive case where an object is not awaitable.
```py
async def main() -> None:
await 1 # error: [invalid-await]
```
## Custom type with missing `__await__`
This diagnostic also points to the class definition if available.
```py
class MissingAwait:
pass
async def main() -> None:
await MissingAwait() # error: [invalid-await]
```
## Custom type with possibly unbound `__await__`
This diagnostic also points to the method definition if available.
```py
from datetime import datetime
class PossiblyUnbound:
if datetime.today().weekday() == 0:
def __await__(self):
yield
async def main() -> None:
await PossiblyUnbound() # error: [invalid-await]
```
## `__await__` definition with extra arguments
Currently, the signature of `__await__` isn't checked for conformity with the `Awaitable` protocol
directly. Instead, individual anomalies are reported, such as the following. Here, the diagnostic
reports that the object is not implicitly awaitable, while also pointing at the function parameters.
```py
class InvalidAwaitArgs:
def __await__(self, value: int):
yield value
async def main() -> None:
await InvalidAwaitArgs() # error: [invalid-await]
```
## Non-callable `__await__`
This diagnostic doesn't point to the attribute definition, but complains about it being possibly not
awaitable.
```py
class NonCallableAwait:
__await__ = 42
async def main() -> None:
await NonCallableAwait() # error: [invalid-await]
```
## `__await__` definition with explicit invalid return type
`__await__` must return a valid iterator. This diagnostic also points to the method definition if
available.
```py
class InvalidAwaitReturn:
def __await__(self) -> int:
return 5
async def main() -> None:
await InvalidAwaitReturn() # error: [invalid-await]
```
## Invalid union return type
When multiple potential definitions of `__await__` exist, all of them must be proper in order for an
instance to be awaitable. In this specific case, no specific function definition is highlighted.
```py
import typing
from datetime import datetime
class UnawaitableUnion:
if datetime.today().weekday() == 6:
def __await__(self) -> typing.Generator[typing.Any, None, None]:
yield
else:
def __await__(self) -> int:
return 5
async def main() -> None:
await UnawaitableUnion() # error: [invalid-await]
```

View File

@@ -749,6 +749,51 @@ def singleton_check(value: Singleton) -> str:
assert_never(value)
```
## `__eq__` and `__ne__`
### No `__eq__` or `__ne__` overrides
```py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
reveal_type(Color.RED == Color.RED) # revealed: Literal[True]
reveal_type(Color.RED != Color.RED) # revealed: Literal[False]
```
### Overridden `__eq__`
```py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
def __eq__(self, other: object) -> bool:
return False
reveal_type(Color.RED == Color.RED) # revealed: bool
```
### Overridden `__ne__`
```py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
def __ne__(self, other: object) -> bool:
return False
reveal_type(Color.RED != Color.RED) # revealed: bool
```
## References
- Typing spec: <https://typing.python.org/en/latest/spec/enums.html>

View File

@@ -491,6 +491,24 @@ class A(Generic[T]):
reveal_type(A(x=1)) # revealed: A[int]
```
### Class typevar has another typevar as a default
```py
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U", default=T)
class C(Generic[T, U]): ...
reveal_type(C()) # revealed: C[Unknown, Unknown]
class D(Generic[T, U]):
def __init__(self) -> None: ...
reveal_type(D()) # revealed: D[Unknown, Unknown]
```
## Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types

View File

@@ -455,3 +455,45 @@ def overloaded_outer(t: T | None = None) -> None:
if t is not None:
inner(t)
```
## Unpacking a TypeVar
We can infer precise heterogeneous types from the result of an unpacking operation applied to a type
variable if the type variable's upper bound is a type with a precise tuple spec:
```py
from dataclasses import dataclass
from typing import NamedTuple, Final, TypeVar, Generic
T = TypeVar("T", bound=tuple[int, str])
def f(x: T) -> T:
a, b = x
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
return x
@dataclass
class Team(Generic[T]):
employees: list[T]
def x(team: Team[T]) -> Team[T]:
age, name = team.employees[0]
reveal_type(age) # revealed: int
reveal_type(name) # revealed: str
return team
class Age(int): ...
class Name(str): ...
class Employee(NamedTuple):
age: Age
name: Name
EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),)
team = Team(employees=list(EMPLOYEES))
reveal_type(team.employees) # revealed: list[Employee]
age, name = team.employees[0]
reveal_type(age) # revealed: Age
reveal_type(name) # revealed: Name
```

View File

@@ -237,10 +237,15 @@ If the type of a constructor parameter is a class typevar, we can use that to in
parameter. The types inferred from a type context and from a constructor parameter must be
consistent with each other.
We have to add `x: T` to the classes to ensure they're not bivariant in `T` (__new__ and __init__
signatures don't count towards variance).
### `__new__` only
```py
class C[T]:
x: T
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
@@ -254,6 +259,8 @@ wrong_innards: C[int] = C("five")
```py
class C[T]:
x: T
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[int]
@@ -266,6 +273,8 @@ wrong_innards: C[int] = C("five")
```py
class C[T]:
x: T
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
@@ -281,6 +290,8 @@ wrong_innards: C[int] = C("five")
```py
class C[T]:
x: T
def __new__(cls, *args, **kwargs) -> "C[T]":
return object.__new__(cls)
@@ -292,6 +303,8 @@ reveal_type(C(1)) # revealed: C[int]
wrong_innards: C[int] = C("five")
class D[T]:
x: T
def __new__(cls, x: T) -> "D[T]":
return object.__new__(cls)
@@ -378,6 +391,8 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t
```py
class C[T]:
x: T
def __init__[S](self, x: T, y: S) -> None: ...
reveal_type(C(1, 1)) # revealed: C[int]
@@ -395,6 +410,10 @@ from __future__ import annotations
from typing import overload
class C[T]:
# we need to use the type variable or else the class is bivariant in T, and
# specializations become meaningless
x: T
@overload
def __init__(self: C[str], x: str) -> None: ...
@overload
@@ -438,6 +457,19 @@ class A[T]:
reveal_type(A(x=1)) # revealed: A[int]
```
### Class typevar has another typevar as a default
```py
class C[T, U = T]: ...
reveal_type(C()) # revealed: C[Unknown, Unknown]
class D[T, U = T]:
def __init__(self) -> None: ...
reveal_type(D()) # revealed: D[Unknown, Unknown]
```
## Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
@@ -617,5 +649,34 @@ class C[T](C): ...
class D[T](D[int]): ...
```
## Tuple as a PEP-695 generic class
Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in
typeshed:
```toml
[environment]
python-version = "3.12"
typeshed = "/typeshed"
```
`/typeshed/stdlib/builtins.pyi`:
```pyi
class tuple[T]: ...
```
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```
`main.py`:
```py
reveal_type((1, 2, 3)) # revealed: tuple[Literal[1], Literal[2], Literal[3]]
```
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

View File

@@ -454,3 +454,43 @@ def overloaded_outer[T](t: T | None = None) -> None:
if t is not None:
inner(t)
```
## Unpacking a TypeVar
We can infer precise heterogeneous types from the result of an unpacking operation applied to a
TypeVar if the TypeVar's upper bound is a type with a precise tuple spec:
```py
from dataclasses import dataclass
from typing import NamedTuple, Final
def f[T: tuple[int, str]](x: T) -> T:
a, b = x
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
return x
@dataclass
class Team[T: tuple[int, str]]:
employees: list[T]
def x[T: tuple[int, str]](team: Team[T]) -> Team[T]:
age, name = team.employees[0]
reveal_type(age) # revealed: int
reveal_type(name) # revealed: str
return team
class Age(int): ...
class Name(str): ...
class Employee(NamedTuple):
age: Age
name: Name
EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),)
team = Team(employees=list(EMPLOYEES))
reveal_type(team.employees) # revealed: list[Employee]
age, name = team.employees[0]
reveal_type(age) # revealed: Age
reveal_type(name) # revealed: Name
```

View File

@@ -40,8 +40,6 @@ class C[T]:
class D[U](C[U]):
pass
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A]))
static_assert(not is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
@@ -49,8 +47,6 @@ static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(D[B], C[A]))
static_assert(not is_assignable_to(D[A], C[B]))
static_assert(is_assignable_to(D[A], C[Any]))
@@ -58,8 +54,6 @@ static_assert(is_assignable_to(D[B], C[Any]))
static_assert(is_assignable_to(D[Any], C[A]))
static_assert(is_assignable_to(D[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
@@ -67,8 +61,6 @@ static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(D[B], C[A]))
static_assert(not is_subtype_of(D[A], C[B]))
static_assert(not is_subtype_of(D[A], C[Any]))
@@ -124,8 +116,6 @@ class D[U](C[U]):
pass
static_assert(not is_assignable_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
@@ -133,8 +123,6 @@ static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
static_assert(not is_assignable_to(D[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(D[A], C[B]))
static_assert(is_assignable_to(D[A], C[Any]))
static_assert(is_assignable_to(D[B], C[Any]))
@@ -142,8 +130,6 @@ static_assert(is_assignable_to(D[Any], C[A]))
static_assert(is_assignable_to(D[Any], C[B]))
static_assert(not is_subtype_of(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
@@ -151,8 +137,6 @@ static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not is_subtype_of(D[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(D[A], C[B]))
static_assert(not is_subtype_of(D[A], C[Any]))
static_assert(not is_subtype_of(D[B], C[Any]))
@@ -297,34 +281,22 @@ class C[T]:
class D[U](C[U]):
pass
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(C[A], C[B]))
static_assert(is_assignable_to(C[A], C[Any]))
static_assert(is_assignable_to(C[B], C[Any]))
static_assert(is_assignable_to(C[Any], C[A]))
static_assert(is_assignable_to(C[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(D[B], C[A]))
static_assert(is_subtype_of(C[A], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_assignable_to(D[A], C[B]))
static_assert(is_assignable_to(D[A], C[Any]))
static_assert(is_assignable_to(D[B], C[Any]))
static_assert(is_assignable_to(D[Any], C[A]))
static_assert(is_assignable_to(D[Any], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
@@ -332,11 +304,7 @@ static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not is_subtype_of(C[Any], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(D[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_subtype_of(D[A], C[B]))
static_assert(not is_subtype_of(D[A], C[Any]))
static_assert(not is_subtype_of(D[B], C[Any]))
@@ -345,23 +313,11 @@ static_assert(not is_subtype_of(D[Any], C[B]))
static_assert(is_equivalent_to(C[A], C[A]))
static_assert(is_equivalent_to(C[B], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[B], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[A], C[B]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[A], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[B], C[Any]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[Any], C[A]))
# TODO: no error
# error: [static-assert-error]
static_assert(is_equivalent_to(C[Any], C[B]))
static_assert(not is_equivalent_to(D[A], C[A]))
@@ -380,4 +336,504 @@ static_assert(not is_equivalent_to(D[Any], C[Any]))
static_assert(not is_equivalent_to(D[Any], C[Unknown]))
```
## Mutual Recursion
This example due to Martin Huschenbett's PyCon 2025 talk,
[Linear Time variance Inference for PEP 695][linear-time-variance-talk]
```py
from ty_extensions import is_subtype_of, static_assert
from typing import Any
class A: ...
class B(A): ...
class C[X]:
def f(self) -> "D[X]":
return D()
def g(self, x: X) -> None: ...
class D[Y]:
def h(self) -> C[Y]:
return C()
```
`C` is contravariant in `X`, and `D` in `Y`:
- `C` has two occurrences of `X`
- `X` occurs in the return type of `f` as `D[X]` (`X` is substituted in for `Y`)
- `D` has one occurrence of `Y`
- `Y` occurs in the return type of `h` as `C[Y]`
- `X` occurs contravariantly as a parameter in `g`
Thus the variance of `X` in `C` depends on itself. We want to infer the least restrictive possible
variance, so in such cases we begin by assuming that the point where we detect the cycle is
bivariant.
If we thus assume `X` is bivariant in `C`, then `Y` will be bivariant in `D`, as `D`'s only
occurrence of `Y` is in `C`. Then we consider `X` in `C` once more. We have two occurrences: `D[X]`
covariantly in a return type, and `X` contravariantly in an argument type. With one bivariant and
one contravariant occurrence, we update our inference of `X` in `C` to contravariant---the supremum
of contravariant and bivariant in the lattice.
Now that we've updated the variance of `X` in `C`, we re-evaluate `Y` in `D`. It only has the one
occurrence `C[Y]`, which we now infer is contravariant, and so we infer contravariance for `Y` in
`D` as well.
Because the variance of `X` in `C` depends on that of `Y` in `D`, we have to re-evaluate now that
we've updated the latter to contravariant. The variance of `X` in `C` is now the supremum of
contravariant and contravariant---giving us contravariant---and so remains unchanged.
Once we've completed a turn around the cycle with nothing changed, we've reached a fixed-point---the
variance inference will not change any further---and so we finally conclude that both `X` in `C` and
`Y` in `D` are contravariant.
```py
static_assert(not is_subtype_of(C[B], C[A]))
static_assert(is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(C[A], C[Any]))
static_assert(not is_subtype_of(C[B], C[Any]))
static_assert(not is_subtype_of(C[Any], C[A]))
static_assert(not is_subtype_of(C[Any], C[B]))
static_assert(not is_subtype_of(D[B], D[A]))
static_assert(is_subtype_of(D[A], D[B]))
static_assert(not is_subtype_of(D[A], D[Any]))
static_assert(not is_subtype_of(D[B], D[Any]))
static_assert(not is_subtype_of(D[Any], D[A]))
static_assert(not is_subtype_of(D[Any], D[B]))
```
## Class Attributes
### Mutable Attributes
Normal attributes are mutable, and so make the enclosing class invariant in this typevar (see
[inv]).
```py
from ty_extensions import is_subtype_of, static_assert
class A: ...
class B(A): ...
class C[T]:
x: T
static_assert(not is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
```
One might think that occurrences in the types of normal attributes are covariant, but they are
mutable, and thus the occurrences are invariant.
### Immutable Attributes
Immutable attributes can't be written to, and thus constrain the typevar to covariance, not
invariance.
#### Final attributes
```py
from typing import Final
from ty_extensions import is_subtype_of, static_assert
class A: ...
class B(A): ...
class C[T]:
x: Final[T]
static_assert(is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
```
#### Underscore-prefixed attributes
Underscore-prefixed instance attributes are considered private, and thus are assumed not externally
mutated.
```py
from ty_extensions import is_subtype_of, static_assert
class A: ...
class B(A): ...
class C[T]:
_x: T
@property
def x(self) -> T:
return self._x
static_assert(is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
class D[T]:
def __init__(self, x: T):
self._x = x
@property
def x(self) -> T:
return self._x
static_assert(is_subtype_of(D[B], D[A]))
static_assert(not is_subtype_of(D[A], D[B]))
```
#### Frozen dataclasses in Python 3.12 and earlier
```py
from dataclasses import dataclass, field
from ty_extensions import is_subtype_of, static_assert
class A: ...
class B(A): ...
@dataclass(frozen=True)
class D[U]:
y: U
static_assert(is_subtype_of(D[B], D[A]))
static_assert(not is_subtype_of(D[A], D[B]))
@dataclass(frozen=True)
class E[U]:
y: U = field()
static_assert(is_subtype_of(E[B], E[A]))
static_assert(not is_subtype_of(E[A], E[B]))
```
#### Frozen dataclasses in Python 3.13 and later
```toml
[environment]
python-version = "3.13"
```
Python 3.13 introduced a new synthesized `__replace__` method on dataclasses, which uses every field
type in a contravariant position (as a parameter to `__replace__`). This means that frozen
dataclasses on Python 3.13+ can't be covariant in their field types.
```py
from dataclasses import dataclass
from ty_extensions import is_subtype_of, static_assert
class A: ...
class B(A): ...
@dataclass(frozen=True)
class D[U]:
y: U
static_assert(not is_subtype_of(D[B], D[A]))
static_assert(not is_subtype_of(D[A], D[B]))
```
#### NamedTuple
```py
from typing import NamedTuple
from ty_extensions import is_subtype_of, static_assert
class A: ...
class B(A): ...
class E[V](NamedTuple):
z: V
static_assert(is_subtype_of(E[B], E[A]))
static_assert(not is_subtype_of(E[A], E[B]))
```
A subclass of a `NamedTuple` can still be covariant:
```py
class D[T](E[T]):
pass
static_assert(is_subtype_of(D[B], D[A]))
static_assert(not is_subtype_of(D[A], D[B]))
```
But adding a new generic attribute on the subclass makes it invariant (the added attribute is not a
`NamedTuple` field, and thus not immutable):
```py
class C[T](E[T]):
w: T
static_assert(not is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
```
### Properties
Properties constrain to covariance if they are get-only and invariant if they are get-set:
```py
from ty_extensions import static_assert, is_subtype_of
class A: ...
class B(A): ...
class C[T]:
@property
def x(self) -> T | None:
return None
class D[U]:
@property
def y(self) -> U | None:
return None
@y.setter
def y(self, value: U): ...
static_assert(is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
static_assert(not is_subtype_of(D[B], D[A]))
static_assert(not is_subtype_of(D[A], D[B]))
```
### Implicit Attributes
Implicit attributes work like normal ones
```py
from ty_extensions import static_assert, is_subtype_of
class A: ...
class B(A): ...
class C[T]:
def f(self) -> None:
self.x: T | None = None
static_assert(not is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
```
### Constructors: excluding `__init__` and `__new__`
We consider it invalid to call `__init__` explicitly on an existing object. Likewise, `__new__` is
only used at the beginning of an object's life. As such, we don't need to worry about the variance
impact of these methods.
```py
from ty_extensions import static_assert, is_subtype_of
class A: ...
class B(A): ...
class C[T]:
def __init__(self, x: T): ...
def __new__(self, x: T): ...
static_assert(is_subtype_of(C[B], C[A]))
static_assert(is_subtype_of(C[A], C[B]))
```
This example is then bivariant because it doesn't use `T` outside of the two exempted methods.
This holds likewise for dataclasses with synthesized `__init__`:
```py
from dataclasses import dataclass
@dataclass(init=True, frozen=True)
class D[T]:
x: T
# Covariant due to the read-only T-typed attribute; the `__init__` is ignored and doesn't make it
# invariant:
static_assert(is_subtype_of(D[B], D[A]))
static_assert(not is_subtype_of(D[A], D[B]))
```
## Union Types
Union types are covariant in all their members. If `A <: B`, then `A | C <: B | C` and
`C | A <: C | B`.
```py
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
class A: ...
class B(A): ...
class C: ...
# Union types are covariant in their members
static_assert(is_subtype_of(B | C, A | C))
static_assert(is_subtype_of(C | B, C | A))
static_assert(not is_subtype_of(A | C, B | C))
static_assert(not is_subtype_of(C | A, C | B))
# Assignability follows the same pattern
static_assert(is_assignable_to(B | C, A | C))
static_assert(is_assignable_to(C | B, C | A))
static_assert(not is_assignable_to(A | C, B | C))
static_assert(not is_assignable_to(C | A, C | B))
```
## Intersection Types
Intersection types cannot be expressed directly in Python syntax, but they occur when type narrowing
creates constraints through control flow. In ty's representation, intersection types are covariant
in their positive conjuncts and contravariant in their negative conjuncts.
```py
from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Intersection, Not
class A: ...
class B(A): ...
class C: ...
# Test covariance in positive conjuncts
# If B <: A, then Intersection[X, B] <: Intersection[X, A]
static_assert(is_subtype_of(Intersection[C, B], Intersection[C, A]))
static_assert(not is_subtype_of(Intersection[C, A], Intersection[C, B]))
static_assert(is_assignable_to(Intersection[C, B], Intersection[C, A]))
static_assert(not is_assignable_to(Intersection[C, A], Intersection[C, B]))
# Test contravariance in negative conjuncts
# If B <: A, then Intersection[X, Not[A]] <: Intersection[X, Not[B]]
# (excluding supertype A is more restrictive than excluding subtype B)
static_assert(is_subtype_of(Intersection[C, Not[A]], Intersection[C, Not[B]]))
static_assert(not is_subtype_of(Intersection[C, Not[B]], Intersection[C, Not[A]]))
static_assert(is_assignable_to(Intersection[C, Not[A]], Intersection[C, Not[B]]))
static_assert(not is_assignable_to(Intersection[C, Not[B]], Intersection[C, Not[A]]))
```
## Subclass Types (type[T])
The `type[T]` construct represents the type of classes that are subclasses of `T`. It is covariant
in `T` because if `A <: B`, then `type[A] <: type[B]` holds.
```py
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
class A: ...
class B(A): ...
# type[T] is covariant in T
static_assert(is_subtype_of(type[B], type[A]))
static_assert(not is_subtype_of(type[A], type[B]))
static_assert(is_assignable_to(type[B], type[A]))
static_assert(not is_assignable_to(type[A], type[B]))
# With generic classes using type[T]
class ClassContainer[T]:
def __init__(self, cls: type[T]) -> None:
self.cls = cls
def create_instance(self) -> T:
return self.cls()
# ClassContainer is covariant in T due to type[T]
static_assert(is_subtype_of(ClassContainer[B], ClassContainer[A]))
static_assert(not is_subtype_of(ClassContainer[A], ClassContainer[B]))
static_assert(is_assignable_to(ClassContainer[B], ClassContainer[A]))
static_assert(not is_assignable_to(ClassContainer[A], ClassContainer[B]))
# Practical example: you can pass a ClassContainer[B] where ClassContainer[A] is expected
# because type[B] can safely be used where type[A] is expected
def use_a_class_container(container: ClassContainer[A]) -> A:
return container.create_instance()
b_container = ClassContainer[B](B)
a_instance: A = use_a_class_container(b_container) # This should work
```
## Inheriting from generic classes with inferred variance
When inheriting from a generic class with our type variable substituted in, we count its occurrences
as well. In the following example, `T` is covariant in `C`, and contravariant in the subclass `D` if
you only count its own occurrences. Because we count both then, `T` is invariant in `D`.
```py
from ty_extensions import is_subtype_of, static_assert
class A:
pass
class B(A):
pass
class C[T]:
def f() -> T | None:
pass
static_assert(is_subtype_of(C[B], C[A]))
static_assert(not is_subtype_of(C[A], C[B]))
class D[T](C[T]):
def g(x: T) -> None:
pass
static_assert(not is_subtype_of(D[B], D[A]))
static_assert(not is_subtype_of(D[A], D[B]))
```
## Inheriting from generic classes with explicit variance
```py
from typing import TypeVar, Generic
from ty_extensions import is_subtype_of, static_assert
T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
T_contra = TypeVar("T_contra", contravariant=True)
class A:
pass
class B(A):
pass
class Invariant(Generic[T]):
pass
static_assert(not is_subtype_of(Invariant[B], Invariant[A]))
static_assert(not is_subtype_of(Invariant[A], Invariant[B]))
class DerivedInvariant[T](Invariant[T]):
pass
static_assert(not is_subtype_of(DerivedInvariant[B], DerivedInvariant[A]))
static_assert(not is_subtype_of(DerivedInvariant[A], DerivedInvariant[B]))
class Covariant(Generic[T_co]):
pass
static_assert(is_subtype_of(Covariant[B], Covariant[A]))
static_assert(not is_subtype_of(Covariant[A], Covariant[B]))
class DerivedCovariant[T](Covariant[T]):
pass
static_assert(is_subtype_of(DerivedCovariant[B], DerivedCovariant[A]))
static_assert(not is_subtype_of(DerivedCovariant[A], DerivedCovariant[B]))
class Contravariant(Generic[T_contra]):
pass
static_assert(not is_subtype_of(Contravariant[B], Contravariant[A]))
static_assert(is_subtype_of(Contravariant[A], Contravariant[B]))
class DerivedContravariant[T](Contravariant[T]):
pass
static_assert(not is_subtype_of(DerivedContravariant[B], DerivedContravariant[A]))
static_assert(is_subtype_of(DerivedContravariant[A], DerivedContravariant[B]))
```
[linear-time-variance-talk]: https://www.youtube.com/watch?v=7uixlNTOY4s&t=9705s
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance

View File

@@ -0,0 +1,470 @@
# Partial stub packages
Partial stub packages are stubs that are allowed to be missing modules. See the
[specification](https://peps.python.org/pep-0561/#partial-stub-packages). Partial stubs are also
called "incomplete" packages, and non-partial stubs are called "complete" packages.
Normally a stub package is expected to define a copy of every module the real implementation
defines. Module resolution is consequently required to report a module doesn't exist if it finds
`mypackage-stubs` and fails to find `mypackage.mymodule` *even if* `mypackage` does define
`mymodule`.
If a stub package declares that it's partial, we instead are expected to fall through to the
implementation package and try to discover `mymodule` there. This is described as follows:
> Type checkers should merge the stub package and runtime package or typeshed directories. This can
> be thought of as the functional equivalent of copying the stub package into the same directory as
> the corresponding runtime package or typeshed folder and type checking the combined directory
> structure. Thus type checkers MUST maintain the normal resolution order of checking `*.pyi` before
> `*.py` files.
Namespace stub packages are always considered partial by necessity. Regular stub packages are only
considered partial if they define a `py.typed` file containing the string `partial\n` (due to real
stubs in the wild, we relax this and look case-insensitively for `partial`).
The `py.typed` file was originally specified as an empty marker for "this package supports types",
as a way to opt into having typecheckers run on a package. However ty and pyright choose to largely
ignore this and just type check every package.
In its original specification it was specified that subpackages inherit any `py.typed` declared in a
parent package. However the precise interaction with `partial\n` was never specified. We currently
implement a simple inheritance scheme where a subpackage can always declare its own `py.typed` and
override whether it's partial or not.
## Partial stub with missing module
A stub package that includes a partial `py.typed` file.
Here "both" is found in the stub, while "impl" is found in the implementation. "fake" is found in
neither.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/py.typed`:
```text
partial
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/both.pyi`:
```pyi
class Both:
both: str
other: int
```
`/packages/foo/__init__.py`:
```py
```
`/packages/foo/both.py`:
```py
class Both: ...
```
`/packages/foo/impl.py`:
```py
class Impl:
impl: str
other: int
```
`main.py`:
```py
from foo.both import Both
from foo.impl import Impl
from foo.fake import Fake # error: "Cannot resolve"
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: str
reveal_type(Fake().fake) # revealed: Unknown
```
## Non-partial stub with missing module
Omitting the partial `py.typed`, we see "impl" now cannot be found.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/both.pyi`:
```pyi
class Both:
both: str
other: int
```
`/packages/foo/__init__.py`:
```py
```
`/packages/foo/both.py`:
```py
class Both: ...
```
`/packages/foo/impl.py`:
```py
class Impl:
impl: str
other: int
```
`main.py`:
```py
from foo.both import Both
from foo.impl import Impl # error: "Cannot resolve"
from foo.fake import Fake # error: "Cannot resolve"
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: Unknown
reveal_type(Fake().fake) # revealed: Unknown
```
## Full-typed stub with missing module
Including a blank py.typed we still don't conclude it's partial.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/py.typed`:
```text
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/both.pyi`:
```pyi
class Both:
both: str
other: int
```
`/packages/foo/__init__.py`:
```py
```
`/packages/foo/both.py`:
```py
class Both: ...
```
`/packages/foo/impl.py`:
```py
class Impl:
impl: str
other: int
```
`main.py`:
```py
from foo.both import Both
from foo.impl import Impl # error: "Cannot resolve"
from foo.fake import Fake # error: "Cannot resolve"
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: Unknown
reveal_type(Fake().fake) # revealed: Unknown
```
## Inheriting a partial `py.typed`
`foo-stubs` defines a partial py.typed which is used by `foo-stubs/bar`
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/py.typed`:
```text
# Also testing permissive parsing
# PARTIAL\n
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/bar/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/bar/both.pyi`:
```pyi
class Both:
both: str
other: int
```
`/packages/foo/__init__.py`:
```py
```
`/packages/foo/bar/__init__.py`:
```py
```
`/packages/foo/bar/both.py`:
```py
class Both: ...
```
`/packages/foo/bar/impl.py`:
```py
class Impl:
impl: str
other: int
```
`main.py`:
```py
from foo.bar.both import Both
from foo.bar.impl import Impl
from foo.bar.fake import Fake # error: "Cannot resolve"
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: str
reveal_type(Fake().fake) # revealed: Unknown
```
## Overloading a full `py.typed`
`foo-stubs` defines a full py.typed which is overloaded to partial by `foo-stubs/bar`
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/py.typed`:
```text
```
`/packages/foo-stubs/bar/py.typed`:
```text
# Also testing permissive parsing
partial/n
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/bar/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/bar/both.pyi`:
```pyi
class Both:
both: str
other: int
```
`/packages/foo/__init__.py`:
```py
```
`/packages/foo/bar/__init__.py`:
```py
```
`/packages/foo/bar/both.py`:
```py
class Both: ...
```
`/packages/foo/bar/impl.py`:
```py
class Impl:
impl: str
other: int
```
`main.py`:
```py
from foo.bar.both import Both
from foo.bar.impl import Impl
from foo.bar.fake import Fake # error: "Cannot resolve"
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: str
reveal_type(Fake().fake) # revealed: Unknown
```
## Overloading a partial `py.typed`
`foo-stubs` defines a partial py.typed which is overloaded to full by `foo-stubs/bar`
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/foo-stubs/py.typed`:
```text
# Also testing permissive parsing
pArTiAl\n
```
`/packages/foo-stubs/bar/py.typed`:
```text
```
`/packages/foo-stubs/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/bar/__init__.pyi`:
```pyi
```
`/packages/foo-stubs/bar/both.pyi`:
```pyi
class Both:
both: str
other: int
```
`/packages/foo/__init__.py`:
```py
```
`/packages/foo/bar/__init__.py`:
```py
```
`/packages/foo/bar/both.py`:
```py
class Both: ...
```
`/packages/foo/bar/impl.py`:
```py
class Impl:
impl: str
other: int
```
`main.py`:
```py
from foo.bar.both import Both
from foo.bar.impl import Impl # error: "Cannot resolve"
from foo.bar.fake import Fake # error: "Cannot resolve"
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: Unknown
reveal_type(Fake().fake) # revealed: Unknown
```
## Namespace stub with missing module
Namespace stubs are always partial.
This is a regression test for <https://github.com/astral-sh/ty/issues/520>.
```toml
[environment]
extra-paths = ["/packages"]
```
`/packages/parent-stubs/foo/both.pyi`:
```pyi
class Both:
both: str
other: int
```
`/packages/parent/foo/both.py`:
```py
class Both: ...
```
`/packages/parent/foo/impl.py`:
```py
class Impl:
impl: str
other: int
```
`main.py`:
```py
from parent.foo.both import Both
from parent.foo.impl import Impl
from parent.foo.fake import Fake # error: "Cannot resolve"
reveal_type(Both().both) # revealed: str
reveal_type(Impl().impl) # revealed: str
reveal_type(Fake().fake) # revealed: Unknown
```

View File

@@ -74,8 +74,16 @@ Person(3, "Eve", 99, "extra")
# error: [invalid-argument-type]
Person(id="3", name="Eve")
# TODO: over-writing NamedTuple fields should be an error
reveal_type(Person.id) # revealed: property
reveal_type(Person.name) # revealed: property
reveal_type(Person.age) # revealed: property
# TODO... the error is correct, but this is not the friendliest error message
# for assigning to a read-only property :-)
#
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `id` on type `Person` with custom `__set__` method"
alice.id = 42
# error: [invalid-assignment]
bob.age = None
```
@@ -94,28 +102,59 @@ reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax)
### Definition
TODO: Fields without default values should come before fields with.
<!-- snapshot-diagnostics -->
Fields without default values must come before fields with.
```py
from typing import NamedTuple
class Location(NamedTuple):
altitude: float = 0.0
latitude: float # this should be an error
# error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitude` defined here without a default value"
latitude: float
# error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitude` defined here without a default value"
longitude: float
class StrangeLocation(NamedTuple):
altitude: float
altitude: float = 0.0
altitude: float
altitude: float = 0.0
latitude: float # error: [invalid-named-tuple]
longitude: float # error: [invalid-named-tuple]
class VeryStrangeLocation(NamedTuple):
altitude: float = 0.0
latitude: float # error: [invalid-named-tuple]
longitude: float # error: [invalid-named-tuple]
altitude: float = 0.0
```
### Multiple Inheritance
Multiple inheritance is not supported for `NamedTuple` classes:
<!-- snapshot-diagnostics -->
Multiple inheritance is not supported for `NamedTuple` classes except with `Generic`:
```py
from typing import NamedTuple
from typing import NamedTuple, Protocol
# This should ideally emit a diagnostic
# error: [invalid-named-tuple] "NamedTuple class `C` cannot use multiple inheritance except with `Generic[]`"
class C(NamedTuple, object):
id: int
name: str
# fmt: off
class D(
int, # error: [invalid-named-tuple]
NamedTuple
): ...
# fmt: on
# error: [invalid-named-tuple]
class E(NamedTuple, Protocol): ...
```
### Inheriting from a `NamedTuple`
@@ -151,9 +190,42 @@ from typing import NamedTuple
class User(NamedTuple):
id: int
name: str
age: int | None
nickname: str
class SuperUser(User):
id: int # this should be an error
# TODO: this should be an error because it implies that the `id` attribute on
# `SuperUser` is mutable, but the read-only `id` property from the superclass
# has not been overridden in the class body
id: int
# this is fine; overriding a read-only attribute with a mutable one
# does not conflict with the Liskov Substitution Principle
name: str = "foo"
# this is also fine
@property
def age(self) -> int:
return super().age or 42
def now_called_robert(self):
self.name = "Robert" # fine because overridden with a mutable attribute
# TODO: this should cause us to emit an error as we're assigning to a read-only property
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
self.nickname = "Bob"
james = SuperUser(0, "James", 42, "Jimmy")
# fine because the property on the superclass was overridden with a mutable attribute
# on the subclass
james.name = "Robert"
# TODO: the error is correct (can't assign to the read-only property inherited from the superclass)
# but the error message could be friendlier :-)
#
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `nickname` on type `SuperUser` with custom `__set__` method"
james.nickname = "Bob"
```
### Generic named tuples
@@ -164,13 +236,29 @@ python-version = "3.12"
```
```py
from typing import NamedTuple
from typing import NamedTuple, Generic, TypeVar
class Property[T](NamedTuple):
name: str
value: T
reveal_type(Property("height", 3.4)) # revealed: Property[float]
reveal_type(Property.value) # revealed: property
reveal_type(Property.value.fget) # revealed: (self, /) -> Unknown
reveal_type(Property[str].value.fget) # revealed: (self, /) -> str
reveal_type(Property("height", 3.4).value) # revealed: float
T = TypeVar("T")
class LegacyProperty(NamedTuple, Generic[T]):
name: str
value: T
reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int]
reveal_type(LegacyProperty.value) # revealed: property
reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown
reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str
reveal_type(LegacyProperty("height", 3.4).value) # revealed: float
```
## Attributes on `NamedTuple`
@@ -186,7 +274,7 @@ class Person(NamedTuple):
reveal_type(Person._field_defaults) # revealed: dict[str, Any]
reveal_type(Person._fields) # revealed: tuple[str, ...]
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Self@_make
reveal_type(Person._make) # revealed: bound method <class 'Person'>._make(iterable: Iterable[Any]) -> Person
reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Person._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
@@ -211,6 +299,91 @@ alice = Person(1, "Alice", 42)
bob = Person(2, "Bob")
```
## The symbol `NamedTuple` itself
At runtime, `NamedTuple` is a function, and we understand this:
```py
import types
import typing
def expects_functiontype(x: types.FunctionType): ...
expects_functiontype(typing.NamedTuple)
```
This means we also understand that all attributes on function objects are available on the symbol
`typing.NamedTuple`:
```py
reveal_type(typing.NamedTuple.__name__) # revealed: str
reveal_type(typing.NamedTuple.__qualname__) # revealed: str
reveal_type(typing.NamedTuple.__kwdefaults__) # revealed: dict[str, Any] | None
# TODO: this should cause us to emit a diagnostic and reveal `Unknown` (function objects don't have an `__mro__` attribute),
# but the fact that we don't isn't actually a `NamedTuple` bug (https://github.com/astral-sh/ty/issues/986)
reveal_type(typing.NamedTuple.__mro__) # revealed: tuple[<class 'FunctionType'>, <class 'object'>]
```
By the normal rules, `NamedTuple` and `type[NamedTuple]` should not be valid in type expressions --
there is no object at runtime that is an "instance of `NamedTuple`", nor is there any class at
runtime that is a "subclass of `NamedTuple`" -- these are both impossible, since `NamedTuple` is a
function and not a class. However, for compatibility with other type checkers, we allow `NamedTuple`
in type expressions and understand it as describing an interface that all `NamedTuple` classes would
satisfy:
```py
def expects_named_tuple(x: typing.NamedTuple):
reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike
reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike
reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(**kwargs) -> NamedTupleLike
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], (value: tuple[_T@__add__, ...], /) -> tuple[object, ...]]
reveal_type(x.__add__)
reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object]
def _(y: type[typing.NamedTuple]):
reveal_type(y) # revealed: @Todo(unsupported type[X] special form)
# error: [invalid-type-form] "Special form `typing.NamedTuple` expected no type parameter"
def _(z: typing.NamedTuple[int]): ...
```
Any instance of a `NamedTuple` class can therefore be passed for a function parameter that is
annotated with `NamedTuple`:
```py
from typing import NamedTuple, Protocol, Iterable, Any
from ty_extensions import static_assert, is_assignable_to
class Point(NamedTuple):
x: int
y: int
reveal_type(Point._make) # revealed: bound method <class 'Point'>._make(iterable: Iterable[Any]) -> Point
reveal_type(Point._asdict) # revealed: def _asdict(self) -> dict[str, Any]
reveal_type(Point._replace) # revealed: def _replace(self, **kwargs: Any) -> Self@_replace
static_assert(is_assignable_to(Point, NamedTuple))
expects_named_tuple(Point(x=42, y=56)) # fine
# error: [invalid-argument-type] "Argument to function `expects_named_tuple` is incorrect: Expected `tuple[object, ...] & NamedTupleLike`, found `tuple[Literal[1], Literal[2]]`"
expects_named_tuple((1, 2))
```
The type described by `NamedTuple` in type expressions is understood as being assignable to
`tuple[object, ...]` and `tuple[Any, ...]`:
```py
static_assert(is_assignable_to(NamedTuple, tuple))
static_assert(is_assignable_to(NamedTuple, tuple[object, ...]))
static_assert(is_assignable_to(NamedTuple, tuple[Any, ...]))
def expects_tuple(x: tuple[object, ...]): ...
def _(x: NamedTuple):
expects_tuple(x) # fine
```
## NamedTuple with custom `__getattr__`
This is a regression test for <https://github.com/astral-sh/ty/issues/322>. Make sure that the

View File

@@ -240,6 +240,21 @@ def f(x: str | None):
# When there is a reassignment, any narrowing constraints on the place are invalidated in lazy scopes.
x = None
def f(x: str | None):
def _():
if x is not None:
def closure():
reveal_type(x) # revealed: str | None
x = None
def f(x: str | None):
class C:
def _():
if x is not None:
def closure():
reveal_type(x) # revealed: str
x = None # This assignment is not visible in the inner lazy scope, so narrowing is still valid.
```
If a variable defined in a private scope is never reassigned, narrowing remains in effect in the
@@ -256,6 +271,12 @@ def f(const: str | None):
reveal_type(const) # revealed: str
[reveal_type(const) for _ in range(1)] # revealed: str
def f(const: str | None):
def _():
if const is not None:
def closure():
reveal_type(const) # revealed: str
```
And even if there is an attribute or subscript assignment to the variable, narrowing of the variable

View File

@@ -3,10 +3,15 @@
## `type(x) is C`
```py
from typing import final
class A: ...
class B: ...
def _(x: A | B):
@final
class C: ...
def _(x: A | B, y: A | C):
if type(x) is A:
reveal_type(x) # revealed: A
else:
@@ -14,20 +19,105 @@ def _(x: A | B):
# of `x` could be a subclass of `A`, so we need
# to infer the full union type:
reveal_type(x) # revealed: A | B
if type(y) is C:
reveal_type(y) # revealed: C
else:
# here, however, inferring `A` is fine,
# because `C` is `@final`: no subclass of `A`
# and `C` could exist
reveal_type(y) # revealed: A
if type(y) is A:
reveal_type(y) # revealed: A
else:
# but here, `type(y)` could be a subclass of `A`,
# in which case the `type(y) is A` call would evaluate
# to `False` even if `y` was an instance of `A`,
# so narrowing cannot occur
reveal_type(y) # revealed: A | C
```
## `type(x) is not C`
```py
from typing import final
class A: ...
class B: ...
def _(x: A | B):
@final
class C: ...
def _(x: A | B, y: A | C):
if type(x) is not A:
# Same reasoning as above: no narrowing should occur here.
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A
if type(y) is not C:
# same reasoning as above: narrowing *can* occur here because `C` is `@final`
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: C
if type(y) is not A:
# same reasoning as above: narrowing *cannot* occur here
# because `A` is not `@final`
reveal_type(y) # revealed: A | C
else:
reveal_type(y) # revealed: A
```
## No narrowing for `type(x) is C[int]`
At runtime, `type(x)` will never return a generic alias object (only ever a class-literal object),
so no narrowing can occur if `type(x)` is compared with a generic alias object.
```toml
[environment]
python-version = "3.12"
```
```py
class A[T]: ...
class B: ...
def f(x: A[int] | B):
if type(x) is A[int]:
# this branch is actually unreachable -- we *could* reveal `Never` here!
reveal_type(x) # revealed: A[int] | B
else:
reveal_type(x) # revealed: A[int] | B
if type(x) is A:
# TODO: this should be `A[int]`, but `A[int] | B` would be better than `Never`
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: A[int] | B
if type(x) is B:
reveal_type(x) # revealed: B
else:
reveal_type(x) # revealed: A[int] | B
if type(x) is not A[int]:
reveal_type(x) # revealed: A[int] | B
else:
# this branch is actually unreachable -- we *could* reveal `Never` here!
reveal_type(x) # revealed: A[int] | B
if type(x) is not A:
reveal_type(x) # revealed: A[int] | B
else:
# TODO: this should be `A[int]`, but `A[int] | B` would be better than `Never`
reveal_type(x) # revealed: Never
if type(x) is not B:
reveal_type(x) # revealed: A[int] | B
else:
reveal_type(x) # revealed: B
```
## `type(x) == C`, `type(x) != C`
@@ -127,12 +217,23 @@ class B: ...
def _[T](x: A | B):
if type(x) is A[str]:
# `type()` never returns a generic alias, so `type(x)` cannot be `A[str]`
reveal_type(x) # revealed: Never
# TODO: `type()` never returns a generic alias, so `type(x)` cannot be `A[str]`
reveal_type(x) # revealed: A[int] | B
else:
reveal_type(x) # revealed: A[int] | B
```
## Narrowing for tuple
An early version of <https://github.com/astral-sh/ruff/pull/19920> caused us to crash on this:
```py
def _(val):
if type(val) is tuple:
# TODO: better would be `Unknown & tuple[object, ...]`
reveal_type(val) # revealed: Unknown & tuple[Unknown, ...]
```
## Limitations
```py

View File

@@ -64,6 +64,17 @@ x: MyIntOrStr = 1
y: MyIntOrStr = None
```
## Unpacking from a type alias
```py
type T = tuple[int, str]
def f(x: T):
a, b = x
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
```
## Generic type aliases
```py

View File

@@ -150,6 +150,14 @@ class AlsoInvalid(MyProtocol, OtherProtocol, NotAProtocol, Protocol): ...
# revealed: tuple[<class 'AlsoInvalid'>, <class 'MyProtocol'>, <class 'OtherProtocol'>, <class 'NotAProtocol'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(AlsoInvalid.__mro__)
class NotAGenericProtocol[T]: ...
# error: [invalid-protocol] "Protocol class `StillInvalid` cannot inherit from non-protocol class `NotAGenericProtocol`"
class StillInvalid(NotAGenericProtocol[int], Protocol): ...
# revealed: tuple[<class 'StillInvalid'>, <class 'NotAGenericProtocol[int]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(StillInvalid.__mro__)
```
But two exceptions to this rule are `object` and `Generic`:
@@ -347,7 +355,9 @@ And as a corollary, `type[MyProtocol]` can also be called:
```py
def f(x: type[MyProtocol]):
reveal_type(x()) # revealed: MyProtocol
# TODO: add a `reveal_type` call here once it's no longer a `Todo` type
# (which doesn't work well with snapshots)
x()
```
## Members of a protocol
@@ -387,7 +397,7 @@ To see the kinds and types of the protocol members, you can use the debugging ai
```py
from ty_extensions import reveal_protocol_interface
from typing import SupportsIndex, SupportsAbs
from typing import SupportsIndex, SupportsAbs, ClassVar
# error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`"
reveal_protocol_interface(Foo)
@@ -405,6 +415,33 @@ reveal_protocol_interface("foo")
#
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
reveal_protocol_interface(SupportsAbs[int])
class BaseProto(Protocol):
def member(self) -> int: ...
class SubProto(BaseProto, Protocol):
def member(self) -> bool: ...
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(BaseProto)
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> bool`)}`"
reveal_protocol_interface(SubProto)
class ProtoWithClassVar(Protocol):
x: ClassVar[int]
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
reveal_protocol_interface(ProtoWithClassVar)
class ProtocolWithDefault(Protocol):
x: int = 0
# We used to incorrectly report this as having an `x: Literal[0]` member;
# declared types should take priority over inferred types for protocol interfaces!
#
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`)}`"
reveal_protocol_interface(ProtocolWithDefault)
```
Certain special attributes and methods are not considered protocol members at runtime, and should
@@ -445,6 +482,8 @@ reveal_type(get_protocol_members(Baz2))
## Protocol members in statically known branches
<!-- snapshot-diagnostics -->
The list of protocol members does not include any members declared in branches that are statically
known to be unreachable:
@@ -455,7 +494,7 @@ python-version = "3.9"
```py
import sys
from typing_extensions import Protocol, get_protocol_members
from typing_extensions import Protocol, get_protocol_members, reveal_type
class Foo(Protocol):
if sys.version_info >= (3, 10):
@@ -464,7 +503,7 @@ class Foo(Protocol):
def c(self) -> None: ...
else:
d: int
e = 56
e = 56 # error: [ambiguous-protocol-member]
def f(self) -> None: ...
reveal_type(get_protocol_members(Foo)) # revealed: frozenset[Literal["d", "e", "f"]]
@@ -613,13 +652,26 @@ class HasXWithDefault(Protocol):
class FooWithZero:
x: int = 0
# TODO: these should pass
static_assert(is_subtype_of(FooWithZero, HasXWithDefault)) # error: [static-assert-error]
static_assert(is_assignable_to(FooWithZero, HasXWithDefault)) # error: [static-assert-error]
static_assert(not is_subtype_of(Foo, HasXWithDefault))
static_assert(not is_assignable_to(Foo, HasXWithDefault))
static_assert(not is_subtype_of(Qux, HasXWithDefault))
static_assert(not is_assignable_to(Qux, HasXWithDefault))
static_assert(is_subtype_of(FooWithZero, HasXWithDefault))
static_assert(is_assignable_to(FooWithZero, HasXWithDefault))
# TODO: whether or not any of these four assertions should pass is not clearly specified.
#
# A test in the typing conformance suite implies that they all should:
# that a nominal class with an instance attribute `x`
# (*without* a default value on the class body)
# should be understood as satisfying a protocol that has an attribute member `x`
# even if the protocol's `x` member has a default value on the class body.
#
# See <https://github.com/python/typing/blob/d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc/conformance/tests/protocols_definition.py#L56-L79>.
#
# The implications of this for meta-protocols are not clearly spelled out, however,
# and the fact that attribute members on protocols can have defaults is only mentioned
# in a throwaway comment in the spec's prose.
static_assert(is_subtype_of(Foo, HasXWithDefault))
static_assert(is_assignable_to(Foo, HasXWithDefault))
static_assert(is_subtype_of(Qux, HasXWithDefault))
static_assert(is_assignable_to(Qux, HasXWithDefault))
class HasClassVarX(Protocol):
x: ClassVar[int]
@@ -747,9 +799,9 @@ def f(arg: HasXWithDefault):
```
Assignments in a class body of a protocol -- of any kind -- are not permitted by ty unless the
symbol being assigned to is also explicitly declared in the protocol's class body. Note that this is
stricter validation of protocol members than many other type checkers currently apply (as of
2025/04/21).
symbol being assigned to is also explicitly declared in the body of the protocol class or one of its
superclasses. Note that this is stricter validation of protocol members than many other type
checkers currently apply (as of 2025/04/21).
The reason for this strict validation is that undeclared variables in the class body would lead to
an ambiguous interface being declared by the protocol.
@@ -773,24 +825,75 @@ class LotsOfBindings(Protocol):
class Nested: ... # also weird, but we should also probably allow it
class NestedProtocol(Protocol): ... # same here...
e = 72 # TODO: this should error with `[invalid-protocol]` (`e` is not declared)
e = 72 # error: [ambiguous-protocol-member]
f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared)
# error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `f: int = ...`"
# error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `g: int = ...`"
f, g = (1, 2)
h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared)
h: int = (i := 3) # error: [ambiguous-protocol-member]
for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared)
for j in range(42): # error: [ambiguous-protocol-member]
pass
with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared)
with MyContext() as k: # error: [ambiguous-protocol-member]
pass
match object():
case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared)
case l: # error: [ambiguous-protocol-member]
...
# revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]]
reveal_type(get_protocol_members(LotsOfBindings))
class Foo(Protocol):
a: int
class Bar(Foo, Protocol):
a = 42 # fine, because it's declared in the superclass
reveal_type(get_protocol_members(Bar)) # revealed: frozenset[Literal["a"]]
```
A binding-without-declaration will not be reported if it occurs in a branch that we can statically
determine to be unreachable. The reason is that we don't consider it to be a protocol member at all
if all definitions for the variable are in unreachable blocks:
```py
import sys
class Protocol694(Protocol):
if sys.version_info > (3, 694):
x = 42 # no error!
```
If there are multiple bindings of the variable in the class body, however, and at least one of the
bindings occurs in a block of code that is understood to be (possibly) reachable, a diagnostic will
be reported. The diagnostic will be attached to the first binding that occurs in the class body,
even if that first definition occurs in an unreachable block:
```py
class Protocol695(Protocol):
if sys.version_info > (3, 695):
x = 42
else:
x = 42
x = 56 # error: [ambiguous-protocol-member]
```
In order for the variable to be considered declared, the declaration of the variable must also take
place in a block of code that is understood to be (possibly) reachable:
```py
class Protocol696(Protocol):
if sys.version_info > (3, 696):
x: int
else:
x = 42 # error: [ambiguous-protocol-member]
y: int
y = 56 # no error
```
Attribute members are allowed to have assignments in methods on the protocol class, just like
@@ -893,6 +996,40 @@ static_assert(not is_assignable_to(HasX, Foo))
static_assert(not is_subtype_of(HasX, Foo))
```
## Diagnostics for protocols with invalid attribute members
This is a short appendix to the previous section with the `snapshot-diagnostics` directive enabled
(enabling snapshots for the previous section in its entirety would lead to a huge snapshot, since
it's a large section).
<!-- snapshot-diagnostics -->
```py
from typing import Protocol
def coinflip() -> bool:
return True
class A(Protocol):
# The `x` and `y` members attempt to use Python-2-style type comments
# to indicate that the type should be `int | None` and `str` respectively,
# but we don't support those
# error: [ambiguous-protocol-member]
a = None # type: int
# error: [ambiguous-protocol-member]
b = ... # type: str
if coinflip():
c = 1 # error: [ambiguous-protocol-member]
else:
c = 2
# error: [ambiguous-protocol-member]
for d in range(42):
pass
```
## Equivalence of protocols
Two protocols are considered equivalent types if they specify the same interface, even if they have
@@ -1923,7 +2060,7 @@ def _(r: Recursive):
reveal_type(r.t) # revealed: tuple[int, tuple[str, Recursive]]
reveal_type(r.callable1) # revealed: (int, /) -> Recursive
reveal_type(r.callable2) # revealed: (Recursive, /) -> int
reveal_type(r.subtype_of) # revealed: type[Recursive]
reveal_type(r.subtype_of) # revealed: @Todo(type[T] for protocols)
reveal_type(r.generic) # revealed: GenericC[Recursive]
reveal_type(r.method(r)) # revealed: Recursive
reveal_type(r.nested) # revealed: Recursive | ((Recursive, tuple[Recursive, Recursive], /) -> Recursive)
@@ -2061,6 +2198,86 @@ def f(value: Iterator):
cast(Iterator, value) # error: [redundant-cast]
```
## Meta-protocols
Where `P` is a protocol type, a class object `N` can be said to inhabit the type `type[P]` if:
- All `ClassVar` members on `P` exist on the class object `N`
- All method members on `P` exist on the class object `N`
- Instantiating `N` creates an object that would satisfy the protocol `P`
Currently meta-protocols are not fully supported by ty, but we try to keep false positives to a
minimum in the meantime.
```py
from typing import Protocol, ClassVar
from ty_extensions import static_assert, is_assignable_to, TypeOf, is_subtype_of
class Foo(Protocol):
x: int
y: ClassVar[str]
def method(self) -> bytes: ...
def _(f: type[Foo]):
reveal_type(f) # revealed: type[@Todo(type[T] for protocols)]
# TODO: we should emit `unresolved-attribute` here: although we would accept this for a
# nominal class, we would see any class `N` as inhabiting `Foo` if it had an implicit
# instance attribute `x`, and implicit instance attributes are rarely bound on the class
# object.
reveal_type(f.x) # revealed: @Todo(type[T] for protocols)
# TODO: should be `str`
reveal_type(f.y) # revealed: @Todo(type[T] for protocols)
f.y = "foo" # fine
# TODO: should be `Callable[[Foo], bytes]`
reveal_type(f.method) # revealed: @Todo(type[T] for protocols)
class Bar: ...
# TODO: these should pass
static_assert(not is_assignable_to(type[Bar], type[Foo])) # error: [static-assert-error]
static_assert(not is_assignable_to(TypeOf[Bar], type[Foo])) # error: [static-assert-error]
class Baz:
x: int
y: ClassVar[str] = "foo"
def method(self) -> bytes:
return b"foo"
static_assert(is_assignable_to(type[Baz], type[Foo]))
static_assert(is_assignable_to(TypeOf[Baz], type[Foo]))
# TODO: these should pass
static_assert(is_subtype_of(type[Baz], type[Foo])) # error: [static-assert-error]
static_assert(is_subtype_of(TypeOf[Baz], type[Foo])) # error: [static-assert-error]
```
## Regression test for `ClassVar` members in stubs
In an early version of our protocol implementation, we didn't retain the `ClassVar` qualifier for
protocols defined in stub files.
`stub.pyi`:
```pyi
from typing import ClassVar, Protocol
class Foo(Protocol):
x: ClassVar[int]
```
`main.py`:
```py
from stub import Foo
from ty_extensions import reveal_protocol_interface
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`"
reveal_protocol_interface(Foo)
```
## TODO
Add tests for:

View File

@@ -49,6 +49,22 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.
35 | y: str
36 | _2: KW_ONLY
37 | z: float
38 | from dataclasses import dataclass, KW_ONLY
39 |
40 | @dataclass
41 | class D:
42 | x: int
43 | _: KW_ONLY
44 | y: str
45 |
46 | @dataclass
47 | class E(D):
48 | z: bytes
49 |
50 | # This should work: x=1 (positional), z=b"foo" (positional), y="foo" (keyword-only)
51 | E(1, b"foo", y="foo")
52 |
53 | reveal_type(E.__init__) # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
```
# Diagnostics
@@ -141,3 +157,15 @@ info: `KW_ONLY` fields: `_1`, `_2`
info: rule `duplicate-kw-only` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:53:13
|
51 | E(1, b"foo", y="foo")
52 |
53 | reveal_type(E.__init__) # revealed: (self: E, x: int, z: bytes, *, y: str) -> None
| ^^^^^^^^^^ `(self: E, x: int, z: bytes, *, y: str) -> None`
|
```

View File

@@ -0,0 +1,41 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_await.md - Invalid await diagnostics - Basic
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
---
# Python source files
## mdtest_snippet.py
```
1 | async def main() -> None:
2 | await 1 # error: [invalid-await]
```
# Diagnostics
```
error[invalid-await]: `Literal[1]` is not awaitable
--> src/mdtest_snippet.py:2:11
|
1 | async def main() -> None:
2 | await 1 # error: [invalid-await]
| ^
|
::: stdlib/builtins.pyi:337:7
|
335 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
336 |
337 | class int:
| --- type defined here
338 | """int([x]) -> integer
339 | int(x, base=10) -> integer
|
info: `__await__` is missing
info: rule `invalid-await` is enabled by default
```

View File

@@ -0,0 +1,41 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with missing `__await__`
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
---
# Python source files
## mdtest_snippet.py
```
1 | class MissingAwait:
2 | pass
3 |
4 | async def main() -> None:
5 | await MissingAwait() # error: [invalid-await]
```
# Diagnostics
```
error[invalid-await]: `MissingAwait` is not awaitable
--> src/mdtest_snippet.py:5:11
|
4 | async def main() -> None:
5 | await MissingAwait() # error: [invalid-await]
| ^^^^^^^^^^^^^^
|
::: src/mdtest_snippet.py:1:7
|
1 | class MissingAwait:
| ------------ type defined here
2 | pass
|
info: `__await__` is missing
info: rule `invalid-await` is enabled by default
```

View File

@@ -0,0 +1,47 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with possibly unbound `__await__`
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
---
# Python source files
## mdtest_snippet.py
```
1 | from datetime import datetime
2 |
3 | class PossiblyUnbound:
4 | if datetime.today().weekday() == 0:
5 | def __await__(self):
6 | yield
7 |
8 | async def main() -> None:
9 | await PossiblyUnbound() # error: [invalid-await]
```
# Diagnostics
```
error[invalid-await]: `PossiblyUnbound` is not awaitable
--> src/mdtest_snippet.py:9:11
|
8 | async def main() -> None:
9 | await PossiblyUnbound() # error: [invalid-await]
| ^^^^^^^^^^^^^^^^^
|
::: src/mdtest_snippet.py:5:13
|
3 | class PossiblyUnbound:
4 | if datetime.today().weekday() == 0:
5 | def __await__(self):
| --------------- method defined here
6 | yield
|
info: `__await__` is possibly unbound
info: rule `invalid-await` is enabled by default
```

View File

@@ -0,0 +1,45 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_await.md - Invalid await diagnostics - Invalid union return type
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
---
# Python source files
## mdtest_snippet.py
```
1 | import typing
2 | from datetime import datetime
3 |
4 | class UnawaitableUnion:
5 | if datetime.today().weekday() == 6:
6 |
7 | def __await__(self) -> typing.Generator[typing.Any, None, None]:
8 | yield
9 | else:
10 |
11 | def __await__(self) -> int:
12 | return 5
13 |
14 | async def main() -> None:
15 | await UnawaitableUnion() # error: [invalid-await]
```
# Diagnostics
```
error[invalid-await]: `UnawaitableUnion` is not awaitable
--> src/mdtest_snippet.py:15:11
|
14 | async def main() -> None:
15 | await UnawaitableUnion() # error: [invalid-await]
| ^^^^^^^^^^^^^^^^^^
|
info: `__await__` returns `Generator[Any, None, None] | int`, which is not a valid iterator
info: rule `invalid-await` is enabled by default
```

View File

@@ -0,0 +1,35 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_await.md - Invalid await diagnostics - Non-callable `__await__`
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NonCallableAwait:
2 | __await__ = 42
3 |
4 | async def main() -> None:
5 | await NonCallableAwait() # error: [invalid-await]
```
# Diagnostics
```
error[invalid-await]: `NonCallableAwait` is not awaitable
--> src/mdtest_snippet.py:5:11
|
4 | async def main() -> None:
5 | await NonCallableAwait() # error: [invalid-await]
| ^^^^^^^^^^^^^^^^^^
|
info: `__await__` is possibly not callable
info: rule `invalid-await` is enabled by default
```

View File

@@ -0,0 +1,43 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_await.md - Invalid await diagnostics - `__await__` definition with extra arguments
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
---
# Python source files
## mdtest_snippet.py
```
1 | class InvalidAwaitArgs:
2 | def __await__(self, value: int):
3 | yield value
4 |
5 | async def main() -> None:
6 | await InvalidAwaitArgs() # error: [invalid-await]
```
# Diagnostics
```
error[invalid-await]: `InvalidAwaitArgs` is not awaitable
--> src/mdtest_snippet.py:6:11
|
5 | async def main() -> None:
6 | await InvalidAwaitArgs() # error: [invalid-await]
| ^^^^^^^^^^^^^^^^^^
|
::: src/mdtest_snippet.py:2:18
|
1 | class InvalidAwaitArgs:
2 | def __await__(self, value: int):
| ------------------ parameters here
3 | yield value
|
info: `__await__` requires arguments and cannot be called implicitly
info: rule `invalid-await` is enabled by default
```

View File

@@ -0,0 +1,43 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_await.md - Invalid await diagnostics - `__await__` definition with explicit invalid return type
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
---
# Python source files
## mdtest_snippet.py
```
1 | class InvalidAwaitReturn:
2 | def __await__(self) -> int:
3 | return 5
4 |
5 | async def main() -> None:
6 | await InvalidAwaitReturn() # error: [invalid-await]
```
# Diagnostics
```
error[invalid-await]: `InvalidAwaitReturn` is not awaitable
--> src/mdtest_snippet.py:6:11
|
5 | async def main() -> None:
6 | await InvalidAwaitReturn() # error: [invalid-await]
| ^^^^^^^^^^^^^^^^^^^^
|
::: src/mdtest_snippet.py:2:9
|
1 | class InvalidAwaitReturn:
2 | def __await__(self) -> int:
| ---------------------- method defined here
3 | return 5
|
info: `__await__` returns `int`, which is not a valid iterator
info: rule `invalid-await` is enabled by default
```

View File

@@ -0,0 +1,140 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: named_tuple.md - `NamedTuple` - `typing.NamedTuple` - Definition
mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import NamedTuple
2 |
3 | class Location(NamedTuple):
4 | altitude: float = 0.0
5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitude` defined here without a default value"
6 | latitude: float
7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitude` defined here without a default value"
8 | longitude: float
9 |
10 | class StrangeLocation(NamedTuple):
11 | altitude: float
12 | altitude: float = 0.0
13 | altitude: float
14 | altitude: float = 0.0
15 | latitude: float # error: [invalid-named-tuple]
16 | longitude: float # error: [invalid-named-tuple]
17 |
18 | class VeryStrangeLocation(NamedTuple):
19 | altitude: float = 0.0
20 | latitude: float # error: [invalid-named-tuple]
21 | longitude: float # error: [invalid-named-tuple]
22 | altitude: float = 0.0
```
# Diagnostics
```
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
--> src/mdtest_snippet.py:4:5
|
3 | class Location(NamedTuple):
4 | altitude: float = 0.0
| --------------------- Earlier field `altitude` defined here with a default value
5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitud…
6 | latitude: float
| ^^^^^^^^ Field `latitude` defined here without a default value
7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitu…
8 | longitude: float
|
info: rule `invalid-named-tuple` is enabled by default
```
```
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
--> src/mdtest_snippet.py:4:5
|
3 | class Location(NamedTuple):
4 | altitude: float = 0.0
| --------------------- Earlier field `altitude` defined here with a default value
5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitu…
6 | latitude: float
7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longit…
8 | longitude: float
| ^^^^^^^^^ Field `longitude` defined here without a default value
9 |
10 | class StrangeLocation(NamedTuple):
|
info: rule `invalid-named-tuple` is enabled by default
```
```
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
--> src/mdtest_snippet.py:14:5
|
12 | altitude: float = 0.0
13 | altitude: float
14 | altitude: float = 0.0
| --------------------- Earlier field `altitude` defined here with a default value
15 | latitude: float # error: [invalid-named-tuple]
| ^^^^^^^^ Field `latitude` defined here without a default value
16 | longitude: float # error: [invalid-named-tuple]
|
info: rule `invalid-named-tuple` is enabled by default
```
```
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
--> src/mdtest_snippet.py:14:5
|
12 | altitude: float = 0.0
13 | altitude: float
14 | altitude: float = 0.0
| --------------------- Earlier field `altitude` defined here with a default value
15 | latitude: float # error: [invalid-named-tuple]
16 | longitude: float # error: [invalid-named-tuple]
| ^^^^^^^^^ Field `longitude` defined here without a default value
17 |
18 | class VeryStrangeLocation(NamedTuple):
|
info: rule `invalid-named-tuple` is enabled by default
```
```
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
--> src/mdtest_snippet.py:20:5
|
18 | class VeryStrangeLocation(NamedTuple):
19 | altitude: float = 0.0
20 | latitude: float # error: [invalid-named-tuple]
| ^^^^^^^^ Field `latitude` defined here without a default value
21 | longitude: float # error: [invalid-named-tuple]
22 | altitude: float = 0.0
|
info: Earlier field `altitude` was defined with a default value
info: rule `invalid-named-tuple` is enabled by default
```
```
error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s)
--> src/mdtest_snippet.py:21:5
|
19 | altitude: float = 0.0
20 | latitude: float # error: [invalid-named-tuple]
21 | longitude: float # error: [invalid-named-tuple]
| ^^^^^^^^^ Field `longitude` defined here without a default value
22 | altitude: float = 0.0
|
info: Earlier field `altitude` was defined with a default value
info: rule `invalid-named-tuple` is enabled by default
```

Some files were not shown because too many files have changed in this diff Show More