Compare commits

...

72 Commits

Author SHA1 Message Date
Dylan
f7b48510b5 Bump 0.11.7 (#17613) 2025-04-24 13:06:38 -05:00
Dhruv Manilawala
9937064761 [red-knot] Use iterative approach to collect overloads (#17607)
## Summary

This PR updates the `to_overloaded` method to use an iterative approach
instead of a recursive one.

Refer to
https://github.com/astral-sh/ruff/pull/17585#discussion_r2056804587 for
context.

The main benefit here is that it avoids calling the `to_overloaded`
function in a recursive manner which is a salsa query. So, this is a bit
hand wavy but we should also see less memory used because the cache will
only contain a single entry which should be the entire overload chain.
Previously, the recursive approach would mean that each of the function
involved in an overload chain would have a cache entry. This reduce in
memory shouldn't be too much and I haven't looked at the actual data for
it.

## Test Plan

Existing test cases should pass.
2025-04-24 22:23:50 +05:30
Andrew Gallant
8d2c79276d red_knot_python_semantic: avoid Rust's screaming snake case convention in mdtest 2025-04-24 11:43:01 -04:00
Andrew Gallant
0f47810768 red_knot_python_semantic: improve diagnostics for unsupported boolean conversions
This mostly only improves things for incorrect arguments and for an
incorrect return type. It doesn't do much to improve the case where
`__bool__` isn't callable and leaves the union/other cases untouched
completely.

I picked this one because, at first glance, this _looked_ like a lower
hanging fruit. The conceptual improvement here is pretty
straight-forward: add annotations for relevant data. But it took me a
bit to figure out how to connect all of the pieces.
2025-04-24 11:43:01 -04:00
Andrew Gallant
eb1d2518c1 red_knot_python_semantic: add "return type span" helper method
This is very similar to querying for the span of a parameter
in a function definition, but instead we look for the span of
a return type.
2025-04-24 11:43:01 -04:00
Andrew Gallant
a45a0a92bd red_knot_python_semantic: move parameter span helper method
I wanted to use this method in other places, so I moved it
to what appears to be a God-type. I also made it slightly
more versatile: callers can ask for the entire parameter list
by omitting a specific parameter index.
2025-04-24 11:43:01 -04:00
Andrew Gallant
43bd043755 ruff_db: add a From impl for FileRange to Span
These types are almost equivalent. The only difference
is that a `Span`'s range is optional.
2025-04-24 11:43:01 -04:00
Andrew Gallant
9a54ee3a1c red_knot_python_semantic: add snapshot tests for unsupported boolean conversions
This just captures the status quo before we try to improve them.
2025-04-24 11:43:01 -04:00
Carl Meyer
25c3be51d2 [red-knot] simplify != narrowing (#17610)
## Summary

Follow-up from review comment in
https://github.com/astral-sh/ruff/pull/17567#discussion_r2058649527

## Test Plan

Existing tests.
2025-04-24 15:11:45 +00:00
Matthew Mckee
e71f3ed2c5 [red-knot] Update == and != narrowing (#17567)
## Summary

Historically we have avoided narrowing on `==` tests because in many
cases it's unsound, since subclasses of a type could compare equal to
who-knows-what. But there are a lot of types (literals and unions of
them, as well as some known instances like `None` -- single-valued
types) whose `__eq__` behavior we know, and which we can safely narrow
away based on equality comparisons.

This PR implements equality narrowing in the cases where it is sound.
The most elegant way to do this (and the way that is most in-line with
our approach up until now) would be to introduce new Type variants
`NeverEqualTo[...]` and `AlwaysEqualTo[...]`, and then implement all
type relations for those variants, narrow by intersection, and let union
and intersection simplification sort it all out. This is analogous to
our existing handling for `AlwaysFalse` and `AlwaysTrue`.

But I'm reluctant to add new `Type` variants for this, mostly because
they could end up un-simplified in some types and make types even more
complex. So let's try this approach, where we handle more of the
narrowing logic as a special case.

## Test Plan

Updated and added tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-24 07:56:39 -07:00
Carl Meyer
ac6219ec38 [red-knot] fix collapsing literal and its negation to object (#17605)
## Summary

Another follow-up to the unions-of-large-literals optimization. Restore
the behavior that e.g. `Literal[""] | ~Literal[""]` collapses to
`object`.

## Test Plan

Added mdtests.
2025-04-24 13:55:05 +00:00
Alex Waygood
e93fa7062c [red-knot] Add more tests for protocols (#17603) 2025-04-24 13:11:31 +01:00
Alex Waygood
21fd28d713 [red-knot] Ban direct instantiations of Protocol classes (#17597) 2025-04-24 09:31:35 +00:00
Max Mynter
a01f25107a [pyupgrade] Preserve parenthesis when fixing native literals containing newlines (UP018) (#17220) 2025-04-24 08:48:02 +02:00
camper42
48a85c4ed4 [airflow] fix typos (AIR302, AIR312) (#17574) 2025-04-24 08:06:32 +02:00
Dhruv Manilawala
1796ca97d5 [red-knot] Special case @abstractmethod for function type (#17591)
## Summary

This is required because otherwise the inferred type is not going to be
`Type::FunctionLiteral` but a todo type because we don't recognize
`TypeVar` yet:

```py
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])

def abstractmethod(funcobj: _FuncT) -> _FuncT: ...
```

This is mainly required to raise diagnostic when only some (and not all)
`@overload`-ed functions are decorated with `@abstractmethod`.
2025-04-24 03:54:52 +05:30
Alex Waygood
e897f37911 [red-knot] Emit diagnostics for isinstance() and issubclass() calls where a non-runtime-checkable protocol is the second argument (#17561) 2025-04-23 21:40:23 +00:00
Alex Waygood
00e73dc331 [red-knot] Infer the members of a protocol class (#17556) 2025-04-23 21:36:12 +00:00
Dhruv Manilawala
7b6222700b [red-knot] Add FunctionType::to_overloaded (#17585)
## Summary

This PR adds a new method `FunctionType::to_overloaded` which converts a
`FunctionType` into an `OverloadedFunction` which contains all the
`@overload`-ed `FunctionType` and the implementation `FunctionType` if
it exists.

There's a big caveat here (it's the way overloads work) which is that
this method can only "see" all the overloads that comes _before_ itself.
Consider the following example:

```py
from typing import overload

@overload
def foo() -> None: ...
@overload
def foo(x: int) -> int: ...
def foo(x: int | None) -> int | None:
	return x
```

Here, when the `to_overloaded` method is invoked on the
1. first `foo` definition, it would only contain a single overload which
is itself and no implementation.
2. second `foo` definition, it would contain both overloads and still no
implementation
3. third `foo` definition, it would contain both overloads and the
implementation which is itself

### Usages

This method will be used in the logic for checking invalid overload
usages. It can also be used for #17541.

## Test Plan

Make sure that existing tests pass.
2025-04-24 02:57:05 +05:30
Brent Westbrook
bfc1650198 [red-knot] Add mdtests for global statement (#17563)
## Summary

This is a first step toward `global` support in red-knot (#15385). I
went through all the matches for `global` in the `mypy/test-data`
directory, but I didn't find anything too interesting that wasn't
already covered by @carljm's suggestions on Discord. I still pulled in a
couple of cases for a little extra variety. I also included a section
from the
[PLE0118](https://docs.astral.sh/ruff/rules/load-before-global-declaration/)
tests in ruff that will become syntax errors once #17463 is merged and
we handle `global` statements.

I don't think I figured out how to use `@Todo` properly, so please let
me know if I need to fix that. I hope this is a good start to the test
suite otherwise.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-23 17:18:42 -04:00
Brent Westbrook
d5410ef9fe [syntax-errors] Make duplicate parameter names a semantic error (#17131)
Status
--

This is a pretty minor change, but it was breaking a red-knot mdtest
until #17463 landed. Now this should close #11934 as the last syntax
error being tracked there!

Summary
--

Moves `Parser::validate_parameters` to
`SemanticSyntaxChecker::duplicate_parameter_name`.

Test Plan
--

Existing tests, with `## Errors` replaced with `## Semantic Syntax
Errors`.
2025-04-23 15:45:51 -04:00
Douglas Creager
9db63fc58c [red-knot] Handle generic constructors of generic classes (#17552)
We now handle generic constructor methods on generic classes correctly:

```py
class C[T]:
    def __init__[S](self, t: T, s: S): ...

x = C(1, "str")
```

Here, constructing `C` requires us to infer a specialization for the
generic contexts of `C` and `__init__` at the same time.

At first I thought I would need to track the full stack of nested
generic contexts here (since the `[S]` context is nested within the
`[T]` context). But I think this is the only way that we might need to
specialize more than one generic context at once — in all other cases, a
containing generic context must be specialized before we get to a nested
one, and so we can just special-case this.

While we're here, we also construct the generic context for a generic
function lazily, when its signature is accessed, instead of eagerly when
inferring the function body.
2025-04-23 15:06:18 -04:00
David Peter
61e73481fe [red-knot] Assignability of class instances to Callable (#17590)
## Summary

Model assignability of class instances with a `__call__` method to
`Callable` types. This should solve some false positives related to
`functools.partial` (yes, 1098 fewer diagnostics!).

Reference:
https://github.com/astral-sh/ruff/issues/17343#issuecomment-2824618483

## Test Plan

New Markdown tests.
2025-04-23 20:34:13 +02:00
David Peter
e170fe493d [red-knot] Trust all symbols in stub files (#17588)
## Summary

*Generally* trust undeclared symbols in stubs, not just at the module
level.

Follow-up on the discussion
[here](https://github.com/astral-sh/ruff/pull/17577#discussion_r2055945909).

## Test Plan

New Markdown test.
2025-04-23 20:07:29 +02:00
David Peter
e91e2f49db [red-knot] Trust module-level undeclared symbols in stubs (#17577)
## Summary

Many symbols in typeshed are defined without being declared. For
example:
```pyi
# builtins:
IOError = OSError

# types
LambdaType = FunctionType
NotImplementedType = _NotImplementedType

# typing
Text = str

# random
uniform = _inst.uniform

# optparse
make_option = Option

# all over the place:
_T = TypeVar("_T")
```

Here, we introduce a change that skips widening the public type of these
symbols (by unioning with `Unknown`).

fixes #17032

## Ecosystem analysis

This is difficult to analyze in detail, but I went over most changes and
it looks very favorable to me overall. The diff on the overall numbers
is:
```
errors: 1287 -> 859 (reduction by 428)
warnings: 45 -> 59 (increase by 14)
```

### Removed false positives

`invalid-base` examples:

```diff
- error[lint:invalid-base] /tmp/mypy_primer/projects/pip/src/pip/_vendor/rich/console.py:548:27: Invalid class base with type `Unknown | Literal[_local]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
- error[lint:invalid-base] /tmp/mypy_primer/projects/tornado/tornado/iostream.py:84:25: Invalid class base with type `Unknown | Literal[OSError]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
- error[lint:invalid-base] /tmp/mypy_primer/projects/mitmproxy/test/conftest.py:35:40: Invalid class base with type `Unknown | Literal[_UnixDefaultEventLoopPolicy]` (all bases must be a class, `Any`, `Unknown` or `Todo`)
```

`invalid-exception-caught` examples:

```diff
- error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/cloud-init/cloudinit/cmd/status.py:334:16: Cannot catch object of type `Literal[ProcessExecutionError]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)
- error[lint:invalid-exception-caught] /tmp/mypy_primer/projects/jinja/src/jinja2/loaders.py:537:16: Cannot catch object of type `Literal[TemplateNotFound]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)
```

`unresolved-reference` examples


7a0265d36e/cloudinit/handlers/jinja_template.py (L120-L123)
(we now understand the `isinstance` narrowing)

```diff
- error[lint:unresolved-attribute] /tmp/mypy_primer/projects/cloud-init/cloudinit/handlers/jinja_template.py:123:16: Type `Exception` has no attribute `errno`
```

`unknown-argument` examples


https://github.com/hauntsaninja/boostedblob/blob/master/boostedblob/request.py#L53

```diff
- error[lint:unknown-argument] /tmp/mypy_primer/projects/boostedblob/boostedblob/request.py:53:17: Argument `connect` does not match any known parameter of bound method `__init__`
```

`unknown-argument`

There are a lot of `__init__`-related changes because we now understand
[`@attr.s`](3d42a6978a/src/attr/__init__.pyi (L387))
as a `@dataclass_transform` annotated symbol. For example:

```diff
- error[lint:unknown-argument] /tmp/mypy_primer/projects/attrs/tests/test_hooks.py:72:18: Argument `x` does not match any known parameter of bound method `__init__`
```

### New false positives

This can happen if a symbol that previously was inferred as `X |
Unknown` was assigned-to, but we don't yet understand the assignability
to `X`:


https://github.com/strawberry-graphql/strawberry/blob/main/strawberry/exceptions/handler.py#L90

```diff
+ error[lint:invalid-assignment] /tmp/mypy_primer/projects/strawberry/strawberry/exceptions/handler.py:90:9: Object of type `def strawberry_threading_exception_handler(args: tuple[type[BaseException], BaseException | None, TracebackType | None, Thread | None]) -> None` is not assignable to attribute `excepthook` of type `(_ExceptHookArgs, /) -> Any`
```

### New true positives


6bbb5519fe/tests/tracer/test_span.py (L714)

```diff
+ error[lint:invalid-argument-type] /tmp/mypy_primer/projects/dd-trace-py/tests/tracer/test_span.py:714:33: Argument to this function is incorrect: Expected `str`, found `Literal[b"\xf0\x9f\xa4\x94"]`
```

### Changed diagnostics

A lot of changed diagnostics because we now show `@Todo(Support for
`typing.TypeVar` instances in type expressions)` instead of `Unknown`
for all kinds of symbols that used a `_T = TypeVar("_T")` as a type. One
prominent example is the `list.__getitem__` method:

`builtins.pyi`:
```pyi
_T = TypeVar("_T")  # previously `TypeVar | Unknown`, now just `TypeVar`

# …

class list(MutableSequence[_T]):
    # …
    @overload
    def __getitem__(self, i: SupportsIndex, /) -> _T: ...
    # …
```

which causes this change in diagnostics:
```py
xs = [1, 2]
reveal_type(xs[0])  # previously `Unknown`, now `@Todo(Support for `typing.TypeVar` instances in type expressions)`
```

## Test Plan

Updated Markdown tests
2025-04-23 19:31:14 +02:00
Wei Lee
b537552927 [airflow] Apply auto fixes to cases where the names have changed in Airflow 3 (AIR301) (#17355)
## Summary

Apply auto fixes to cases where the names have changed in Airflow 3

## Test Plan

Add `AIR301_names_fix.py` and `AIR301_provider_names_fix.py` test fixtures
2025-04-23 12:43:41 -04:00
Navdeep K
5a719f2d60 [pycodestyle] Auto-fix redundant boolean comparison (E712) (#17090)
This pull request fixes https://github.com/astral-sh/ruff/issues/17014

changes this
```python
from __future__ import annotations

flag1 = True
flag2 = True

if flag1 == True or flag2 == True:
    pass

if flag1 == False and flag2 == False:
    pass

flag3 = True
if flag1 == flag3 and (flag2 == False or flag3 == True):  # Should become: if flag1==flag3 and (not flag2 or flag3)
    pass

if flag1 == True and (flag2 == False or not flag3 == True):  # Should become: if flag1 and (not flag2 or not flag3)
    pass

if flag1 != True and (flag2 != False or not flag3 == True):  # Should become: if not flag1 and (flag2 or not flag3)
    pass


flag = True
while flag == True:  # Should become: while flag
    flag = False

flag = True
x = 5
if flag == True and x > 0:  # Should become: if flag and x > 0
    print("ok")

flag = True
result = "yes" if flag == True else "no"  # Should become: result = "yes" if flag else "no"

x = flag == True < 5

x = (flag == True) == False < 5
```

to this 
```python
from __future__ import annotations

flag1 = True
flag2 = True

if flag1 or flag2:
    pass

if not flag1 and not flag2:
    pass

flag3 = True
if flag1 == flag3 and (not flag2 or flag3):  # Should become: if flag1 == flag3 and (not flag2 or flag3)
    pass

if flag1 and (not flag2 or not flag3):  # Should become: if flag1 and (not flag2 or not flag3)
    pass

if not flag1 and (flag2 or not flag3):  # Should become: if not flag1 and (flag2 or not flag3)
    pass


flag = True
while flag:  # Should become: while flag
    flag = False

flag = True
x = 5
if flag and x > 0:  # Should become: if flag and x > 0
    print("ok")

flag = True
result = "yes" if flag else "no"  # Should become: result = "yes" if flag else "no"

x = flag is True < 5

x = (flag) is False < 5
```

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-04-23 11:49:20 -04:00
Brent Westbrook
e7f38fe74b [red-knot] Detect semantic syntax errors (#17463)
Summary
--

This PR extends semantic syntax error detection to red-knot. The main
changes here are:

1. Adding `SemanticSyntaxChecker` and `Vec<SemanticSyntaxError>` fields
to the `SemanticIndexBuilder`
2. Calling `SemanticSyntaxChecker::visit_stmt` and `visit_expr` in the
`SemanticIndexBuilder`'s `visit_stmt` and `visit_expr` methods
3. Implementing `SemanticSyntaxContext` for `SemanticIndexBuilder`
4. Adding new mdtests to test the context implementation and show
diagnostics

(3) is definitely the trickiest and required (I think) a minor addition
to the `SemanticIndexBuilder`. I tried to look around for existing code
performing the necessary checks, but I definitely could have missed
something or misused the existing code even when I found it.

There's still one TODO around `global` statement handling. I don't think
there's an existing way to look this up, but I'm happy to work on that
here or in a separate PR. This currently only affects detection of one
error (`LoadBeforeGlobalDeclaration` or
[PLE0118](https://docs.astral.sh/ruff/rules/load-before-global-declaration/)
in ruff), so it's not too big of a problem even if we leave the TODO.

Test Plan
--

New mdtests, as well as new errors for existing mdtests

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-23 09:52:58 -04:00
Micha Reiser
624f5c6c22 Fix stale diagnostics in Ruff playground (#17583) 2025-04-23 15:47:54 +02:00
Micha Reiser
8abf93f5fb [red-knot] Early return from project.is_file_open for vendored files (#17580) 2025-04-23 15:32:41 +02:00
Micha Reiser
5407249467 [red-knot] Make BoundMethodType a salsa interned (#17581) 2025-04-23 15:11:20 +02:00
Alex Waygood
0a1f9d090e [red-knot] Emit a diagnostic if a non-protocol is passed to get_protocol_members (#17551) 2025-04-23 10:13:20 +00:00
Alex Waygood
f9c7908bb7 [red-knot] Add more tests for protocol members (#17550) 2025-04-23 11:03:52 +01:00
David Peter
99fa850e53 [red-knot] Assignability for subclasses of Any and Unknown (#17557)
## Summary

Allow (instances of) subclasses of `Any` and `Unknown` to be assignable
to (instances of) other classes, unless they are final. This allows us
to get rid of ~1000 false positives, mostly when mock-objects like
`unittest.mock.MagicMock` are assigned to various targets.

## Test Plan

Adapted and new Markdown tests.
2025-04-23 11:37:30 +02:00
David Peter
a241321735 [red-knot] mypy_primer: add strawberry, print compilation errors to stderr (#17578)
## Summary

mypy_primer changes included here:
ebaa9fd27b..4c22d192a4

- Add strawberry as a `good.txt` project (was previously included in our
fork)
- Print Red Knot compilation errors to stderr (thanks @MichaReiser)
2025-04-23 10:57:11 +02:00
David Peter
b1b8ca3bcd [red-knot] GenericAlias instances as a base class (#17575)
## Summary

We currently emit a diagnostic for code like the following:
```py
from typing import Any

# error: Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)
class C(tuple[Any, ...]): ...
```

The changeset here silences this diagnostic by recognizing instances of
`GenericAlias` in `ClassBase::try_from_type`, and inferring a `@Todo`
type for them. This is a change in preparation for #17557, because `C`
previously had `Unknown` in its MRO …
```py
reveal_type(C.__mro__)  # tuple[Literal[C], Unknown, Literal[object]]
```
… which would cause us to think that `C` is assignable to everything.

The changeset also removes some false positive `invalid-base`
diagnostics across the ecosystem.

## Test Plan

Updated Markdown tests.
2025-04-23 10:39:10 +02:00
Shaygan Hooshyari
3fae176345 Remove redundant type_to_visitor_function entries (#17564) 2025-04-23 09:27:00 +02:00
David Salvisberg
f36262d970 Fixes how the checker visits typing.cast/typing.NewType arguments (#17538) 2025-04-23 09:26:00 +02:00
Matthew Mckee
e45f23b0ec [red-knot] Class literal __new__ function callable subtyping (#17533)
## Summary

From
https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable

this covers step 2 and partially step 3 (always respecting the
`__new__`)

## Test Plan

Update is_subtype_of.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-22 22:40:33 -07:00
Matthew Mckee
aa46047649 [red-knot] Surround intersections with () in potentially ambiguous contexts (#17568)
## Summary

Add parentheses to multi-element intersections, when displayed in a
context that's otherwise potentially ambiguous.

## Test Plan

Update mdtest files

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-23 04:18:20 +00:00
Brent Westbrook
f9da115fdc [minor] Delete outdated TODO comment (#17565)
Summary
--

Delete a TODO I left that was handled in the last minor release
(#16125).

Test Plan
--

N/a
2025-04-22 20:23:08 +00:00
Carl Meyer
3872d57463 [red-knot] add regression test for fixed cycle panic (#17535)
Add a regression test for the cycle documented in
https://github.com/astral-sh/ruff/issues/14767, which no longer panics
(or even causes a cycle at all.)

Fixes https://github.com/astral-sh/ruff/issues/14767
2025-04-22 09:20:53 -07:00
Carl Meyer
27ada26ddb [red-knot] fix unions of literals, again (#17534)
## Summary

#17451 was incomplete. `AlwaysFalsy` and `AlwaysTruthy` are not the only
two types that are super-types of some literals (of a given kind) and
not others. That set also includes intersections containing
`AlwaysTruthy` or `AlwaysFalsy`, and intersections containing literal
types of the same kind. Cover these cases as well.

Fixes #17478.

## Test Plan

Added mdtests.

`QUICKCHECK_TESTS=1000000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable` failed on both
`all_fully_static_type_pairs_are_subtypes_of_their_union` and
`all_type_pairs_are_assignable_to_their_union` prior to this PR, passes
after it.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-04-22 16:12:52 +00:00
Andrew Gallant
810478f68b red_knot_python_semantic: remove last vestige of old diagnostics! 2025-04-22 12:08:03 -04:00
Andrew Gallant
17f799424a red_knot_python_semantic: migrate types to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
c12640fea8 red_knot_python_semantic: migrate types/diagnostic to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
3796b13ea2 red_knot_python_semantic: migrate types/call/bind to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
ad5a659f29 red_knot_python_semantic: migrate types/string_annotation to new diagnostics 2025-04-22 12:08:03 -04:00
Andrew Gallant
27a377f077 red_knot_python_semantic: migrate types/infer to new diagnostic model
I gave up trying to do this one lint at a time and just (mostly)
mechanically translated this entire file in one go.

Generally the messages stay the same (with most moving from an
annotation message to the diagnostic's main message). I added a couple
of `info` sub-diagnostics where it seemed to be the obvious intent.
2025-04-22 12:08:03 -04:00
Andrew Gallant
b8b624d890 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for inference
This finishes the migration for the `INVALID_ASSIGNMENT` lint.

Notice how I'm steadily losing steam in terms of actually improving the
diagnostics. This change is more mechanical, because taking the time to
revamp every diagnostic is a ton of effort. Probably future migrations
will be similar unless there are easy pickings.
2025-04-22 12:08:03 -04:00
Andrew Gallant
6dc2d29966 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for shadowing
We mostly keep things the same here, but the message has been moved from
the annotation to the diagnostic's top-line message. I think this is
perhaps a little worse, but some bigger improvements could be made here.
Indeed, we could perhaps even add a "fix" here.
2025-04-22 12:08:03 -04:00
Andrew Gallant
890ba725d9 red_knot_python_semantic: migrate INVALID_ASSIGNMENT for unpacking
This moves all INVALID_ASSIGNMENT lints related to unpacking over to the new
diagnostic model.

While we're here, we improve the diagnostic a bit by adding a secondary
annotation covering where the value is. We also split apart the original
singular message into one message for the diagnostic and the "expected
versus got" into annotation messages.
2025-04-22 12:08:03 -04:00
Andrew Gallant
298f43f34e red_knot_python_semantic: add invalid assignment diagnostic snapshot
This tests the diagnostic rendering of a case that wasn't previously
covered by snapshots: when unpacking fails because there are too few
values, but where the left hand side can tolerate "N or more." In the
code, this is a distinct diagnostic, so we capture it here.

(Sorry about the diff here, but it made sense to rename the other
sections and that changes the name of the snapshot file.)
2025-04-22 12:08:03 -04:00
Andrew Gallant
3b300559ab red_knot_python_semantic: remove #[must_use] on diagnostic guard constructor
I believe this was an artifact of an older iteration of the diagnostic
reporting API. But this is strictly not necessary now, and indeed, might
even be annoying. It is okay, but perhaps looks a little odd, to do
`builder.into_diagnostic("...")` if you don't want to add anything else
to the diagnostic.
2025-04-22 12:08:03 -04:00
Andrew Gallant
14f71ceb83 red_knot_python_semantic: add helper method for creating a secondary annotation
I suspect this will be used pretty frequently (I wanted it
immediately). And more practically, this avoids needing to
import `Annotation` to create it.
2025-04-22 12:08:03 -04:00
David Peter
4775719abf [red-knot] mypy_primer: larger depot runner (#17547)
## Summary

A switch from 16 to 32 cores reduces the `mypy_primer` CI time from
3.5-4 min to 2.5-3 min. There's also a 64-core runner, but the 4 min ->
3 min change when doubling the cores once does suggest that it doesn't
parallelize *this* well.
2025-04-22 17:36:13 +02:00
Alex Waygood
6bdffc3cbf [red-knot] Consider two instance types disjoint if the underlying classes have disjoint metaclasses (#17545) 2025-04-22 15:14:10 +01:00
Aria Desires
775815ef22 Update cargo-dist and apply config improvements (#17453) 2025-04-22 10:05:15 -04:00
Carl Meyer
0299a52fb1 [red-knot] Add list of failing/slow ecosystem projects (#17474)
## Summary

I ran red-knot on every project in mypy-primer. I moved every project
where red-knot ran to completion (fast enough, and mypy-primer could
handle its output) into `good.txt`, so it will run in our CI.

The remaining projects I left listed in `bad.txt`, with a comment
summarizing the failure mode (a few don't fail, they are just slow -- on
a debug build, at least -- or output too many diagnostics for
mypy-primer to handle.)

We will now run CI on 109 projects; 34 are left in `bad.txt`.

## Test Plan

CI on this PR!

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-04-22 14:15:36 +02:00
David Peter
83d5ad8983 [red-knot] mypy_primer: extend ecosystem checks (#17544)
## Summary

Takes the `good.txt` changes from #17474, and removes the following
projects:
- arrow (not part of mypy_primer upstream)
- freqtrade, hydpy, ibis, pandera, xarray (saw panics locally, all
related to try_metaclass cycles)

Increases the mypy_primer CI run time to ~4 min.

## Test Plan

Three successful CI runs.
2025-04-22 13:39:42 +02:00
Alex Waygood
ae6fde152c [red-knot] Move InstanceType to its own submodule (#17525) 2025-04-22 11:34:46 +00:00
David Peter
d2b20f7367 [red-knot] mypy_primer: capture backtraces (#17543)
## Summary

`mypy_primer` is not deterministic (we pin `mypy_primer` itself, but
projects change over time and we just pull in the latest version). We've
also seen occasional panics being caught in `mypy_primer` runs, so this
is trying to make these CI failures more helpful.
2025-04-22 12:05:57 +02:00
David Peter
38a3b056e3 [red-knot] mypy_primer: Use upstream repo (#17500)
## Summary

Switch to the official version of
[`mypy_primer`](https://github.com/hauntsaninja/mypy_primer), now that
Red Knot support has been upstreamed (see
https://github.com/hauntsaninja/mypy_primer/pull/138,
https://github.com/hauntsaninja/mypy_primer/pull/135,
https://github.com/hauntsaninja/mypy_primer/pull/151,
https://github.com/hauntsaninja/mypy_primer/pull/155).

## Test Plan

Locally and in CI
2025-04-22 11:55:16 +02:00
David Peter
37a0836bd2 [red-knot] typing.dataclass_transform (#17445)
## Summary

* Add initial support for `typing.dataclass_transform`
* Support decorating a function decorator with `@dataclass_transform(…)`
(used by `attrs`, `strawberry`)
* Support decorating a metaclass with `@dataclass_transform(…)` (used by
`pydantic`, but doesn't work yet, because we don't seem to model
`__new__` calls correctly?)
* *No* support yet for decorating base classes with
`@dataclass_transform(…)`. I haven't figured out how this even supposed
to work. And haven't seen it being used.
* Add `strawberry` as an ecosystem project, as it makes heavy use of
`@dataclass_transform`

## Test Plan

New Markdown tests
2025-04-22 10:33:02 +02:00
renovate[bot]
f83295fe51 Update dependency react-resizable-panels to v2.1.8 (#17513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:30:07 +02:00
renovate[bot]
c4581788b2 Update dependency smol-toml to v1.3.3 (#17505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:19:55 +02:00
renovate[bot]
2894aaa943 Update dependency uuid to v11.1.0 (#17517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:18:54 +02:00
renovate[bot]
ed4866a00b Update actions/setup-node action to v4.4.0 (#17514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-22 09:18:13 +02:00
Matthew Mckee
9b5fe51b32 [red-knot] Fix variable name (#17532) 2025-04-21 17:20:04 -07:00
Matthew Mckee
53ffe7143f [red-knot] Add basic subtyping between class literal and callable (#17469)
## Summary

This covers step 1 from
https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable

Part of #17343

## Test Plan

Update is_subtype_of.md and is_assignable_to.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-04-21 22:29:36 +00:00
Hans
21561000b1 [pyupgrade] Add fix safety section to docs (UP030) (#17443)
## Summary

add fix safety section to format_literals, for #15584
2025-04-21 14:14:58 -04:00
w0nder1ng
9c0772d8f0 [perflint] Allow list function calls to be replaced with a comprehension (PERF401) (#17519)
This is an implementation of the discussion from #16719. 

This change will allow list function calls to be replaced with
comprehensions:

```python
result = list()
for i in range(3):
    result.append(i + 1)
# becomes
result = [i + 1 for i in range(3)]
```

I added a new test to `PERF401.py` to verify that this fix will now work
for `list()`.
2025-04-21 13:29:24 -04:00
199 changed files with 7392 additions and 3139 deletions

6
.gitattributes vendored
View File

@@ -12,6 +12,12 @@ crates/ruff_python_parser/resources/invalid/re_lexing/line_continuation_windows_
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_windows_eol.py text eol=crlf
crates/ruff_python_parser/resources/invalid/re_lex_logical_token_mac_eol.py text eol=cr
crates/ruff_linter/resources/test/fixtures/ruff/RUF046_CR.py text eol=cr
crates/ruff_linter/resources/test/fixtures/ruff/RUF046_LF.py text eol=lf
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_CR.py text eol=cr
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018_LF.py text eol=lf
crates/ruff_python_parser/resources/inline linguist-generated=true
ruff.schema.json -diff linguist-generated=true text=auto eol=lf

View File

@@ -6,5 +6,6 @@ self-hosted-runner:
labels:
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- depot-ubuntu-22.04-32
- github-windows-2025-x86_64-8
- github-windows-2025-x86_64-16

View File

@@ -346,7 +346,7 @@ jobs:
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: "npm"
@@ -821,7 +821,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"

View File

@@ -21,11 +21,12 @@ env:
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: depot-ubuntu-22.04-16
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -45,7 +46,7 @@ jobs:
- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5"
uv tool install "git+https://github.com/hauntsaninja/mypy_primer@4c22d192a456e27badf85b3ea0f830707375d2b7"
- name: Run mypy_primer
shell: bash

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"

View File

@@ -45,7 +45,7 @@ jobs:
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -40,6 +40,7 @@ permissions:
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
workflow_dispatch:
inputs:
tag:
@@ -60,7 +61,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
@@ -68,9 +69,9 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4-prerelease.1/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
@@ -86,7 +87,7 @@ jobs:
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
@@ -123,7 +124,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
@@ -153,7 +154,7 @@ jobs:
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
name: artifacts-build-global
path: |
@@ -174,7 +175,7 @@ jobs:
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive
@@ -200,7 +201,7 @@ jobs:
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
@@ -250,7 +251,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
with:
persist-credentials: false
submodules: recursive

View File

@@ -1,5 +1,30 @@
# Changelog
## 0.11.7
### Preview features
- \[`airflow`\] Apply auto fixes to cases where the names have changed in Airflow 3 (`AIR301`) ([#17355](https://github.com/astral-sh/ruff/pull/17355))
- \[`perflint`\] Implement fix for `manual-dict-comprehension` (`PERF403`) ([#16719](https://github.com/astral-sh/ruff/pull/16719))
- [syntax-errors] Make duplicate parameter names a semantic error ([#17131](https://github.com/astral-sh/ruff/pull/17131))
### Bug fixes
- \[`airflow`\] Fix typos in provider package names (`AIR302`, `AIR312`) ([#17574](https://github.com/astral-sh/ruff/pull/17574))
- \[`flake8-type-checking`\] Visit keyword arguments in checks involving `typing.cast`/`typing.NewType` arguments ([#17538](https://github.com/astral-sh/ruff/pull/17538))
- \[`pyupgrade`\] Preserve parenthesis when fixing native literals containing newlines (`UP018`) ([#17220](https://github.com/astral-sh/ruff/pull/17220))
- \[`refurb`\] Mark the `FURB161` fix unsafe except for integers and booleans ([#17240](https://github.com/astral-sh/ruff/pull/17240))
### Rule changes
- \[`perflint`\] Allow list function calls to be replaced with a comprehension (`PERF401`) ([#17519](https://github.com/astral-sh/ruff/pull/17519))
- \[`pycodestyle`\] Auto-fix redundant boolean comparison (`E712`) ([#17090](https://github.com/astral-sh/ruff/pull/17090))
- \[`pylint`\] make fix unsafe if delete comments (`PLR1730`) ([#17459](https://github.com/astral-sh/ruff/pull/17459))
### Documentation
- Add fix safety sections to docs for several rules ([#17410](https://github.com/astral-sh/ruff/pull/17410),[#17440](https://github.com/astral-sh/ruff/pull/17440),[#17441](https://github.com/astral-sh/ruff/pull/17441),[#17443](https://github.com/astral-sh/ruff/pull/17443),[#17444](https://github.com/astral-sh/ruff/pull/17444))
## 0.11.6
### Preview features

6
Cargo.lock generated
View File

@@ -2772,7 +2772,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.11.6"
version = "0.11.7"
dependencies = [
"anyhow",
"argfile",
@@ -3007,7 +3007,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.11.6"
version = "0.11.7"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3333,7 +3333,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.11.6"
version = "0.11.7"
dependencies = [
"console_error_panic_hook",
"console_log",

View File

@@ -231,6 +231,10 @@ unused_peekable = "warn"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
# Salsa generates functions with parameters for each field of a `salsa::interned` struct.
# If we don't allow this, we get warnings for structs with too many fields.
too_many_arguments = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
@@ -272,7 +276,9 @@ inherits = "release"
# Config for 'dist'
[workspace.metadata.dist]
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.28.4-prerelease.1"
cargo-dist-version = "0.28.4"
# Make distability of apps opt-in instead of opt-out
dist = false
# CI backends to support
ci = "github"
# The installers to generate for each app
@@ -306,7 +312,7 @@ auto-includes = false
# Whether dist should create a Github Release or use an existing draft
create-release = true
# Which actions to run on pull requests
pr-run-mode = "skip"
pr-run-mode = "plan"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Which phase dist should use to create the GitHub release
@@ -334,7 +340,7 @@ install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
global = "depot-ubuntu-latest-4"
[workspace.metadata.dist.github-action-commits]
"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4
"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2
"actions/checkout" = "85e6279cec87321a52edac9c87bce653a07cf6c2" # v4
"actions/upload-artifact" = "6027e3dd177782cd8ab9af838c04fd81a07f1d47" # v4.6.2
"actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3

View File

@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.11.6/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.6/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.11.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.11.7/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.6
rev: v0.11.7
hooks:
# Run the linter.
- id: ruff

View File

@@ -2,16 +2,16 @@
## Basics
For now, we use our own [fork of mypy primer]. It can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
`mypy_primer` can be run using `uvx --from "…" mypy_primer`. For example, to see the help message, run:
```sh
uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer -h
uvx --from "git+https://github.com/hauntsaninja/mypy_primer" mypy_primer -h
```
Alternatively, you can install the forked version of `mypy_primer` using:
```sh
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
uv tool install "git+https://github.com/hauntsaninja/mypy_primer"
```
and then run it using `uvx mypy_primer` or just `mypy_primer`, if your `PATH` is set up accordingly (see: [Tool executables]).
@@ -56,6 +56,5 @@ mypy_primer --repo /path/to/ruff --old origin/main --new my/local-branch …
Note that you might need to clean up `/tmp/mypy_primer` in order for this to work correctly.
[fork of mypy primer]: https://github.com/astral-sh/mypy_primer/tree/add-red-knot-support
[full list of ecosystem projects]: https://github.com/astral-sh/mypy_primer/blob/add-red-knot-support/mypy_primer/projects.py
[full list of ecosystem projects]: https://github.com/hauntsaninja/mypy_primer/blob/master/mypy_primer/projects.py
[tool executables]: https://docs.astral.sh/uv/concepts/tools/#tool-executables

View File

@@ -32,12 +32,12 @@ fn config_override_python_version() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-attribute
error: lint:unresolved-attribute: Type `<module 'sys'>` has no attribute `last_exc`
--> <temp_dir>/test.py:5:7
|
4 | # Access `sys.last_exc` that was only added in Python 3.12
5 | print(sys.last_exc)
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
| ^^^^^^^^^^^^
|
Found 1 diagnostic
@@ -165,11 +165,11 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `utils`
--> <temp_dir>/child/test.py:2:6
|
2 | from utils import add
| ^^^^^ Cannot resolve import `utils`
| ^^^^^
3 |
4 | stat = add(10, 15)
|
@@ -265,22 +265,22 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -301,11 +301,11 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
@@ -341,33 +341,33 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/test.py:9:7
|
7 | x = a
8 |
9 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 3 diagnostics
@@ -388,22 +388,22 @@ fn cli_rule_severity() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-import
warning: lint:unresolved-import: Cannot resolve import `does_not_exit`
--> <temp_dir>/test.py:2:8
|
2 | import does_not_exit
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
| ^^^^^^^^^^^^^
3 |
4 | y = 4 / 0
|
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:4:5
|
2 | import does_not_exit
3 |
4 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
5 |
6 | for a in range(0, int(y)):
|
@@ -439,22 +439,22 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/test.py:7:7
|
5 | x = a
6 |
7 | print(x) # possibly-unresolved-reference
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -476,11 +476,11 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/test.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
@@ -555,11 +555,11 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -638,11 +638,11 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -670,11 +670,11 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
|
Found 1 diagnostic
@@ -699,20 +699,20 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -737,20 +737,20 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -775,20 +775,20 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference
warning: lint:unresolved-reference: Name `x` used when not defined
--> <temp_dir>/test.py:2:7
|
2 | print(x) # [unresolved-reference]
| ^ Name `x` used when not defined
| ^
3 | print(4[1]) # [non-subscriptable]
|
error: lint:non-subscriptable
error: lint:non-subscriptable: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
--> <temp_dir>/test.py:3:7
|
2 | print(x) # [unresolved-reference]
3 | print(4[1]) # [non-subscriptable]
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
| ^
|
Found 2 diagnostics
@@ -835,22 +835,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -877,22 +877,22 @@ fn user_configuration() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero
warning: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
3 |
4 | for a in range(0, int(y)):
|
error: lint:possibly-unresolved-reference
error: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
| ^
|
Found 2 diagnostics
@@ -935,25 +935,25 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
| ^^^^^^^^^^^^^^
|
error: lint:division-by-zero
error: lint:division-by-zero: Cannot divide object of type `Literal[4]` by zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0 # error: division-by-zero
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
| ^^^^^
|
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `main2`
--> <temp_dir>/project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
| ^^^^^
3 |
4 | print(z)
|
@@ -972,18 +972,18 @@ fn check_specific_paths() -> anyhow::Result<()> {
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `does_not_exist`
--> <temp_dir>/project/tests/test_main.py:2:8
|
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
| ^^^^^^^^^^^^^^
|
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `main2`
--> <temp_dir>/project/other.py:2:6
|
2 | from main2 import z # error: unresolved-import
| ^^^^^ Cannot resolve import `main2`
| ^^^^^
3 |
4 | print(z)
|

View File

@@ -0,0 +1,4 @@
from __future__ import annotations
def foo(a: foo()):
pass

View File

@@ -314,20 +314,23 @@ impl Project {
/// * It has a [`SystemPath`] and belongs to a package's `src` files
/// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath)
pub fn is_file_open(self, db: &dyn Db, file: File) -> bool {
let path = file.path(db);
// Try to return early to avoid adding a dependency on `open_files` or `file_set` which
// both have a durability of `LOW`.
if path.is_vendored_path() {
return false;
}
if let Some(open_files) = self.open_files(db) {
open_files.contains(&file)
} else if file.path(db).is_system_path() {
self.contains_file(db, file)
self.files(db).contains(&file)
} else {
file.path(db).is_system_virtual_path()
}
}
/// Returns `true` if `file` is a first-party file part of this package.
pub fn contains_file(self, db: &dyn Db, file: File) -> bool {
self.files(db).contains(&file)
}
#[tracing::instrument(level = "debug", skip(self, db))]
pub fn remove_file(self, db: &mut dyn Db, file: File) {
tracing::debug!(

View File

@@ -50,10 +50,9 @@ y: Any = "not an Any" # error: [invalid-assignment]
The spec allows you to define subclasses of `Any`.
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
assignable to `int`.
`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
```py
from typing import Any
@@ -63,13 +62,33 @@ class Subclass(Any): ...
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
x: Subclass = 1 # error: [invalid-assignment]
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
y: int = Subclass()
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
be a subclass of `FinalClass`:
```py
from typing import final
@final
class FinalClass: ...
f: FinalClass = Subclass() # error: [invalid-assignment]
```
A use case where this comes up is with mocking libraries, where the mock object should be assignable
to any type:
```py
from unittest.mock import MagicMock
x: int = MagicMock()
```
## Invalid
`Any` cannot be parameterized:

View File

@@ -56,40 +56,41 @@ def _(
def bar() -> None:
return None
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
async def outer(): # avoid unrelated syntax errors on yield, yield from, and await
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: Unknown
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: int | Unknown
reveal_type(r) # revealed: @Todo(unknown type subscript)
```
## Invalid Collection based AST nodes

View File

@@ -106,13 +106,13 @@ reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]]
# revealed: tuple[Literal[CounterSubclass], Literal[Counter], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]]
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], @Todo(GenericAlias instance), Literal[object]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
@@ -124,6 +124,6 @@ reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]]
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], @Todo(GenericAlias instance), Literal[object]]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -292,3 +292,66 @@ reveal_type(a) # revealed: Unknown
# Modifications allowed in this case:
a = None
```
## In stub files
In stub files, we have a minor modification to the rules above: we do not union with `Unknown` for
undeclared symbols.
### Undeclared and bound
`mod.pyi`:
```pyi
MyInt = int
class C:
MyStr = str
```
```py
from mod import MyInt, C
reveal_type(MyInt) # revealed: Literal[int]
reveal_type(C.MyStr) # revealed: Literal[str]
```
### Undeclared and possibly unbound
`mod.pyi`:
```pyi
def flag() -> bool:
return True
if flag():
MyInt = int
class C:
MyStr = str
```
```py
# error: [possibly-unbound-import]
# error: [possibly-unbound-import]
from mod import MyInt, C
reveal_type(MyInt) # revealed: Literal[int]
reveal_type(C.MyStr) # revealed: Literal[str]
```
### Undeclared and unbound
`mod.pyi`:
```pyi
if False:
MyInt = int
```
```py
# error: [unresolved-import]
from mod import MyInt
reveal_type(MyInt) # revealed: Unknown
```

View File

@@ -162,6 +162,44 @@ def _(flag: bool):
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
```
## Unions with literals and negations
```py
from typing import Literal
from knot_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy]))
static_assert(is_subtype_of(Literal["a", ""], Not[AlwaysFalsy] | Literal["a", ""]))
static_assert(is_subtype_of(Not[AlwaysFalsy], Not[AlwaysFalsy] | Literal["a", ""]))
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[Literal[""]]))
static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""]]))
static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""]))
static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""]))
def _(
a: Literal["a", ""] | Not[AlwaysFalsy],
b: Literal["a", ""] | Not[Literal[""]],
c: Literal[""] | Not[Literal[""]],
d: Not[Literal[""]] | Literal[""],
e: Literal["a"] | Not[Literal["a"]],
f: Literal[b"b"] | Not[Literal[b"b"]],
g: Not[Literal[b"b"]] | Literal[b"b"],
h: Literal[42] | Not[Literal[42]],
i: Not[Literal[42]] | Literal[42],
):
reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy
reveal_type(b) # revealed: object
reveal_type(c) # revealed: object
reveal_type(d) # revealed: object
reveal_type(e) # revealed: object
reveal_type(f) # revealed: object
reveal_type(g) # revealed: object
reveal_type(h) # revealed: object
reveal_type(i) # revealed: object
```
## Cannot use an argument as both a value and a type form
```py

View File

@@ -13,7 +13,7 @@ reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True]
```
## Integer instance

View File

@@ -37,7 +37,7 @@ class C:
return self
x = A() < B() < C()
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: Literal[False] | A

View File

@@ -127,8 +127,9 @@ class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
async def _():
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
```
### Invalid async comprehension
@@ -145,6 +146,7 @@ class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
async def _():
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
```

View File

@@ -42,6 +42,6 @@ def _(flag: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
3 if NotBoolable() else 4
```

View File

@@ -154,10 +154,10 @@ def _(flag: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
if NotBoolable():
...
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
elif NotBoolable():
...
```

View File

@@ -292,7 +292,7 @@ class NotBoolable:
def _(target: int, flag: NotBoolable):
y = 1
match target:
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
case 1 if flag:
y = 2
case 2:

View File

@@ -0,0 +1,293 @@
# `typing.dataclass_transform`
```toml
[environment]
python-version = "3.12"
```
`dataclass_transform` is a decorator that can be used to let type checkers know that a function,
class, or metaclass is a `dataclass`-like construct.
## Basic example
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
# modify cls
return cls
@my_dataclass
class Person:
name: str
age: int | None = None
Person("Alice", 20)
Person("Bob", None)
Person("Bob")
# error: [missing-argument]
Person()
```
## Decorating decorators that take parameters themselves
If we want our `dataclass`-like decorator to also take parameters, that is also possible:
```py
from typing_extensions import dataclass_transform, Callable
@dataclass_transform()
def versioned_class[T](*, version: int = 1):
def decorator(cls):
# modify cls
return cls
return decorator
@versioned_class(version=2)
class Person:
name: str
age: int | None = None
Person("Alice", 20)
# error: [missing-argument]
Person()
```
We properly type-check the arguments to the decorator:
```py
from typing_extensions import dataclass_transform, Callable
# error: [invalid-argument-type]
@versioned_class(version="a string")
class C:
name: str
```
## Types of decorators
The examples from this section are straight from the Python documentation on
[`typing.dataclass_transform`].
### Decorating a decorator function
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def create_model[T](cls: type[T]) -> type[T]:
...
return cls
@create_model
class CustomerModel:
id: int
name: str
CustomerModel(id=1, name="Test")
```
### Decorating a metaclass
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelMeta(type): ...
class ModelBase(metaclass=ModelMeta): ...
class CustomerModel(ModelBase):
id: int
name: str
CustomerModel(id=1, name="Test")
# error: [missing-argument]
CustomerModel()
```
### Decorating a base class
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
class ModelBase: ...
class CustomerModel(ModelBase):
id: int
name: str
# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")
```
## Arguments to `dataclass_transform`
### `eq_default`
`eq=True/False` does not have a observable effect (apart from a minor change regarding whether
`other` is positional-only or not, which is not modelled at the moment).
### `order_default`
The `order_default` argument controls whether methods such as `__lt__` are generated by default.
This can be overwritten using the `order` argument to the custom decorator:
```py
from typing_extensions import dataclass_transform
@dataclass_transform()
def normal(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=False)
def order_default_false(*, order: bool = False):
raise NotImplementedError
@dataclass_transform(order_default=True)
def order_default_true(*, order: bool = True):
raise NotImplementedError
@normal
class Normal:
inner: int
Normal(1) < Normal(2) # error: [unsupported-operator]
@normal(order=True)
class NormalOverwritten:
inner: int
NormalOverwritten(1) < NormalOverwritten(2)
@order_default_false
class OrderFalse:
inner: int
OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator]
@order_default_false(order=True)
class OrderFalseOverwritten:
inner: int
OrderFalseOverwritten(1) < OrderFalseOverwritten(2)
@order_default_true
class OrderTrue:
inner: int
OrderTrue(1) < OrderTrue(2)
@order_default_true(order=False)
class OrderTrueOverwritten:
inner: int
# error: [unsupported-operator]
OrderTrueOverwritten(1) < OrderTrueOverwritten(2)
```
### `kw_only_default`
To do
### `field_specifiers`
To do
## Overloaded dataclass-like decorators
In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the
implementation, or to *one* of the overloads.
### Applying `dataclass_transform` to the implementation
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
@dataclass_transform()
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
D1("a")
D2("a")
D1(1.2) # error: [invalid-argument-type]
D2(1.2) # error: [invalid-argument-type]
```
### Applying `dataclass_transform` to an overload
```py
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
T = TypeVar("T", bound=type)
@overload
@dataclass_transform()
def versioned_class(
cls: T,
*,
version: int = 1,
) -> T: ...
@overload
def versioned_class(
*,
version: int = 1,
) -> Callable[[T], T]: ...
def versioned_class(
cls: T | None = None,
*,
version: int = 1,
) -> T | Callable[[T], T]:
raise NotImplementedError
@versioned_class
class D1:
x: str
@versioned_class(version=2)
class D2:
x: str
# TODO: these should not be errors
D1("a") # error: [too-many-positional-arguments]
D2("a") # error: [too-many-positional-arguments]
# TODO: these should be invalid-argument-type errors
D1(1.2) # error: [too-many-positional-arguments]
D2(1.2) # error: [too-many-positional-arguments]
```
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform

View File

@@ -689,7 +689,7 @@ from dataclasses import dataclass
dataclass_with_order = dataclass(order=True)
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclasses.dataclass>
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclass-like function>
@dataclass_with_order
class C:

View File

@@ -0,0 +1,165 @@
# Semantic syntax error diagnostics
## `async` comprehensions in synchronous comprehensions
### Python 3.10
<!-- snapshot-diagnostics -->
Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even
within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)):
```toml
[environment]
python-version = "3.10"
```
```py
async def elements(n):
yield n
async def f():
# error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)"
return {n: [x async for x in elements(n)] for n in range(3)}
```
If all of the comprehensions are `async`, on the other hand, the code was still valid:
```py
async def test():
return [[x async for x in elements(n)] async for n in range(3)]
```
These are a couple of tricky but valid cases to check that nested scope handling is wired up
correctly in the `SemanticSyntaxContext` trait:
```py
async def f():
[x for x in [1]] and [x async for x in elements(1)]
async def f():
def g():
pass
[x async for x in elements(1)]
```
### Python 3.11
All of these same examples are valid after Python 3.11:
```toml
[environment]
python-version = "3.11"
```
```py
async def elements(n):
yield n
async def f():
return {n: [x async for x in elements(n)] for n in range(3)}
```
## Late `__future__` import
```py
from collections import namedtuple
# error: [invalid-syntax] "__future__ imports must be at the top of the file"
from __future__ import print_function
```
## Invalid annotation
This one might be a bit redundant with the `invalid-type-form` error.
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
# error: [invalid-syntax] "named expression cannot be used within a type annotation"
def f() -> (y := 3): ...
```
## Duplicate `match` key
```toml
[environment]
python-version = "3.10"
```
```py
match 2:
# error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`"
case {"x": 1, "x": 2}:
...
```
## `return`, `yield`, `yield from`, and `await` outside function
```py
# error: [invalid-syntax] "`return` statement outside of a function"
return
# error: [invalid-syntax] "`yield` statement outside of a function"
yield
# error: [invalid-syntax] "`yield from` statement outside of a function"
yield from []
# error: [invalid-syntax] "`await` statement outside of a function"
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
def f():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
```
Generators are evaluated lazily, so `await` is allowed, even outside of a function.
```py
async def g():
yield 1
(x async for x in g())
```
## `await` outside async function
This error includes `await`, `async for`, `async with`, and `async` comprehensions.
```python
async def elements(n):
yield n
def _():
# error: [invalid-syntax] "`await` outside of an asynchronous function"
await 1
# error: [invalid-syntax] "`async for` outside of an asynchronous function"
async for _ in elements(1):
...
# error: [invalid-syntax] "`async with` outside of an asynchronous function"
async with elements(1) as x:
...
# error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)"
# error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function"
[x async for x in elements(1)]
```
## Load before `global` declaration
This should be an error, but it's not yet.
TODO implement `SemanticSyntaxContext::global`
```py
def f():
x = 1
global x
```

View File

@@ -0,0 +1,19 @@
# Shadowing
<!-- snapshot-diagnostics -->
## Implicit class shadowing
```py
class C: ...
C = 1 # error: [invalid-assignment]
```
## Implicit function shadowing
```py
def f(): ...
f = 1 # error: [invalid-assignment]
```

View File

@@ -8,14 +8,20 @@
a, b = 1 # error: [not-iterable]
```
## Too many values to unpack
## Exactly too many values to unpack
```py
a, b = (1, 2, 3) # error: [invalid-assignment]
```
## Too few values to unpack
## Exactly too few values to unpack
```py
a, b = (1,) # error: [invalid-assignment]
```
## Too few values to unpack
```py
[a, *b, c, d] = (1, 2) # error: [invalid-assignment]
```

View File

@@ -0,0 +1,61 @@
<!-- snapshot-diagnostics -->
# Different ways that `unsupported-bool-conversion` can occur
## Has a `__bool__` method, but has incorrect parameters
```py
class NotBoolable:
def __bool__(self, foo):
return False
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Has a `__bool__` method, but has an incorrect return type
```py
class NotBoolable:
def __bool__(self) -> str:
return "wat"
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Has a `__bool__` attribute, but it's not callable
```py
class NotBoolable:
__bool__: int = 3
a = NotBoolable()
# error: [unsupported-bool-conversion]
10 and a and True
```
## Part of a union where at least one member has incorrect `__bool__` method
```py
class NotBoolable1:
def __bool__(self) -> str:
return "wat"
class NotBoolable2:
pass
class NotBoolable3:
__bool__: int = 3
def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3:
return NotBoolable2()
# error: [unsupported-bool-conversion]
10 and get() and True
```

View File

@@ -4,6 +4,6 @@
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
assert NotBoolable()
```

View File

@@ -10,8 +10,8 @@ def _(foo: str):
reveal_type(False or "z") # revealed: Literal["z"]
reveal_type(False or True) # revealed: Literal[True]
reveal_type(False or False) # revealed: Literal[False]
reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False]
reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True]
reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False]
reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True]
```
## AND
@@ -20,8 +20,8 @@ def _(foo: str):
def _(foo: str):
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False]
reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True]
reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False]
reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True]
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]
@@ -123,7 +123,7 @@ if NotBoolable():
class NotBoolable:
__bool__: None = None
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
if NotBoolable():
...
```
@@ -135,7 +135,7 @@ def test(cond: bool):
class NotBoolable:
__bool__: int | None = None if cond else 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
if NotBoolable():
...
```
@@ -149,7 +149,7 @@ def test(cond: bool):
a = 10 if cond else NotBoolable()
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`"
if a:
...
```

View File

@@ -232,21 +232,11 @@ TODO: These do not currently work yet, because we don't correctly model the nest
class C[T]:
def __init__[S](self, x: T, y: S) -> None: ...
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, 1)) # revealed: C[Unknown]
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, "string")) # revealed: C[Unknown]
# TODO: no error
# TODO: revealed: C[Literal[1]]
# error: [invalid-argument-type]
reveal_type(C(1, True)) # revealed: C[Unknown]
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
# TODO: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `S`, found `Literal[1]`"
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```
@@ -326,8 +316,6 @@ class Sub(Base[Sub]): ...
## Another cyclic case
```pyi
# TODO no error (generics)
# error: [invalid-base]
class Derived[T](list[Derived[T]]): ...
```

View File

@@ -189,7 +189,7 @@ match 42:
...
case [O]:
...
case P | Q:
case P | Q: # error: [invalid-syntax] "name capture `P` makes remaining patterns unreachable"
...
case object(foo=R):
...
@@ -289,7 +289,7 @@ match 42:
...
case [D]:
...
case E | F:
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
...
case object(foo=G):
...
@@ -357,7 +357,7 @@ match 42:
...
case [D]:
...
case E | F:
case E | F: # error: [invalid-syntax] "name capture `E` makes remaining patterns unreachable"
...
case object(foo=G):
...

View File

@@ -191,9 +191,9 @@ def _(
i2: Intersection[P | Q | R, S],
i3: Intersection[P | Q, R | S],
) -> None:
reveal_type(i1) # revealed: P & Q | P & R | P & S
reveal_type(i2) # revealed: P & S | Q & S | R & S
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
reveal_type(i1) # revealed: (P & Q) | (P & R) | (P & S)
reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S)
reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S)
def simplifications_for_same_elements(
i1: Intersection[P, Q | P],
@@ -216,7 +216,7 @@ def simplifications_for_same_elements(
# = P & Q | P & R | Q | Q & R
# = Q | P & R
# (again, because Q is a supertype of P & Q and of Q & R)
reveal_type(i3) # revealed: Q | P & R
reveal_type(i3) # revealed: Q | (P & R)
# (P | Q) & (P | Q)
# = P & P | P & Q | Q & P | Q & Q

View File

@@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool):
class NotBoolable:
__bool__: int = 3
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
while NotBoolable():
...
```

View File

@@ -22,6 +22,7 @@ We can then place custom stub files in `/typeshed/stdlib`, for example:
`/typeshed/stdlib/builtins.pyi`:
```pyi
class object: ...
class BuiltinClass: ...
builtin_symbol: BuiltinClass

View File

@@ -29,7 +29,7 @@ def _(x: Literal[1, 2, 3], y: Literal[1, 2, 3]):
assert x is 2
reveal_type(x) # revealed: Literal[2]
assert y == 2
reveal_type(y) # revealed: Literal[1, 2, 3]
reveal_type(y) # revealed: Literal[2]
```
## `assert` with `isinstance`

View File

@@ -10,7 +10,7 @@ def _(x: A | B):
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: B & ~A | A & ~B
reveal_type(x) # revealed: (B & ~A) | (A & ~B)
```
## Arms might not add narrowing constraints
@@ -131,8 +131,8 @@ def _(x: A | B | C, y: A | B | C):
# The same for `y`
reveal_type(y) # revealed: A | B | C
else:
reveal_type(x) # revealed: B & ~A | C & ~A
reveal_type(y) # revealed: B & ~A | C & ~A
reveal_type(x) # revealed: (B & ~A) | (C & ~A)
reveal_type(y) # revealed: (B & ~A) | (C & ~A)
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
@@ -155,7 +155,7 @@ def _(x: A | B | C):
reveal_type(x) # revealed: B & ~C
else:
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
reveal_type(x) # revealed: A & ~B | C
reveal_type(x) # revealed: (A & ~B) | C
```
## mixing `or` and `not`
@@ -167,7 +167,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | A & ~C
reveal_type(x) # revealed: B | (A & ~C)
else:
reveal_type(x) # revealed: C & ~B
```
@@ -181,7 +181,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
reveal_type(x) # revealed: A | B & ~C
reveal_type(x) # revealed: A | (B & ~C)
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
@@ -197,7 +197,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
# A & (B | ~C) -> (A & B) | (A & ~C)
reveal_type(x) # revealed: A & B | A & ~C
reveal_type(x) # revealed: (A & B) | (A & ~C)
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
@@ -206,7 +206,7 @@ def _(x: A | B | C):
# ~A | (~A & C) | (~B & C) ->
# ~A | (C & ~B) ->
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B)
```
## Boolean expression internal narrowing

View File

@@ -20,11 +20,9 @@ def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
if x == 1:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
reveal_type(x) # revealed: Literal[1]
elif x == 2:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(x) # revealed: Literal[2]
else:
reveal_type(x) # revealed: Literal[3]
```
@@ -38,14 +36,11 @@ def _(flag1: bool, flag2: bool):
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x != 2:
# TODO should be `Literal[1]`
reveal_type(x) # revealed: Literal[1, 3]
reveal_type(x) # revealed: Literal[1]
elif x == 3:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
reveal_type(x) # revealed: Never
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(x) # revealed: Never
```
## Assignment expressions

View File

@@ -9,8 +9,7 @@ def _(flag: bool):
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be None
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None
```
## `!=` for other singleton types
@@ -22,8 +21,7 @@ def _(flag: bool):
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
# TODO should be Literal[False]
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[False]
```
## `x != y` where `y` is of literal type
@@ -47,8 +45,7 @@ def _(flag: bool):
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
# TODO should be Literal[A]
reveal_type(C) # revealed: Literal[A, B]
reveal_type(C) # revealed: Literal[A]
```
## `x != y` where `y` has multiple single-valued options
@@ -61,8 +58,7 @@ def _(flag1: bool, flag2: bool):
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(x) # revealed: Literal[2]
```
## `!=` for non-single-valued types
@@ -101,6 +97,61 @@ def f() -> Literal[1, 2, 3]:
if (x := f()) != 1:
reveal_type(x) # revealed: Literal[2, 3]
else:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
reveal_type(x) # revealed: Literal[1]
```
## Union with `Any`
```py
from typing import Any
def _(x: Any | None, y: Any | None):
if x != 1:
reveal_type(x) # revealed: (Any & ~Literal[1]) | None
if y == 1:
reveal_type(y) # revealed: Any & ~None
```
## Booleans and integers
```py
from typing import Literal
def _(b: bool, i: Literal[1, 2]):
if b == 1:
reveal_type(b) # revealed: Literal[True]
else:
reveal_type(b) # revealed: Literal[False]
if b == 6:
reveal_type(b) # revealed: Never
else:
reveal_type(b) # revealed: bool
if b == 0:
reveal_type(b) # revealed: Literal[False]
else:
reveal_type(b) # revealed: Literal[True]
if i == True:
reveal_type(i) # revealed: Literal[1]
else:
reveal_type(i) # revealed: Literal[2]
```
## Narrowing `LiteralString` in union
```py
from typing_extensions import Literal, LiteralString, Any
def _(s: LiteralString | None, t: LiteralString | Any):
if s == "foo":
reveal_type(s) # revealed: Literal["foo"]
if s == 1:
reveal_type(s) # revealed: Never
if t == "foo":
# TODO could be `Literal["foo"] | Any`
reveal_type(t) # revealed: LiteralString | Any
```

View File

@@ -31,17 +31,14 @@ def _(flag1: bool, flag2: bool):
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x == 2:
# TODO should be `Literal[2]`
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(x) # revealed: Literal[2]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
elif x != 2:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 3]
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
reveal_type(x) # revealed: Never
```

View File

@@ -82,19 +82,19 @@ class B: ...
def f(x: A | B):
if x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy)
if x and not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy)
else:
reveal_type(x) # revealed: A | B
if x or not x:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy)
```
### Truthiness of Types
@@ -111,9 +111,9 @@ x = int if flag() else str
reveal_type(x) # revealed: Literal[int, str]
if x:
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
reveal_type(x) # revealed: (Literal[int] & ~AlwaysFalsy) | (Literal[str] & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
reveal_type(x) # revealed: (Literal[int] & ~AlwaysTruthy) | (Literal[str] & ~AlwaysTruthy)
```
## Determined Truthiness
@@ -176,12 +176,12 @@ if isinstance(x, str) and not isinstance(x, B):
z = x if flag() else y
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"]
if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""]
```
## Narrowing Multiple Variables
@@ -264,13 +264,13 @@ def _(
):
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`"
if d:
# TODO: Should be `Unknown`
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
@@ -296,12 +296,12 @@ def _(x: Literal[0, 1]):
reveal_type(x and A()) # revealed: Literal[0] | A
def _(x: str):
reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A
reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A
def _(x: bool | str):
reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A
class Falsy:
def __bool__(self) -> Literal[False]:

View File

@@ -127,7 +127,7 @@ class B: ...
def _[T](x: A | B):
if type(x) is A[str]:
reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown]
reveal_type(x) # revealed: (A[int] & A[Unknown]) | (B & A[Unknown])
else:
reveal_type(x) # revealed: A[int] | B
```

View File

@@ -242,7 +242,7 @@ def f(
Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime:
```py
# TODO: should be `Literal[True]`
# Could also be `Literal[True]`, but `bool` is fine:
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
```
@@ -304,10 +304,12 @@ reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool
## Calls to protocol classes
<!-- snapshot-diagnostics -->
Neither `Protocol`, nor any protocol class, can be directly instantiated:
```py
from typing import Protocol
from typing_extensions import Protocol, reveal_type
# error: [call-non-callable]
reveal_type(Protocol()) # revealed: Unknown
@@ -315,7 +317,7 @@ reveal_type(Protocol()) # revealed: Unknown
class MyProtocol(Protocol):
x: int
# error
# error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
reveal_type(MyProtocol()) # revealed: MyProtocol
```
@@ -363,25 +365,8 @@ class Foo(Protocol):
def method_member(self) -> bytes:
return b"foo"
# TODO: at runtime, `get_protocol_members` returns a `frozenset`,
# but for now we might pretend it returns a `tuple`, as we support heterogeneous `tuple` types
# but not yet generic `frozenset`s
#
# So this should either be
#
# `tuple[Literal["x"], Literal["y"], Literal["z"], Literal["method_member"]]`
#
# `frozenset[Literal["x", "y", "z", "method_member"]]`
reveal_type(get_protocol_members(Foo)) # revealed: @Todo(specialized non-generic class)
```
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
```py
class NotAProtocol: ...
# TODO: should emit `[invalid-protocol]` error, should reveal `Unknown`
reveal_type(get_protocol_members(NotAProtocol)) # revealed: @Todo(specialized non-generic class)
# TODO: actually a frozenset (requires support for legacy generics)
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["method_member"], Literal["x"], Literal["y"], Literal["z"]]
```
Certain special attributes and methods are not considered protocol members at runtime, and should
@@ -399,8 +384,87 @@ class Lumberjack(Protocol):
def __init__(self, x: int) -> None:
self.x = x
# TODO: `tuple[Literal["x"]]` or `frozenset[Literal["x"]]`
reveal_type(get_protocol_members(Lumberjack)) # revealed: @Todo(specialized non-generic class)
# TODO: actually a frozenset
reveal_type(get_protocol_members(Lumberjack)) # revealed: tuple[Literal["x"]]
```
A sub-protocol inherits and extends the members of its superclass protocol(s):
```py
class Bar(Protocol):
spam: str
class Baz(Bar, Protocol):
ham: memoryview
# TODO: actually a frozenset
reveal_type(get_protocol_members(Baz)) # revealed: tuple[Literal["ham"], Literal["spam"]]
class Baz2(Bar, Foo, Protocol): ...
# TODO: actually a frozenset
# revealed: tuple[Literal["method_member"], Literal["spam"], Literal["x"], Literal["y"], Literal["z"]]
reveal_type(get_protocol_members(Baz2))
```
## Protocol members in statically known branches
The list of protocol members does not include any members declared in branches that are statically
known to be unreachable:
```toml
[environment]
python-version = "3.9"
```
```py
import sys
from typing_extensions import Protocol, get_protocol_members
class Foo(Protocol):
if sys.version_info >= (3, 10):
a: int
b = 42
def c(self) -> None: ...
else:
d: int
e = 56
def f(self) -> None: ...
# TODO: actually a frozenset
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["d"], Literal["e"], Literal["f"]]
```
## Invalid calls to `get_protocol_members()`
<!-- snapshot-diagnostics -->
Calling `get_protocol_members` on a non-protocol class raises an error at runtime:
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Protocol, get_protocol_members
class NotAProtocol: ...
get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
class AlsoNotAProtocol(NotAProtocol, object): ...
get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
```
The original class object must be passed to the function; a specialised version of a generic version
does not suffice:
```py
class GenericProtocol[T](Protocol): ...
get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549)
```
## Subtyping of protocols with attribute members
@@ -409,6 +473,11 @@ In the following example, the protocol class `HasX` defines an interface such th
static type can be said to be a subtype of `HasX` if all inhabitants of that other type have a
mutable `x` attribute of type `int`:
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol
from knot_extensions import static_assert, is_assignable_to, is_subtype_of
@@ -548,9 +617,61 @@ def f(arg: HasXWithDefault):
reveal_type(type(arg).x) # revealed: int
```
Assignments in a class body of a protocol -- of any kind -- are not permitted by red-knot 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).
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.
```py
from typing_extensions import TypeAlias, get_protocol_members
class MyContext:
def __enter__(self) -> int:
return 42
def __exit__(self, *args) -> None: ...
class LotsOfBindings(Protocol):
a: int
a = 42 # this is fine, since `a` is declared in the class body
b: int = 56 # this is also fine, by the same principle
type c = str # this is very strange but I can't see a good reason to disallow it
d: TypeAlias = bytes # same here
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)
f, g = (1, 2) # TODO: this should error with `[invalid-protocol]` (`f` and `g` are not declared)
h: int = (i := 3) # TODO: this should error with `[invalid-protocol]` (`i` is not declared)
for j in range(42): # TODO: this should error with `[invalid-protocol]` (`j` is not declared)
pass
with MyContext() as k: # TODO: this should error with `[invalid-protocol]` (`k` is not declared)
pass
match object():
case l: # TODO: this should error with `[invalid-protocol]` (`l` is not declared)
...
# TODO: actually a frozenset
# revealed: tuple[Literal["Nested"], Literal["NestedProtocol"], Literal["a"], Literal["b"], Literal["c"], Literal["d"], Literal["e"], Literal["f"], Literal["g"], Literal["h"], Literal["i"], Literal["j"], Literal["k"], Literal["l"]]
reveal_type(get_protocol_members(LotsOfBindings))
```
Attribute members are allowed to have assignments in methods on the protocol class, just like
non-protocol classes. Unlike other classes, however, *implicit* instance attributes -- those that
are not declared in the class body -- are not allowed:
non-protocol classes. Unlike other classes, however, instance attributes that are not declared in
the class body are disallowed. This is mandated by [the spec][spec_protocol_members]:
> Additional attributes *only* defined in the body of a method by assignment via `self` are not
> allowed. The rationale for this is that the protocol class implementation is often not shared by
> subtypes, so the interface should not depend on the default implementation.
```py
class Foo(Protocol):
@@ -559,11 +680,33 @@ class Foo(Protocol):
def __init__(self) -> None:
self.x = 42 # fine
self.a = 56 # error
self.a = 56 # TODO: should emit diagnostic
self.b: int = 128 # TODO: should emit diagnostic
def non_init_method(self) -> None:
self.y = 64 # fine
self.b = 72 # error
self.c = 72 # TODO: should emit diagnostic
# Note: the list of members does not include `a`, `b` or `c`,
# as none of these attributes is declared in the class body.
#
# TODO: actually a frozenset
reveal_type(get_protocol_members(Foo)) # revealed: tuple[Literal["non_init_method"], Literal["x"], Literal["y"]]
```
If a member is declared in a superclass of a protocol class, it is fine for it to be assigned to in
the sub-protocol class without a redeclaration:
```py
class Super(Protocol):
x: int
class Sub(Super, Protocol):
x = 42 # no error here, since it's declared in the superclass
# TODO: actually frozensets
reveal_type(get_protocol_members(Super)) # revealed: tuple[Literal["x"]]
reveal_type(get_protocol_members(Sub)) # revealed: tuple[Literal["x"]]
```
If a protocol has 0 members, then all other types are assignable to it, and all fully static types
@@ -1077,23 +1220,25 @@ static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # er
## Narrowing of protocols
<!-- snapshot-diagnostics -->
By default, a protocol class cannot be used as the second argument to `isinstance()` or
`issubclass()`, and a type checker must emit an error on such calls. However, we still narrow the
type inside these branches (this matches the behaviour of other type checkers):
```py
from typing import Protocol
from typing_extensions import Protocol, reveal_type
class HasX(Protocol):
x: int
def f(arg: object, arg2: type):
if isinstance(arg, HasX): # error
if isinstance(arg, HasX): # error: [invalid-argument-type]
reveal_type(arg) # revealed: HasX
else:
reveal_type(arg) # revealed: ~HasX
if issubclass(arg2, HasX): # error
if issubclass(arg2, HasX): # error: [invalid-argument-type]
reveal_type(arg2) # revealed: type[HasX]
else:
reveal_type(arg2) # revealed: type & ~type[HasX]
@@ -1128,10 +1273,10 @@ class OnlyMethodMembers(Protocol):
def method(self) -> None: ...
def f(arg1: type, arg2: type):
if issubclass(arg1, OnlyMethodMembers): # error
reveal_type(arg1) # revealed: type[OnlyMethodMembers]
if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members)
reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
else:
reveal_type(arg1) # revealed: type & ~type[OnlyMethodMembers]
reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
if issubclass(arg2, OnlyMethodMembers): # no error!
reveal_type(arg2) # revealed: type[OnlyMethodMembers]
@@ -1139,6 +1284,140 @@ def f(arg1: type, arg2: type):
reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
```
## Truthiness of protocol instance
An instance of a protocol type generally has ambiguous truthiness:
```py
from typing import Protocol
class Foo(Protocol):
x: int
def f(foo: Foo):
reveal_type(bool(foo)) # revealed: bool
```
But this is not the case if the protocol has a `__bool__` method member that returns `Literal[True]`
or `Literal[False]`:
```py
from typing import Literal
class Truthy(Protocol):
def __bool__(self) -> Literal[True]: ...
class FalsyFoo(Foo, Protocol):
def __bool__(self) -> Literal[False]: ...
class FalsyFooSubclass(FalsyFoo, Protocol):
y: str
def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass):
reveal_type(bool(a)) # revealed: Literal[True]
reveal_type(bool(b)) # revealed: Literal[False]
reveal_type(bool(c)) # revealed: Literal[False]
```
It is not sufficient for a protocol to have a callable `__bool__` instance member that returns
`Literal[True]` for it to be considered always truthy. Dunder methods are looked up on the class
rather than the instance. If a protocol `X` has an instance-attribute `__bool__` member, it is
unknowable whether that attribute can be accessed on the type of an object that satisfies `X`'s
interface:
```py
from typing import Callable
class InstanceAttrBool(Protocol):
__bool__: Callable[[], Literal[True]]
def h(obj: InstanceAttrBool):
reveal_type(bool(obj)) # revealed: bool
```
## Fully static protocols; gradual protocols
A protocol is only fully static if all of its members are fully static:
```py
from typing import Protocol, Any
from knot_extensions import is_fully_static, static_assert
class FullyStatic(Protocol):
x: int
class NotFullyStatic(Protocol):
x: Any
static_assert(is_fully_static(FullyStatic))
# TODO: should pass
static_assert(not is_fully_static(NotFullyStatic)) # error: [static-assert-error]
```
Non-fully-static protocols do not participate in subtyping, only assignability:
```py
from knot_extensions import is_subtype_of, is_assignable_to
class NominalWithX:
x: int = 42
# TODO: these should pass
static_assert(is_assignable_to(NominalWithX, FullyStatic)) # error: [static-assert-error]
static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) # error: [static-assert-error]
static_assert(is_subtype_of(NominalWithX, FullyStatic)) # error: [static-assert-error]
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))
```
Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to
the nominal type `object` (as described above):
```py
class Empty(Protocol): ...
static_assert(is_fully_static(Empty))
```
A method member is only considered fully static if all its parameter annotations and its return
annotation are fully static:
```py
class FullyStaticMethodMember(Protocol):
def method(self, x: int) -> str: ...
class DynamicParameter(Protocol):
def method(self, x: Any) -> str: ...
class DynamicReturn(Protocol):
def method(self, x: int) -> Any: ...
static_assert(is_fully_static(FullyStaticMethodMember))
# TODO: these should pass
static_assert(not is_fully_static(DynamicParameter)) # error: [static-assert-error]
static_assert(not is_fully_static(DynamicReturn)) # error: [static-assert-error]
```
The [typing spec][spec_protocol_members] states:
> If any parameters of a protocol method are not annotated, then their types are assumed to be `Any`
Thus, a partially unannotated method member can also not be considered to be fully static:
```py
class NoParameterAnnotation(Protocol):
def method(self, x) -> str: ...
class NoReturnAnnotation(Protocol):
def method(self, x: int): ...
# TODO: these should pass
static_assert(not is_fully_static(NoParameterAnnotation)) # error: [static-assert-error]
static_assert(not is_fully_static(NoReturnAnnotation)) # error: [static-assert-error]
```
## `typing.SupportsIndex` and `typing.Sized`
`typing.SupportsIndex` is already somewhat supported through some special-casing in red-knot.
@@ -1167,10 +1446,10 @@ def _(some_list: list, some_tuple: tuple[int, str], some_sized: Sized):
Add tests for:
- Assignments without declarations in protocol class bodies. And various weird ways of creating
attributes in a class body or instance method. [Example mypy tests][mypy_weird_protocols].
- More tests for protocols inside `type[]`. [Spec reference][protocols_inside_type_spec].
- Protocols with instance-method members
- Protocols with instance-method members, including:
- Protocols with methods that have parameters or the return type unannotated
- Protocols with methods that have parameters or the return type annotated with `Any`
- Protocols with `@classmethod` and `@staticmethod`
- Assignability of non-instance types to protocols with instance-method members (e.g. a
class-literal type can be a subtype of `Sized` if its metaclass has a `__len__` method)
@@ -1184,16 +1463,13 @@ Add tests for:
- Protocols with instance attributes annotated with `Callable` (can a nominal type with a method
satisfy that protocol, and if so in what cases?)
- Protocols decorated with `@final`
- Protocols with attribute members annotated with `Any`
- Protocols with methods that have parameters or the return type unannotated
- Protocols with methods that have parameters or the return type annotated with `Any`
- Equivalence and subtyping between `Callable` types and protocols that define `__call__`
[mypy_protocol_docs]: https://mypy.readthedocs.io/en/stable/protocols.html#protocols-and-structural-subtyping
[mypy_protocol_tests]: https://github.com/python/mypy/blob/master/test-data/unit/check-protocols.test
[mypy_weird_protocols]: https://github.com/python/mypy/blob/a3ce6d5307e99a1b6c181eaa7c5cf134c53b7d8b/test-data/unit/check-protocols.test#L2131-L2132
[protocol conformance tests]: https://github.com/python/typing/tree/main/conformance/tests
[protocols_inside_type_spec]: https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols
[recursive_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#recursive-protocols
[self_types_protocols_spec]: https://typing.python.org/en/latest/spec/protocol.html#self-types-in-protocols
[spec_protocol_members]: https://typing.python.org/en/latest/spec/protocol.html#protocol-members
[typing_spec_protocols]: https://typing.python.org/en/latest/spec/protocol.html

View File

@@ -404,7 +404,7 @@ x = int
class C:
var: ClassVar[x]
reveal_type(C.var) # revealed: Unknown | str
reveal_type(C.var) # revealed: str
x = str
```

View File

@@ -0,0 +1,177 @@
# `global` references
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Explicit global in function
```py
x = 1
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Unassignable type in function
```py
x: int = 1
def f():
y: int = 1
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
y = ""
global x
# TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
x = ""
```
## Nested intervening scope
A `global` statement causes lookup to skip any bindings in intervening scopes:
```py
x: int = 1
def outer():
x: str = ""
def inner():
global x
# TODO: revealed: int
reveal_type(x) # revealed: str
```
## Narrowing
An assignment following a `global` statement should narrow the type in the local scope after the
assignment.
```py
x: int | None
def f():
global x
x = 1
reveal_type(x) # revealed: Literal[1]
```
## `nonlocal` and `global`
A binding cannot be both `nonlocal` and `global`. This should emit a semantic syntax error. CPython
marks the `nonlocal` line, while `mypy`, `pyright`, and `ruff` (`PLE0115`) mark the `global` line.
```py
x = 1
def f():
x = 1
def g() -> None:
nonlocal x
global x # TODO: error: [invalid-syntax] "name 'x' is nonlocal and global"
x = None
```
## Global declaration after `global` statement
```py
def f():
global x
# TODO this should also not be an error
y = x # error: [unresolved-reference] "Name `x` used when not defined"
x = 1 # No error.
x = 2
```
## Semantic syntax errors
Using a name prior to its `global` declaration in the same scope is a syntax error.
```py
x = 1
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
global x
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
global x, y
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
def f():
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
global x
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
global x
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
def f():
print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
# still an error in module scope
x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
```

View File

@@ -43,14 +43,3 @@ def f():
def h():
reveal_type(x) # revealed: Unknown | Literal[1]
```
## Implicit global in function
A name reference to a never-defined symbol in a function is implicitly a global lookup.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[1]
```

View File

@@ -5,7 +5,7 @@
```py
class C: ...
C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"
C = 1 # error: "Implicit shadowing of class `C`"
```
## Explicit

View File

@@ -15,7 +15,7 @@ def f(x: str):
```py
def f(): ...
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
f = 1 # error: "Implicit shadowing of function `f`"
```
## Explicit shadowing

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
--> /src/mdtest_snippet.py:11:1
|
10 | # TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
11 | instance.attr = 1 # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
| ^^^^^^^^^^^^^
|
```

View File

@@ -29,12 +29,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
--> /src/mdtest_snippet.py:12:1
|
11 | # TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
12 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method
| ^^^^^^^^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:6:1
|
4 | instance = C()
5 | instance.attr = 1 # fine
6 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^^^^^^^^
7 |
8 | C.attr = 1 # fine
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:9:1
|
8 | C.attr = 1 # fine
9 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
warning: lint:possibly-unbound-attribute
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `Literal[C]` is possibly unbound
--> /src/mdtest_snippet.py:6:5
|
4 | attr: int = 0
5 |
6 | C.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^ Attribute `attr` on type `Literal[C]` is possibly unbound
| ^^^^^^
7 |
8 | instance = C()
|
@@ -40,12 +40,12 @@ warning: lint:possibly-unbound-attribute
```
```
warning: lint:possibly-unbound-attribute
warning: lint:possibly-unbound-attribute: Attribute `attr` on type `C` is possibly unbound
--> /src/mdtest_snippet.py:9:5
|
8 | instance = C()
9 | instance.attr = 1 # error: [possibly-unbound-attribute]
| ^^^^^^^^^^^^^ Attribute `attr` on type `C` is possibly unbound
| ^^^^^^^^^^^^^
|
```

View File

@@ -26,13 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:7:1
|
5 | instance = C()
6 | instance.attr = 1 # fine
7 | instance.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^^^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^^^^^^^^
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
|
@@ -40,13 +40,13 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-attribute-access
error: lint:invalid-attribute-access: Cannot assign to instance attribute `attr` from the class object `Literal[C]`
--> /src/mdtest_snippet.py:9:1
|
7 | instance.attr = "wrong" # error: [invalid-assignment]
8 |
9 | C.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^ Cannot assign to instance attribute `attr` from the class object `Literal[C]`
| ^^^^^^
|
```

View File

@@ -37,12 +37,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
--> /src/mdtest_snippet.py:11:5
|
10 | # TODO: The error message here could be improved to explain why the assignment fails.
11 | C1.attr = 1 # error: [invalid-assignment]
| ^^^^^^^ Object of type `Literal[1]` is not assignable to attribute `attr` on type `Literal[C1, C1]`
| ^^^^^^^
12 |
13 | class C2:
|

View File

@@ -23,13 +23,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:unresolved-attribute
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `Literal[C]`.
--> /src/mdtest_snippet.py:3:1
|
1 | class C: ...
2 |
3 | C.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `Literal[C]`.
| ^^^^^^^^^^^^^^
4 |
5 | instance = C()
|
@@ -37,12 +37,12 @@ error: lint:unresolved-attribute
```
```
error: lint:unresolved-attribute
error: lint:unresolved-attribute: Unresolved attribute `non_existent` on type `C`.
--> /src/mdtest_snippet.py:6:1
|
5 | instance = C()
6 | instance.non_existent = 1 # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^^^^^^^ Unresolved attribute `non_existent` on type `C`.
| ^^^^^^^^^^^^^^^^^^^^^
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/attrib
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
--> /src/mdtest_snippet.py:7:1
|
6 | C.attr = 1 # fine
7 | C.attr = "wrong" # error: [invalid-assignment]
| ^^^^^^ Object of type `Literal["wrong"]` is not assignable to attribute `attr` of type `int`
| ^^^^^^
8 |
9 | instance = C()
|
@@ -40,12 +40,12 @@ error: lint:invalid-assignment
```
```
error: lint:invalid-attribute-access
error: lint:invalid-attribute-access: Cannot assign to ClassVar `attr` from an instance of type `C`
--> /src/mdtest_snippet.py:10:1
|
9 | instance = C()
10 | instance.attr = 1 # error: [invalid-attribute-access]
| ^^^^^^^^^^^^^ Cannot assign to ClassVar `attr` from an instance of type `C`
| ^^^^^^^^^^^^^
|
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `zqzqzqzqzqzqzq`
--> /src/mdtest_snippet.py:1:8
|
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
| ^^^^^^^^^^^^^^
|
```

View File

@@ -27,12 +27,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
# Diagnostics
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `a.foo`
--> /src/mdtest_snippet.py:2:8
|
1 | # Topmost component resolvable, submodule not resolvable:
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
| ^^^^^ Cannot resolve import `a.foo`
| ^^^^^
3 |
4 | # Topmost component unresolvable:
|
@@ -40,12 +40,12 @@ error: lint:unresolved-import
```
```
error: lint:unresolved-import
error: lint:unresolved-import: Cannot resolve import `b.foo`
--> /src/mdtest_snippet.py:5:8
|
4 | # Topmost component unresolvable:
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
| ^^^^^ Cannot resolve import `b.foo`
| ^^^^^
|
```

View File

@@ -28,12 +28,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:10:10
|
9 | # error: [not-iterable]
10 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^
11 | reveal_type(x) # revealed: int
|

View File

@@ -20,12 +20,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
--> /src/mdtest_snippet.py:2:10
|
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
| ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
| ^^^^^^^^
3 | pass
|

View File

@@ -24,13 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
--> /src/mdtest_snippet.py:6:10
|
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
| ^^^^^^^^^^^^^
7 | pass
|

View File

@@ -25,12 +25,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
--> /src/mdtest_snippet.py:7:10
|
6 | # error: [not-iterable]
7 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
| ^^^^^
8 | reveal_type(x) # revealed: Unknown
|

View File

@@ -46,12 +46,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
--> /src/mdtest_snippet.py:22:14
|
21 | # error: [not-iterable]
22 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
| ^^^^^^^^^^^
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
|
@@ -73,12 +73,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
--> /src/mdtest_snippet.py:27:14
|
26 | # error: [not-iterable]
27 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable2.__getitem__(key: int) -> int) | None`) may not be callable
| ^^^^^^^^^^^
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
|

View File

@@ -43,12 +43,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
--> /src/mdtest_snippet.py:20:14
|
19 | # error: [not-iterable]
20 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
| ^^^^^^^^^^^
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
|
@@ -70,12 +70,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:25:14
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^^
26 | reveal_type(y) # revealed: str | int
|

View File

@@ -47,12 +47,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`)
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `(bound method Iterable1.__iter__() -> Iterator) | (bound method Iterable1.__iter__(invalid_extra_arg) -> Iterator)`) may have an invalid signature (expected `def __iter__(self): ...`)
| ^^^^^^^^^^^
18 | reveal_type(x) # revealed: int
|
@@ -73,12 +73,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable
| ^^^^^^^^^^^
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
|

View File

@@ -51,12 +51,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
| ^^^^^^^^^^^
29 | reveal_type(x) # revealed: int | str
|
@@ -77,12 +77,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
--> /src/mdtest_snippet.py:32:14
|
31 | # error: [not-iterable]
32 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
| ^^^^^^^^^^^
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
|

View File

@@ -36,12 +36,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:18:14
|
17 | # error: [not-iterable]
18 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^
19 | reveal_type(x) # revealed: int | bytes
|

View File

@@ -54,12 +54,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
--> /src/mdtest_snippet.py:31:14
|
30 | # error: [not-iterable]
31 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `(bound method Iterable1.__getitem__(item: int) -> str) | None`) may not be callable
| ^^^^^^^^^^^
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
@@ -81,12 +81,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
--> /src/mdtest_snippet.py:36:14
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `(bound method Iterable2.__getitem__(item: int) -> str) | (bound method Iterable2.__getitem__(item: str) -> int)`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
| ^^^^^^^^^^^
37 | reveal_type(y) # revealed: bytes | str | int
|

View File

@@ -35,12 +35,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
| ^^^^^^^^^^
18 | reveal_type(x) # revealed: int | bytes
|

View File

@@ -36,13 +36,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
--> /src/mdtest_snippet.py:18:14
|
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
19 | reveal_type(x) # revealed: int
|

View File

@@ -31,13 +31,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
--> /src/mdtest_snippet.py:13:14
|
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
| ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
| ^^^^^^^^^^^^^^^^^^^^^^
14 | reveal_type(x) # revealed: int
|

View File

@@ -33,25 +33,25 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
--> /src/mdtest_snippet.py:11:14
|
10 | # error: [not-iterable]
11 | for x in NotIterable():
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
| ^^^^^^^^^^^^^
12 | pass
|
```
```
warning: lint:possibly-unresolved-reference
warning: lint:possibly-unresolved-reference: Name `x` used when possibly not defined
--> /src/mdtest_snippet.py:16:17
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| ^ Name `x` used when possibly not defined
| ^
|
```

View File

@@ -26,12 +26,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
--> /src/mdtest_snippet.py:8:10
|
7 | # error: [not-iterable]
8 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
| ^^^^^
9 | reveal_type(x) # revealed: Unknown
|

View File

@@ -30,12 +30,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
--> /src/mdtest_snippet.py:12:10
|
11 | # error: [not-iterable]
12 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
| ^^^^^^^^^^
13 | reveal_type(x) # revealed: int
|

View File

@@ -41,12 +41,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
--> /src/mdtest_snippet.py:19:10
|
18 | # error: [not-iterable]
19 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
| ^^^^^^^^^^^
20 | reveal_type(x) # revealed: int
|
@@ -67,12 +67,12 @@ info: revealed-type: Revealed type
```
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
--> /src/mdtest_snippet.py:23:10
|
22 | # error: [not-iterable]
23 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
| ^^^^^^^^^^^
24 | reveal_type(y) # revealed: Unknown
|

View File

@@ -24,12 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
--> /src/mdtest_snippet.py:7:8
|
6 | # error: [unsupported-bool-conversion]
7 | 10 and a and True
| ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -28,26 +28,28 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
--> /src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | 10 in WithContains()
| ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^^^^
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
|
info: `__bool__` on `NotBoolable` must be callable
```
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
--> /src/mdtest_snippet.py:11:1
|
9 | 10 in WithContains()
10 | # error: [unsupported-bool-conversion]
11 | 10 not in WithContains()
| ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_mat
# Diagnostics
```
error: lint:no-matching-overload
error: lint:no-matching-overload: No overload of class `type` matches arguments
--> /src/mdtest_snippet.py:1:1
|
1 | type("Foo", ()) # error: [no-matching-overload]
| ^^^^^^^^^^^^^^^ No overload of class `type` matches arguments
| ^^^^^^^^^^^^^^^
|
```

View File

@@ -22,12 +22,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
--> /src/mdtest_snippet.py:5:1
|
4 | # error: [unsupported-bool-conversion]
5 | not NotBoolable()
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -0,0 +1,117 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: protocols.md - Protocols - Calls to protocol classes
mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, reveal_type
2 |
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
5 |
6 | class MyProtocol(Protocol):
7 | x: int
8 |
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
```
# Diagnostics
```
error: lint:call-non-callable: Object of type `typing.Protocol` is not callable
--> /src/mdtest_snippet.py:4:13
|
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
| ^^^^^^^^^^
5 |
6 | class MyProtocol(Protocol):
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:4:1
|
3 | # error: [call-non-callable]
4 | reveal_type(Protocol()) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
5 |
6 | class MyProtocol(Protocol):
|
```
```
error: lint:call-non-callable: Cannot instantiate class `MyProtocol`
--> /src/mdtest_snippet.py:10:13
|
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^ This call will raise `TypeError` at runtime
11 | class SubclassOfMyProtocol(MyProtocol): ...
|
info: Protocol classes cannot be instantiated
--> /src/mdtest_snippet.py:6:7
|
4 | reveal_type(Protocol()) # revealed: Unknown
5 |
6 | class MyProtocol(Protocol):
| ^^^^^^^^^^^^^^^^^^^^ `MyProtocol` declared as a protocol here
7 | x: int
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:10:1
|
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol`
11 | class SubclassOfMyProtocol(MyProtocol): ...
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:13:1
|
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol`
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:15:5
|
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^ `MyProtocol`
|
```

View File

@@ -0,0 +1,82 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: protocols.md - Protocols - Invalid calls to `get_protocol_members()`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, get_protocol_members
2 |
3 | class NotAProtocol: ...
4 |
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
6 |
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
8 |
9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
10 | class GenericProtocol[T](Protocol): ...
11 |
12 | get_protocol_members(GenericProtocol[int]) # TODO: should emit a diagnostic here (https://github.com/astral-sh/ruff/issues/17549)
```
# Diagnostics
```
error: lint:invalid-argument-type: Invalid argument to `get_protocol_members`
--> /src/mdtest_snippet.py:5:1
|
3 | class NotAProtocol: ...
4 |
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime
6 |
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
|
info: Only protocol classes can be passed to `get_protocol_members`
info: `NotAProtocol` is declared here, but it is not a protocol class:
--> /src/mdtest_snippet.py:3:7
|
1 | from typing_extensions import Protocol, get_protocol_members
2 |
3 | class NotAProtocol: ...
| ^^^^^^^^^^^^
4 |
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
|
info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol`
info: See https://typing.python.org/en/latest/spec/protocol.html#
```
```
error: lint:invalid-argument-type: Invalid argument to `get_protocol_members`
--> /src/mdtest_snippet.py:9:1
|
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
8 |
9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime
10 | class GenericProtocol[T](Protocol): ...
|
info: Only protocol classes can be passed to `get_protocol_members`
info: `AlsoNotAProtocol` is declared here, but it is not a protocol class:
--> /src/mdtest_snippet.py:7:7
|
5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type]
6 |
7 | class AlsoNotAProtocol(NotAProtocol, object): ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 |
9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type]
|
info: A class is only a protocol class if it directly inherits from `typing.Protocol` or `typing_extensions.Protocol`
info: See https://typing.python.org/en/latest/spec/protocol.html#
```

View File

@@ -0,0 +1,241 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: protocols.md - Protocols - Narrowing of protocols
mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import Protocol, reveal_type
2 |
3 | class HasX(Protocol):
4 | x: int
5 |
6 | def f(arg: object, arg2: type):
7 | if isinstance(arg, HasX): # error: [invalid-argument-type]
8 | reveal_type(arg) # revealed: HasX
9 | else:
10 | reveal_type(arg) # revealed: ~HasX
11 |
12 | if issubclass(arg2, HasX): # error: [invalid-argument-type]
13 | reveal_type(arg2) # revealed: type[HasX]
14 | else:
15 | reveal_type(arg2) # revealed: type & ~type[HasX]
16 | from typing import runtime_checkable
17 |
18 | @runtime_checkable
19 | class RuntimeCheckableHasX(Protocol):
20 | x: int
21 |
22 | def f(arg: object):
23 | if isinstance(arg, RuntimeCheckableHasX): # no error!
24 | reveal_type(arg) # revealed: RuntimeCheckableHasX
25 | else:
26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX
27 | @runtime_checkable
28 | class OnlyMethodMembers(Protocol):
29 | def method(self) -> None: ...
30 |
31 | def f(arg1: type, arg2: type):
32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members)
33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
34 | else:
35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
36 |
37 | if issubclass(arg2, OnlyMethodMembers): # no error!
38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers]
39 | else:
40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
```
# Diagnostics
```
error: lint:invalid-argument-type: Class `HasX` cannot be used as the second argument to `isinstance`
--> /src/mdtest_snippet.py:7:8
|
6 | def f(arg: object, arg2: type):
7 | if isinstance(arg, HasX): # error: [invalid-argument-type]
| ^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime
8 | reveal_type(arg) # revealed: HasX
9 | else:
|
info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable
--> /src/mdtest_snippet.py:3:7
|
1 | from typing_extensions import Protocol, reveal_type
2 |
3 | class HasX(Protocol):
| ^^^^^^^^^^^^^^ `HasX` declared here
4 | x: int
|
info: A protocol class can only be used in `isinstance` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable`
info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:8:9
|
6 | def f(arg: object, arg2: type):
7 | if isinstance(arg, HasX): # error: [invalid-argument-type]
8 | reveal_type(arg) # revealed: HasX
| ^^^^^^^^^^^^^^^^ `HasX`
9 | else:
10 | reveal_type(arg) # revealed: ~HasX
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:10:9
|
8 | reveal_type(arg) # revealed: HasX
9 | else:
10 | reveal_type(arg) # revealed: ~HasX
| ^^^^^^^^^^^^^^^^ `~HasX`
11 |
12 | if issubclass(arg2, HasX): # error: [invalid-argument-type]
|
```
```
error: lint:invalid-argument-type: Class `HasX` cannot be used as the second argument to `issubclass`
--> /src/mdtest_snippet.py:12:8
|
10 | reveal_type(arg) # revealed: ~HasX
11 |
12 | if issubclass(arg2, HasX): # error: [invalid-argument-type]
| ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime
13 | reveal_type(arg2) # revealed: type[HasX]
14 | else:
|
info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable
--> /src/mdtest_snippet.py:3:7
|
1 | from typing_extensions import Protocol, reveal_type
2 |
3 | class HasX(Protocol):
| ^^^^^^^^^^^^^^ `HasX` declared here
4 | x: int
|
info: A protocol class can only be used in `issubclass` checks if it is decorated with `@typing.runtime_checkable` or `@typing_extensions.runtime_checkable`
info: See https://docs.python.org/3/library/typing.html#typing.runtime_checkable
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:13:9
|
12 | if issubclass(arg2, HasX): # error: [invalid-argument-type]
13 | reveal_type(arg2) # revealed: type[HasX]
| ^^^^^^^^^^^^^^^^^ `type[HasX]`
14 | else:
15 | reveal_type(arg2) # revealed: type & ~type[HasX]
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:15:9
|
13 | reveal_type(arg2) # revealed: type[HasX]
14 | else:
15 | reveal_type(arg2) # revealed: type & ~type[HasX]
| ^^^^^^^^^^^^^^^^^ `type & ~type[HasX]`
16 | from typing import runtime_checkable
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:24:9
|
22 | def f(arg: object):
23 | if isinstance(arg, RuntimeCheckableHasX): # no error!
24 | reveal_type(arg) # revealed: RuntimeCheckableHasX
| ^^^^^^^^^^^^^^^^ `RuntimeCheckableHasX`
25 | else:
26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:26:9
|
24 | reveal_type(arg) # revealed: RuntimeCheckableHasX
25 | else:
26 | reveal_type(arg) # revealed: ~RuntimeCheckableHasX
| ^^^^^^^^^^^^^^^^ `~RuntimeCheckableHasX`
27 | @runtime_checkable
28 | class OnlyMethodMembers(Protocol):
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:33:9
|
31 | def f(arg1: type, arg2: type):
32 | if issubclass(arg1, RuntimeCheckableHasX): # TODO: should emit an error here (has non-method members)
33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
| ^^^^^^^^^^^^^^^^^ `type[RuntimeCheckableHasX]`
34 | else:
35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:35:9
|
33 | reveal_type(arg1) # revealed: type[RuntimeCheckableHasX]
34 | else:
35 | reveal_type(arg1) # revealed: type & ~type[RuntimeCheckableHasX]
| ^^^^^^^^^^^^^^^^^ `type & ~type[RuntimeCheckableHasX]`
36 |
37 | if issubclass(arg2, OnlyMethodMembers): # no error!
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:38:9
|
37 | if issubclass(arg2, OnlyMethodMembers): # no error!
38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers]
| ^^^^^^^^^^^^^^^^^ `type[OnlyMethodMembers]`
39 | else:
40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
|
```
```
info: revealed-type: Revealed type
--> /src/mdtest_snippet.py:40:9
|
38 | reveal_type(arg2) # revealed: type[OnlyMethodMembers]
39 | else:
40 | reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
| ^^^^^^^^^^^^^^^^^ `type & ~type[OnlyMethodMembers]`
|
```

View File

@@ -56,12 +56,12 @@ error: lint:invalid-return-type: Return type does not match returned value
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:7:22
|
6 | # error: [invalid-return-type]
7 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
8 | if cond:
9 | return 1
|
@@ -69,12 +69,12 @@ error: lint:invalid-return-type
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:12:22
|
11 | # error: [invalid-return-type]
12 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
13 | if cond:
14 | raise ValueError()
|
@@ -82,12 +82,12 @@ error: lint:invalid-return-type
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:17:22
|
16 | # error: [invalid-return-type]
17 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
18 | if cond:
19 | cond = False
|

View File

@@ -35,12 +35,12 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty
# Diagnostics
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.py:2:12
|
1 | # error: [invalid-return-type]
2 | def f() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
3 | 1
|

View File

@@ -45,12 +45,12 @@ error: lint:invalid-return-type: Return type does not match returned value
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.pyi:6:14
|
5 | # error: [invalid-return-type]
6 | def foo() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
7 | print("...")
8 | ...
|
@@ -58,12 +58,12 @@ error: lint:invalid-return-type
```
```
error: lint:invalid-return-type
error: lint:invalid-return-type: Function can implicitly return `None`, which is not assignable to return type `int`
--> /src/mdtest_snippet.pyi:11:14
|
10 | # error: [invalid-return-type]
11 | def foo() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
| ^^^
12 | f"""{foo} is a function that ..."""
13 | ...
|

View File

@@ -33,28 +33,30 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
--> /src/mdtest_snippet.py:12:1
|
11 | # error: [unsupported-bool-conversion]
12 | 10 < Comparable() < 20
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^
13 | # error: [unsupported-bool-conversion]
14 | 10 < Comparable() < Comparable()
|
info: `__bool__` on `NotBoolable` must be callable
```
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
--> /src/mdtest_snippet.py:14:1
|
12 | 10 < Comparable() < 20
13 | # error: [unsupported-bool-conversion]
14 | 10 < Comparable() < Comparable()
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^^
15 |
16 | Comparable() < Comparable() # fine
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -0,0 +1,46 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: semantic_syntax_errors.md - Semantic syntax error diagnostics - `async` comprehensions in synchronous comprehensions - Python 3.10
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md
---
# Python source files
## mdtest_snippet.py
```
1 | async def elements(n):
2 | yield n
3 |
4 | async def f():
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)"
6 | return {n: [x async for x in elements(n)] for n in range(3)}
7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)]
9 | async def f():
10 | [x for x in [1]] and [x async for x in elements(1)]
11 |
12 | async def f():
13 | def g():
14 | pass
15 | [x async for x in elements(1)]
```
# Diagnostics
```
error: invalid-syntax
--> /src/mdtest_snippet.py:6:19
|
4 | async def f():
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax...
6 | return {n: [x async for x in elements(n)] for n in range(3)}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.10 (syntax was added in 3.11)
7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)]
|
```

View File

@@ -0,0 +1,33 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: shadowing.md - Shadowing - Implicit class shadowing
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C: ...
2 |
3 | C = 1 # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment: Implicit shadowing of class `C`
--> /src/mdtest_snippet.py:3:1
|
1 | class C: ...
2 |
3 | C = 1 # error: [invalid-assignment]
| ^
|
info: Annotate to make it explicit if this is intentional
```

View File

@@ -0,0 +1,33 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: shadowing.md - Shadowing - Implicit function shadowing
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/shadowing.md
---
# Python source files
## mdtest_snippet.py
```
1 | def f(): ...
2 |
3 | f = 1 # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment: Implicit shadowing of function `f`
--> /src/mdtest_snippet.py:3:1
|
1 | def f(): ...
2 |
3 | f = 1 # error: [invalid-assignment]
| ^
|
info: Annotate to make it explicit if this is intentional
```

View File

@@ -34,14 +34,15 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]`
--> /src/mdtest_snippet.py:15:1
|
14 | # error: [unsupported-bool-conversion]
15 | a < b < b
| ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
| ^^^^^
16 |
17 | a < b # fine
|
info: `__bool__` on `NotBoolable | Literal[False]` must be callable
```

View File

@@ -26,12 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
# Diagnostics
```
error: lint:unsupported-bool-conversion
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
--> /src/mdtest_snippet.py:9:1
|
8 | # error: [unsupported-bool-conversion]
9 | (A(),) == (A(),)
| ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
| ^^^^^^^^^^^^^^^^
|
info: `__bool__` on `NotBoolable` must be callable
```

View File

@@ -0,0 +1,30 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Exactly too few values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
# Python source files
## mdtest_snippet.py
```
1 | a, b = (1,) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment: Not enough values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1,) # error: [invalid-assignment]
| ^^^^ ---- Got 1
| |
| Expected 2
|
```

View File

@@ -3,7 +3,7 @@ source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unpacking.md - Unpacking - Too many values to unpack
mdtest name: unpacking.md - Unpacking - Exactly too many values to unpack
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
---
@@ -18,11 +18,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Too many values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
| ^^^^ Too many values to unpack (expected 2, got 3)
| ^^^^ --------- Got 3
| |
| Expected 2
|
```

View File

@@ -18,11 +18,11 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
# Diagnostics
```
error: lint:not-iterable
error: lint:not-iterable: Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
--> /src/mdtest_snippet.py:1:8
|
1 | a, b = 1 # error: [not-iterable]
| ^ Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
| ^
|
```

View File

@@ -12,17 +12,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpack
## mdtest_snippet.py
```
1 | a, b = (1,) # error: [invalid-assignment]
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
```
# Diagnostics
```
error: lint:invalid-assignment
error: lint:invalid-assignment: Not enough values to unpack
--> /src/mdtest_snippet.py:1:1
|
1 | a, b = (1,) # error: [invalid-assignment]
| ^^^^ Not enough values to unpack (expected 2, got 1)
1 | [a, *b, c, d] = (1, 2) # error: [invalid-assignment]
| ^^^^^^^^^^^^^ ------ Got 2
| |
| Expected 3 or more
|
```

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