diff --git a/crates/ty_module_resolver/src/module.rs b/crates/ty_module_resolver/src/module.rs index 02694404e24c64..99c6db0f8d7ef9 100644 --- a/crates/ty_module_resolver/src/module.rs +++ b/crates/ty_module_resolver/src/module.rs @@ -334,6 +334,7 @@ pub enum KnownModule { UnittestMock, Uuid, Warnings, + Numbers, } impl KnownModule { @@ -362,6 +363,7 @@ impl KnownModule { Self::UnittestMock => "unittest.mock", Self::Uuid => "uuid", Self::Templatelib => "string.templatelib", + Self::Numbers => "numbers", } } diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index b6796ecbd57031..383fa0fffd5fd2 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -16,6 +16,16 @@ reveal_type(y) # revealed: Literal[1] x: int = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`" ``` +## Numbers special case + + + +```py +from numbers import Number + +a: Number = 1 # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Number`" +``` + ## Violates previous annotation ```py diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno\342\200\246_-_Numbers_special_case_(457f31497da6a6af).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno\342\200\246_-_Numbers_special_case_(457f31497da6a6af).snap" new file mode 100644 index 00000000000000..40253e704bfb95 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno\342\200\246_-_Numbers_special_case_(457f31497da6a6af).snap" @@ -0,0 +1,38 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: annotations.md - Assignment with annotations - Numbers special case +mdtest path: crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from numbers import Number +2 | +3 | a: Number = 1 # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Number`" +``` + +# Diagnostics + +``` +error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `Number` + --> src/mdtest_snippet.py:3:4 + | +1 | from numbers import Number +2 | +3 | a: Number = 1 # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Number`" + | ------ ^ Incompatible value of type `Literal[1]` + | | + | Declared type + | +info: Types from the `numbers` module aren't supported for static type checking +help: Consider using a protocol instead, such as `typing.SupportsFloat` +info: rule `invalid-assignment` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 287c81dd32596b..7e631ae6ac9649 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -44,7 +44,7 @@ use ruff_python_ast::{self as ast, AnyNodeRef, PythonVersion, StringFlags}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use std::fmt::{self, Formatter}; -use ty_module_resolver::{Module, ModuleName}; +use ty_module_resolver::{KnownModule, Module, ModuleName, file_to_module}; const RUNTIME_CHECKABLE_DOCS_URL: &str = "https://docs.python.org/3/library/typing.html#typing.runtime_checkable"; @@ -2968,6 +2968,26 @@ pub(super) fn report_invalid_assignment<'db>( let message = diag.primary_message().to_string(); diag.set_concise_message(message); } + + // special case message + if let Type::NominalInstance(target_instance) = target_ty { + let db = context.db(); + let file = target_instance.class(db).class_literal(db).file(db); + if let Some(module) = file_to_module(db, file) + && module.is_known(db, KnownModule::Numbers) + { + let is_numeric = [KnownClass::Int, KnownClass::Float, KnownClass::Complex] + .iter() + .any(|numeric| value_ty.is_subtype_of(db, numeric.to_instance(db))); + + if is_numeric { + diag.info( + "Types from the `numbers` module aren't supported for static type checking", + ); + diag.help("Consider using a protocol instead, such as `typing.SupportsFloat`"); + } + } + } } pub(super) fn report_invalid_attribute_assignment(