Skip to content

Commit 547fced

Browse files
committed
Merge branch 'main' into dcreager/legacy-class
* main: Improve messages outputted by py-fuzzer (#17764) [`red-knot`] Allow subclasses of Any to be assignable to Callable types (#17717) [red-knot] Increase durability of read-only `File` fields (#17757) [red-knot] Cache source type during semanic index building (#17756) [`flake8-use-pathlib`] Fix `PTH116` false positive when `stat` is passed a file descriptor (#17709) Sync vendored typeshed stubs (#17753)
2 parents e94c708 + 41f3f21 commit 547fced

File tree

26 files changed

+197
-81
lines changed

26 files changed

+197
-81
lines changed

crates/red_knot_python_semantic/resources/mdtest/annotations/any.md

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,42 +46,71 @@ def f():
4646
y: Any = "not an Any" # error: [invalid-assignment]
4747
```
4848

49-
## Subclass
49+
## Subclasses of `Any`
5050

5151
The spec allows you to define subclasses of `Any`.
5252

53-
`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
53+
`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be
5454
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
5555
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
5656

5757
```py
5858
from typing import Any
5959

60-
class Subclass(Any): ...
60+
class SubclassOfAny(Any): ...
6161

62-
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
62+
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]
6363

64-
x: Subclass = 1 # error: [invalid-assignment]
65-
y: int = Subclass()
66-
67-
def _(s: Subclass):
68-
reveal_type(s) # revealed: Subclass
64+
x: SubclassOfAny = 1 # error: [invalid-assignment]
65+
y: int = SubclassOfAny()
6966
```
7067

71-
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
72-
be a subclass of `FinalClass`:
68+
`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not
69+
possibly be a subclass of `FinalClass`:
7370

7471
```py
7572
from typing import final
7673

7774
@final
7875
class FinalClass: ...
7976

80-
f: FinalClass = Subclass() # error: [invalid-assignment]
77+
f: FinalClass = SubclassOfAny() # error: [invalid-assignment]
78+
79+
@final
80+
class OtherFinalClass: ...
81+
82+
f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment]
83+
```
84+
85+
A subclass of `Any` can also be assigned to arbitrary `Callable` types:
86+
87+
```py
88+
from typing import Callable, Any
89+
90+
def takes_callable1(f: Callable):
91+
f()
92+
93+
takes_callable1(SubclassOfAny())
94+
95+
def takes_callable2(f: Callable[[int], None]):
96+
f(1)
97+
98+
takes_callable2(SubclassOfAny())
99+
```
100+
101+
A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed:
102+
103+
```py
104+
from typing import Any, Literal
105+
106+
class MockAny(Any):
107+
pass
108+
109+
x: Literal[1] = MockAny() # error: [invalid-assignment]
81110
```
82111

83-
A use case where this comes up is with mocking libraries, where the mock object should be assignable
84-
to any type:
112+
A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should
113+
be assignable to (almost) any type:
85114

86115
```py
87116
from unittest.mock import MagicMock

crates/red_knot_python_semantic/src/semantic_index/builder.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use ruff_db::source::{source_text, SourceText};
1010
use ruff_index::IndexVec;
1111
use ruff_python_ast::name::Name;
1212
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
13-
use ruff_python_ast::{self as ast, PythonVersion};
13+
use ruff_python_ast::{self as ast, PySourceType, PythonVersion};
1414
use ruff_python_parser::semantic_errors::{
1515
SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError,
1616
};
@@ -75,6 +75,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
7575
// Builder state
7676
db: &'db dyn Db,
7777
file: File,
78+
source_type: PySourceType,
7879
module: &'db ParsedModule,
7980
scope_stack: Vec<ScopeInfo>,
8081
/// The assignments we're currently visiting, with
@@ -118,6 +119,7 @@ impl<'db> SemanticIndexBuilder<'db> {
118119
let mut builder = Self {
119120
db,
120121
file,
122+
source_type: file.source_type(db.upcast()),
121123
module: parsed,
122124
scope_stack: Vec::new(),
123125
current_assignments: vec![],
@@ -445,7 +447,7 @@ impl<'db> SemanticIndexBuilder<'db> {
445447
#[allow(unsafe_code)]
446448
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
447449
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
448-
let category = kind.category(self.file.is_stub(self.db.upcast()));
450+
let category = kind.category(self.source_type.is_stub());
449451
let is_reexported = kind.is_reexported();
450452

451453
let definition = Definition::new(

crates/red_knot_python_semantic/src/types.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,12 @@ impl<'db> Type<'db> {
14651465
self_callable.is_assignable_to(db, target_callable)
14661466
}
14671467

1468+
(Type::NominalInstance(instance), Type::Callable(_))
1469+
if instance.class().is_subclass_of_any_or_unknown(db) =>
1470+
{
1471+
true
1472+
}
1473+
14681474
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
14691475
let call_symbol = self.member(db, "__call__").symbol;
14701476
match call_symbol {

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,16 @@ impl<'db> ClassType<'db> {
249249
class_literal.is_final(db)
250250
}
251251

252+
/// Is this class a subclass of `Any` or `Unknown`?
253+
pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool {
254+
self.iter_mro(db).any(|base| {
255+
matches!(
256+
base,
257+
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
258+
)
259+
})
260+
}
261+
252262
/// If `self` and `other` are generic aliases of the same generic class, returns their
253263
/// corresponding specializations.
254264
fn compatible_specializations(
@@ -328,13 +338,7 @@ impl<'db> ClassType<'db> {
328338
}
329339
}
330340

331-
if self.iter_mro(db).any(|base| {
332-
matches!(
333-
base,
334-
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
335-
)
336-
}) && !other.is_final(db)
337-
{
341+
if self.is_subclass_of_any_or_unknown(db) && !other.is_final(db) {
338342
return true;
339343
}
340344

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
f65bdc1acde54fda93c802459280da74518d2eef
1+
eec809d049d10a5ae9b88780eab15fe36a9768d7

crates/red_knot_vendored/vendor/typeshed/stdlib/_frozen_importlib_external.pyi

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ def spec_from_file_location(
3636
loader: LoaderProtocol | None = None,
3737
submodule_search_locations: list[str] | None = ...,
3838
) -> importlib.machinery.ModuleSpec | None: ...
39-
39+
@deprecated(
40+
"Deprecated as of Python 3.6: Use site configuration instead. "
41+
"Future versions of Python may not enable this finder by default."
42+
)
4043
class WindowsRegistryFinder(importlib.abc.MetaPathFinder):
4144
if sys.version_info < (3, 12):
4245
@classmethod
@@ -118,6 +121,13 @@ class FileLoader:
118121
class SourceFileLoader(importlib.abc.FileLoader, FileLoader, importlib.abc.SourceLoader, SourceLoader): # type: ignore[misc] # incompatible method arguments in base classes
119122
def set_data(self, path: str, data: ReadableBuffer, *, _mode: int = 0o666) -> None: ...
120123
def path_stats(self, path: str) -> Mapping[str, Any]: ...
124+
def source_to_code( # type: ignore[override] # incompatible with InspectLoader.source_to_code
125+
self,
126+
data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive,
127+
path: ReadableBuffer | StrPath,
128+
*,
129+
_optimize: int = -1,
130+
) -> types.CodeType: ...
121131

122132
class SourcelessFileLoader(importlib.abc.FileLoader, FileLoader, _LoaderBasics):
123133
def get_code(self, fullname: str) -> types.CodeType | None: ...

crates/red_knot_vendored/vendor/typeshed/stdlib/ast.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,8 +1893,12 @@ if sys.version_info >= (3, 14):
18931893
def compare(left: AST, right: AST, /, *, compare_attributes: bool = False) -> bool: ...
18941894

18951895
class NodeVisitor:
1896+
# All visit methods below can be overwritten by subclasses and return an
1897+
# arbitrary value, which is passed to the caller.
18961898
def visit(self, node: AST) -> Any: ...
18971899
def generic_visit(self, node: AST) -> Any: ...
1900+
# The following visit methods are not defined on NodeVisitor, but can
1901+
# be implemented by subclasses and are called during a visit if defined.
18981902
def visit_Module(self, node: Module) -> Any: ...
18991903
def visit_Interactive(self, node: Interactive) -> Any: ...
19001904
def visit_Expression(self, node: Expression) -> Any: ...

crates/red_knot_vendored/vendor/typeshed/stdlib/contextlib.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class AsyncExitStack(_BaseExitStack[_ExitT_co], metaclass=abc.ABCMeta):
179179
async def __aenter__(self) -> Self: ...
180180
async def __aexit__(
181181
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
182-
) -> bool: ...
182+
) -> _ExitT_co: ...
183183

184184
if sys.version_info >= (3, 10):
185185
class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]):

crates/red_knot_vendored/vendor/typeshed/stdlib/curses/__init__.pyi

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ COLOR_PAIRS: int
2323

2424
def wrapper(func: Callable[Concatenate[window, _P], _T], /, *arg: _P.args, **kwds: _P.kwargs) -> _T: ...
2525

26-
# typeshed used the name _CursesWindow for the underlying C class before
27-
# it was mapped to the name 'window' in 3.8.
28-
# Kept here as a legacy alias in case any third-party code is relying on it.
29-
_CursesWindow = window
30-
3126
# At runtime this class is unexposed and calls itself curses.ncurses_version.
3227
# That name would conflict with the actual curses.ncurses_version, which is
3328
# an instance of this class.

crates/red_knot_vendored/vendor/typeshed/stdlib/email/_header_value_parser.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ TOKEN_ENDS: Final[set[str]]
1717
ASPECIALS: Final[set[str]]
1818
ATTRIBUTE_ENDS: Final[set[str]]
1919
EXTENDED_ATTRIBUTE_ENDS: Final[set[str]]
20-
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
20+
# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5
2121
NLSET: Final[set[str]]
22-
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
22+
# Added in Python 3.9.20, 3.10.15, 3.11.10, 3.12.5
2323
SPECIALSNL: Final[set[str]]
2424

2525
if sys.version_info >= (3, 10):

0 commit comments

Comments
 (0)