Compare commits

...

377 Commits

Author SHA1 Message Date
Dhruv Manilawala
a2b3c6217b Resolve editor-specified config file once 2025-01-15 18:25:53 +05:30
Dhruv Manilawala
6aef4ad008 Fix LSP show message macro to allow format args (#15487)
## Summary

This PR fixes the `show_*_msg` macros to pass all the tokens instead of
just a single token. This allows for using various expressions right in
the macro similar to how it would be in `format_args!`.

## Test Plan

`cargo clippy`
2025-01-15 08:11:49 +00:00
Micha Reiser
18d5dbfb7f Remove workspace support (#15472) 2025-01-15 09:03:38 +01:00
Dhruv Manilawala
bec8441cf5 Use tool specific function to perform exclude checks (#15486)
## Summary

This PR creates separate functions to check whether the document path is
excluded for linting or formatting. The main motivation is to avoid the
double `Option` for the call sites and makes passing the correct
settings simpler.
2025-01-15 13:18:46 +05:30
InSync
aefb607405 [red-knot] Migrate is_equivalent_to unit tests to Markdown tests (#15470)
## Summary

Part of #15397, built on top of #15469.

## Test Plan

Markdown tests.
2025-01-14 18:57:23 +00:00
Alex Waygood
bcf0a715c2 [red-knot] Corrections and improvements to intersection simplification (#15475) 2025-01-14 18:15:38 +00:00
InSync
5ed7b55b15 [red-knot] Migrate is_subtype_of unit tests to Markdown tests (#15469)
## Summary

Part of #15397.

## Test Plan

Markdown tests.

---------

Co-authored-by: David Peter <mail@david-peter.de>
2025-01-14 15:57:24 +01:00
David Peter
8aac69bb2e [red-knot] Add boundness and declaredness tests (#15453)
## Summary

This changeset adds new tests for public uses of symbols,
considering all possible declaredness and boundness states.

Note that this is a mere documentation of the current behavior. There is
still an [open ticket] questioning some of these choices (or unintential
behaviors).

## Test plan

Made sure that the respective test fails if I add the questionable case
again in `symbol_by_id`:

```rs
Symbol::Type(inferred_ty, Boundness::Bound) => {
    Symbol::Type(inferred_ty, Boundness::Bound)
}
```

[open ticket]: https://github.com/astral-sh/ruff/issues/14297
2025-01-14 13:07:16 +01:00
Tom Kuson
9dfc61bf09 [flake8-pytest-style] Tweak documentation and message (#15465) 2025-01-14 08:47:45 +01:00
Tom Kuson
369cbb5424 [flake8-builtins] Improve A005 documentation (#15466) 2025-01-14 08:42:13 +01:00
Garrett Reynolds
dc491e8ade [ruff] Fix false positive on global keyword (RUF052) (#15235) 2025-01-14 08:36:40 +01:00
Wei Lee
a2dc8c93ef [airflow] Replace typo "security_managr" as "security_manager" (AIR303) (#15463)
## Summary

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

Replace typo "security_managr" in AIR303 as "security_manager"

## Test Plan

<!-- How was it tested? -->

a test fixture has been updated
2025-01-13 18:38:09 -05:00
Carl Meyer
d54c19b983 [red-knot] remove CallOutcome::Cast variant (#15461)
## Summary

Simplification follow-up to #15413.

There's no need to have a dedicated `CallOutcome` variant for every
known function, it's only necessary if the special-cased behavior of the
known function includes emitting extra diagnostics. For `typing.cast`,
there's no such need; we can use the regular `Callable` outcome variant,
and update the return type according to the cast. (This is the same way
we already handle `len`.)

One reason to avoid proliferating unnecessary `CallOutcome` variants is
that currently we have to explicitly add emitting call-binding
diagnostics, for each outcome variant. So we were previously wrongly
silencing any binding diagnostics on calls to `typing.cast`. Fixing this
revealed a separate bug, that we were emitting a bogus error anytime
more than one keyword argument mapped to a `**kwargs` parameter. So this
PR also adds test and fix for that bug.

## Test Plan

Existing `cast` tests pass unchanged, added new test for `**kwargs` bug.
2025-01-13 10:58:53 -08:00
Micha Reiser
5ad546f187 Change ProgramSettings::python_platform to return a reference (#15457) 2025-01-13 16:23:34 +01:00
InSync
47d0a8ba96 [flake8-pytest-style] Test function parameters with default arguments (PT028) (#15449) 2025-01-13 13:40:54 +01:00
Dhruv Manilawala
56b14454dc Display context for ruff.configuration errors (#15452)
## Summary

I noticed this while trying out
https://github.com/astral-sh/ruff-vscode/issues/665 that we use the
`Display` implementation to show the error which hides the context. This
PR changes it to use the `Debug` implementation and adds the message as
a context.

## Test Plan

**Before:**

```
   0.001228084s ERROR main ruff_server::session::index::ruff_settings: Unable to find editor-specified configuration file: Failed to parse /private/tmp/hatch-test/ruff.toml
```

**After:**

```
   0.002348750s ERROR main ruff_server::session::index::ruff_settings: Unable to load editor-specified configuration file

Caused by:
    0: Failed to parse /private/tmp/hatch-test/ruff.toml
    1: TOML parse error at line 2, column 18
         |
       2 | extend-select = ["ASYNC101"]
         |                  ^^^^^^^^^^
       Unknown rule selector: `ASYNC101`
```
2025-01-13 15:43:20 +05:30
David Peter
eb3cb8d4b2 [red-knot] Use BitSet::union for merging of declarations (#15451)
## Summary

In `SymbolState` merging, use `BitSet::union` instead of inserting
declarations one by one. This used to be the case but was changed in
https://github.com/astral-sh/ruff/pull/15019 because we had to iterate
over declarations anyway.

This is an alternative to https://github.com/astral-sh/ruff/pull/15419
by @MichaReiser. It's similar in performance, but a bit more
declarative and less imperative.
2025-01-13 11:10:42 +01:00
InSync
6f35a4d8d5 [fastapi] Handle parameters with Depends correctly (FAST003) (#15364)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-13 08:51:02 +00:00
cake-monotone
82d06a198d [red-knot] Remove duplicate property test (#15450)
## Summary

Follow-up PR from https://github.com/astral-sh/ruff/pull/15415  🥲 

The exact same property test already exists:
`intersection_assignable_to_both` and
`all_type_pairs_can_be_assigned_from_their_intersection`

## Test Plan

`cargo test -p red_knot_python_semantic -- --ignored
types::property_tests::flaky`
2025-01-13 08:18:41 +01:00
InSync
70c3be88b9 [flake8-pie] Reuse parsed tokens (PIE800) (#15438)
## Summary

Follow-up to #15394. See [this review
comment](https://github.com/astral-sh/ruff/pull/15394#discussion_r1910526741).

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-01-12 21:03:11 -05:00
Tom Kuson
347ab5b47a [flake8-pytest-style] Implement pytest.warns diagnostics (PT029, PT030, PT031) (#15444)
## Summary

Implements upstream diagnostics `PT029`, `PT030`, `PT031` that function
as pytest.warns corollaries of `PT010`, `PT011`, `PT012` respectively.
Most of the implementation and documentation is designed to mirror those
existing diagnostics.

Closes #14239

## Test Plan

Tests for `PT029`, `PT030`, `PT031` largely copied from `PT010`,
`PT011`, `PT012` respectively.

`cargo nextest run`

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2025-01-13 01:46:59 +00:00
renovate[bot]
fa11b08766 Update dependency @types/react to v19.0.6 (#15448) 2025-01-13 01:11:51 +00:00
renovate[bot]
6f3e4e5062 Update NPM Development dependencies to v19.0.5 (#15445) 2025-01-12 20:06:02 -05:00
Charlie Marsh
2454305ef8 [flake8-pathlib] Fix --select for os-path-dirname (PTH120) (#15446)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15439.
2025-01-13 00:55:46 +00:00
InSync
4f37fdeff2 [flake8-bandit] Check for builtins instead of builtin (S102, PTH123) (#15443)
## Summary

Resolves #15442.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-01-12 19:45:31 -05:00
InSync
d1666fbbee [red-knot] Add AlwaysTruthy and AlwaysFalsy to knot_extensions (#15437)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-12 17:00:57 +00:00
Alex Waygood
06b7f4495e [red-knot] Minor improvements to KnownFunction API (#15441)
A small PR to reduce some of the code duplication between the various
branches, make it a little more readable and move the API closer to what
we already have for `KnownClass`
2025-01-12 16:06:31 +00:00
Alex Waygood
c8795fcb37 [red-knot] Minor improvements to property_tests.rs (#15440) 2025-01-12 13:55:18 +00:00
cake-monotone
ccfde37619 [red-knot] Add Property Tests for Intersection and Union (#15415) 2025-01-12 13:21:29 +00:00
InSync
6ae3e8f8d7 [red-knot] Support cast (#15413)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-12 13:05:45 +00:00
renovate[bot]
60d7a464fb Update Rust crate colored to v3 (#15434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:52:52 +00:00
renovate[bot]
c0259e7bf2 Update dependency ruff to v0.9.1 (#15432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-11 17:18:38 +00:00
renovate[bot]
22edee2353 Update pre-commit dependencies (#15433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-01-11 17:18:13 +00:00
renovate[bot]
7d20277111 Update Rust crate libcst to v1.6.0 (#15431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:14:44 +00:00
renovate[bot]
bce07f6564 Update Rust crate bitflags to v2.7.0 (#15430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:14:19 +00:00
renovate[bot]
8ea6605a6d Update NPM Development dependencies (#15428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:13:32 +00:00
renovate[bot]
d323f2019b Update dependency uuid to v11.0.5 (#15427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:13:17 +00:00
renovate[bot]
ad883d9b31 Update Rust crate uuid to v1.11.1 (#15426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:12:54 +00:00
renovate[bot]
7240212d27 Update Rust crate thiserror to v2.0.11 (#15425)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:12:34 +00:00
renovate[bot]
925ee41317 Update Rust crate syn to v2.0.96 (#15424)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:12:23 +00:00
renovate[bot]
78b242fe3f Update Rust crate proc-macro2 to v1.0.93 (#15422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:12:07 +00:00
renovate[bot]
7ed46d0823 Update Rust crate serde_json to v1.0.135 (#15423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:11:48 +00:00
renovate[bot]
bff4edb717 Update Rust crate clap to v4.5.26 (#15420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 17:09:54 +00:00
Dhruv Manilawala
38f873ba52 Remove flatten to improve deserialization error messages (#15414)
## Summary

Closes: #9719  

## Test Plan

**Before:**

```
ruff failed
  Cause: Failed to parse /Users/dhruv/playground/ruff/pyproject.toml
  Cause: TOML parse error at line 22, column 1
   |
22 | [tool.ruff.lint]
   | ^^^^^^^^^^^^^^^^
invalid type: string "false", expected a boolean
```

**After:**

```
ruff failed
  Cause: Failed to parse /Users/dhruv/playground/ruff/pyproject.toml
  Cause: TOML parse error at line 27, column 20
   |
27 | mypy-init-return = "false"
   |                    ^^^^^^^
invalid type: string "false", expected a boolean
```
2025-01-11 22:08:21 +05:30
Micha Reiser
c39ca8fe6d Upgrade Rust toolchain to 1.84.0 (#15408) 2025-01-11 09:51:58 +01:00
David Peter
2d82445794 [red-knot] Simplify unions of T and ~T (#15400)
## Summary

Simplify unions of `T` and `~T` to `object`.

## Test Plan

Adapted existing tests.
2025-01-10 23:00:52 +01:00
David Peter
398f2e8b0c [red-knot] Minor fixes in intersection-types tests (#15410)
## Summary

Minor fixes in intersection-types tests
2025-01-10 22:53:03 +01:00
InSync
232fbc1300 [red-knot] Understand type[Unknown] (#15409)
## Summary

Follow-up to #15194.

## Test Plan

Markdown tests.
2025-01-10 13:25:59 -08:00
Alex Waygood
c82932e580 [red-knot] Refactor KnownFunction::takes_expression_arguments() (#15406) 2025-01-10 19:09:03 +00:00
Micha Reiser
12f86f39a4 Ruff 0.9.1 (#15407) 2025-01-10 19:45:06 +01:00
Micha Reiser
2b28d566a4 Associate a trailing end-of-line comment in a parenthesized implicit concatenated string with the last literal (#15378) 2025-01-10 19:21:34 +01:00
Calum Young
adca7bd95c Remove pygments pin (#15404)
## Summary

The recent release of Pygments
([2.19.1](https://github.com/pygments/pygments/releases/tag/2.19.1))
allows the pinned version to be removed as the PYI alias for Python
syntax highlighting has been removed.

## Test Plan

- Follow the steps outlined in
https://github.com/astral-sh/ruff/blob/main/CONTRIBUTING.md#mkdocs to
get the documentation site running locally.
- Spot test rules pages that have PYI code blocks to ensure that syntax
highlighting remains e.g.
[http://127.0.0.1:8000/ruff/rules/complex-if-statement-in-stub/](http://127.0.0.1:8000/ruff/rules/complex-if-statement-in-stub/).

**Note:** I am unable to test the insiders build but would assume that
it functions locally as I do not have access to MkDocs Insiders, but I
would like to assume that it functions in the same way as the
non-insiders build.
2025-01-10 12:15:13 -05:00
InSync
6b98a26452 [red-knot] Support assert_type (#15194)
## Summary

See #15103.

## Test Plan

Markdown tests and unit tests.
2025-01-10 08:45:02 -08:00
David Peter
c87463842a [red-knot] Move tuple-containing-Never tests to Markdown (#15402)
## Summary

See title.

Part of #15397

## Test Plan

Ran new Markdown test.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-10 15:31:30 +00:00
InSync
c364b586f9 [flake8-pie] Correctly remove wrapping parentheses (PIE800) (#15394)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-10 14:52:32 +00:00
Antoine Dechaume
73d424ee5e Fix outdated doc for handling the default file types with the pre-commit hook (#15401)
Co-authored-by: Antoine DECHAUME <>
2025-01-10 15:49:23 +01:00
Dhruv Manilawala
6e9ff445fd Insert the cells from the start position (#15398)
## Summary

The cause of this bug is from
https://github.com/astral-sh/ruff/pull/12575 which was itself a bug fix
but the fix wasn't completely correct.

fixes: #14768 
fixes: https://github.com/astral-sh/ruff-vscode/issues/644

## Test Plan

Consider the following three cells:

1.
```python
class Foo:
    def __init__(self):
        self.x = 1

    def __str__(self):
        return f"Foo({self.x})"
```

2.
```python
def hello():
    print("hello world")
```

3.
```python
y = 1
```

The test case is moving cell 2 to the top i.e., cell 2 goes to position
1 and cell 1 goes to position 2.

Before this fix, it can be seen that the cells were pushed at the end of
the vector:

```
  12.643269917s  INFO ruff:main ruff_server::edit:📓 Before update: [
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
]
  12.643777667s  INFO ruff:main ruff_server::edit:📓 After update: [
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
]
```

After the fix in this PR, it can be seen that the cells are being pushed
at the correct `start` index:

```
   6.520570917s  INFO ruff:main ruff_server::edit:📓 Before update: [
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
]
   6.521084792s  INFO ruff:main ruff_server::edit:📓 After update: [
    NotebookCell {
        document: TextDocument {
            contents: "def hello():\n    print(\"hello world\")",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "class Foo:\n    def __init__(self):\n        self.x = 1\n\n    def __str__(self):\n        return f\"Foo({self.x})\"",
        },
    },
    NotebookCell {
        document: TextDocument {
            contents: "y = 1",
        },
    },
]
```
2025-01-10 13:11:56 +00:00
David Peter
f2c3ddc5ea [red-knot] Move intersection type tests to Markdown (#15396)
## Summary

[**Rendered version of the new test
suite**](https://github.com/astral-sh/ruff/blob/david/intersection-type-tests/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md)

Moves most of our existing intersection-types tests to a dedicated
Markdown test suite, extends the test coverage, unifies the notation for
these tests, groups tests into a proper structure, and adds some
explanations for various simplification strategies.

This changeset also:
- Adds a new simplification where `~Never` is removed from
intersections.
- Adds a new simplification where adding `~object` simplifies the whole
intersection to `Never`
- Avoids unnecessary assignment-checks between inferred and declared
type. This was added to this changeset to avoid many false positive
errors in this test suite.

Resolves the task described in this old comment
[here](e01da82a5a..e7e432bca2 (r1819924085)).

## Test Plan

Running the new Markdown tests

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-10 14:04:03 +01:00
Dhruv Manilawala
b861551b6a Remove unnecessary backticks (#15393)
Ref: https://github.com/astral-sh/ruff/pull/15367#discussion_r1909448140
2025-01-10 09:22:26 +00:00
Dylan
443bf38565 [ruff] Omit diagnostic for shadowed private function parameters in used-dummy-variable (RUF052) (#15376) 2025-01-10 03:09:25 -06:00
Tom Kuson
23ad319b55 [flake8-bugbear] Improve assert-raises-exception (B017) message (#15389) 2025-01-10 08:48:18 +01:00
InSync
3d9433ca66 [pyupgrade] Handle comments and multiline expressions correctly (UP037) (#15337) 2025-01-10 08:46:01 +01:00
Douglas Creager
baf068361a [red-knot] Consolidate all gradual types into single Type variant (#15386)
Prompted by

> One nit: I think we need to consider `Any` and `Unknown` and `Todo` as
all (gradually) equivalent to each other, and thus `type & Any` and
`type & Unknown` and `type & Todo` as also equivalent. The distinction
between `Any` vs `Unknown` vs `Todo` is entirely about
provenance/debugging, there is no type level distinction. (And I've been
wondering if the `Any` vs `Unknown` distinction is really worth it.)

The thought here is that _most_ places want to treat `Any`, `Unknown`,
and `Todo` identically. So this PR simplifies things by having a single
`Type::Any` variant, and moves the provenance part into a new `AnyType`
type. If you need to treat e.g. `Todo` differently, you still can by
pattern-matching into the `AnyType`. But if you don't, you can just use
`Type::Any(_)`.

(This would also allow us to (more easily) distinguish "unknown via an
unannotated value" from "unknown because of a typing error" should we
want to do that in the future)

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-01-09 21:32:20 -05:00
David Peter
b33cf5baba [red-knot] Move UnionBuilder tests to Markdown (#15374)
## Summary

This moves almost all of our existing `UnionBuilder` tests to a
Markdown-based test suite.

I see how this could be a more controversial change, since these tests
where written specifically for `UnionBuilder`, and by creating the union
types using Python type expressions, we add an additional layer on top
(parsing and inference of these expressions) that moves these tests away
from clean unit tests more in the direction of integration tests. Also,
there are probably a few implementation details of `UnionBuilder` hidden
in the test assertions (e.g. order of union elements after
simplifications).

That said, I think we would like to see all those properties that are
being tested here from *any* implementation of union types. And the
Markdown tests come with the usual advantages:

- More consice
- Better readability
- No re-compiliation when working on tests
- Easier to add additional explanations and structure to the test suite

This changeset adds a few additional tests, but keeps the logic of the
existing tests except for a few minor modifications for consistency.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>
2025-01-09 21:45:06 +01:00
Dylan
b0905c4b04 [pycodestyle] Handle each cell separately for too-many-newlines-at-end-of-file (W391) (#15308)
Jupyter notebooks are converted into source files by joining with
newlines, which confuses the check [too-many-newlines-at-end-of-file
(W391)](https://docs.astral.sh/ruff/rules/too-many-newlines-at-end-of-file/#too-many-newlines-at-end-of-file-w391).
This PR introduces logic to apply the check cell-wise (and, in
particular, correctly handles empty cells.)

Closes #13763
2025-01-09 10:50:39 -06:00
Micha Reiser
d0b2bbd55e Release 0.9.0 (#15371)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-09 14:53:08 +01:00
Josiah Outram Halstead
8628f169e9 [ruff] Stop parsing diagnostics from other sources for code action requests (#15373) 2025-01-09 14:38:13 +01:00
InSync
8bc11c49b2 [flake8-django] Recognize other magic methods (DJ012) (#15365) 2025-01-09 14:36:42 +01:00
David Peter
bf5b0c2688 [red-knot] Minor refactor of red_knot_vendored/build.rs (#15372)
## Summary

See https://github.com/astral-sh/ruff/pull/15370#discussion_r1908611461:

- Rename `zip_dir` to `write_zipped_typeshed_to` to clarify it's not a
generic function (anymore)
- Hard-code `TYPESHED_SOURCE_DIR` instead of using a `directory_path`
argument
2025-01-09 12:23:42 +00:00
David Peter
097aa04c04 [red-knot] Typeshed patching: use build.rs instead of workflow (#15370)
## Summary

The symlink-approach in the typeshed-sync workflow caused some problems
on Windows, even though it seemed to work fine in CI:

https://github.com/astral-sh/ruff/pull/15138#issuecomment-2578642129

Here, we rely on `build.rs` to patch typeshed instead, which allows us
to get rid of the modifications in the workflow (thank you
@MichaReiser for the idea).

## Test Plan

- Made sure that changes to `knot_extensions.pyi` result in a recompile
  of `red_knot_vendored`.
2025-01-09 11:50:32 +01:00
Dhruv Manilawala
f706c3fdf2 Add f-string formatting to the docs (#15367)
Revive https://github.com/astral-sh/ruff/pull/15341 as it got removed
from the latest rebase in https://github.com/astral-sh/ruff/pull/15238.
2025-01-09 10:20:06 +01:00
Micha Reiser
29f6653318 [ruff] Stabilize useless-if-else (RUF034) (#15351) 2025-01-09 10:20:06 +01:00
Micha Reiser
d645525afc [pylint]: Stabilize boolean-chained-comparison (PLR1716) (#15354) 2025-01-09 10:20:06 +01:00
Micha Reiser
6dcf7b35b9 [ruff] Stabilize post-init-default (RUF033) (#15352) 2025-01-09 10:20:06 +01:00
Micha Reiser
943d4fc160 Update formatter preview documentation (#15349) 2025-01-09 10:20:06 +01:00
Micha Reiser
3ea4c63d2c [flake8-pyi] Stabilize: include all python file types for PYI006 (#15340) 2025-01-09 10:20:06 +01:00
Micha Reiser
8e8a07144d [flake8-pyi]: Stabilize: Provide more automated fixes for duplicate-union-members (PYI016) (#15342) 2025-01-09 10:20:06 +01:00
Micha Reiser
225dd0a027 [ruff] Stabilize: Detect attrs dataclasses (RUF008, RUF009) (#15345) 2025-01-09 10:20:06 +01:00
Micha Reiser
52aeb8ae11 [flake8-pyi] Stabilize autofix for redundant-numeric-union (PYI041) (#15343) 2025-01-09 10:20:06 +01:00
Micha Reiser
71b6ac81a6 Remove unnecessary PreviewMode::Enabled in tests (#15344) 2025-01-09 10:20:06 +01:00
Alex Waygood
75fc2c3116 [ruff-0.9] Stabilise two flake8-builtins rules (#15322) 2025-01-09 10:20:06 +01:00
Micha Reiser
9c4d124ba0 [pycodestyle] Stabilize: Exempt pytest.importorskip calls (E402) (#15338) 2025-01-09 10:20:06 +01:00
InSync
8c620b9b4b [flake8-pytest-style] Stabilize "Detect more pytest.mark.parametrize calls" (PT006) (#15327)
Co-authored-by: Micha Reiser <micha@reiser.io>
Resolves #15324. Stabilizes the behavior changes introduced in #14515.
2025-01-09 10:20:06 +01:00
Dylan
1eda27d1a5 [ruff-0.9] Stabilize decimal-from-float-literal (RUF032) (#15333) 2025-01-09 10:20:06 +01:00
Alex Waygood
aaa86cf38d [ruff-0.9] Stabilise slice-to-remove-prefix-or-suffix (FURB188) (#15329)
Stabilise [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`) for the Ruff 0.9 release.

This is a stylistic rule, but I think it's a pretty uncontroversial one. There are no open issues or PRs regarding it and it's been in preview for a while now.
2025-01-09 10:20:06 +01:00
Micha Reiser
7821206b7b Update Black deviations to reflect 2025 style changes (#15127) 2025-01-09 10:20:06 +01:00
Micha Reiser
b76d05e283 Remove formatter incompatibility warning for ISC001 (#15123) 2025-01-09 10:20:06 +01:00
Micha Reiser
424b720c19 Ruff 2025 style guide (#13906)
Closes #13371
2025-01-09 10:20:06 +01:00
Carl Meyer
a95deec00f [red-knot] handle synthetic 'self' argument in call-binding diagnostics (#15362) 2025-01-09 00:36:48 -08:00
InSync
21aa12a073 [red-knot] More precise inference for classes with non-class metaclasses (#15138)
## Summary

Resolves #14208.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2025-01-09 00:34:04 +00:00
Douglas Creager
5f5eb7c0dd [red-knot] Print non-string panic payloads and (sometimes) backtraces (#15363)
More refinements to the panic messages for failing mdtests to mimic the
output of the default panic hook more closely:

- We now print out `Box<dyn Any>` if the panic payload is not a string
(which is typically the case for salsa panics).
- We now include the panic's backtrace if you set the `RUST_BACKTRACE`
environment variable.
2025-01-08 18:12:16 -05:00
David Peter
b6562ed57e [red-knot] Property test workflow: Fix issue label, link to CI run (#15361)
## Summary

See title. Had to make a minor change, because it failed the zizmor
pre-commit check otherwise:

```
error[template-injection]: code injection via template expansion
  --> /home/shark/ruff/.github/workflows/daily_fuzz.yaml:68:9
   |
68 |          - uses: actions/github-script@v7
   |  __________^
69 | |          with:
70 | |            github-token: ${{ secrets.GITHUB_TOKEN }}
71 | |            script: |
   | | ___________^
72 | ||             await github.rest.issues.create({
...  ||
77 | ||               labels: ["bug", "parser", "fuzzer"],
78 | ||             })
   | ||               ^
   | ||_______________|
   |  |_______________this step
   |                  github.server_url may expand into attacker-controllable code
   |
   = note: audit confidence → High
```
2025-01-08 22:47:16 +01:00
David Peter
4fd82d5f35 [red-knot] Property test improvements (#15358)
## Summary

- Add a workflow to run property tests on a daily basis (based on
`daily_fuzz.yaml`)
- Mark `assignable_to_is_reflexive` as flaky (related to #14899)
- Add new (failing) `intersection_assignable_to_both` test (also related
to #14899)

## Test Plan

Ran:

```bash
export QUICKCHECK_TESTS=100000
while cargo test --release -p red_knot_python_semantic -- \
  --ignored types::property_tests::stable; do :; done
```

Observed successful property_tests CI run
2025-01-08 22:24:57 +01:00
David Peter
beb8e2dfe0 [red-knot] More comprehensive is_assignable_to tests (#15353)
## Summary

This changeset migrates all existing `is_assignable_to` tests to a
Markdown-based test. It also increases our test coverage in a hopefully
meaningful way (not claiming to be complete in any sense). But at least
I found and fixed one bug while doing so.

## Test Plan

Ran property tests to make sure the new test succeeds after fixing it.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-08 20:25:08 +01:00
Alex Waygood
88d07202c1 [red-knot] Reduce Name clones in call signature checking (#15335) 2025-01-08 18:29:35 +00:00
Douglas Creager
2ca31e4b43 Fall back on previous panic hook when not in catch_unwind wrapper (#15319)
This fixes #15317. Our `catch_unwind` wrapper installs a panic hook that
captures (the rendered contents of) the panic info when a panic occurs.
Since the intent is that the caller will render the panic info in some
custom way, the hook silences the default stderr panic output.

However, the panic hook is a global resource, so if any one thread was
in the middle of a `catch_unwind` call, we would silence the default
panic output for _all_ threads.

The solution is to also keep a thread local that indicates whether the
current thread is in the middle of our `catch_unwind`, and to fall back
on the default panic hook if not.

## Test Plan

Artificially added an mdtest parse error, ran tests via `cargo test -p
red_knot_python_semantic` to run a large number of tests in parallel.
Before this patch, the panic message was swallowed as reported in
#15317. After, the panic message was shown.
2025-01-08 11:34:51 -05:00
Auguste Lalande
450d4e0e0c [pylint] Fix unreachable infinite loop (PLW0101) (#15278)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Fix infinite loop issue reported here #15248.
The issue was caused by the break inside the if block, which caused the
flow to exit in an unforeseen way. This caused other issues, eventually
leading to an infinite loop.

Resolves #15248. Resolves #15336.

## Test Plan

Added failing code to fixture.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-01-08 09:45:04 -06:00
Alex Waygood
7284d68157 fix invalid syntax in workflow file (#15357) 2025-01-08 15:34:33 +00:00
InSync
3820af2f1b [pycodestyle] Avoid false positives related to type aliases (E252) (#15356) 2025-01-08 16:04:08 +01:00
Alex Waygood
ee9a912f47 [flake8-builtins] Disapply A005 to stub files (#15350) 2025-01-08 12:59:27 +00:00
Dhruv Manilawala
1447553bc2 Improve logging system using logLevel, avoid trace value (#15232)
## Summary

Refer to the VS Code PR
(https://github.com/astral-sh/ruff-vscode/pull/659) for details on the
change.

This PR changes the following:

1. Add tracing span for both request (request id and method name) and
notification (method name) handler
2. Remove the `RUFF_TRACE` environment variable. This was being used to
turn on / off logging for the server
3. Similarly, remove reading the `trace` value from the initialization
options
4. Remove handling the `$/setTrace` notification
5. Remove the specialized `TraceLogWriter` used for Zed and VS Code
(https://github.com/astral-sh/ruff/pull/12564)

Regarding the (5) for the Zed editor, the reason that was implemented
was because there was no way of looking at the stderr messages in the
editor which has been changed. Now, it captures the stderr as part of
the "Server Logs".
(82492d74a8/crates/language_tools/src/lsp_log.rs (L548-L552))

### Question

Regarding (1), I think having just a simple trace level message should
be good for now as the spans are not hierarchical. This could be tackled
with #12744. The difference between the two:

<details><summary>Using <code>tracing::trace</code></summary>
<p>

```
   0.019243416s DEBUG ThreadId(08) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.026398750s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.026802125s TRACE ruff:main ruff_server::server::api: Received notification "textDocument/didOpen"
   0.026930666s TRACE ruff:main ruff_server::server::api: Received notification "textDocument/didOpen"
   0.026962333s TRACE ruff:main ruff_server::server::api: Received request "textDocument/diagnostic" (1)
   0.027042875s TRACE ruff:main ruff_server::server::api: Received request "textDocument/diagnostic" (2)
   0.027097500s TRACE ruff:main ruff_server::server::api: Received request "textDocument/codeAction" (3)
   0.027107458s DEBUG ruff:worker:0 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.027123541s DEBUG ruff:worker:3 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/organize_imports.py
   0.027514875s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
   0.285689833s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/codeAction" (4)
  45.741101666s TRACE     ruff:main ruff_server::server::api: Received notification "textDocument/didClose"
  47.108745500s TRACE     ruff:main ruff_server::server::api: Received notification "textDocument/didOpen"
  47.109802041s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/diagnostic" (5)
  47.109926958s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/codeAction" (6)
  47.110027791s DEBUG ruff:worker:6 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  51.863679125s TRACE     ruff:main ruff_server::server::api: Received request "textDocument/hover" (7)
```

</p>
</details> 

<details><summary>Using <code>tracing::trace_span</code></summary>
<p>

Only logging the enter event:

```
   0.018638750s DEBUG ThreadId(11) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.025895791s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.026378791s TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   0.026531208s TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   0.026567583s TRACE ruff:main request{id=1 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   0.026652541s TRACE ruff:main request{id=2 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   0.026711041s DEBUG ruff:worker:2 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/organize_imports.py
   0.026729166s DEBUG ruff:worker:1 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.027023083s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
   5.197554750s TRACE     ruff:main notification{method="textDocument/didClose"}: ruff_server::server::api: enter
   6.534458000s TRACE     ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   6.535027958s TRACE     ruff:main request{id=3 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   6.535271166s DEBUG ruff:worker:3 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/organize_imports.py
   6.544240583s TRACE     ruff:main request{id=4 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.049692458s TRACE     ruff:main request{id=5 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.508142541s TRACE     ruff:main request{id=6 method="textDocument/hover"}: ruff_server::server::api: enter
   7.872421958s TRACE     ruff:main request{id=7 method="textDocument/hover"}: ruff_server::server::api: enter
   8.024498583s TRACE     ruff:main request{id=8 method="textDocument/codeAction"}: ruff_server::server::api: enter
  13.895063666s TRACE     ruff:main request{id=9 method="textDocument/codeAction"}: ruff_server::server::api: enter
  14.774706083s TRACE     ruff:main request{id=10 method="textDocument/hover"}: ruff_server::server::api: enter
  16.058918958s TRACE     ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
  16.060562208s TRACE     ruff:main request{id=11 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  16.061109083s DEBUG ruff:worker:8 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  21.561742875s TRACE     ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
  21.563573791s TRACE     ruff:main request{id=12 method="textDocument/diagnostic"}: ruff_server::server::api: enter
  21.564206750s DEBUG ruff:worker:4 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  21.826691375s TRACE     ruff:main request{id=13 method="textDocument/codeAction"}: ruff_server::server::api: enter
  22.091080125s TRACE     ruff:main request{id=14 method="textDocument/codeAction"}: ruff_server::server::api: enter
```

</p>
</details> 


**Todo**

- [x] Update documentation (I'll be adding a troubleshooting section
under "Editors" as a follow-up which is for all editors)
- [x] Check for backwards compatibility. I don't think this should break
backwards compatibility as it's mainly targeted towards improving the
debugging experience.

~**Before I go on to updating the documentation, I'd appreciate initial
review on the chosen approach.**~

resolves: #14959 

## Test Plan

Refer to the test plan in
https://github.com/astral-sh/ruff-vscode/pull/659.

Example logs at `debug` level:

```
   0.010770083s DEBUG ThreadId(15) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.018101916s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.018559916s DEBUG ruff:worker:4 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.018992375s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
  23.408802375s DEBUG ruff:worker:11 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
  24.329127416s DEBUG  ruff:worker:6 ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
```

Example logs at `trace` level:

```
   0.010296375s DEBUG ThreadId(13) ruff_server::session::index::ruff_settings: Ignored path via `exclude`: /Users/dhruv/playground/ruff/.vscode
   0.017422583s  INFO main ruff_server::session::index: Registering workspace: /Users/dhruv/playground/ruff
   0.018034458s TRACE ruff:main notification{method="textDocument/didOpen"}: ruff_server::server::api: enter
   0.018199708s TRACE ruff:worker:0 request{id=1 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   0.018251167s DEBUG ruff:worker:0 request{id=1 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   0.018528708s  INFO     ruff:main ruff_server::server: Configuration file watcher successfully registered
   1.611798417s TRACE ruff:worker:1 request{id=2 method="textDocument/codeAction"}: ruff_server::server::api: enter
   1.861757542s TRACE ruff:worker:4 request{id=3 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.027361792s TRACE ruff:worker:2 request{id=4 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.851361500s TRACE ruff:worker:5 request{id=5 method="textDocument/codeAction"}: ruff_server::server::api: enter
   7.901690875s TRACE     ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
   7.903063167s TRACE ruff:worker:10 request{id=6 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   7.903183500s DEBUG ruff:worker:10 request{id=6 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   8.702385292s TRACE      ruff:main notification{method="textDocument/didChange"}: ruff_server::server::api: enter
   8.704106625s TRACE  ruff:worker:3 request{id=7 method="textDocument/diagnostic"}: ruff_server::server::api: enter
   8.704304875s DEBUG  ruff:worker:3 request{id=7 method="textDocument/diagnostic"}: ruff_server::resolve: Included path via `include`: /Users/dhruv/playground/ruff/lsp/play.py
   8.966853458s TRACE  ruff:worker:9 request{id=8 method="textDocument/codeAction"}: ruff_server::server::api: enter
   9.229622792s TRACE  ruff:worker:6 request{id=9 method="textDocument/codeAction"}: ruff_server::server::api: enter
  10.513111583s TRACE  ruff:worker:7 request{id=10 method="textDocument/codeAction"}: ruff_server::server::api: enter
```
2025-01-08 18:18:00 +05:30
Alex Waygood
9a27b37a91 [flake8-builtins] Rename A005 and improve its error message (#15348) 2025-01-08 12:38:34 +00:00
Alex Waygood
487f2f5df0 Spruce up docs for pydoclint rules (#15325) 2025-01-08 12:22:37 +00:00
David Salvisberg
339167d372 [flake8-type-checking] Apply TC008 more eagerly in TYPE_CHECKING blocks and disapply it in stubs (#15180)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-08 12:09:06 +00:00
David Peter
235fdfc57a [red-knot] knot_extensions Python API (#15103)
## Summary

Adds a type-check-time Python API that allows us to create and
manipulate types and to test various of their properties. For example,
this can be used to write a Markdown test to make sure that `A & B` is a
subtype of `A` and `B`, but not of an unrelated class `C` (something
that requires quite a bit more code to do in Rust):
```py
from knot_extensions import Intersection, is_subtype_of, static_assert

class A: ...
class B: ...

type AB = Intersection[A, B]

static_assert(is_subtype_of(AB, A))
static_assert(is_subtype_of(AB, B))

class C: ...
static_assert(not is_subtype_of(AB, C))
```

I think this functionality is also helpful for interactive debugging
sessions, in order to query various properties of Red Knot's type
system. Which is something that otherwise requires a custom Rust unit
test, some boilerplate code and constant re-compilation.

## Test Plan

- New Markdown tests
- Tested the modified typeshed_sync workflow locally
2025-01-08 12:52:07 +01:00
Shaygan Hooshyari
03ff883626 Display Union of Literals as a Literal (#14993)
## Summary

Resolves #14988

Display union of Literals like other type checkers do.

With this change we lose the sorting behavior. And we show the types as
they appeared. So it's deterministic and tests should not be flaky.
This is similar to how Mypy [reveals the
type](https://mypy-play.net/?mypy=latest&python=3.12&gist=51ad03b153bfca3b940d5084345e230f).

In some cases this makes it harder to know what is the order in revealed
type when writing tests but since it's consistent after the test fails
we know the order.

## Test Plan

I adjusted mdtests for this change. Basically merged the int and string
types of the unions.

In cases where we have types other than numbers and strings like this
[one](https://github.com/astral-sh/ruff/pull/14993/files#diff-ac50bce02b9f0ad4dc7d6b8e1046d60dad919ac52d0aeb253e5884f89ea42bfeL51).
We only group the strings and numbers as the issue suggsted.

```
def _(flag: bool, flag2: bool):
    if flag:
        f = 1
    elif flag2:
        f = "foo"
    else:
        def f() -> int:
            return 1
    # error: "Object of type `Literal[1, "foo", f]` is not callable (due to union elements Literal[1], Literal["foo"])"
    # revealed: Unknown | int
    reveal_type(f())
```

[pyright
example](https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAySMApiAIYA2AUNQCYnBQD6AFMJeWgFxQBGYMJQA0UDlwBMvAUICU3alCWYm4nouWamAXigBGDUpKUkqzmimHNYqLoBEwQXavGAziQXXlDVa1lQAWgA%2BTBQYTy9rEBIYAFcQFH0rAGIoMnAQXjsAeT4AKxIAY3wwJngEEigAAyJSCkoAbT1RBydRYABdKsxXKBQwfEKqTj5KStY6WMqYMChYlCQwROMSCBIw3tqyKiaO0S36htawOw7ZZ01U6IA3EioSOl4AVRQAa36Ad0SAH1CYKxud0ozHKJHYflk1CAA)

[mypy
example](https://mypy-play.net/?mypy=latest&python=3.12&gist=31c8bdaa5521860cfeca4b92841cb3b7)

---------

Co-authored-by: Carl Meyer <carl@oddbird.net>
2025-01-08 00:58:38 +00:00
Carl Meyer
fdca2b422e [red-knot] all types are assignable to object (#15332)
## Summary

`Type[Any]` should be assignable to `object`. All types should be
assignable to `object`.

We specifically didn't understand the former; this PR adds a test for
it, and a case to ensure that `Type[Any]` is assignable to anything that
`type` is assignable to (which includes `object`).

This PR also adds a property test that all types are assignable to
object. In order to make it pass, I added a special case to check early
if we are assigning to `object` and just return `true`. In principle,
once we get all the more general cases correct, this special case might
be removable. But having the special case for now allows the property
test to pass.

And we add a property test that all types are subtypes of object. This
failed for the case of an intersection with no positive elements (that
is, a negation type). This really does need to be a special case for
`object`, because there is no other type we can know that a negation
type is a subtype of.

## Test Plan

Added unit test and property test.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-07 15:19:07 -08:00
Dylan
71ad9a2ab1 [ruff] Parenthesize arguments to int when removing int would change semantics in unnecessary-cast-to-int (RUF046) (#15277)
When removing `int` in calls like `int(expr)` we may need to keep
parentheses around `expr` even when it is a function call or subscript,
since there may be newlines in between the function/value name and the
opening parentheses/bracket of the argument.

This PR implements that logic.

Closes #15263

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2025-01-07 21:43:50 +00:00
InSync
3b3c2c5aa4 [eradicate] Correctly handle metadata blocks directly followed by normal blocks (ERA001) (#15330)
## Summary

Resolves #15321.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-01-07 16:22:22 -05:00
Douglas Creager
b2a0d68d70 Narrowing for class patterns in match statements (#15223)
We now support class patterns in a match statement, adding a narrowing
constraint that within the body of that match arm, we can assume that
the subject is an instance of that class.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-07 15:58:12 -05:00
Carl Meyer
f2a86fcfda [red-knot] add call checking (#15200)
## Summary

This implements checking of calls.

I ended up following Micha's original suggestion from back when the
signature representation was first introduced, and flattening it to a
single array of parameters. This turned out to be easier to manage,
because we can represent parameters using indices into that array, and
represent the bound argument types as an array of the same length.

Starred and double-starred arguments are still TODO; these won't be very
useful until we have generics.

The handling of diagnostics is just hacked into `return_ty_result`,
which was already inconsistent about whether it emitted diagnostics or
not; now it's even more inconsistent. This needs to be addressed, but
could be a follow-up.

The new benchmark errors here surface the need for intersection support
in `is_assignable_to`.

Fixes #14161.

## Test Plan

Added mdtests.
2025-01-07 20:39:45 +00:00
Alex Waygood
ac72aca27c Spruce up docs for slice-to-remove-prefix-or-suffix (FURB188) (#15328) 2025-01-07 19:58:35 +00:00
Dylan
a876090715 [internal] Return statements in finally block point to end block for unreachable (PLW0101) (#15276)
Note: `PLW0101` remains in testing rather than preview, so this PR does
not modify any public behavior (hence the title beginning with
`internal` rather than `pylint`, for the sake of the changelog.)

Fixes an error in the processing of `try` statements in the control flow
graph builder.

When processing a try statement, the block following a `return` was
forced to point to the `finally` block. However, if the return was _in_
the `finally` block, this caused the block to point to itself. In the
case where the whole `try-finally` statement was also included inside of
a loop, this caused an infinite loop in the builder for the control flow
graph as it attempted to resolve edges.

Closes #15248

## Test function
### Source
```python
def l():
    while T:
        try:
            while ():
                if 3:
                    break
        finally:
            return
```

### Control Flow Graph
```mermaid
flowchart TD
  start(("Start"))
  return(("End"))
  block0[["`*(empty)*`"]]
  block1[["Loop continue"]]
  block2["return\n"]
  block3[["Loop continue"]]
  block4["break\n"]
  block5["if 3:
                    break\n"]
  block6["while ():
                if 3:
                    break\n"]
  block7[["Exception raised"]]
  block8["try:
            while ():
                if 3:
                    break
        finally:
            return\n"]
  block9["while T:
        try:
            while ():
                if 3:
                    break
        finally:
            return\n"]
  start --> block9
  block9 -- "T" --> block8
  block9 -- "else" --> block0
  block8 -- "Exception raised" --> block7
  block8 -- "else" --> block6
  block7 --> block2
  block6 -- "()" --> block5
  block6 -- "else" --> block2
  block5 -- "3" --> block4
  block5 -- "else" --> block3
  block4 --> block2
  block3 --> block6
  block2 --> return
  block1 --> block9
  block0 --> return
```
2025-01-07 11:26:04 -06:00
InSync
e4139568b8 [ruff] Treat ) as a regex metacharacter (RUF043, RUF055) (#15318)
## Summary

Resolves #15316.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-01-07 12:11:05 -05:00
Charlie Marsh
5567e7c26b Use uv consistently throughout the documentation (#15302)
## Summary

Closes
https://github.com/astral-sh/ruff/issues/15301#issuecomment-2573350821.
2025-01-07 14:43:25 +00:00
Alex Waygood
95294e657c [red-knot] Eagerly normalize type[] types (#15272)
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-01-07 12:53:07 +00:00
InSync
0dc00e63f4 [pyupgrade] Split UP007 to two individual rules for Union and Optional (UP007, UP045) (#15313)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-07 10:22:59 +00:00
David Peter
ce9c4968ae [red-knot] Improve symbol-lookup tracing (#14907)
## Summary

When debugging, I frequently want to know which symbols are being looked
up. `symbol_by_id` adds tracing information, but it only shows the
`ScopedSymbolId`. Since `symbol_by_id` is only called from `symbol`, it
seems reasonable to move the tracing call one level up from
`symbol_by_id` to `symbol`, where we can also show the name of the
symbol.

**Before**:

```
6      └─┐red_knot_python_semantic::types::infer::infer_expression_types{expression=Id(60de), file=/home/shark/tomllib_modified/_parser.py}
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(33)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(123)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(54)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(122)}
6        ┌─┘
6        └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(165)}
6        ┌─┘
6      ┌─┘
6      └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(32)}
6      ┌─┘
6      └─┐red_knot_python_semantic::types::symbol_by_id{symbol=ScopedSymbolId(232)}
6      ┌─┘
6    ┌─┘
6  ┌─┘
6┌─┘
```

**After**:

```
5      └─┐red_knot_python_semantic::types::infer::infer_expression_types{expression=Id(60de), file=/home/shark/tomllib_modified/_parser.py}
5        └─┐red_knot_python_semantic::types::symbol{name="dict"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="dict"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="list"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="list"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="isinstance"}
5        ┌─┘
5        └─┐red_knot_python_semantic::types::symbol{name="isinstance"}
5        ┌─┘
5      ┌─┘
5      └─┐red_knot_python_semantic::types::symbol{name="ValueError"}
5      ┌─┘
5      └─┐red_knot_python_semantic::types::symbol{name="ValueError"}
5      ┌─┘
5    ┌─┘
5  ┌─┘
5┌─┘
```

## Test Plan

```
cargo run --bin red_knot -- --current-directory path/to/tomllib -vvv
```
2025-01-07 10:41:27 +01:00
Raphael Gaschignard
066239fe5b [red-knot] improve type shrinking coverage in red-knot property tests (#15297)
## Summary

While looking at #14899, I looked at seeing if I could get shrinking on
the examples. It turned out to be straightforward, with a couple of
caveats.

I'm calling `clone` a lot during shrinking. Since by the shrink step
we're already looking at a test failure this feels fine? Unless I
misunderstood `quickcheck`'s core loop

When shrinking `Intersection`s, in order to just rely on `quickcheck`'s
`Vec` shrinking without thinking about it too much, the shrinking
strategy is:
- try to shrink the negative side (keeping the positive side the same)
- try to shrink the positive side (keeping the negative side the same)

This means that you can't shrink from `(A & B & ~C & ~D)` directly to
`(A & ~C)`! You would first need an intermediate failure at `(A & B &
~C)` or `(A & ~C & ~D)`. This feels good enough. Shrinking the negative
side first also has the benefit of trying to strip down negative
elements in these intersections.

## Test Plan
`cargo test -p red_knot_python_semantic -- --ignored
types::property_tests::stable` still fails as it current does on `main`,
but now the errors seem more minimal.
2025-01-07 10:09:18 +01:00
Victor Westerhuis
1e948f739c [flake8-return] Recognize functions returning Never as non-returning (RET503) (#15298)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-07 07:57:34 +00:00
Steve C
78e26cec02 [flake8-bugbear] Implement class-as-data-structure (B903) (#9601)
## Summary

Adds `class-as-data-structure` rule (`B903`). Also compare pylint's `too-few-public-methods` (`PLR0903`).

Took some creative liberty with this by allowing the class to have any
decorators or base classes. There are years-old issues on pylint that
don't approve of the strictness when it comes to these things.

Especially considering that dataclass is a decorator and namedtuple _can
be_ a base class. I feel ignoring those explicitly is redundant all
things considered, but it's not a hill I'm willing to die on!

See: #970 

## Test Plan

`cargo test`

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-01-06 21:18:28 -06:00
Charlie Marsh
e7248ee43e Avoid treating newline-separated sections as sub-sections (#15311)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15224.
2025-01-06 22:13:35 -05:00
Charlie Marsh
065274d353 Remove call when removing final argument from format (#15309)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15303.
2025-01-07 02:53:42 +00:00
Charlie Marsh
75a24bbc67 Don't enforce object-without-hash-method in stubs (#15310)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15292.
2025-01-07 02:51:06 +00:00
Douglas Creager
5e9259c96c Don't special-case class instances in binary expression inference (#15161)
Just like in #15045 for unary expressions: In binary expressions, we
were only looking for dunder expressions for `Type::Instance` types. We
had some special cases for coercing the various `Literal` types into
their corresponding `Instance` types before doing the lookup. But we can
side-step all of that by using the existing `Type::to_meta_type` and
`Type::to_instance` methods.
2025-01-06 13:50:20 -05:00
Alex Waygood
d45c1ee44f Upgrade zizmor to the latest version in CI (#15300)
## Summary

This PR upgrades zizmor to the latest release in our CI. zizmor is a
static analyzer checking for security issues in GitHub workflows. The
new release finds some new issues in our workflows; this PR fixes some
of the issues, and adds ignores for some other issues.

The issues fixed in this PR are new cases of zizmor's
[`template-injection`](https://woodruffw.github.io/zizmor/audits/#template-injection)
rule being emitted. The issues I'm ignoring for now are all to do with
the
[`cache-poisoning`](https://woodruffw.github.io/zizmor/audits/#cache-poisoning)
rule. The main reason I'm fixing some but ignoring others is that I'm
confident fixing the template-injection diagnostics won't have any
impact on how our workflows operate in CI, but I'm worried that fixing
the cache-poisoning diagnostics could slow down our CI a fair bit. I
don't mind if somebody else is motivated to try to fix these
diagnostics, but for now I think I'd prefer to just ignore them; it
doesn't seem high-priority enough to try to fix them right now :-)

## Test Plan

- `uvx pre-commit run -a --hook-stage=manual` passes locally
- Let's see if CI passes on this PR...
2025-01-06 15:07:46 +00:00
InSync
636288038f [ruff] Dataclass enums (RUF049) (#15299)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-06 14:44:20 +01:00
InSync
832c0fa04b Better error message when --config is given a table key and a non-inline-table value (#15266)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-06 13:20:28 +00:00
renovate[bot]
f29c9e48a4 Update pre-commit dependencies (#15289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-06 11:35:02 +00:00
Enric Calabuig
bafe8714a8 Don't fix in ecosystem check (#15267) 2025-01-06 10:21:34 +01:00
renovate[bot]
e5270e2ac2 Update Rust crate itertools to 0.14.0 (#15287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-06 07:55:32 +00:00
Avasam
643fd7fe07 Remove accidental empty block at the bottom of split-static-string (SIM905) doc (#15290)
## Summary

Removes the following empty code block

![image](https://github.com/user-attachments/assets/4adbacac-0bd9-4dac-af3a-93da2619a1cb)
2025-01-06 04:24:40 +00:00
renovate[bot]
bd02beec85 Update Rust crate clearscreen to v4 (#15288) 2025-01-05 20:06:13 -05:00
renovate[bot]
7f299fceef Update Rust crate insta to v1.42.0 (#15286) 2025-01-05 20:06:02 -05:00
renovate[bot]
6e2800df85 Update NPM Development dependencies (#15285) 2025-01-05 20:05:51 -05:00
renovate[bot]
391332a835 Update dependency uuid to v11.0.4 (#15284) 2025-01-05 20:05:45 -05:00
renovate[bot]
84e13cea14 Update dependency ruff to v0.8.6 (#15283) 2025-01-05 20:05:36 -05:00
renovate[bot]
bcb5f621c5 Update Rust crate syn to v2.0.95 (#15282) 2025-01-05 20:05:32 -05:00
renovate[bot]
47c8f1ad65 Update Rust crate matchit to v0.8.6 (#15281) 2025-01-05 20:04:02 -05:00
renovate[bot]
a4f8b9311e Update Rust crate bstr to v1.11.3 (#15280) 2025-01-05 20:03:54 -05:00
Alex Waygood
6097fd9bbe [red-knot] Future-proof Type::is_disjoint_from() (#15262) 2025-01-05 22:56:16 +00:00
Alex Waygood
0743838438 [red-knot] Improve Type::is_disjoint_from() for KnownInstanceTypes (#15261) 2025-01-05 22:49:42 +00:00
Alex Waygood
980ce941c7 [red-knot] Minor simplifications and improvements to constraint narrowing logic (#15270) 2025-01-05 21:51:22 +00:00
Shaygan Hooshyari
b26448926a Allow assigning ellipsis literal as parameter default value (#14982)
Resolves #14840

## Summary

Usage of ellipsis literal as default argument is allowed in stub files.

## Test Plan

Added mdtest for both python files and stub files.


---------

Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-05 13:11:32 -06:00
Carl Meyer
2ea63620cf [red-knot] fix control flow for assignment expressions in elif tests (#15274)
## Summary

The test expression in an `elif` clause is evaluated whether or not we
take the branch. Our control flow model for if/elif chains failed to
reflect this, causing wrong inference in cases where an assignment
expression occurs inside an `elif` test expression. Our "no branch taken
yet" snapshot (which is the starting state for every new elif branch)
can't simply be the pre-if state, it must be updated after visiting each
test expression.

Once we do this, it also means we no longer need to track a vector of
narrowing constraints to reapply for each new branch, since our "branch
not taken" state (which is the initial state for each branch) is
continuously updated to include the negative narrowing constraints of
all previous branches.

Fixes #15033.

## Test Plan

Added mdtests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-01-05 18:35:29 +00:00
InSync
00aa387d9d [refurb] Mark fix as unsafe when the right-hand side is a string (FURB171) (#15273) 2025-01-05 17:54:32 +00:00
Alex Waygood
eb82089551 [red-knot] Type::SubclassOf(SubclassOfType { base: ClassBase::Unknown }).to_instance() should be Unknown, not Any (#15269) 2025-01-05 15:14:01 +00:00
InSync
f144b9684d Add a test for overshadowing redirects (#15259)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-05 09:47:01 +01:00
InSync
df6e5c0293 [ruff] Recode RUF025 to RUF037 (RUF037) (#15258) 2025-01-05 09:35:08 +01:00
Alex Waygood
8f0e01787f [red-knot] Minor cleanup to Type::is_disjoint_from() and Type::is_subtype_of() (#15260) 2025-01-04 17:34:37 +00:00
Micha Reiser
6b907c1305 Ruff 0.8.6 (#15253) 2025-01-04 13:09:26 +01:00
Micha Reiser
f319531632 Make unreachable a test rule for now (#15252) 2025-01-04 12:52:08 +01:00
Micha Reiser
e4d9fe036a Revert "Add all PEP-585 names to UP006 rule" (#15250) 2025-01-04 12:23:53 +01:00
Micha Reiser
baf0d660eb Update salsa (#15243) 2025-01-03 20:04:35 +01:00
Alex Waygood
bde8ecddca [red-knot] Remove unneeded branch in Type::is_equivalent_to() (#15242)
## Summary

We understand `sys.version_info` branches now! As such, I _believe_ this
branch is no longer required; all tests pass without it. I also ran
`QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable`, and no tests failed except for
the known issue with `Type::is_assignable_to()`
(https://github.com/astral-sh/ruff/issues/14899)

## Test Plan

See above
2025-01-03 19:04:01 +00:00
InSync
842f882ef0 [ruff] Avoid reporting when ndigits is possibly negative (RUF057) (#15234) 2025-01-03 19:48:03 +01:00
Douglas Creager
75015b0ed9 Attribute panics to the mdtests that cause them (#15241)
This updates the mdtest harness to catch any panics that occur during
type checking, and to display the panic message as an mdtest failure.
(We don't know which specific line causes the failure, so we attribute
panics to the first line of the test case.)
2025-01-03 13:45:56 -05:00
Dylan
706d87f239 Show errors for attempted fixes only when passed --verbose (#15237)
The default logging level for diagnostics includes logs written using
the `log` crate with level `error`, `warn`, and `info`. An unsuccessful
fix attached to a diagnostic via `try_set_fix` or `try_set_optional_fix`
was logged at level `error`. Note that the user would see these messages
even without passing `--fix`, and possibly also on lines with `noqa`
comments.

This PR changes the logging level here to a `debug`. We also found
ad-hoc instances of error logging in the implementations of several
rules, and have replaced those with either a `debug` or call to
`try_set{_optional}_fix`.

Closes #15229
2025-01-03 08:50:13 -06:00
w0nder1ng
0837cdd931 [RUF] Add rule to detect empty literal in deque call (RUF025) (#15104) 2025-01-03 11:57:13 +01:00
Mike Bernard
0dbfa8d0e0 TD003: remove issue code length restriction (#15175)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-03 10:42:04 +01:00
Micha Reiser
1218bc65ed Preserve multiline implicit concatenated strings in docstring positions (#15126) 2025-01-03 10:27:14 +01:00
InSync
6180f78da4 [pyflakes] Ignore errors in @no_type_check string annotations (F722, F821) (#15215) 2025-01-03 10:05:45 +01:00
Wei Lee
835b453bfd style(AIR302): rename removed_airflow_plugin_extension as check_airflow_plugin_extension (#15233)
## Summary

during the previous refactor, this renaming was missed

## Test Plan

no functionality changed
2025-01-03 10:37:21 +05:30
Auguste Lalande
a3d873ef66 [pylint] Re-implement unreachable (PLW0101) (#10891)
## Summary

This PR re-introduces the control-flow graph implementation which was
first introduced in #5384, and then removed in #9463 due to not being
feature complete. Mainly, it lacked the ability to process
`try`-`except` blocks, along with some more minor bugs.

Closes #8958 and #8959 and #14881.

## Overview of Changes

I will now highlight the major changes implemented in this PR, in order
of implementation.

1. Introduced a post-processing step in loop handling to find any
`continue` or `break` statements within the loop body and redirect them
appropriately.
2. Introduced a loop-continue block which is always placed at the end of
loop blocks, and ensures proper looping regardless of the internal logic
of the block. This resolves #8958.
3. Implemented `try` processing with the following logic (resolves
#8959):
1. In the example below the cfg first encounters a conditional
`ExceptionRaised` forking if an exception was (or will be) raised in the
try block. This is not possible to know (except for trivial cases) so we
assume both paths can be taken unconditionally.
2. Going down the `try` path the cfg goes `try`->`else`->`finally`
unconditionally.
3. Going down the `except` path the cfg will meet several conditional
`ExceptionCaught` which fork depending on the nature of the exception
caught. Again there's no way to know which exceptions may be raised so
both paths are assumed to be taken unconditionally.
4. If none of the exception blocks catch the exception then the cfg
terminates by raising a new exception.
5. A post-processing step is also implemented to redirect any `raises`
or `returns` within the blocks appropriately.
```python
def func():
    try:
        print("try")
    except Exception:
        print("Exception")
    except OtherException as e:
        print("OtherException")
    else:
        print("else")
    finally:
        print("finally")
```
```mermaid
flowchart TD
  start(("Start"))
  return(("End"))
  block0[["`*(empty)*`"]]
  block1["print(#quot;finally#quot;)\n"]
  block2["print(#quot;else#quot;)\n"]
  block3["print(#quot;try#quot;)\n"]
  block4[["Exception raised"]]
  block5["print(#quot;OtherException#quot;)\n"]
  block6["try:
        print(#quot;try#quot;)
    except Exception:
        print(#quot;Exception#quot;)
    except OtherException as e:
        print(#quot;OtherException#quot;)
    else:
        print(#quot;else#quot;)
    finally:
        print(#quot;finally#quot;)\n"]
  block7["print(#quot;Exception#quot;)\n"]
  block8["try:
        print(#quot;try#quot;)
    except Exception:
        print(#quot;Exception#quot;)
    except OtherException as e:
        print(#quot;OtherException#quot;)
    else:
        print(#quot;else#quot;)
    finally:
        print(#quot;finally#quot;)\n"]
  block9["try:
        print(#quot;try#quot;)
    except Exception:
        print(#quot;Exception#quot;)
    except OtherException as e:
        print(#quot;OtherException#quot;)
    else:
        print(#quot;else#quot;)
    finally:
        print(#quot;finally#quot;)\n"]

  start --> block9
  block9 -- "Exception raised" --> block8
  block9 -- "else" --> block3
  block8 -- "Exception" --> block7
  block8 -- "else" --> block6
  block7 --> block1
  block6 -- "OtherException" --> block5
  block6 -- "else" --> block4
  block5 --> block1
  block4 --> return
  block3 --> block2
  block2 --> block1
  block1 --> block0
  block0 --> return
``` 
6. Implemented `with` processing with the following logic:
1. `with` statements have no conditional execution (apart from the
hidden logic handling the enter and exit), so the block is assumed to
execute unconditionally.
2. The one exception is that exceptions raised within the block may
result in control flow resuming at the end of the block. Since it is not
possible know if an exception will be raised, or if it will be handled
by the context manager, we assume that execution always continues after
`with` blocks even if the blocks contain `raise` or `return` statements.
This is handled in a post-processing step.

## Test Plan

Additional test fixtures and control-flow fixtures were added.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-01-02 21:54:59 -06:00
Wei Lee
d464ef67cf refactor(AIR303): move duplicate qualified_name.to_string() to Diagnostic argument (#15220)
## Summary

Refactor airflow rule logic like
86bdc2e7b1

## Test Plan

No functionality change. Existing test cases work as it was
2025-01-03 09:10:37 +05:30
Charlie Marsh
2355472d61 Misc. clean up to rounding rules (#15231) 2025-01-02 17:51:35 -05:00
Charlie Marsh
3c3f35a548 Avoid syntax error when removing int over multiple lines (#15230)
## Summary

Closes https://github.com/astral-sh/ruff/issues/15226.
2025-01-02 17:43:15 -05:00
renovate[bot]
2327082c43 Migrate renovate config (#15228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-01-02 21:53:45 +00:00
David Peter
7671a3bbc7 Remove Type::tuple in favor of TupleType::from_elements (#15218)
## Summary

Remove `Type::tuple` in favor of `TupleType::from_elements`, avoid a few
intermediate `Vec`tors. Resolves an old [review
comment](https://github.com/astral-sh/ruff/pull/14744#discussion_r1867493706).

## Test Plan

New regression test for something I ran into while implementing this.
2025-01-02 17:22:32 +01:00
Dhruv Manilawala
11e873eb45 Bump version to 0.8.5 (#15219) 2025-01-02 17:21:21 +05:30
InSync
89ea0371a4 [ruff] Unnecessary rounding (RUF057) (#14828)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-01-02 10:00:57 +01:00
Wei Lee
f8c9665742 [airflow] Extend names moved from core to provider (AIR303) (#15216)
## Summary

Many core Airflow features have been deprecated and moved to Airflow
Providers since users might need to install an additional package (e.g.,
`apache-airflow-provider-fab==1.0.0`); a separate rule (AIR303) is
created for this.

* `airflow.kubernetes.kubernetes_helper_functions.add_pod_suffix` →
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix`
*
`airflow.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata`
→
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_for_logging_task_metadata`
* `airflow.kubernetes.kubernetes_helper_functions.annotations_to_key` →
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.annotations_to_key`
* `airflow.kubernetes.kubernetes_helper_functions.create_pod_id` →
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.create_pod_id`
*
`airflow.kubernetes.kubernetes_helper_functions.get_logs_task_metadata`
→
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.get_logs_task_metadata`
* `airflow.kubernetes.kubernetes_helper_functions.rand_str` →
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str`
* `airflow.kubernetes.pod.Port` →
`kubernetes.client.models.V1ContainerPort`
* `airflow.kubernetes.pod.Resources` →
`kubernetes.client.models.V1ResourceRequirements`
* `airflow.kubernetes.pod_launcher.PodLauncher` →
`airflow.providers.cncf.kubernetes.pod_launcher.PodLauncher`
* `airflow.kubernetes.pod_launcher.PodStatus` →
`airflow.providers.cncf.kubernetes.pod_launcher.PodStatus`
* `airflow.kubernetes.pod_launcher_deprecated.PodLauncher` →
`airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodLauncher`
* `airflow.kubernetes.pod_launcher_deprecated.PodStatus` →
`airflow.providers.cncf.kubernetes.pod_launcher_deprecated.PodStatus`
* `airflow.kubernetes.pod_launcher_deprecated.get_kube_client` →
`airflow.providers.cncf.kubernetes.kube_client.get_kube_client`
* `airflow.kubernetes.pod_launcher_deprecated.PodDefaults` →
`airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodDefaults`
* `airflow.kubernetes.pod_runtime_info_env.PodRuntimeInfoEnv` →
`kubernetes.client.models.V1EnvVar`
* `airflow.kubernetes.volume.Volume` →
`kubernetes.client.models.V1Volume`
* `airflow.kubernetes.volume_mount.VolumeMount` →
`kubernetes.client.models.V1VolumeMount`
* `airflow.kubernetes.k8s_model.K8SModel` →
`airflow.providers.cncf.kubernetes.k8s_model.K8SModel`
* `airflow.kubernetes.k8s_model.append_to_pod` →
`airflow.providers.cncf.kubernetes.k8s_model.append_to_pod`
* `airflow.kubernetes.kube_client._disable_verify_ssl` →
`airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._disable_verify_ssl`
* `airflow.kubernetes.kube_client._enable_tcp_keepalive` →
`airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client._enable_tcp_keepalive`
* `airflow.kubernetes.kube_client.get_kube_client` →
`airflow.kubernetes.airflow.providers.cncf.kubernetes.kube_client.get_kube_client`
* `airflow.kubernetes.pod_generator.datetime_to_label_safe_datestring` →
`airflow.providers.cncf.kubernetes.pod_generator.datetime_to_label_safe_datestring`
* `airflow.kubernetes.pod_generator.extend_object_field` →
`airflow.kubernetes.airflow.providers.cncf.kubernetes.pod_generator.extend_object_field`
* `airflow.kubernetes.pod_generator.label_safe_datestring_to_datetime` →
`airflow.providers.cncf.kubernetes.pod_generator.label_safe_datestring_to_datetime`
* `airflow.kubernetes.pod_generator.make_safe_label_value` →
`airflow.providers.cncf.kubernetes.pod_generator.make_safe_label_value`
* `airflow.kubernetes.pod_generator.merge_objects` →
`airflow.providers.cncf.kubernetes.pod_generator.merge_objects`
* `airflow.kubernetes.pod_generator.PodGenerator` →
`airflow.providers.cncf.kubernetes.pod_generator.PodGenerator`
* `airflow.kubernetes.pod_generator.PodGeneratorDeprecated` →
`airflow.providers.cncf.kubernetes.pod_generator.PodGenerator`
* `airflow.kubernetes.pod_generator.PodDefaults` →
`airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodDefaults`
* `airflow.kubernetes.pod_generator.add_pod_suffix` →
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.add_pod_suffix`
* `airflow.kubernetes.pod_generator.rand_str` →
`airflow.providers.cncf.kubernetes.kubernetes_helper_functions.rand_str`
* `airflow.kubernetes.pod_generator_deprecated.make_safe_label_value` →
`airflow.providers.cncf.kubernetes.pod_generator_deprecated.make_safe_label_value`
* `airflow.kubernetes.pod_generator_deprecated.PodDefaults` →
`airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodDefaults`
* `airflow.kubernetes.pod_generator_deprecated.PodGenerator` →
`airflow.providers.cncf.kubernetes.pod_generator_deprecated.PodGenerator`
* `airflow.kubernetes.secret.Secret` →
`airflow.providers.cncf.kubernetes.secret.Secret`
* `airflow.kubernetes.secret.K8SModel` →
`airflow.providers.cncf.kubernetes.k8s_model.K8SModel`

## Test Plan

A test fixture has been included for the rule.
2025-01-02 10:30:12 +05:30
InSync
af95f6b577 [pycodestyle] Avoid false positives and negatives related to type parameter default syntax (E225, E251) (#15214) 2025-01-01 11:28:25 +01:00
github-actions[bot]
79682a28b8 Sync vendored typeshed stubs (#15213)
Co-authored-by: typeshedbot <>
2025-01-01 01:14:40 +00:00
David Salvisberg
1ef0f615f1 [flake8-type-checking] Improve flexibility of runtime-evaluated-decorators (#15204)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-31 16:28:10 +00:00
Christian Clauss
7ca3f9515c Update references to astral-sh/ruff-action from v2 to v3 (#15212) 2024-12-31 16:43:17 +01:00
Wei Lee
32de5801f7 [airflow]: extend names moved from core to provider (AIR303) (#15196) 2024-12-31 15:16:07 +01:00
InSync
cfd6093579 [pydocstyle] Add setting to ignore missing documentation for*args and **kwargs parameters (D417) (#15210)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-31 12:16:55 +01:00
Arnav Gupta
3c9021ffcb [ruff] Implement falsy-dict-get-fallback (RUF056) (#15160)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-31 11:40:51 +01:00
Wei Lee
68d2466832 [airflow] Remove additional spaces (AIR302) (#15211)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

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

During https://github.com/astral-sh/ruff/pull/15209, additional spaces
was accidentally added to the rule
`airflow.operators.latest_only.LatestOnlyOperator`. This PR fixes this
issue

## Test Plan

<!-- How was it tested? -->

A test fixture has been included for the rule.
2024-12-31 13:58:11 +05:30
Avasam
ecf00cdf6a Fix incorrect doc in shebang-not-executable (EXE001) and add git+windows solution to executable bit (#15208)
## Summary


I noticed that the solution mentioned in [shebang-not-executable
(EXE001)](https://docs.astral.sh/ruff/rules/shebang-not-executable/#shebang-not-executable-exe001)
was incorrect and likely copy-pasted from
[shebang-missing-executable-file
(EXE002)](https://docs.astral.sh/ruff/rules/shebang-missing-executable-file/#shebang-missing-executable-file-exe002)

It was telling users to remove the executable bit from a non-executable
file. Which does nothing.

I also noticed locally that:
- `chmod` wouldn't cause any file change to be noticed by git (`EXE` was
also passing locally) under WSL
- Using git allows anyone to fix this lint across OSes, for projects
with CIs using git

So I added a solution using [git update-index
--chmod](https://git-scm.com/docs/git-update-index#Documentation/git-update-index.txt---chmod-x)

## Test Plan

No test plan, doc changes only.
As for running the chmod commands:
https://github.com/python/typeshed/pull/13346
2024-12-31 11:05:44 +05:30
Dhruv Manilawala
86bdc2e7b1 Refactor Airflow removal in 3 code (#15209)
This PR contains a couple of refactors for the Airflow removal in 3
code, nothing major just some minor nits.
2024-12-31 05:27:12 +00:00
Wei Lee
253c274afa [airflow] Extend rule to check class attributes, methods, arguments (AIR302) (#15083)
## Summary

Airflow 3.0 removes various deprecated functions, members, modules, and
other values. They have been deprecated in 2.x, but the removal causes
incompatibilities that we want to detect. This PR add rules for the
following.

* Removed class attribute
* `airflow.providers_manager.ProvidersManager.dataset_factories` →
`airflow.providers_manager.ProvidersManager.asset_factories`
* `airflow.providers_manager.ProvidersManager.dataset_uri_handlers` →
`airflow.providers_manager.ProvidersManager.asset_uri_handlers`
*
`airflow.providers_manager.ProvidersManager.dataset_to_openlineage_converters`
→
`airflow.providers_manager.ProvidersManager.asset_to_openlineage_converters`
* `airflow.lineage.hook.DatasetLineageInfo.dataset` →
`airflow.lineage.hook.AssetLineageInfo.asset`
* Removed class method (subclasses in airflow should also checked)
* `airflow.secrets.base_secrets.BaseSecretsBackend.get_conn_uri` →
`airflow.secrets.base_secrets.BaseSecretsBackend.get_conn_value`
* `airflow.secrets.base_secrets.BaseSecretsBackend.get_connections` →
`airflow.secrets.base_secrets.BaseSecretsBackend.get_connection`
* `airflow.hooks.base.BaseHook.get_connections` → use `get_connection`
* `airflow.datasets.BaseDataset.iter_datasets` →
`airflow.sdk.definitions.asset.BaseAsset.iter_assets`
* `airflow.datasets.BaseDataset.iter_dataset_aliases` →
`airflow.sdk.definitions.asset.BaseAsset.iter_asset_aliases`
* Removed constructor args (subclasses in airflow should also checked)
* argument `filename_template`
in`airflow.utils.log.file_task_handler.FileTaskHandler`
    * in `BaseOperator`
        * `sla`
        * `task_concurrency` → `max_active_tis_per_dag`
    * in `BaseAuthManager`
        * `appbuilder`
* Removed class variable (subclasses anywhere should be checked)
    * in `airflow.plugins_manager.AirflowPlugin`
        * `executors` (from #43289)
        * `hooks`
        * `operators`
        * `sensors`
* Replaced names
	* `airflow.hooks.base_hook.BaseHook` → `airflow.hooks.base.BaseHook`
* `airflow.operators.dagrun_operator.TriggerDagRunLink` →
`airflow.operators.trigger_dagrun.TriggerDagRunLink`
* `airflow.operators.dagrun_operator.TriggerDagRunOperator` →
`airflow.operators.trigger_dagrun.TriggerDagRunOperator`
* `airflow.operators.python_operator.BranchPythonOperator` →
`airflow.operators.python.BranchPythonOperator`
* `airflow.operators.python_operator.PythonOperator` →
`airflow.operators.python.PythonOperator`
* `airflow.operators.python_operator.PythonVirtualenvOperator` →
`airflow.operators.python.PythonVirtualenvOperator`
* `airflow.operators.python_operator.ShortCircuitOperator` →
`airflow.operators.python.ShortCircuitOperator`
* `airflow.operators.latest_only_operator.LatestOnlyOperator` →
`airflow.operators.latest_only.LatestOnlyOperator`


In additional to the changes above, this PR also add utility functions
and improve docstring.


## Test Plan

A test fixture is included in the PR.
2024-12-31 09:49:18 +05:30
InSync
2a1aa29366 [pylint] Detect nested methods correctly (PLW1641) (#15032)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-30 16:55:14 +01:00
purajit
42cc67a87c [docs] improve and fix entry for analyze.include-dependencies (#15197)
## Summary

Changes two things about the entry:
* make the example valid TOML - inline tables must be a single line, at
least till v1.1.0 is released,
but also while in the future the toml version used by ruff might handle
it, it would probably be
good to stick to a spec that's readable by the vast majority of other
tools and versions as well,
especially if people are using `pyproject.toml`. The current example
leads to `ruff` failure.
See https://github.com/toml-lang/toml/pull/904
* adds a line about the ability to add non-Python files to the map,
which I think is a specific and
important feature people should know about (in fact, I would assume this
could potentially
become the single biggest use-case for this).

## Test Plan

Ran doc creation as described in the
[contribution](https://docs.astral.sh/ruff/contributing/#mkdocs) guide.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-12-30 15:45:19 +00:00
InSync
280ba75100 [flake8-pie] Allow cast(SomeType, ...) (PIE796) (#15141) 2024-12-30 15:52:35 +01:00
wookie184
04d538113a Add all PEP-585 names to UP006 rule (#5454)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Co-authored-by: dylwil3 <dylwil3@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-30 12:21:42 +01:00
InSync
0b15f17939 [flake8-simplify] More precise inference for dictionaries (SIM300) (#15164)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-30 10:41:33 +00:00
Micha Reiser
0caab81d3d @no_type_check support (#15122)
Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-30 09:42:18 +00:00
InSync
d4ee6abf4a Visit PEP 764 inline TypedDicts' keys as non-type-expressions (#15073)
## Summary

Resolves #10812.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-12-30 15:04:55 +05:30
Dhruv Manilawala
8a98d88847 [red-knot] Add diagnostic for invalid unpacking (#15086)
## Summary

Part of #13773 

This PR adds diagnostics when there is a length mismatch during
unpacking between the number of target expressions and the number of
types for the unpack value expression.

There are 3 cases of diagnostics here where the first two occurs when
there isn't a starred expression and the last one occurs when there's a
starred expression:
1. Number of target expressions is **less** than the number of types
that needs to be unpacked
2. Number of target expressions is **greater** then the number of types
that needs to be unpacked
3. When there's a starred expression as one of the target expression and
the number of target expressions is greater than the number of types

Examples for all each of the above cases:
```py
# red-knot: Too many values to unpack (expected 2, got 3) [lint:invalid-assignment]
a, b = (1, 2, 3)

# red-knot: Not enough values to unpack (expected 2, got 1) [lint:invalid-assignment]
a, b = (1,)

# red-knot: Not enough values to unpack (expected 3 or more, got 2) [lint:invalid-assignment]
a, *b, c, d = (1, 2)
```

The (3) case is a bit special because it uses a distinct wording
"expected n or more" instead of "expected n" because of the starred
expression.

### Location

The diagnostic location is the target expression that's being unpacked.
For nested targets, the location will be the nested expression. For
example:

```py
(a, (b, c), d) = (1, (2, 3, 4), 5)
#   ^^^^^^
#   red-knot: Too many values to unpack (expected 2, got 3) [lint:invalid-assignment]
```

For future improvements, it would be useful to show the context for why
this unpacking failed. For example, for why the expected number of
targets is `n`, we can highlight the relevant elements for the value
expression.

In the **ecosystem**, **Pyright** uses the target expressions for
location while **mypy** uses the value expression for the location. For
example:

```py
if 1:
#          mypy: Too many values to unpack (2 expected, 3 provided)  [misc]
#          vvvvvvvvv
	a, b = (1, 2, 3)
#   ^^^^
#   Pyright: Expression with type "tuple[Literal[1], Literal[2], Literal[3]]" cannot be assigned to target tuple
#     Type "tuple[Literal[1], Literal[2], Literal[3]]" is incompatible with target tuple
#       Tuple size mismatch; expected 2 but received 3 [reportAssignmentType]
#   red-knot: Too many values to unpack (expected 2, got 3) [lint:invalid-assignment]
```

## Test Plan

Update existing test cases TODO with the error directives.
2024-12-30 13:10:29 +05:30
InSync
901b7dd8f8 [flake8-use-pathlib] Catch redundant joins in PTH201 and avoid syntax errors (#15177)
## Summary

Resolves #10453, resolves #15165.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-12-30 03:31:35 +00:00
renovate[bot]
d3492178e1 Update Rust crate glob to v0.3.2 (#15185) 2024-12-29 21:29:46 -05:00
renovate[bot]
383f6d0967 Update astral-sh/setup-uv action to v5 (#15193) 2024-12-29 21:29:37 -05:00
renovate[bot]
e953ecf42f Update dependency mdformat-mkdocs to v4.1.1 (#15192) 2024-12-29 21:29:33 -05:00
renovate[bot]
e8cf2d3027 Update Rust crate serde_with to v3.12.0 (#15191) 2024-12-29 21:29:27 -05:00
renovate[bot]
0701437104 Update NPM Development dependencies (#15190) 2024-12-29 21:29:23 -05:00
renovate[bot]
db0468bae5 Update pre-commit hook rhysd/actionlint to v1.7.5 (#15189) 2024-12-29 21:29:19 -05:00
renovate[bot]
7e491c4d94 Update Rust crate syn to v2.0.93 (#15188) 2024-12-29 21:29:15 -05:00
renovate[bot]
957df82400 Update Rust crate serde to v1.0.217 (#15187) 2024-12-29 21:29:11 -05:00
renovate[bot]
9ba2fb0a48 Update Rust crate quote to v1.0.38 (#15186) 2024-12-29 21:29:04 -05:00
renovate[bot]
8ff9cb75cd Update Rust crate compact_str to v0.8.1 (#15184) 2024-12-29 21:28:56 -05:00
David Salvisberg
f170932585 [flake8-type-checking] Disable TC006 & TC007 in stub files (#15179)
Fixes: #15176

## Summary

Neither of these rules make any sense in stub files. Technically TC007
should already not have triggered, due to the typing only context of the
binding, but it's better to be explicit.

Keeping TC008 enabled on the other hand makes sense to me, although we
could probably be more aggressive with unquoting in a typing runtime
context.

## Test Plan

`cargo nextest run`
2024-12-29 14:39:16 -05:00
Shantanu
bc3a735d93 Test explicit shadowing involving defs (#15174)
Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-29 00:47:03 +00:00
Victorien
7ea3a549b2 Fix typo in NameImport.qualified_name docstring (#15170) 2024-12-28 23:44:01 +01:00
Wei Lee
2288cc7478 [airflow]: extend names moved from core to provider (AIR303) (#15159) 2024-12-27 17:04:02 +00:00
Enoch Kan
79816f965c Fix SyntaxError in example replacement snippet in nonlocal-without-binding (#15157) 2024-12-27 12:15:13 +00:00
InSync
8d2d1a73c5 [red-knot] Report classes inheriting from bases with incompatible __slots__ (#15129) 2024-12-27 11:43:48 +00:00
Victorien
2419fdb2ef Fix typo in LogicalLine docstring (#15150) 2024-12-26 18:45:45 +01:00
Micha Reiser
6ed27c3786 Rename the knot|type-ignore mdtest files (#15147) 2024-12-26 10:25:05 +00:00
Wei Lee
5d6aae839e [airflow]: extend moved names (AIR303) (#15145) 2024-12-26 10:55:07 +01:00
sobolevn
8b9c843c72 Fix docs highlight in dict_iter_missing_items.rs (#15140) 2024-12-25 18:52:17 +01:00
Enoch Kan
5bc9d6d3aa Rename rules currently not conforming to naming convention (#15102)
## Summary

This pull request renames 19 rules which currently do not conform to
Ruff's [naming
convention](https://github.com/astral-sh/ruff/blob/main/CONTRIBUTING.md#rule-naming-convention).

## Description

Fixes astral-sh/ruff#15009.
2024-12-23 15:48:45 -06:00
Micha Reiser
97965ff114 Rename --current-directory to --project in Red Knot benchmark script (#15124) 2024-12-23 12:50:35 +00:00
Micha Reiser
8d327087ef Add invalid-ignore-comment rule (#15094) 2024-12-23 10:38:10 +00:00
Micha Reiser
2835d94ec5 Add unknown-rule (#15085)
Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-23 11:30:54 +01:00
Dhruv Manilawala
68ada05b00 [red-knot] Infer value expr for empty list / tuple target (#15121)
## Summary

This PR resolves
https://github.com/astral-sh/ruff/pull/15058#discussion_r1893868406 by
inferring the value expression even if there are no targets in the list
/ tuple expression.

## Test Plan

Remove TODO from corpus tests, making sure it doesn't panic.
2024-12-23 16:00:35 +05:30
Micha Reiser
2a99c0be02 Add unused-ignore-comment rule (#15084) 2024-12-23 11:15:28 +01:00
Micha Reiser
dcb85b7088 Update benchmark scripts, use uv (#15120) 2024-12-23 11:14:15 +01:00
Harutaka Kawamura
8440f3ea9f [fastapi] Update FAST002 to check keyword-only arguments (#15119)
## Summary

Close #15117. Update `FAST002` to check keyword-only arguments.

## Test Plan

New test case
2024-12-23 15:32:42 +05:30
Micha Reiser
1c3d11e8a8 Support file-level type: ignore comments (#15081) 2024-12-23 09:59:04 +00:00
Micha Reiser
2f85749fa0 type: ignore[codes] and knot: ignore (#15078) 2024-12-23 10:52:43 +01:00
InSync
9eb73cb7e0 [pycodestyle] Preserve original value format (E731) (#15097)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-23 09:29:46 +00:00
Dhruv Manilawala
03bb9425df [red-knot] Avoid Ranged for definition target range (#15118)
## Summary

Ref:
3533d7f5b4 (r150651102)

This PR removes the `Ranged` implementation on `DefinitionKind` and
instead uses a method called `target_range` to avoid any confusion about
what range this is for i.e., it's not the range of the node that
represents the definition.
2024-12-23 14:07:09 +05:30
Dhruv Manilawala
113c804a62 [red-knot] Add support for unpacking for target (#15058)
## Summary

Related to #13773 

This PR adds support for unpacking `for` statement targets.

This involves updating the `value` field in the `Unpack` target to use
an enum which specifies the "where did the value expression came from?".
This is because for an iterable expression, we need to unpack the
iterator type while for assignment statement we need to unpack the value
type itself. And, this needs to be done in the unpack query.

### Question

One of the ways unpacking works in `for` statement is by looking at the
union of the types because if the iterable expression is a tuple then
the iterator type will be union of all the types in the tuple. This
means that the test cases that will test the unpacking in `for`
statement will also implicitly test the unpacking union logic. I was
wondering if it makes sense to merge these cases and only add the ones
that are specific to the union unpacking or for statement unpacking
logic.

## Test Plan

Add test cases involving iterating over a tuple type. I've intentionally
left out certain cases for now and I'm curious to know any thoughts on
the above query.
2024-12-23 06:13:49 +00:00
Arnav Gupta
b6c8f5d79e Fix RUF200 doc to have name and email in single object (#15099)
## Summary
Closes #14975 by modifying the docstring of the InvalidPyprojectToml
rule. Previously the docs were incorrectly stating that author name and
emails must be individual items in the authors list, rather than part of
a single object for each respective author.

## Test Plan
This was a docstring change, no tests needed.
2024-12-23 10:30:16 +05:30
renovate[bot]
da8acabc55 Update dependency mdformat to v0.7.21 (#15113)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [mdformat](https://redirect.github.com/hukkin/mdformat)
([changelog](https://mdformat.readthedocs.io/en/stable/users/changelog.html))
| `==0.7.19` -> `==0.7.21` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/mdformat/0.7.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mdformat/0.7.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mdformat/0.7.19/0.7.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mdformat/0.7.19/0.7.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>hukkin/mdformat (mdformat)</summary>

###
[`v0.7.21`](https://redirect.github.com/hukkin/mdformat/compare/0.7.20...0.7.21)

[Compare
Source](https://redirect.github.com/hukkin/mdformat/compare/0.7.20...0.7.21)

###
[`v0.7.20`](https://redirect.github.com/hukkin/mdformat/compare/0.7.19...0.7.20)

[Compare
Source](https://redirect.github.com/hukkin/mdformat/compare/0.7.19...0.7.20)

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:14:06 +05:30
renovate[bot]
fd2b8deddd Update dependency ruff to v0.8.4 (#15114)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.8.3` -> `==0.8.4` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.8.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/ruff/0.8.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/ruff/0.8.3/0.8.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.8.3/0.8.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>astral-sh/ruff (ruff)</summary>

###
[`v0.8.4`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#084)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.8.3...0.8.4)

##### Preview features

- \[`airflow`] Extend `AIR302` with additional functions and classes
([#&#8203;15015](https://redirect.github.com/astral-sh/ruff/pull/15015))
- \[`airflow`] Implement `moved-to-provider-in-3` for modules that has
been moved to Airflow providers (`AIR303`)
([#&#8203;14764](https://redirect.github.com/astral-sh/ruff/pull/14764))
- \[`flake8-use-pathlib`] Extend check for invalid path suffix to
include the case `"."` (`PTH210`)
([#&#8203;14902](https://redirect.github.com/astral-sh/ruff/pull/14902))
- \[`perflint`] Fix panic in `PERF401` when list variable is after the
`for` loop
([#&#8203;14971](https://redirect.github.com/astral-sh/ruff/pull/14971))
- \[`perflint`] Simplify finding the loop target in `PERF401`
([#&#8203;15025](https://redirect.github.com/astral-sh/ruff/pull/15025))
- \[`pylint`] Preserve original value format (`PLR6104`)
([#&#8203;14978](https://redirect.github.com/astral-sh/ruff/pull/14978))
- \[`ruff`] Avoid false positives for `RUF027` for typing context
bindings
([#&#8203;15037](https://redirect.github.com/astral-sh/ruff/pull/15037))
- \[`ruff`] Check for ambiguous pattern passed to `pytest.raises()`
(`RUF043`)
([#&#8203;14966](https://redirect.github.com/astral-sh/ruff/pull/14966))

##### Rule changes

- \[`flake8-bandit`] Check `S105` for annotated assignment
([#&#8203;15059](https://redirect.github.com/astral-sh/ruff/pull/15059))
- \[`flake8-pyi`] More autofixes for `redundant-none-literal` (`PYI061`)
([#&#8203;14872](https://redirect.github.com/astral-sh/ruff/pull/14872))
- \[`pydocstyle`] Skip leading whitespace for `D403`
([#&#8203;14963](https://redirect.github.com/astral-sh/ruff/pull/14963))
- \[`ruff`] Skip `SQLModel` base classes for `mutable-class-default`
(`RUF012`)
([#&#8203;14949](https://redirect.github.com/astral-sh/ruff/pull/14949))

##### Bug

- \[`perflint`] Parenthesize walrus expressions in autofix for
`manual-list-comprehension` (`PERF401`)
([#&#8203;15050](https://redirect.github.com/astral-sh/ruff/pull/15050))

##### Server

- Check diagnostic refresh support from client capability which enables
dynamic configuration for various editors
([#&#8203;15014](https://redirect.github.com/astral-sh/ruff/pull/15014))

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:12:07 +05:30
renovate[bot]
efbadbd966 Update pre-commit dependencies (#15115)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.8.3` -> `v0.8.4` |
| [crate-ci/typos](https://redirect.github.com/crate-ci/typos) |
repository | patch | `v1.28.3` -> `v1.28.4` |
|
[executablebooks/mdformat](https://redirect.github.com/executablebooks/mdformat)
| repository | patch | `0.7.19` -> `0.7.21` |
|
[woodruffw/zizmor-pre-commit](https://redirect.github.com/woodruffw/zizmor-pre-commit)
| repository | minor | `v0.9.2` -> `v0.10.0` |

Note: The `pre-commit` manager in Renovate is not supported by the
`pre-commit` maintainers or community. Please do not report any problems
there, instead [create a Discussion in the Renovate
repository](https://redirect.github.com/renovatebot/renovate/discussions/new)
if you have any questions.

---

### Release Notes

<details>
<summary>astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit)</summary>

###
[`v0.8.4`](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)

[Compare
Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)

</details>

<details>
<summary>crate-ci/typos (crate-ci/typos)</summary>

###
[`v1.28.4`](https://redirect.github.com/crate-ci/typos/releases/tag/v1.28.4)

[Compare
Source](https://redirect.github.com/crate-ci/typos/compare/v1.28.3...v1.28.4)

#### \[1.28.4] - 2024-12-16

##### Features

-   `--format sarif` support

</details>

<details>
<summary>executablebooks/mdformat (executablebooks/mdformat)</summary>

###
[`v0.7.21`](https://redirect.github.com/executablebooks/mdformat/compare/0.7.20...0.7.21)

[Compare
Source](https://redirect.github.com/executablebooks/mdformat/compare/0.7.20...0.7.21)

###
[`v0.7.20`](https://redirect.github.com/executablebooks/mdformat/compare/0.7.19...0.7.20)

[Compare
Source](https://redirect.github.com/executablebooks/mdformat/compare/0.7.19...0.7.20)

</details>

<details>
<summary>woodruffw/zizmor-pre-commit
(woodruffw/zizmor-pre-commit)</summary>

###
[`v0.10.0`](https://redirect.github.com/woodruffw/zizmor-pre-commit/releases/tag/v0.10.0)

[Compare
Source](https://redirect.github.com/woodruffw/zizmor-pre-commit/compare/v0.9.2...v0.10.0)

See: https://github.com/woodruffw/zizmor/releases/tag/v0.10.0

</details>

---

### Configuration

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

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

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

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:11:52 +05:30
renovate[bot]
130cc24e2c Update Rust crate anyhow to v1.0.95 (#15106)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.94` -> `1.0.95` |

---

### Release Notes

<details>
<summary>dtolnay/anyhow (anyhow)</summary>

###
[`v1.0.95`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.95)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.94...1.0.95)

- Add
[`Error::from_boxed`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html#method.from_boxed)
([#&#8203;401](https://redirect.github.com/dtolnay/anyhow/issues/401),
[#&#8203;402](https://redirect.github.com/dtolnay/anyhow/issues/402))

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:07:48 +05:30
renovate[bot]
d555eca729 Update Rust crate env_logger to v0.11.6 (#15107)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [env_logger](https://redirect.github.com/rust-cli/env_logger) |
workspace.dependencies | patch | `0.11.5` -> `0.11.6` |

---

### Release Notes

<details>
<summary>rust-cli/env_logger (env_logger)</summary>

###
[`v0.11.6`](https://redirect.github.com/rust-cli/env_logger/blob/HEAD/CHANGELOG.md#0116---2024-12-20)

[Compare
Source](https://redirect.github.com/rust-cli/env_logger/compare/v0.11.5...v0.11.6)

##### Features

-   Opt-in file and line rendering

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:05:45 +05:30
renovate[bot]
489f4fb27a Update Rust crate libc to v0.2.169 (#15108)
This PR contains the following updates:

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

---

### Release Notes

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

###
[`v0.2.169`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.169)

[Compare
Source](https://redirect.github.com/rust-lang/libc/compare/0.2.168...0.2.169)

##### Added

- FreeBSD: add more socket TCP stack constants
[#&#8203;4193](https://redirect.github.com/rust-lang/libc/pull/4193)
- Fuchsia: add a `sockaddr_vm` definition
[#&#8203;4194](https://redirect.github.com/rust-lang/libc/pull/4194)

##### Fixed

**Breaking**:
[rust-lang/rust#132975](https://redirect.github.com/rust-lang/rust/pull/132975)
corrected the signedness of `core::ffi::c_char` on various Tier 2 and
Tier 3 platforms (mostly Arm and RISC-V) to match Clang. This release
contains the corresponding changes to `libc`, including the following
specific pull requests:

- ESP-IDF: Replace arch-conditional `c_char` with a reexport
[#&#8203;4195](https://redirect.github.com/rust-lang/libc/pull/4195)
- Fix `c_char` on various targets
[#&#8203;4199](https://redirect.github.com/rust-lang/libc/pull/4199)
- Mirror `c_char` configuration from `rust-lang/rust`
[#&#8203;4198](https://redirect.github.com/rust-lang/libc/pull/4198)

##### Cleanup

- Do not re-export `c_void` in target-specific code
[#&#8203;4200](https://redirect.github.com/rust-lang/libc/pull/4200)

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:04:20 +05:30
renovate[bot]
429d3d78b8 Update Rust crate lsp-server to v0.7.8 (#15109)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [lsp-server](https://redirect.github.com/rust-lang/rust-analyzer)
([source](https://redirect.github.com/rust-lang/rust-analyzer/tree/HEAD/lib/lsp-server))
| workspace.dependencies | patch | `0.7.7` -> `0.7.8` |

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 09:57:13 +05:30
renovate[bot]
4dae9ee1cf Update Rust crate serde_json to v1.0.134 (#15110)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.133` -> `1.0.134` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.134`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.134)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.133...v1.0.134)

- Add `RawValue` associated constants for literal `null`, `true`,
`false`
([#&#8203;1221](https://redirect.github.com/serde-rs/json/issues/1221),
thanks [@&#8203;bheylin](https://redirect.github.com/bheylin))

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 09:55:26 +05:30
renovate[bot]
d8b4779a87 Update Rust crate syn to v2.0.91 (#15111)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [syn](https://redirect.github.com/dtolnay/syn) |
workspace.dependencies | patch | `2.0.90` -> `2.0.91` |

---

### Release Notes

<details>
<summary>dtolnay/syn (syn)</summary>

###
[`v2.0.91`](https://redirect.github.com/dtolnay/syn/releases/tag/2.0.91)

[Compare
Source](https://redirect.github.com/dtolnay/syn/compare/2.0.90...2.0.91)

- Support parsing `Vec<Arm>` using `parse_quote!`
([#&#8203;1796](https://redirect.github.com/dtolnay/syn/issues/1796),
[#&#8203;1797](https://redirect.github.com/dtolnay/syn/issues/1797))

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 09:54:41 +05:30
renovate[bot]
88cdfcfb11 Update Rust crate thiserror to v2.0.9 (#15112)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [thiserror](https://redirect.github.com/dtolnay/thiserror) |
workspace.dependencies | patch | `2.0.7` -> `2.0.9` |

---

### Release Notes

<details>
<summary>dtolnay/thiserror (thiserror)</summary>

###
[`v2.0.9`](https://redirect.github.com/dtolnay/thiserror/releases/tag/2.0.9)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/2.0.8...2.0.9)

- Work around `missing_inline_in_public_items` clippy restriction being
triggered in macro-generated code
([#&#8203;404](https://redirect.github.com/dtolnay/thiserror/issues/404))

###
[`v2.0.8`](https://redirect.github.com/dtolnay/thiserror/releases/tag/2.0.8)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/2.0.7...2.0.8)

- Improve support for macro-generated `derive(Error)` call sites
([#&#8203;399](https://redirect.github.com/dtolnay/thiserror/issues/399))

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 09:53:37 +05:30
InSync
f764f59971 [red-knot] Treat classes as instances of their respective metaclasses in boolean tests (#15105)
## Summary

Follow-up to #15089.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-23 01:30:51 +00:00
InSync
3b27d5dbad [red-knot] More precise inference for chained boolean expressions (#15089)
## Summary

Resolves #13632.

## Test Plan

Markdown tests.
2024-12-22 10:02:28 -08:00
InSync
60e433c3b5 Rename round_applicability.rs back to unnecessary_cast_to_int.rs (#15095) 2024-12-21 21:32:26 +01:00
TomerBin
2fb6b320d8 Use TypeChecker for detecting fastapi routes (#15093) 2024-12-21 15:45:28 +01:00
Aaron Miller
fd4bea52e5 Fix type subscript on older python versions (#15090) 2024-12-21 15:28:02 +01:00
InSync
bd023c4500 [ruff] Detect more strict-integer expressions (RUF046) (#14833)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-21 14:23:26 +00:00
David Peter
000948ad3b [red-knot] Statically known branches (#15019)
## Summary

This changeset adds support for precise type-inference and
boundness-handling of definitions inside control-flow branches with
statically-known conditions, i.e. test-expressions whose truthiness we
can unambiguously infer as *always false* or *always true*.

This branch also includes:
- `sys.platform` support
- statically-known branches handling for Boolean expressions and while
  loops
- new `target-version` requirements in some Markdown tests which were
  now required due to the understanding of `sys.version_info` branches.

closes #12700 
closes #15034 

## Performance

### `tomllib`, -7%, needs to resolve one additional module (sys)

| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `./red_knot_main --project /home/shark/tomllib` | 22.2 ± 1.3 | 19.1 |
25.6 | 1.00 |
| `./red_knot_feature --project /home/shark/tomllib` | 23.8 ± 1.6 | 20.8
| 28.6 | 1.07 ± 0.09 |

### `black`, -6%

| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `./red_knot_main --project /home/shark/black` | 129.3 ± 5.1 | 119.0 |
137.8 | 1.00 |
| `./red_knot_feature --project /home/shark/black` | 136.5 ± 6.8 | 123.8
| 147.5 | 1.06 ± 0.07 |

## Test Plan

- New Markdown tests for the main feature in
  `statically-known-branches.md`
- New Markdown tests for `sys.platform`
- Adapted tests for `EllipsisType`, `Never`, etc
2024-12-21 11:33:10 +01:00
my1e5
d3f51cf3a6 [pydocstyle] Split on first whitespace character (D403) (#15082)
## Summary

This PR fixes an issue where Ruff's `D403` rule
(`first-word-uncapitalized`) was not detecting some single-word edge
cases that are picked up by `pydocstyle`.

The change involves extracting the first word of the docstring by
identifying the first whitespace character. This is consistent with
`pydocstyle` which uses `.split()` - see
8d0cdfc93e/src/pydocstyle/checker.py (L581C13-L581C64)

## Example

Here is a playground example -
https://play.ruff.rs/eab9ea59-92cf-4e44-b1a9-b54b7f69b178

```py
def example1():
    """foo"""

def example2():
    """foo
    
    Hello world!
    """

def example3():
    """foo bar

    Hello world!
    """

def example4():
    """
    foo
    """

def example5():
    """
    foo bar
    """
```

`pydocstyle` detects all five cases:
```bash
$ pydocstyle test.py --select D403
dev/test.py:2 in public function `example1`:
        D403: First word of the first line should be properly capitalized ('Foo', not 'foo')
dev/test.py:5 in public function `example2`:
        D403: First word of the first line should be properly capitalized ('Foo', not 'foo')
dev/test.py:11 in public function `example3`:
        D403: First word of the first line should be properly capitalized ('Foo', not 'foo')
dev/test.py:17 in public function `example4`:
        D403: First word of the first line should be properly capitalized ('Foo', not 'foo')
dev/test.py:22 in public function `example5`:
        D403: First word of the first line should be properly capitalized ('Foo', not 'foo')
```
Ruff (`0.8.4`) fails to catch example2 and example4.

## Test Plan

* Added two new test cases to cover the previously missed single-word
docstring cases.
2024-12-20 12:55:50 -06:00
Dhruv Manilawala
d47fba1e4a [red-knot] Add support for unpacking union types (#15052)
## Summary

Refer:
https://github.com/astral-sh/ruff/issues/13773#issuecomment-2548020368

This PR adds support for unpacking union types. 

Unpacking a union type requires us to first distribute the types for all
the targets that are involved in an unpacking. For example, if there are
two targets and a union type that needs to be unpacked, each target will
get a type from each element in the union type.

For example, if the type is `tuple[int, int] | tuple[int, str]` and the
target has two elements `(a, b)`, then
* The type of `a` will be a union of `int` and `int` which are at index
0 in the first and second tuple respectively which resolves to an `int`.
* Similarly, the type of `b` will be a union of `int` and `str` which
are at index 1 in the first and second tuple respectively which will be
`int | str`.

### Refactors

There are couple of refactors that are added in this PR:
* Add a `debug_assertion` to validate that the unpack target is a list
or a tuple
* Add a separate method to handle starred expression

## Test Plan

Update `unpacking.md` with additional test cases that uses union types.
This is done using parameter type hints style.
2024-12-20 16:31:15 +05:30
David Salvisberg
089a98e904 [ruff] Adds an allowlist for unsafe-markup-use (RUF035) (#15076)
Closes: #14523

## Summary

Adds a whitelist of calls allowed to be used within a
`markupsafe.Markup` call.

## Test Plan

`cargo nextest run`
2024-12-20 09:36:12 +00:00
Micha Reiser
913bce3cd5 Basic support for type: ignore comments (#15046)
## Summary

This PR adds initial support for `type: ignore`. It doesn't do anything
fancy yet like:

* Detecting invalid type ignore comments
* Detecting type ignore comments that are part of another suppression
comment: `# fmt: skip # type: ignore`
* Suppressing specific lints `type: ignore [code]`
* Detecting unsused type ignore comments
* ...

The goal is to add this functionality in separate PRs.

## Test Plan

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-20 10:35:09 +01:00
Harutaka Kawamura
6195c026ff [flake8-comprehensions] Skip C416 if comprehension contains unpacking (#14909)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

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

Fix #11482. Applies
https://github.com/adamchainz/flake8-comprehensions/pull/205 to ruff.
`C416` should be skipped if comprehension contains unpacking. Here's an
example:

```python
list_of_lists = [[1, 2], [3, 4]]

# ruff suggests `list(list_of_lists)` here, but that would change the result.
# `list(list_of_lists)` is not `[(1, 2), (3, 4)]`
a = [(x, y) for x, y in list_of_lists]

# This is equivalent to `list(list_of_lists)`
b = [x for x in list_of_lists]
```

## Test Plan

<!-- How was it tested? -->

Existing checks

---------

Signed-off-by: harupy <hkawamura0130@gmail.com>
2024-12-20 14:35:30 +05:30
KotlinIsland
c0b0491703 Update docs for eq-without-hash (#14885)
## Summary

resolves #14883

This PR removes the known limitation section in the documentation of
`eq-without-hash`. That is not actually a limitation as a subclass
overriding the `__eq__` method would have its `__hash__` set to `None`
implicitly. The user should explicitly inherit the `__hash__` method
from the parent class.

## Test Plan

<img width="619" alt="Screenshot 2024-12-20 at 2 02 47 PM"
src="https://github.com/user-attachments/assets/552defcd-25e1-4153-9ab9-e5b9d5fbe8cc"
/>

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-12-20 14:03:16 +05:30
Micha Reiser
51863b460b Use &'static str for Replacement (#15075)
## Summary

Smaller nits follow up to https://github.com/astral-sh/ruff/pull/15054
because I wasn't able to push to the branch directly.

## Test Plan

`cargo test`
2024-12-20 07:36:21 +00:00
Wei Lee
14a5a2629e [airflow]: extend removed method calls (AIR302) (#15054)
## Summary


Airflow 3.0 removes various deprecated functions, members, modules, and
other values. They have been deprecated in 2.x, but the removal causes
incompatibilities that we want to detect. This PR deprecates the
following names and add a function for removed methods

* `airflow.datasets.manager.DatasetManager.register_dataset_change` →
`airflow.assets.manager.AssetManager.register_asset_change`
* `airflow.datasets.manager.DatasetManager.create_datasets` →
`airflow.assets.manager.AssetManager.create_assets`
* `airflow.datasets.manager.DatasetManager.notify_dataset_created` →
`airflow.assets.manager.AssetManager.notify_asset_created`
* `airflow.datasets.manager.DatasetManager.notify_dataset_changed` →
`airflow.assets.manager.AssetManager.notify_asset_changed`
* `airflow.datasets.manager.DatasetManager.notify_dataset_alias_created`
→ `airflow.assets.manager.AssetManager.notify_asset_alias_created`
*
`airflow.providers.amazon.auth_manager.aws_auth_manager.AwsAuthManager.is_authorized_dataset`
→
`airflow.providers.amazon.auth_manager.aws_auth_manager.AwsAuthManager.is_authorized_asset`
* `airflow.lineage.hook.HookLineageCollector.create_dataset` →
`airflow.lineage.hook.HookLineageCollector.create_asset`
* `airflow.lineage.hook.HookLineageCollector.add_input_dataset` →
`airflow.lineage.hook.HookLineageCollector.add_input_asset`
* `airflow.lineage.hook.HookLineageCollector.add_output_dataset` →
`airflow.lineage.hook.HookLineageCollector.dd_output_asset`
* `airflow.lineage.hook.HookLineageCollector.collected_datasets` →
`airflow.lineage.hook.HookLineageCollector.collected_assets`
*
`airflow.providers_manager.ProvidersManager.initialize_providers_dataset_uri_resources`
→
`airflow.providers_manager.ProvidersManager.initialize_providers_asset_uri_resources`

## Test Plan

A test fixture is included in the PR.
2024-12-20 08:30:30 +01:00
Alex Waygood
3aed14935d [red-knot] Add support for @final classes (#15070)
Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-19 21:02:14 +00:00
Alex Waygood
bcec5e615b [red-knot] Rename and rework the CoreStdlibModule enum (#15071) 2024-12-19 20:59:00 +00:00
Alex Waygood
a06099dffe [red-knot] Move attribute access on ModuleLiteral types into a dedicated method (#15067) 2024-12-19 16:02:16 +00:00
Alex Waygood
bb43085939 [red-knot] Reduce TODOs in Type::member() (#15066) 2024-12-19 15:54:01 +00:00
Dylan
c1eaf6ff72 Modify parsing of raise with cause when exception is absent (#15049)
When confronted with `raise from exc` the parser will now create a
`StmtRaise` that has `None` for the exception and `exc` for the cause.

Before, the parser created a `StmtRaise` with `from` for the exception,
no cause, and a spurious expression `exc` afterwards.
2024-12-19 13:36:32 +00:00
Dhruv Manilawala
3bb0dac235 Bump version to 0.8.4 (#15064) 2024-12-19 13:15:42 +00:00
Alex Waygood
40cba5dc8a [red-knot] Cleanup various todo_type!() messages (#15063)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-19 13:03:41 +00:00
Dylan
596d80cc8e [perflint] Parenthesize walrus expressions in autofix for manual-list-comprehension (PERF401) (#15050) 2024-12-19 06:56:45 -06:00
Alex Waygood
d8b9a366c8 Disable actionlint hook by default when running pre-commit locally (#15061) 2024-12-19 12:45:17 +00:00
Taras Matsyk
85e71ba91a [flake8-bandit] Check S105 for annotated assignment (#15059)
## Summary

A follow up PR on https://github.com/astral-sh/ruff/issues/14991
Ruff ignores hardcoded passwords for typed variables. Add a rule to
catch passwords in typed code bases

## Test Plan

Includes 2 more test typed variables
2024-12-19 12:26:40 +00:00
Douglas Creager
2802cbde29 Don't special-case class instances in unary expression inference (#15045)
We have a handy `to_meta_type` that does the right thing for class
instances, and also works for all of the other types that are “instances
of” something. Unless I'm missing something, this should let us get rid
of the catch-all clause in one fell swoop.

cf #14548
2024-12-18 14:37:17 -05:00
InSync
ed2bce6ebb [red-knot] Report invalid exceptions (#15042)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-18 18:31:24 +00:00
InSync
f0012df686 Fix typos in RUF043.py (#15044)
(Accidentally introduced in #14966.)
2024-12-18 15:39:55 +00:00
Micha Reiser
0fc4e8f795 Introduce InferContext (#14956)
## Summary

I'm currently on the fence about landing the #14760 PR because it's
unclear how we'd support tracking used and unused suppression comments
in a performant way:
* Salsa adds an "untracked" dependency to every query reading
accumulated values. This has the effect that the query re-runs on every
revision. For example, a possible future query
`unused_suppression_comments(db, file)` would re-run on every
incremental change and for every file. I don't expect the operation
itself to be expensive, but it all adds up in a project with 100k+ files
* Salsa collects the accumulated values by traversing the entire query
dependency graph. It can skip over sub-graphs if it is known that they
contain no accumulated values. This makes accumulators a great tool for
when they are rare; diagnostics are a good example. Unfortunately,
suppressions are more common, and they often appear in many different
files, making the "skip over subgraphs" optimization less effective.

Because of that, I want to wait to adopt salsa accumulators for type
check diagnostics (we could start using them for other diagnostics)
until we have very specific reasons that justify regressing incremental
check performance.

This PR does a "small" refactor that brings us closer to what I have in
#14760 but without using accumulators. To emit a diagnostic, a method
needs:

* Access to the db
* Access to the currently checked file

This PR introduces a new `InferContext` that holds on to the db, the
current file, and the reported diagnostics. It replaces the
`TypeCheckDiagnosticsBuilder`. We pass the `InferContext` instead of the
`db` to methods that *might* emit diagnostics. This simplifies some of
the `Outcome` methods, which can now be called with a context instead of
a `db` and the diagnostics builder. Having the `db` and the file on a
single type like this would also be useful when using accumulators.

This PR doesn't solve the issue that the `Outcome` types feel somewhat
complicated nor that it can be annoying when you need to report a
`Diagnostic,` but you don't have access to an `InferContext` (or the
file). However, I also believe that accumulators won't solve these
problems because:

* Even with accumulators, it's necessary to have a reference to the file
that's being checked. The struggle would be to get a reference to that
file rather than getting a reference to `InferContext`.
* Users of the `HasTy` trait (e.g., a linter) don't want to bother
getting the `File` when calling `Type::return_ty` because they aren't
interested in the created diagnostics. They just want to know what
calling the current expression would return (and if it even is a
callable). This is what the different methods of `Outcome` enable today.
I can ask for the return type without needing extra data that's only
relevant for emitting a diagnostic.

A shortcoming of this approach is that it is now a bit confusing when to
pass `db` and when an `InferContext`. An option is that we'd make the
`file` on `InferContext` optional (it won't collect any diagnostics if
`None`) and change all methods on `Type` to take `InferContext` as the
first argument instead of a `db`. I'm interested in your opinion on
this.

Accumulators are definitely harder to use incorrectly because they
remove the need to merge the diagnostics explicitly and there's no risk
that we accidentally merge the diagnostics twice, resulting in
duplicated diagnostics. I still value performance more over making our
life slightly easier.
2024-12-18 12:22:33 +00:00
InSync
ac81c72bf3 [ruff] Ambiguous pattern passed to pytest.raises() (RUF043) (#14966) 2024-12-18 11:53:48 +00:00
David Salvisberg
c0b7c36d43 [ruff] Avoid false positives for RUF027 for typing context bindings. (#15037)
Closes #14000 

## Summary

For typing context bindings we know that they won't be available at
runtime. We shouldn't recommend a fix, that will result in name errors
at runtime.

## Test Plan

`cargo nextest run`
2024-12-18 08:50:49 +01:00
Douglas Creager
e8e461da6a Prioritize attribute in from/import statement (#15041)
This tweaks the new semantics from #15026 a bit when a symbol could be
interpreted both as an attribute and a submodule of a package. For
`from...import`, we should actually prioritize the attribute, because of
how the statement itself is implemented [1].

> 1. check if the imported module has an attribute by that name
> 2. if not, attempt to import a submodule with that name and then check
the imported module again for that attribute

[1] https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
2024-12-17 16:58:23 -05:00
Douglas Creager
91c9168dd7 Handle nested imports correctly in from ... import (#15026)
#14946 fixed our handling of nested imports with the `import` statement,
but didn't touch `from...import` statements.

cf
https://github.com/astral-sh/ruff/issues/14826#issuecomment-2525344515
2024-12-17 14:23:34 -05:00
Micha Reiser
80577a49f8 Upgrade salsa in fuzzer script (#15040) 2024-12-17 18:01:58 +01:00
cake-monotone
f463fa7b7c [red-knot] Narrowing For Truthiness Checks (if x or if not x) (#14687)
## Summary

Fixes #14550.

Add `AlwaysTruthy` and `AlwaysFalsy` types, representing the set of objects whose `__bool__` method can only ever return `True` or `False`, respectively, and narrow `if x` and `if not x` accordingly.


## Test Plan

- New Markdown test for truthiness narrowing `narrow/truthiness.md`
- unit tests in `types.rs` and `builders.rs` (`cargo test --package
red_knot_python_semantic --lib -- types`)
2024-12-17 08:37:07 -08:00
Micha Reiser
c3b6139f39 Upgrade salsa (#15039)
The only code change is that Salsa now requires the `Db` to implement
`Clone` to create "lightweight" snapshots.
2024-12-17 15:50:33 +00:00
InSync
c9fdb1f5e3 [pylint] Preserve original value format (PLR6104) (#14978)
## Summary

Resolves #11672.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-17 16:07:07 +01:00
Alex Waygood
463046ae07 [red-knot] Explicitly test diagnostics are emitted for unresolvable submodule imports (#15035) 2024-12-17 12:55:50 +00:00
Micha Reiser
dcb99cc817 Fix stale File status in tests (#15030)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/15027

The `MemoryFileSystem::write_file` API automatically creates
non-existing ancestor directoryes
but we failed to update the status of the now created ancestor
directories in the `Files` data structure.


## Test Plan

Tested that the case in https://github.com/astral-sh/ruff/issues/15027
now passes regardless of whether the *Simple* case is commented out or
not
2024-12-17 12:45:36 +01:00
InSync
7c2e7cf25e [red-knot] Basic support for other legacy typing aliases (#14998)
## Summary

Resolves #14997.

## Test Plan

Markdown tests.
2024-12-17 09:33:15 +00:00
Wei Lee
867a8f9497 feat(AIR302): extend the following rules (#15015)
## Summary


Airflow 3.0 removes various deprecated functions, members, modules, and
other values. They have been deprecated in 2.x, but the removal causes
incompatibilities that we want to detect. This PR deprecates the
following names.

* `airflow.api_connexion.security.requires_access_dataset` →
`airflow.api_connexion.security.requires_access_asset`
* `airflow.auth.managers.base_auth_manager.is_authorized_dataset` →
`airflow.auth.managers.base_auth_manager.is_authorized_asset`
* `airflow.auth.managers.models.resource_details.DatasetDetails` →
`airflow.auth.managers.models.resource_details.AssetDetails`
* `airflow.lineage.hook.DatasetLineageInfo` →
`airflow.lineage.hook.AssetLineageInfo`
* `airflow.security.permissions.RESOURCE_DATASET` →
`airflow.security.permissions.RESOURCE_ASSET`
* `airflow.www.auth.has_access_dataset` →
`airflow.www.auth.has_access_dataset.has_access_asset`
* remove `airflow.datasets.DatasetAliasEvent`
* `airflow.datasets.Dataset` → `airflow.sdk.definitions.asset.Asset`
* `airflow.Dataset` → `airflow.sdk.definitions.asset.Asset`
* `airflow.datasets.DatasetAlias` →
`airflow.sdk.definitions.asset.AssetAlias`
* `airflow.datasets.DatasetAll` →
`airflow.sdk.definitions.asset.AssetAll`
* `airflow.datasets.DatasetAny` →
`airflow.sdk.definitions.asset.AssetAny`
* `airflow.datasets.metadata` → `airflow.sdk.definitions.asset.metadata`
* `airflow.datasets.expand_alias_to_datasets` →
`airflow.sdk.definitions.asset.expand_alias_to_assets`
* `airflow.datasets.manager.dataset_manager` → `airflow.assets.manager`
* `airflow.datasets.manager.resolve_dataset_manager` →
`airflow.assets.resolve_asset_manager`
* `airflow.datasets.manager.DatasetManager` →
`airflow.assets.AssetManager`
* `airflow.listeners.spec.dataset.on_dataset_created` →
`airflow.listeners.spec.asset.on_asset_created`
* `airflow.listeners.spec.dataset.on_dataset_changed` →
`airflow.listeners.spec.asset.on_asset_changed`
* `airflow.timetables.simple.DatasetTriggeredTimetable` →
`airflow.timetables.simple.AssetTriggeredTimetable`
* `airflow.timetables.datasets.DatasetOrTimeSchedule` →
`airflow.timetables.assets.AssetOrTimeSchedule`
*
`airflow.providers.amazon.auth_manager.avp.entities.AvpEntities.DATASET`
→ `airflow.providers.amazon.auth_manager.avp.entities.AvpEntities.ASSET`
* `airflow.providers.amazon.aws.datasets.s3.create_dataset` →
`airflow.providers.amazon.aws.assets.s3.create_asset`
*
`airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage`
→
`airflow.providers.amazon.aws.datasets.s3.convert_dataset_to_openlineage`
* `airflow.providers.amazon.aws.datasets.s3.sanitize_uri` →
`airflow.providers.amazon.aws.assets.s3.sanitize_uri`
*
`airflow.providers.common.io.datasets.file.convert_dataset_to_openlineage`
→ `airflow.providers.common.io.assets.file.convert_asset_to_openlineage`
* `airflow.providers.common.io.datasets.file.sanitize_uri` →
`airflow.providers.common.io.assets.file.sanitize_uri`
* `airflow.providers.common.io.datasets.file.create_dataset` →
`airflow.providers.common.io.assets.file.create_asset`
* `airflow.providers.google.datasets.bigquery.sanitize_uri` →
`airflow.providers.google.assets.bigquery.sanitize_uri`
* `airflow.providers.google.datasets.gcs.create_dataset` →
`airflow.providers.google.assets.gcs.create_asset`
* `airflow.providers.google.datasets.gcs.sanitize_uri` →
`airflow.providers.google.assets.gcs.sanitize_uri`
* `airflow.providers.google.datasets.gcs.convert_dataset_to_openlineage`
→ `airflow.providers.google.assets.gcs.convert_asset_to_openlineage`
*
`airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_dataset`
→
`airflow.providers.fab.auth_manager.fab_auth_manager.is_authorized_asset`
* `airflow.providers.openlineage.utils.utils.DatasetInfo` →
`airflow.providers.openlineage.utils.utils.AssetInfo`
* `airflow.providers.openlineage.utils.utils.translate_airflow_dataset`
→ `airflow.providers.openlineage.utils.utils.translate_airflow_asset`
* `airflow.providers.postgres.datasets.postgres.sanitize_uri` →
`airflow.providers.postgres.assets.postgres.sanitize_uri`
* `airflow.providers.mysql.datasets.mysql.sanitize_uri` →
`airflow.providers.mysql.assets.mysql.sanitize_uri`
* `airflow.providers.trino.datasets.trino.sanitize_uri` →
`airflow.providers.trino.assets.trino.sanitize_uri`

In additional to the newly added rules above, the message for
`airflow.contrib.*` and `airflow.subdag.*` has been extended,
`airflow.sensors.external_task.ExternalTaskSensorLink` error has been
fixed and the test fixture has been reorganized

## Test Plan

A test fixture is included in the PR.
2024-12-17 08:32:48 +01:00
w0nder1ng
e22718f25f [perflint] Simplify finding the loop target in PERF401 (#15025)
Fixes #15012.

```python
def f():
    # panics when the code can't find the loop variable
    values = [1, 2, 3]
    result = []
    for i in values:
        result.append(i + 1)
    del i
```

I'm not sure exactly why this test case panics, but I suspect the `del
i` removes the binding from the semantic model's symbols.

I changed the code to search for the correct binding by directly
iterating through the bindings. Since we know exactly which binding we
want, this should find the loop variable without any complications.
2024-12-17 08:30:32 +01:00
Dhruv Manilawala
dcdc6e7c64 [red-knot] Avoid undeclared path when raising conflicting declarations (#14958)
## Summary

This PR updates the logic when raising conflicting declarations
diagnostic to avoid the undeclared path if present.

The conflicting declaration diagnostics is added when there are two or
more declarations in the control flow path of a definition whose type
isn't equivalent to each other. This can be seen in the following
example:

```py
if flag:
	x: int
x = 1  # conflicting-declarations: Unknown, int
```

After this PR, we'd avoid considering "Unknown" as part of the
conflicting declarations. This means we'd still flag it for the
following case:

```py
if flag:
	x: int
else:
	x: str
x = 1  # conflicting-declarations: int, str
```

A solution that's local to the exception control flow was also explored
which required updating the logic for merging the flow snapshot to avoid
considering declarations using a flag. This is preserved here:
https://github.com/astral-sh/ruff/compare/dhruv/control-flow-no-declarations?expand=1.

The main motivation to avoid that is we don't really understand what the
user experience is w.r.t. the Unknown type and the
conflicting-declaration diagnostics. This makes us unsure on what the
right semantics are as to whether that diagnostics should be raised or
not and when to raise them. For now, we've decided to move forward with
this PR and could decide to adopt another solution or remove the
conflicting-declaration diagnostics in the future.

Closes: #13966 

## Test Plan

Update the existing mdtest case. Add an additional case specific to
exception control flow to verify that the diagnostic is not being raised
now.
2024-12-17 09:49:39 +05:30
Douglas Creager
4ddf9228f6 Bind top-most parent when importing nested module (#14946)
When importing a nested module, we were correctly creating a binding for
the top-most parent, but we were binding that to the nested module, not
to that parent module. Moreover, we weren't treating those submodules as
members of their containing parents. This PR addresses both issues, so
that nested imports work as expected.

As discussed in ~Slack~ whatever chat app I find myself in these days
😄, this requires keeping track of which modules have been imported
within the current file, so that when we resolve member access on a
module reference, we can see if that member has been imported as a
submodule. If so, we return the submodule reference immediately, instead
of checking whether the parent module's definition defines the symbol.

This is currently done in a flow insensitive manner. The `SemanticIndex`
now tracks all of the modules that are imported (via `import`, not via
`from...import`). The member access logic mentioned above currently only
considers module imports in the file containing the attribute
expression.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-16 16:15:40 -05:00
Alex Waygood
6d72be2683 Bump zizmor pre-commit hook to the latest version and fix new warnings (#15022) 2024-12-16 17:45:46 +00:00
Alex Waygood
712c886749 Add actionlint as a pre-commit hook (with shellcheck integration) (#15021) 2024-12-16 17:32:49 +00:00
renovate[bot]
50739f91dc Update dependency mdformat-mkdocs to v4 (#15011)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[mdformat-mkdocs](https://redirect.github.com/kyleking/mdformat-mkdocs)
([changelog](https://redirect.github.com/kyleking/mdformat-mkdocs/releases))
| `==3.1.1` -> `==4.0.0` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/mdformat-mkdocs/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mdformat-mkdocs/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mdformat-mkdocs/3.1.1/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mdformat-mkdocs/3.1.1/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>kyleking/mdformat-mkdocs (mdformat-mkdocs)</summary>

###
[`v4.0.0`](https://redirect.github.com/KyleKing/mdformat-mkdocs/releases/tag/v4.0.0)

[Compare
Source](https://redirect.github.com/kyleking/mdformat-mkdocs/compare/v3.1.1...v4.0.0)

#### What's Changed

- fix!: add newline after title for consistency with MKDocs style by
[@&#8203;KyleKing](https://redirect.github.com/KyleKing) in
[https://github.com/KyleKing/mdformat-mkdocs/pull/44](https://redirect.github.com/KyleKing/mdformat-mkdocs/pull/44)

**Full Changelog**:
https://github.com/KyleKing/mdformat-mkdocs/compare/v3.1.1...v4.0.0

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Kyle King <KyleKing@users.noreply.github.com>
2024-12-16 22:48:37 +05:30
Dylan
6a5eff6017 [pydocstyle] Skip leading whitespace for D403 (#14963)
This PR introduces three changes to `D403`, which has to do with
capitalizing the first word in a docstring.

1. The diagnostic and fix now skip leading whitespace when determining
what counts as "the first word".
2. The name has been changed to `first-word-uncapitalized` from
`first-line-capitalized`, for both clarity and compliance with our rule
naming policy.
3. The diagnostic message and documentation has been modified slightly
to reflect this.

Closes #14890
2024-12-16 09:09:27 -06:00
renovate[bot]
a623d8f7c4 Update pre-commit dependencies (#15008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-16 11:13:49 +00:00
Dhruv Manilawala
aa429b413f Check diagnostic refresh support from client capability (#15014)
## Summary

Per the LSP spec, the property name is `workspace.diagnostics` with an
`s` at the end but the `lsp-types` dependency uses
`workspace.diagnostic` (without an `s`). Our fork contains this fix
(0f58d62879)
so we should avoid the hardcoded value.

The implication of this is that the client which doesn't support
workspace refresh capability didn't support the [dynamic
configuration](https://docs.astral.sh/ruff/editors/features/#dynamic-configuration)
feature because the server would _always_ send the workspace refresh
request but the client would ignore it. We have a fallback logic to
publish the diagnostics instead:


5f6fc3988b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs (L28-L40)

fixes: #15013 

## Test Plan

### VS Code


https://github.com/user-attachments/assets/61ac8e6f-aa20-41cc-b398-998e1866b5bc

### Neovim



https://github.com/user-attachments/assets/4131e13c-3fba-411c-9bb7-478d26eb8d56
2024-12-16 16:26:40 +05:30
renovate[bot]
425c248232 Update Rust crate colored to v2.2.0 (#15010)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [colored](https://redirect.github.com/mackwic/colored) |
workspace.dependencies | minor | `2.1.0` -> `2.2.0` |

---

### Release Notes

<details>
<summary>mackwic/colored (colored)</summary>

###
[`v2.2.0`](https://redirect.github.com/mackwic/colored/compare/v2.1.0...v2.2.0)

[Compare
Source](https://redirect.github.com/mackwic/colored/compare/v2.1.0...v2.2.0)

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 08:48:51 +01:00
renovate[bot]
bcd944347d Update dependency monaco-editor to v0.52.2 (#15006) 2024-12-15 20:26:21 -05:00
renovate[bot]
86eff81c6a Update Rust crate thiserror to v2.0.7 (#15005) 2024-12-15 20:26:14 -05:00
renovate[bot]
24ace68560 Update Rust crate serde to v1.0.216 (#15004) 2024-12-15 20:26:08 -05:00
renovate[bot]
b664505d7b Update Rust crate libc to v0.2.168 (#15003) 2024-12-15 20:25:59 -05:00
renovate[bot]
aa575da1e7 Update Rust crate fern to v0.7.1 (#15002) 2024-12-15 20:25:52 -05:00
renovate[bot]
921eb2acb3 Update Rust crate chrono to v0.4.39 (#15001) 2024-12-15 20:25:46 -05:00
renovate[bot]
8665d2dc95 Update Rust crate bstr to v1.11.1 (#15000) 2024-12-15 20:25:39 -05:00
renovate[bot]
1cc27c995c Update NPM Development dependencies (#14999) 2024-12-15 20:25:10 -05:00
renovate[bot]
a93bc2af6b Update dependency ruff to v0.8.3 (#15007) 2024-12-15 20:25:04 -05:00
Alex Waygood
d848182340 Pin mdformat plugins in pre-commit (#14992) 2024-12-15 19:37:45 +00:00
InSync
7173e6a20b Use stripping block (|-) for page descriptions (#14980)
## Summary

Resolves #14976.

Currently, we uses this "[plain
scalar](https://yaml.org/spec/1.2.2/#733-plain-style)" format:

```yaml
description: Checks for `if key in dictionary: del dictionary[key]`.
```

Plain scalar must not contain the sequence `: `, however, so the above
is invalid.

This PR changes that to:

```yaml
description: |-
  Checks for `if key in dictionary: del dictionary[key]`.
```

`|` denotes a "[block
scalar](https://yaml.org/spec/1.2.2/#81-block-scalar-styles)", whereas
[the `-` chomping
indicator](https://yaml.org/spec/1.2.2/#8112-block-chomping-indicator)
requires that a trailing newline, if any, must be stripped.

## Test Plan


![](https://github.com/user-attachments/assets/f00b606a-d6fe-46ac-a1c5-6a8665204ea3)
2024-12-15 17:07:29 +01:00
w0nder1ng
4a7536dc94 [perflint] Fix panic in perf401 (#14971)
Fixes #14969.

The issue was that this line:

```rust
let from_assign_to_loop = TextRange::new(binding_stmt.end(), for_stmt.start());
```

was not safe if the binding was after the target. The only way (at least
that I can think of) this can happen is if they are in different scopes,
so it now checks for that before checking if there are usages between
the two.
2024-12-15 16:22:04 +01:00
Dimitri Papadopoulos Orfanos
2d15d7d1af Improve the documentation of E201/E202 (#14983)
## Summary

The summary is misleading, as well as the
`whitespace-after-open-bracket` and `whitespace-before-close-bracket`
names - it's not only brackets, but also parentheses and braces. Align
the documentation with the actual behaviour.

Don't change the names, but align the documentation with the behaviour.

## Test Plan

No test (documentation).
2024-12-15 16:20:04 +01:00
Rebecca Chen
112e9d2d82 [ruff_python_ast] Add name and default functions to TypeParam. (#14964)
## Summary

This change adds `name` and `default` functions to `TypeParam` to access
the corresponding attributes more conveniently. I currently have these
as helper functions in code built on top of ruff_python_ast, and they
seemed like they might be generally useful.

## Test Plan

Ran the checks listed in CONTRIBUTING.md#development.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-15 12:04:51 +00:00
Alex Waygood
1389cb8e59 [red-knot] Emit an error if a bare Annotated or Literal is used in a type expression (#14973) 2024-12-15 02:00:52 +00:00
Alex Waygood
fa46ba2306 [red-knot] Fix bugs relating to assignability of dynamic type[] types (#14972) 2024-12-15 01:15:10 +00:00
github-actions[bot]
53c7ef8bfe Sync vendored typeshed stubs (#14977)
Co-authored-by: typeshedbot <>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-15 01:02:41 +00:00
Alex Waygood
4d64cdb83c [red-knot] ClassLiteral(<T>) is not a disjoint type from Instance(<metaclass of T>) (#14970)
## Summary

A class is an instance of its metaclass, so `ClassLiteral("ABC")` is not
disjoint from `Instance("ABCMeta")`. However, we erroneously consider
the two types disjoint on the `main` branch. This PR fixes that.

This bug was uncovered by adding some more core types to the property
tests that provide coverage for classes that have custom metaclasses.
The additions to the property tests are included in this PR.

## Test Plan

New unit tests and property tests added. Tested with:
- `cargo test -p red_knot_python_semantic`
- `QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable`

The assignability property test fails on this branch, but that's a known
issue that exists on `main`, due to
https://github.com/astral-sh/ruff/issues/14899.
2024-12-14 11:28:09 -08:00
Carl Meyer
ac31b26a0e [red-knot] type[] is disjoint from None, LiteralString (#14967)
## Summary

Teach red-knot that `type[...]` is always disjoint from `None` and from
`LiteralString`. Fixes #14925.

This should properly be generalized to "all instances of final types
which are not subclasses of `type`", but until we support finality,
hardcoding `None` (which is known to be final) allows us to fix the
subtype transitivity property test.

## Test Plan

Existing tests pass, added new unit tests for `is_disjoint_from` and
`is_subtype_of`.

`QUICKCHECK_TESTS=100000 cargo test -p red_knot_python_semantic --
--ignored types::property_tests::stable` fails only the "assignability
is reflexive" test, which is known to fail on `main` (#14899).

The same command, with `property_tests.rs` edited to prevent generating
intersection tests (the cause of #14899), passes all quickcheck tests.
2024-12-14 11:02:49 +01:00
InSync
a80e934838 [red-knot] Error out when an mdtest code block is unterminated (#14965)
## Summary

Resolves #14934.

## Test Plan

Added a unit test.
2024-12-13 21:51:21 -08:00
Alex Waygood
224c8438bd [red-knot] Minor simplifications to types.rs (#14962) 2024-12-13 20:31:51 +00:00
Alex Waygood
90a5439791 [red-knot] Use type[Unknown] rather than Unknown as the fallback metaclass for invalid classes (#14961) 2024-12-13 19:48:51 +00:00
Alex Waygood
4b2b126b9f [red-knot] Make is_subtype_of exhaustive (#14924) 2024-12-13 19:31:22 +00:00
InSync
9798556eb5 [red-knot] Alphabetize rules (#14960)
## Summary

Follow-up from #14950.

## Test Plan

Purely stylistic change. Shouldn't affect any functionalities.
2024-12-13 10:39:18 -08:00
InSync
aa1938f6ba [red-knot] Understand Annotated (#14950)
## Summary

Resolves #14922.

## Test Plan

Markdown tests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2024-12-13 09:41:37 -08:00
Dhruv Manilawala
3533d7f5b4 [red-knot] Display definition range in trace logs (#14955)
I've mainly opened this PR to get some opinions. I've found having some
additional information in the tracing logs to be useful to determine
what we are currently inferring. For the `Definition` ingredient, the
range seems to be much useful. I thought of using the identifier name
but we would have to deconstruct the `Expr` to find out the identifier
which seems a lot for just trace logs. Additionally, multiple
identifiers _could_ have the same name where range would be useful.

The ranges are isolated to the names that have been defined by the
definition except for the `except` block where the entire range is being
used because the name is optional.

***Before:***

```
3      ├─   0.074671s  54ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(3), kind: WillExecute { database_key: infer_definition_types(Id(1402)) } }
3      └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(1402), file=/Users/dhruv/playground/ruff/type_inference/isolated3/play.py}
3      ┌─┘
3      ├─   0.074768s  54ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(3), kind: WillExecute { database_key: inner_fn_name_(Id(2800)) } }
3      ├─   0.074807s  54ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(3), kind: WillExecute { database_key: infer_deferred_types(Id(1735)) } }
3      └─┐red_knot_python_semantic::types::infer::infer_deferred_types{definition=Id(1735), file=vendored://stdlib/typing.pyi}
3        ├─   0.074842s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(3), kind: WillExecute { database_key: infer_definition_types(Id(14f3)) } }
3        └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(14f3), file=vendored://stdlib/typing.pyi}
3          ├─   0.074871s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(3), kind: WillExecute { database_key: infer_expression_types(Id(1820)) } }
3          └─┐red_knot_python_semantic::types::infer::infer_expression_types{expression=Id(1820), file=vendored://stdlib/typing.pyi}
3            ├─   0.074924s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(3), kind: WillExecute { database_key: infer_definition_types(Id(1429)) } }
3            └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(1429), file=vendored://stdlib/typing.pyi}
3              ├─   0.074958s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(3), kind: WillExecute { database_key: infer_definition_types(Id(1428)) } }
3              └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(1428), file=vendored://stdlib/typing.pyi}
3              ┌─┘
```

***After:***

```
12      ├─   0.074609s  55ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(12), kind: WillExecute { database_key: infer_definition_types(Id(1402)) } }
12      └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(1402), range=36..37, file=/Users/dhruv/playground/ruff/type_inference/isolated3/play.py}
12      ┌─┘
12      ├─   0.074705s  55ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(12), kind: WillExecute { database_key: inner_fn_name_(Id(2800)) } }
12      ├─   0.074742s  55ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(12), kind: WillExecute { database_key: infer_deferred_types(Id(1735)) } }
12      └─┐red_knot_python_semantic::types::infer::infer_deferred_types{definition=Id(1735), range=30225..30236, file=vendored://stdlib/typing.pyi}
12        ├─   0.074775s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(12), kind: WillExecute { database_key: infer_definition_types(Id(14f3)) } }
12        └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(14f3), range=9472..9474, file=vendored://stdlib/typing.pyi}
12          ├─   0.074803s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(12), kind: WillExecute { database_key: infer_expression_types(Id(1820)) } }
12          └─┐red_knot_python_semantic::types::infer::infer_expression_types{expression=Id(1820), range=9477..9490, file=vendored://stdlib/typing.pyi}
12            ├─   0.074855s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(12), kind: WillExecute { database_key: infer_definition_types(Id(1429)) } }
12            └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(1429), range=3139..3146, file=vendored://stdlib/typing.pyi}
12              ├─   0.074892s   0ms TRACE red_knot_workspace::db Salsa event: Event { thread_id: ThreadId(12), kind: WillExecute { database_key: infer_definition_types(Id(1428)) } }
12              └─┐red_knot_python_semantic::types::infer::infer_definition_types{definition=Id(1428), range=3102..3107, file=vendored://stdlib/typing.pyi}
12              ┌─┘
```
2024-12-13 14:29:53 +00:00
Alex Waygood
0bbe166720 [red-knot] Move the ClassBase enum to its own submodule (#14957) 2024-12-13 13:12:39 +00:00
David Peter
c3a64b44b7 [red-knot] mdtest: python version requirements (#14954)
## Summary

This is not strictly required yet, but makes these tests future-proof.
They need a `python-version` requirement as they rely on language
features that are not available in 3.9.
2024-12-13 10:40:38 +01:00
Wei Lee
dfd7f38009 [airflow]: Import modules that has been moved to airflow providers (AIR303) (#14764)
## Summary

Many core Airflow features have been deprecated and moved to Airflow
Providers since users might need to install an additional package (e.g.,
`apache-airflow-provider-fab==1.0.0`); a separate rule (AIR303) is
created for this.

As some of the changes only relate to the module/package moved, instead
of listing out all the functions, variables, and classes in a module or
a package, it warns the user to import from the new path instead of the
specific name.

The following is the ones that has been moved to
`apache-airflow-provider-fab==1.0.0`

* module moved
* `airflow.api.auth.backend.basic_auth` →
`airflow.providers.fab.auth_manager.api.auth.backend.basic_auth`
* `airflow.api.auth.backend.kerberos_auth` →
`airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth`
* `airflow.auth.managers.fab.api.auth.backend.kerberos_auth` →
`airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth`
* `airflow.auth.managers.fab.security_manager.override` →
`airflow.providers.fab.auth_manager.security_manager.override`
* classes (e.g., functions, classes) moved
* `airflow.www.security.FabAirflowSecurityManagerOverride` →
`airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride`
* `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` →
`airflow.providers.fab.auth_manager.security_manager.FabAuthManager`

## Test Plan


A test fixture has been included for the rule.
2024-12-13 10:38:07 +01:00
David Peter
e96b13c027 [red-knot] Support typing.TYPE_CHECKING (#14952)
## Summary

Add support for `typing.TYPE_CHECKING` and
`typing_extensions.TYPE_CHECKING`.

relates to: https://github.com/astral-sh/ruff/issues/14170

## Test Plan

New Markdown-based tests
2024-12-13 09:24:48 +00:00
Micha Reiser
f52b1f4a4d Add tracing support to mdtest (#14935)
## Summary

This PR extends the mdtest configuration with a `log` setting that can
be any of:

* `true`: Enables tracing
* `false`: Disables tracing (default)
* String: An ENV_FILTER similar to `RED_KNOT_LOG`

```toml
log = true
```

Closes https://github.com/astral-sh/ruff/issues/13865

## Test Plan

I changed a test and tried `log=true`, `log=false`, and `log=INFO`
2024-12-13 09:10:01 +00:00
Micha Reiser
1c8f356e07 Re-enable the fuzzer job on PRs (#14953)
## Summary
This reverts https://github.com/astral-sh/ruff/pull/14478

I now broke main twice because I wasn't aware that the API was used by
the fuzzer.

## Test Plan
2024-12-13 09:07:27 +00:00
David Peter
2ccc9b19a7 [red-knot] Improve match mdtests (#14951)
## Summary

Minor improvement for the `match` tests to make sure we can't infer
statically whether or not a certain `case` applies.
2024-12-13 09:50:17 +01:00
Micha Reiser
c1837e4189 Rename custom-typeshed-dir, target-version and current-directory CLI options (#14930)
## Summary

This PR renames the `--custom-typeshed-dir`, `target-version`, and
`--current-directory` cli options to `--typeshed`,
`--python-version`, and `--project` as discussed in the CLI proposal
document.
I added aliases for `--target-version` (for Ruff compat) and
`--custom-typeshed-dir` (for Alex)

## Test Plan

Long help

```
An extremely fast Python type checker.

Usage: red_knot [OPTIONS] [COMMAND]

Commands:
  server  Start the language server
  help    Print this message or the help of the given subcommand(s)

Options:
      --project <PROJECT>
          Run the command within the given project directory.
          
          All `pyproject.toml` files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (`.venv`).
          
          Other command-line arguments (such as relative paths) will be resolved relative to the current working directory."#,

      --venv-path <PATH>
          Path to the virtual environment the project uses.
          
          If provided, red-knot will use the `site-packages` directory of this virtual environment to resolve type information for the project's third-party dependencies.

      --typeshed-path <PATH>
          Custom directory to use for stdlib typeshed stubs

      --extra-search-path <PATH>
          Additional path to use as a module-resolution source (can be passed multiple times)

      --python-version <VERSION>
          Python version to assume when resolving types
          
          [possible values: 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13]

  -v, --verbose...
          Use verbose output (or `-vv` and `-vvv` for more verbose output)

  -W, --watch
          Run in watch mode by re-running whenever files change

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version
```

Short help 

```
An extremely fast Python type checker.

Usage: red_knot [OPTIONS] [COMMAND]

Commands:
  server  Start the language server
  help    Print this message or the help of the given subcommand(s)

Options:
      --project <PROJECT>         Run the command within the given project directory
      --venv-path <PATH>          Path to the virtual environment the project uses
      --typeshed-path <PATH>      Custom directory to use for stdlib typeshed stubs
      --extra-search-path <PATH>  Additional path to use as a module-resolution source (can be passed multiple times)
      --python-version <VERSION>  Python version to assume when resolving types [possible values: 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13]
  -v, --verbose...                Use verbose output (or `-vv` and `-vvv` for more verbose output)
  -W, --watch                     Run in watch mode by re-running whenever files change
  -h, --help                      Print help (see more with '--help')
  -V, --version                   Print version

```

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-13 08:21:52 +00:00
David Peter
d7ce548893 [red-knot] Add narrowing for 'while' loops (#14947)
## Summary

Add type narrowing for `while` loops and corresponding `else` branches.

closes #14861 

## Test Plan

New Markdown tests.
2024-12-13 07:40:14 +01:00
Krishnan Chandra
be4ce16735 [ruff] Skip SQLModel base classes for mutable-class-default (RUF012) (#14949)
## Summary

Closes https://github.com/astral-sh/ruff/issues/14892, by adding
`sqlmodel.SQLModel` to the list of classes with default copy semantics.

## Test Plan

Added a test into `RUF012.py` containing the example from the original
issue.
2024-12-12 22:19:21 -06:00
David Peter
657d26ff20 [red-knot] Tests for 'while' loop boundness (#14944)
## Summary

Regression test(s) for something that broken while implementing #14759.
We have similar tests for other control flow elements, but feel free to
let me know if this seems superfluous.

## Test Plan

New mdtests
2024-12-12 21:06:56 +01:00
Alex Waygood
dbc191d2d6 [red-knot] Fixes to Type::to_meta_type (#14942) 2024-12-12 19:55:11 +00:00
David Peter
d2712c7669 ruff_python_ast: Make Singleton Copy (#14943)
## Summary

Minor changed pulled out from #14759, as it seems to make sense in
isolation.

## Test Plan

—
2024-12-12 20:49:54 +01:00
Chandra Kiran G
e5cb4d6388 [flake8-pyi]: More autofixes for redundant-none-literal (PYI061) (#14872)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-12 19:44:32 +00:00
Sergey Mezentsev
68e8496260 [flake8-use-pathlib] Extend check for invalid path suffix to include the case "." (PTH210) (#14902)
## Summary

`PTH210` renamed to `invalid-pathlib-with-suffix` and extended to check for `.with_suffix(".")`. This caused the fix availability to be downgraded to "Sometimes", since there is no fix offered in this case.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Dylan <53534755+dylwil3@users.noreply.github.com>
2024-12-12 13:30:17 -06:00
Alex Waygood
71239f248e [red-knot] Add explicit TODO branches for many typing special forms and qualifiers (#14936) 2024-12-12 17:57:26 +00:00
Alex Waygood
58930905eb [red-knot] Fixup a few edge cases regarding type[] (#14918) 2024-12-12 16:53:03 +00:00
Dhruv Manilawala
53f2d72e02 Revert certain double quotes from workflow shell script (#14939)
Follow-up from #14938
2024-12-12 20:29:48 +05:30
Dhruv Manilawala
3629cbf35a Use double quotes consistently for shell scripts (#14938)
## Summary

The release failed
(https://github.com/astral-sh/ruff/actions/runs/12298190472/job/34321509636)
because the shell script in the Docker release workflow was using single
quotes instead of double quotes.

This is related to https://www.shellcheck.net/wiki/SC2016. I found it
via [`actionlint`](https://github.com/rhysd/actionlint). Related #14893.

I also went ahead and fixed https://www.shellcheck.net/wiki/SC2086 which
were raised in a couple of places.
2024-12-12 08:45:08 -06:00
Dylan
37f433814c Bump version to 0.8.3 (#14937)
Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-12-12 14:13:06 +00:00
Alex Waygood
45b565cbb5 [red-knot] Any cannot be parameterized (#14933) 2024-12-12 11:50:34 +00:00
Micha Reiser
82faa9bb62 Add tests demonstrating f-strings with debug expressions in replacements that contain escaped characters (#14929) 2024-12-12 09:33:20 +00:00
w0nder1ng
2eac00c60f [perflint] fix invalid hoist in perf401 (#14369)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-12 09:11:09 +01:00
Alex Waygood
033ecf5a4b Also have zizmor check for low-severity security issues (#14893)
## Summary

This PR changes our zizmor configuration to also flag low-severity
security issues in our GitHub Actions workflows. It's a followup to
https://github.com/astral-sh/ruff/pull/14844. The issues being fixed
here were all flagged by [zizmor's `template-injection`
rule](https://woodruffw.github.io/zizmor/audits/#template-injection):

> Detects potential sources of code injection via template expansion.
>
> GitHub Actions allows workflows to define template expansions, which
occur within special `${{ ... }}` delimiters. These expansions happen
before workflow and job execution, meaning the expansion of a given
expression appears verbatim in whatever context it was performed in.
>
> Template expansions aren't syntax-aware, meaning that they can result
in unintended shell injection vectors. This is especially true when
they're used with attacker-controllable expression contexts, such as
`github.event.issue.title` (which the attacker can fully control by
supplying a new issue title).

[...]

> To fully remediate the vulnerability, you should not use `${{
env.VARNAME }}`, since that is still a template expansion. Instead, you
should use `${VARNAME}` to ensure that the shell itself performs the
variable expansion.

## Test Plan

I tested that this passes all zizmore warnings by running `pre-commit
run -a zizmor` locally. The other test is obviously to check that the
workflows all still run correctly in CI 😄
2024-12-12 07:43:17 +00:00
Peter Tripp
5509a3d7ae Add LSP settings example for Zed editor (#14894)
## Summary

Add Zed settings examples to in addition to NeoVim and VSCode.

<img width="373" alt="Screenshot 2024-12-11 at 9 54 57"
src="https://github.com/user-attachments/assets/2f00cd8b-b23e-4ecb-8a0d-732ec275ee6b"
/>

## Test Plan

[*] Tested locally. No errors. Looks fine to me.
2024-12-12 12:44:56 +05:30
InSync
e4885a2fb2 [red-knot] Understand typing.Tuple (#14927)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-12 00:58:06 +00:00
David Peter
a7e5e42b88 [red-knot] Make attributes.md test future-proof (#14923)
## Summary

Using `typing.LiteralString` breaks as soon as we understand
`sys.version_info` branches, as it's only available in 3.11 and later.

## Test Plan

Made sure it didn't fail on my #14759 branch anymore.
2024-12-11 20:46:24 +01:00
Alex Waygood
c361cf66ad [red-knot] Precise inference for __class__ attributes on objects of all types (#14921) 2024-12-11 17:30:34 +00:00
Alex Waygood
a54353392f [red-knot] Add failing test for use of type[] as a base class (#14913)
We support using `typing.Type[]` as a base class (and we have tests for
it), but not yet `builtins.type[]`. At some point we should fix that,
but I don't think it';s worth spending much time on now (and it might be
easier once we've implemented generics?). This PR just adds a failing
test with a TODO.
2024-12-11 17:08:00 +00:00
Alex Waygood
ef153a0cce [red-knot] Remove an unnecessary branch and a confusing TODO comment (#14915) 2024-12-11 16:57:40 +00:00
InSync
0c85023cd9 Fix a typo in if_key_in_dict_del.rs (#14920)
(Accidentally introduced in #14553).
2024-12-11 16:28:27 +00:00
Alex Waygood
7135a49aea [red-knot] Record the TODO message in ClassBase::Todo, same as in Type::Todo (#14919) 2024-12-11 15:17:56 +00:00
Micha Reiser
6e11086c98 Support lint:<rule> in mdtests (#14914)
## Summary

Fixes a small scoping issue in `DiagnosticId::matches`

Note: I don't think we should use `lint:id` in mdtests just yet. I worry
that it could lead to many unnecessary churns if we decide **not** to
use `lint:<id>` as the format (e.g., `lint/id`).

The reason why users even see `lint:<rule>` is because the mdtest
framework uses the diagnostic infrastructure

Closes #14910

## Test Plan

Added tests
2024-12-11 14:37:12 +01:00
Micha Reiser
28653c7c47 Strip lint: prefix from mdtest diagnostics 2024-12-11 14:33:31 +01:00
Alex Waygood
1d91dae11f [red-knot] Minor simplifications to mro.rs (#14912) 2024-12-11 13:14:12 +00:00
Micha Reiser
01d16e8941 Fix fuzzer build (#14911) 2024-12-11 13:06:51 +00:00
Micha Reiser
881375a8d9 [red-knot] Lint registry and rule selection (#14874)
## Summary

This is the third and last PR in this stack that adds support for
toggling lints at a per-rule level.

This PR introduces a new `LintRegistry`, a central index of known lints.
The registry is required because we want to support lint rules from many
different crates but need a way to look them up by name, e.g., when
resolving a lint from a name in the configuration or analyzing a
suppression comment.

Adding a lint now requires two steps:

1. Declare the lint with `declare_lint`
2. Register the lint in the registry inside the `register_lints`
function.

I considered some more involved macros to avoid changes in two places.
Still, I ultimately decided against it because a) it's just two places
and b) I'd expect that registering a type checker lint will differ from
registering a lint that runs as a rule in the linter. I worry that any
more opinionated design could limit our options when working on the
linter, so I kept it simple.

The second part of this PR is the `RuleSelection`. It stores which lints
are enabled and what severity they should use for created diagnostics.
For now, the `RuleSelection` always gets initialized with all known
lints and it uses their default level.

## Linter crates

Each crate that defines lints should export a `register_lints` function
that accepts a `&mut LintRegistryBuilder` to register all its known
lints in the registry. This should make registering all known lints in a
top-level crate easy: Just call `register_lints` of every crate that
defines lint rules.

I considered defining a `LintCollection` trait and even some fancy
macros to accomplish the same but decided to go for this very simplistic
approach for now. We can add more abstraction once needed.

## Lint rules

This is a bit hand-wavy. I don't have a good sense for how our linter
infrastructure will look like, but I expect we'll need a way to register
the rules that should run as part of the red knot linter. One way is to
keep doing what Ruff does by having one massive `checker` and each lint
rule adds a call to itself in the relevant AST visitor methods. An
alternative is that we have a `LintRule` trait that provides common
hooks and implementations will be called at the "right time". Such a
design would need a way to register all known lint implementations,
possibly with the lint. This is where we'd probably want a dedicated
`register_rule` method. A third option is that lint rules are handled
separately from the `LintRegistry` and are specific to the linter crate.

The current design should be flexible enough to support the three
options.


## Documentation generation

The documentation for all known lints can be generated by creating a
factory, registering all lints by calling the `register_lints` methods,
and then querying the registry for the metadata.

## Deserialization and Schema generation

I haven't fully decided what the best approach is when it comes to
deserializing lint rule names:

* Reject invalid names in the deserializer. This gives us error messages
with line and column numbers (by serde)
* Don't validate lint rule names during deserialization; defer the
validation until the configuration is resolved. This gives us more
control over handling the error, e.g. emit a warning diagnostic instead
of aborting when a rule isn't known.

One technical challenge for both deserialization and schema generation
is that the `Deserialize` and `JSONSchema` traits do not allow passing
the `LintRegistry`, which is required to look up the lints by name. I
suggest that we either rely on the salsa db being set for the current
thread (`salsa::Attach`) or build our own thread-local storage for the
`LintRegistry`. It's the caller's responsibility to make the lint
registry available before calling `Deserialize` or `JSONSchema`.


## CLI support

I prefer deferring adding support for enabling and disabling lints from
the CLI for now because I think it will be easier
to add once I've figured out how to handle configurations. 

## Bitset optimization

Ruff tracks the enabled rules using a cheap copyable `Bitset` instead of
a hash map. This helped improve performance by a few percent (see
https://github.com/astral-sh/ruff/pull/3606). However, this approach is
no longer possible because lints have no "cheap" way to compute their
index inside the registry (other than using a hash map).

We could consider doing something similar to Salsa where each
`LintMetadata` stores a `LazyLintIndex`.

```
pub struct LazyLintIndex {
	cached: OnceLock<(Nonce, LintIndex)>
}

impl LazyLintIndex {
	pub fn get(registry: &LintRegistry, lint: &'static LintMetadata) {
	
	let (nonce, index) = self.cached.get_or_init(|| registry.lint_index(lint));

	if registry.nonce() == nonce {
		index
	} else {
		registry.lint_index(lint)
	}
}
```

Each registry keeps a map from `LintId` to `LintIndex` where `LintIndex`
is in the range of `0...registry.len()`. The `LazyLintIndex` is based on
the assumption that every program has exactly **one** registry. This
assumption allows to cache the `LintIndex` directly on the
`LintMetadata`. The implementation falls back to the "slow" path if
there is more than one registry at runtime.

I was very close to implementing this optimization because it's kind of
fun to implement. I ultimately decided against it because it adds
complexity and I don't think it's worth doing in Red Knot today:

* Red Knot only queries the rule selection when deciding whether or not
to emit a diagnostic. It is rarely used to detect if a certain code
block should run. This is different from Ruff where the rule selection
is queried many times for every single AST node to determine which rules
*should* run.
* I'm not sure if a 2-3% performance improvement is worth the complexity

I suggest revisiting this decision when working on the linter where a
fast path for deciding if a rule is enabled might be more important (but
that depends on how lint rules are implemented)


## Test Plan

I removed a lint from the default rule registry, and the MD tests
started failing because the diagnostics were no longer emitted.
2024-12-11 13:25:19 +01:00
InSync
6f8d8fa36b [ruff] if k in d: del d[k] (RUF051) (#14553)
## Summary

Resolves #7537.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-11 11:12:23 +00:00
InSync
f30227c436 [red-knot] Understand typing.Type (#14904)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-11 11:01:38 +00:00
InSync
c8d505c8ea [pyupgrade] Do not report when a UTF-8 comment is followed by a non-UTF-8 one (UP009) (#14728)
## Summary

Resolves #14704.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-11 10:30:41 +00:00
Dimitri Papadopoulos Orfanos
a55722e740 Revert disjointness->disjointedness (#14906)
## Summary

Partially revert #14880. While `disjointness` is missing from the
[OED](https://www.oed.com/search/dictionary/?q=disjointness) and [SCOWL
(And
Friends)](http://app.aspell.net/lookup?dict=en_US-large;words=disjointness),
it is commonly used in mathematics to describe disjoint sets.

## Test Plan

CI tests.
2024-12-11 08:26:45 +00:00
Dylan
a3bb0cd5ec Raise syntax error for mixing except and except* (#14895)
This PR adds a syntax error if the parser encounters a `TryStmt` that
has except clauses both with and without a star.

The displayed error points to each except clause that contradicts the
original except clause kind. So, for example,

```python
try:
    ....
except:     #<-- we assume this is the desired except kind
    ....
except*:    #<---  error will point here
    ....
except*:    #<--- and here
    ....
```

Closes #14860
2024-12-10 17:50:55 -06:00
Douglas Creager
d4126f6049 Handle type[Any] correctly (#14876)
This adds support for `type[Any]`, which represents an unknown type (not
an instance of an unknown type), and `type`, which we are choosing to
interpret as `type[object]`.

Closes #14546
2024-12-10 16:12:37 -05:00
Carl Meyer
03fb2e5ac1 [red-knot] split call-outcome enums to their own submodule (#14898)
## Summary

This is already several hundred lines of code, and it will get more
complex with call-signature checking.

## Test Plan

This is a pure code move; the moved code wasn't changed, just imports.
Existing tests pass.
2024-12-10 12:03:29 -08:00
David Peter
1a3c311ac5 [red-knot] Property tests: account non-fully-static types (#14897)
## Summary

Add a `is_fully_static` premise to the equivalence on subtyping property tests.

## Test Plan

```
cargo test -p red_knot_python_semantic -- --ignored types::property_tests::stable
```
2024-12-10 19:55:45 +00:00
Andrew Gallant
2ecd164adb ruff: add worktree support to build.rs (#14896)
Without this, `cargo insta test` re-compiles every time it is run, even
if there are no changes. With this, I can re-run `cargo insta test` (or
other `cargo build` commands) without it resulting in re-compiles.

I made an identical change to uv a while back:
https://github.com/astral-sh/uv/pull/6825
2024-12-10 14:06:59 -05:00
Micha Reiser
5fc8e5d80e [red-knot] Add infrastructure to declare lints (#14873)
## Summary

This is the second PR out of three that adds support for
enabling/disabling lint rules in Red Knot. You may want to take a look
at the [first PR](https://github.com/astral-sh/ruff/pull/14869) in this
stack to familiarize yourself with the used terminology.

This PR adds a new syntax to define a lint: 

```rust
declare_lint! {
    /// ## What it does
    /// Checks for references to names that are not defined.
    ///
    /// ## Why is this bad?
    /// Using an undefined variable will raise a `NameError` at runtime.
    ///
    /// ## Example
    ///
    /// ```python
    /// print(x)  # NameError: name 'x' is not defined
    /// ```
    pub(crate) static UNRESOLVED_REFERENCE = {
        summary: "detects references to names that are not defined",
        status: LintStatus::preview("1.0.0"),
        default_level: Level::Warn,
    }
}
```

A lint has a name and metadata about its status (preview, stable,
removed, deprecated), the default diagnostic level (unless the
configuration changes), and documentation. I use a macro here to derive
the kebab-case name and extract the documentation automatically.

This PR doesn't yet add any mechanism to discover all known lints. This
will be added in the next and last PR in this stack.


## Documentation
I documented some rules but then decided that it's probably not my best
use of time if I document all of them now (it also means that I play
catch-up with all of you forever). That's why I left some rules
undocumented (marked with TODO)

## Where is the best place to define all lints?

I'm not sure. I think what I have in this PR is fine but I also don't
love it because most lints are in a single place but not all of them. If
you have ideas, let me know.


## Why is the message not part of the lint, unlike Ruff's `Violation`

I understand that the main motivation for defining `message` on
`Violation` in Ruff is to remove the need to repeat the same message
over and over again. I'm not sure if this is an actual problem. Most
rules only emit a diagnostic in a single place and they commonly use
different messages if they emit diagnostics in different code paths,
requiring extra fields on the `Violation` struct.

That's why I'm not convinced that there's an actual need for it and
there are alternatives that can reduce the repetition when creating a
diagnostic:

* Create a helper function. We already do this in red knot with the
`add_xy` methods
* Create a custom `Diagnostic` implementation that tailors the entire
diagnostic and pre-codes e.g. the message

Avoiding an extra field on the `Violation` also removes the need to
allocate intermediate strings as it is commonly the place in Ruff.
Instead, Red Knot can use a borrowed string with `format_args`

## Test Plan

`cargo test`
2024-12-10 16:14:44 +00:00
Micha Reiser
5f548072d9 [red-knot] Typed diagnostic id (#14869)
## Summary

This PR introduces a structured `DiagnosticId` instead of using a plain
`&'static str`. It is the first of three in a stack that implements a
basic rules infrastructure for Red Knot.

`DiagnosticId` is an enum over all known diagnostic codes. A closed enum
reduces the risk of accidentally introducing two identical diagnostic
codes. It also opens the possibility of generating reference
documentation from the enum in the future (not part of this PR).

The enum isn't *fully closed* because it uses a `&'static str` for lint
names. This is because we want the flexibility to define lints in
different crates, and all names are only known in `red_knot_linter` or
above. Still, lower-level crates must already reference the lint names
to emit diagnostics. We could define all lint-names in `DiagnosticId`
but I decided against it because:

* We probably want to share the `DiagnosticId` type between Ruff and Red
Knot to avoid extra complexity in the diagnostic crate, and both tools
use different lint names.
* Lints require a lot of extra metadata beyond just the name. That's why
I think defining them close to their implementation is important.

In the long term, we may also want to support plugins, which would make
it impossible to know all lint names at compile time. The next PR in the
stack introduces extra syntax for defining lints.

A closed enum does have a few disadvantages:

* rustc can't help us detect unused diagnostic codes because the enum is
public
* Adding a new diagnostic in the workspace crate now requires changes to
at least two crates: It requires changing the workspace crate to add the
diagnostic and the `ruff_db` crate to define the diagnostic ID. I
consider this an acceptable trade. We may want to move `DiagnosticId` to
its own crate or into a shared `red_knot_diagnostic` crate.


## Preventing duplicate diagnostic identifiers

One goal of this PR is to make it harder to introduce ambiguous
diagnostic IDs, which is achieved by defining a closed enum. However,
the enum isn't fully "closed" because it doesn't explicitly list the IDs
for all lint rules. That leaves the possibility that a lint rule and a
diagnostic ID share the same name.

I made the names unambiguous in this PR by separating them into
different namespaces by using `lint/<rule>` for lint rule codes. I don't
mind the `lint` prefix in a *Ruff next* context, but it is a bit weird
for a standalone type checker. I'd like to not overfocus on this for now
because I see a few different options:

* We remove the `lint` prefix and add a unit test in a top-level crate
that iterates over all known lint rules and diagnostic IDs to ensure the
names are non-overlapping.
* We only render `[lint]` as the error code and add a note to the
diagnostic mentioning the lint rule. This is similar to clippy and has
the advantage that the header line remains short
(`lint/some-long-rule-name` is very long ;))
* Any other form of adjusting the diagnostic rendering to make the
distinction clear

I think we can defer this decision for now because the `DiagnosticId`
contains all the relevant information to change the rendering
accordingly.


## Why `Lint` and not `LintRule`

I see three kinds of diagnostics in Red Knot:

* Non-suppressable: Reveal type, IO errors, configuration errors, etc.
(any `DiagnosticId`)
* Lints: code-related diagnostics that are suppressable. 
* Lint rules: The same as lints, but they can be enabled or disabled in
the configuration. The majority of lints in Red Knot and the Ruff
linter.

Our current implementation doesn't distinguish between lints and Lint
rules because we aren't aware of a suppressible code-related lint that
can't be configured in the configuration. The only lint that comes to my
mind is maybe `division-by-zero` if we're 99.99% sure that it is always
right. However, I want to keep the door open to making this distinction
in the future if it proves useful.

Another reason why I chose lint over lint rule (or just rule) is that I
want to leave room for a future lint rule and lint phase concept:

* lint is the *what*: a specific code smell, pattern, or violation 
* the lint rule is the *how*: I could see a future `LintRule` trait in
`red_knot_python_linter` that provides the necessary hooks to run as
part of the linter. A lint rule produces diagnostics for exactly one
lint. A lint rule differs from all lints in `red_knot_python_semantic`
because they don't run as "rules" in the Ruff sense. Instead, they're a
side-product of type inference.
* the lint phase is a different form of *how*: A lint phase can produce
many different lints in a single pass. This is a somewhat common pattern
in Ruff where running one analysis collects the necessary information
for finding many different lints
* diagnostic is the *presentation*: Unlike a lint, the diagnostic isn't
the what, but how a specific lint gets presented. I expect that many
lints can use one generic `LintDiagnostic`, but a few lints might need
more flexibility and implement their custom diagnostic rendering (at
least custom `Diagnostic` implementation).


## Test Plan

`cargo test`
2024-12-10 15:58:07 +00:00
Karthikeyan Singaravelan
dc0d944608 [airflow] Add fix to remove deprecated keyword arguments (AIR302) (#14887)
## Summary

Add replacement fixes to deprecated arguments of a DAG.

Ref #14582 #14626

## Test Plan

Diff was verified and snapshots were updated.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-12-10 18:49:28 +05:30
InSync
15fe540251 Improve mdtests style (#14884)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-10 13:05:51 +00:00
Micha Reiser
7a0e9b34d0 Reference suppress-dummy-regex-options in documentation of rules supporting it (#14888)
## Summary

Fixes https://github.com/astral-sh/ruff/issues/14663
2024-12-10 09:53:53 +00:00
InSync
4b8c815b27 [flake8-bugbear] itertools.batched() without explicit strict (B911) (#14408)
## Summary

Resolves #14387.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-12-10 08:39:46 +00:00
Alex Waygood
e3f34b8f5b [ruff] Mark autofix for RUF052 as always unsafe (#14824) 2024-12-09 23:11:44 +00:00
Alex Waygood
ab26d9cf9a [red-knot] Improve type inference for except handlers (#14838) 2024-12-09 22:49:58 +00:00
Dimitri Papadopoulos Orfanos
64944f2cf5 More typos found by codespell (#14880) 2024-12-09 22:47:34 +00:00
Carl Meyer
533e8a6ee6 [red-knot] move standalone expression_ty to TypeInferenceBuilder::file_expression_ty (#14879)
## Summary

Per suggestion in
https://github.com/astral-sh/ruff/pull/14802#discussion_r1875455417

This is a bit less error-prone and allows us to handle both expressions
in the current scope or a different scope. Also, there's currently no
need for this method outside of `TypeInferenceBuilder`, so no reason to
expose it in `types.rs`.

## Test Plan

Pure refactor, no functional change; existing tests pass.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-12-09 17:02:14 +00:00
InSync
c62ba48ad4 [ruff] Do not simplify round() calls (RUF046) (#14832)
## Summary

Part 1 of the big change introduced in #14828. This temporarily causes
all fixes for `round(...)` to be considered unsafe, but they will
eventually be enhanced.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-12-09 16:51:27 +01:00
Dimitri Papadopoulos Orfanos
68eb0a2511 Stop referring to early ruff versions (#14862)
## Summary

Referring to old versions has become more distracting than useful.

## Test Plan

—
2024-12-09 16:47:26 +01:00
InSync
0f4350e10e Fix a typo in class.rs (#14877)
(Accidentally introduced in #14801.)
2024-12-09 15:36:42 +00:00
InSync
aa6b812a73 [flake8-pyi] Also remove self and cls's annotation (PYI034) (#14801)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-09 14:59:12 +00:00
Harutaka Kawamura
0e9427255f [pyupgrade] Remove unreachable code in UP015 implementation (#14871) 2024-12-09 14:54:57 +00:00
Harutaka Kawamura
9c3c59aca9 [flake8-bugbear] Skip B028 if warnings.warn is called with *args or **kwargs (#14870) 2024-12-09 14:32:37 +00:00
Harutaka Kawamura
172143ae77 [flake8-bugbear] Fix B028 to allow stacklevel to be explicitly assigned as a positional argument (#14868) 2024-12-09 13:15:43 +00:00
InSync
3865fb6641 [red-knot] Understanding type[Union[A, B]] (#14858)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-12-09 12:47:14 +00:00
920 changed files with 64655 additions and 22510 deletions

9
.github/actionlint.yaml vendored Normal file
View File

@@ -0,0 +1,9 @@
# Configuration for the actionlint tool, which we run via pre-commit
# to verify the correctness of the syntax in our GitHub Actions workflows.
self-hosted-runner:
# Various runners we use that aren't recognized out-of-the-box by actionlint:
labels:
- depot-ubuntu-latest-8
- depot-ubuntu-22.04-16
- windows-latest-xlarge

View File

@@ -45,7 +45,7 @@
groupName: "Artifact GitHub Actions dependencies",
matchManagers: ["github-actions"],
matchDatasources: ["gitea-tags", "github-tags"],
matchPackagePatterns: ["actions/.*-artifact"],
matchPackageNames: ["actions/.*-artifact"],
description: "Weekly update of artifact-related GitHub Actions dependencies",
},
{
@@ -61,7 +61,7 @@
{
// Disable updates of `zip-rs`; intentionally pinned for now due to ownership change
// See: https://github.com/astral-sh/uv/issues/3642
matchPackagePatterns: ["zip"],
matchPackageNames: ["zip"],
matchManagers: ["cargo"],
enabled: false,
},
@@ -70,7 +70,7 @@
// with `mkdocs-material-insider`.
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
matchManagers: ["pip_requirements"],
matchPackagePatterns: ["mkdocs-material"],
matchPackageNames: ["mkdocs-material"],
enabled: false,
},
{
@@ -87,13 +87,13 @@
{
groupName: "Monaco",
matchManagers: ["npm"],
matchPackagePatterns: ["monaco"],
matchPackageNames: ["monaco"],
description: "Weekly update of the Monaco editor",
},
{
groupName: "strum",
matchManagers: ["cargo"],
matchPackagePatterns: ["strum"],
matchPackageNames: ["strum"],
description: "Weekly update of strum dependencies",
},
{

View File

@@ -53,9 +53,9 @@ jobs:
args: --out dist
- name: "Test sdist"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
pip install dist/"${PACKAGE_NAME}"-*.tar.gz --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload sdist"
uses: actions/upload-artifact@v4
with:
@@ -125,7 +125,7 @@ jobs:
args: --release --locked --out dist
- name: "Test wheel - aarch64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
ruff --help
python -m ruff --help
- name: "Upload wheels"
@@ -186,9 +186,9 @@ jobs:
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
run: |
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
python -m pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:
@@ -236,9 +236,9 @@ jobs:
- name: "Test wheel"
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
${{ env.MODULE_NAME }} --help
python -m ${{ env.MODULE_NAME }} --help
pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall
"${MODULE_NAME}" --help
python -m "${MODULE_NAME}" --help
- name: "Upload wheels"
uses: actions/upload-artifact@v4
with:

View File

@@ -48,11 +48,13 @@ jobs:
- name: Check tag consistency
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
env:
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then
if [ "${TAG}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2
echo "${TAG}" >&2
echo "${version}" >&2
exit 1
else
@@ -72,7 +74,7 @@ jobs:
- name: Normalize Platform Pair (replace / with -)
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_TUPLE=${platform//\//-}" >> $GITHUB_ENV
echo "PLATFORM_TUPLE=${platform//\//-}" >> "$GITHUB_ENV"
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
- name: Build and push by digest
@@ -87,9 +89,10 @@ jobs:
outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
- name: Export digests
env:
digest: ${{ steps.build.outputs.digest }}
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digests
@@ -141,9 +144,10 @@ jobs:
# The printf will expand the base image with the `<RUFF_BASE_IMG>@sha256:<sha256> ...` for each sha256 in the directory
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
run: |
# shellcheck disable=SC2046
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)
docker-publish-extra:
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
@@ -173,6 +177,8 @@ jobs:
- name: Generate Dynamic Dockerfile Tags
shell: bash
env:
TAG_VALUE: ${{ fromJson(inputs.plan).announcement_tag }}
run: |
set -euo pipefail
@@ -182,7 +188,7 @@ jobs:
# Generate Dockerfile content
cat <<EOF > Dockerfile
FROM ${BASE_IMAGE}
COPY --from=${{ env.RUFF_BASE_IMG }}:latest /ruff /usr/local/bin/ruff
COPY --from=${RUFF_BASE_IMG}:latest /ruff /usr/local/bin/ruff
ENTRYPOINT []
CMD ["/usr/local/bin/ruff"]
EOF
@@ -193,8 +199,8 @@ jobs:
# Loop through all base tags and append its docker metadata pattern to the list
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
IFS=','; for TAG in ${BASE_TAGS}; do
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${TAG_VALUE}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${TAG_VALUE}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"
done
@@ -202,14 +208,14 @@ jobs:
TAG_PATTERNS="${TAG_PATTERNS%\\n}"
# Export image cache name
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> $GITHUB_ENV
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> "$GITHUB_ENV"
# Export tag patterns using the multiline env var syntax
{
echo "TAG_PATTERNS<<EOF"
echo -e "${TAG_PATTERNS}"
echo EOF
} >> $GITHUB_ENV
} >> "$GITHUB_ENV"
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -285,7 +291,9 @@ jobs:
# The final command becomes `docker buildx imagetools create -t tag1 -t tag2 ... <RUFF_BASE_IMG>@sha256:<sha256_1> <RUFF_BASE_IMG>@sha256:<sha256_2> ...`
run: |
readarray -t lines <<< "$DOCKER_METADATA_OUTPUT_ANNOTATIONS"; annotations=(); for line in "${lines[@]}"; do annotations+=(--annotation "$line"); done
# shellcheck disable=SC2046
docker buildx imagetools create \
"${annotations[@]}" \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)

View File

@@ -290,7 +290,9 @@ jobs:
file: "Cargo.toml"
field: "workspace.package.rust-version"
- name: "Install Rust toolchain"
run: rustup default ${{ steps.msrv.outputs.value }}
env:
MSRV: ${{ steps.msrv.outputs.value }}
run: rustup default "${MSRV}"
- name: "Install mold"
uses: rui314/setup-mold@v1
- name: "Install cargo nextest"
@@ -306,13 +308,14 @@ jobs:
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
MSRV: ${{ steps.msrv.outputs.value }}
run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest
cargo-fuzz-build:
name: "cargo fuzz build"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' }}
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
@@ -346,7 +349,7 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v5
- uses: actions/download-artifact@v4
name: Download Ruff binary to test
id: download-cached-binary
@@ -354,16 +357,18 @@ jobs:
name: ruff
path: ruff-to-test
- name: Fuzz
env:
DOWNLOAD_PATH: ${{ steps.download-cached-binary.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
chmod +x "${DOWNLOAD_PATH}/ruff"
(
uvx \
--python=${{ env.PYTHON_VERSION }} \
--python="${PYTHON_VERSION}" \
--from=./python/py-fuzzer \
fuzz \
--test-executable=${{ steps.download-cached-binary.outputs.download-path }}/ruff \
--test-executable="${DOWNLOAD_PATH}/ruff" \
--bin=ruff \
0-500
)
@@ -381,7 +386,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: ./scripts/add_rule.py --name DoTheThing --prefix PL --code C0999 --linter pylint
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
- run: cargo check
- run: cargo fmt --all --check
- run: |
@@ -429,64 +434,72 @@ jobs:
- name: Run `ruff check` stable ecosystem check
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
cat ecosystem-result-check-stable > $GITHUB_STEP_SUMMARY
cat ecosystem-result-check-stable > "$GITHUB_STEP_SUMMARY"
echo "### Linter (stable)" > ecosystem-result
cat ecosystem-result-check-stable >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff check` preview ecosystem check
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem check ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
cat ecosystem-result-check-preview > $GITHUB_STEP_SUMMARY
cat ecosystem-result-check-preview > "$GITHUB_STEP_SUMMARY"
echo "### Linter (preview)" >> ecosystem-result
cat ecosystem-result-check-preview >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff format` stable ecosystem check
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
cat ecosystem-result-format-stable > $GITHUB_STEP_SUMMARY
cat ecosystem-result-format-stable > "$GITHUB_STEP_SUMMARY"
echo "### Formatter (stable)" >> ecosystem-result
cat ecosystem-result-format-stable >> ecosystem-result
echo "" >> ecosystem-result
- name: Run `ruff format` preview ecosystem check
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Make executable, since artifact download doesn't preserve this
chmod +x ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
# Set pipefail to avoid hiding errors with tee
set -eo pipefail
ruff-ecosystem format ./ruff ${{ steps.ruff-target.outputs.download-path }}/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
cat ecosystem-result-format-preview > $GITHUB_STEP_SUMMARY
cat ecosystem-result-format-preview > "$GITHUB_STEP_SUMMARY"
echo "### Formatter (preview)" >> ecosystem-result
cat ecosystem-result-format-preview >> ecosystem-result
echo "" >> ecosystem-result
@@ -541,7 +554,7 @@ jobs:
args: --out dist
- name: "Test wheel"
run: |
pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }}
pip install --force-reinstall --find-links dist "${PACKAGE_NAME}"
ruff --help
python -m ruff --help
- name: "Remove wheels from cache"
@@ -570,13 +583,14 @@ jobs:
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run pre-commit"
run: |
echo '```console' > $GITHUB_STEP_SUMMARY
echo '```console' > "$GITHUB_STEP_SUMMARY"
# Enable color output for pre-commit and remove it for the summary
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always | \
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> $GITHUB_STEP_SUMMARY) >&1
exit_code=${PIPESTATUS[0]}
echo '```' >> $GITHUB_STEP_SUMMARY
exit $exit_code
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
exit_code="${PIPESTATUS[0]}"
echo '```' >> "$GITHUB_STEP_SUMMARY"
exit "$exit_code"
docs:
name: "mkdocs"
@@ -599,7 +613,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v5
- uses: Swatinem/rust-cache@v2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
@@ -637,7 +651,7 @@ jobs:
- name: "Run checks"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
run: cat target/formatter-ecosystem/stats.txt > "$GITHUB_STEP_SUMMARY"
- name: "Remove checkouts from cache"
run: rm -r target/formatter-ecosystem
@@ -676,11 +690,13 @@ jobs:
just install
- name: Run ruff-lsp tests
env:
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
run: |
# Setup development binary
pip uninstall --yes ruff
chmod +x ${{ steps.ruff-target.outputs.download-path }}/ruff
export PATH=${{ steps.ruff-target.outputs.download-path }}:$PATH
chmod +x "${DOWNLOAD_PATH}/ruff"
export PATH="${DOWNLOAD_PATH}:${PATH}"
ruff version
just test

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v4
- uses: astral-sh/setup-uv@v5
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -46,6 +46,7 @@ jobs:
run: cargo build --locked
- name: Fuzz
run: |
# shellcheck disable=SC2046
(
uvx \
--python=3.12 \
@@ -72,6 +73,6 @@ jobs:
owner: "astral-sh",
repo: "ruff",
title: `Daily parser fuzz failed on ${new Date().toDateString()}`,
body: "Runs listed here: https://github.com/astral-sh/ruff/actions/workflows/daily_fuzz.yml",
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
labels: ["bug", "parser", "fuzzer"],
})

View File

@@ -0,0 +1,71 @@
name: Daily property test run
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
pull_request:
paths:
- ".github/workflows/daily_property_tests.yaml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
FORCE_COLOR: 1
jobs:
property_tests:
name: Property tests
runs-on: ubuntu-latest
timeout-minutes: 20
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@v2
- name: Build Red Knot
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
run: cargo build --locked --release --package red_knot_python_semantic --tests
- name: Run property tests
shell: bash
run: |
export QUICKCHECK_TESTS=100000
for _ in {1..5}; do
cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable
done
create-issue-on-failure:
name: Create an issue if the daily property test run surfaced any bugs
runs-on: ubuntu-latest
needs: property_tests
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.property_tests.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.create({
owner: "astral-sh",
repo: "ruff",
title: `Daily property test run failed on ${new Date().toDateString()}`,
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
labels: ["bug", "red-knot", "testing"],
})

View File

@@ -10,12 +10,11 @@ on:
description: The ecosystem workflow that triggers the workflow run
required: true
permissions:
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@v7
name: Download pull request number
@@ -30,7 +29,7 @@ jobs:
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@v7
@@ -66,9 +65,9 @@ jobs:
cat pr/ecosystem/ecosystem-result >> comment.txt
echo "" >> comment.txt
echo 'comment<<EOF' >> $GITHUB_OUTPUT
cat comment.txt >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@v3

View File

@@ -33,8 +33,9 @@ jobs:
python-version: 3.12
- name: "Set docs version"
env:
version: ${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}
run: |
version="${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}"
# if version is missing, use 'latest'
if [ -z "$version" ]; then
echo "Using 'latest' as version"
@@ -44,21 +45,19 @@ jobs:
# Use version as display name for now
display_name="$version"
echo "version=$version" >> $GITHUB_ENV
echo "display_name=$display_name" >> $GITHUB_ENV
echo "version=$version" >> "$GITHUB_ENV"
echo "display_name=$display_name" >> "$GITHUB_ENV"
- name: "Set branch name"
run: |
version="${{ env.version }}"
display_name="${{ env.display_name }}"
timestamp="$(date +%s)"
# create branch_display_name from display_name by replacing all
# characters disallowed in git branch names with hyphens
branch_display_name="$(echo "$display_name" | tr -c '[:alnum:]._' '-' | tr -s '-')"
branch_display_name="$(echo "${display_name}" | tr -c '[:alnum:]._' '-' | tr -s '-')"
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> $GITHUB_ENV
echo "timestamp=$timestamp" >> $GITHUB_ENV
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> "$GITHUB_ENV"
echo "timestamp=$timestamp" >> "$GITHUB_ENV"
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
@@ -93,9 +92,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.public.yml
- name: "Clone docs repo"
run: |
version="${{ env.version }}"
git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
- name: "Copy docs"
run: rm -rf astral-docs/site/ruff && mkdir -p astral-docs/site && cp -r site/ruff astral-docs/site/
@@ -103,12 +100,10 @@ jobs:
- name: "Commit docs"
working-directory: astral-docs
run: |
branch_name="${{ env.branch_name }}"
git config user.name "astral-docs-bot"
git config user.email "176161322+astral-docs-bot@users.noreply.github.com"
git checkout -b $branch_name
git checkout -b "${branch_name}"
git add site/ruff
git commit -m "Update ruff documentation for $version"
@@ -117,12 +112,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
run: |
version="${{ env.version }}"
display_name="${{ env.display_name }}"
branch_name="${{ env.branch_name }}"
# set the PR title
pull_request_title="Update ruff documentation for $display_name"
pull_request_title="Update ruff documentation for ${display_name}"
# Delete any existing pull requests that are open for this version
# by checking against pull_request_title because the new PR will
@@ -131,13 +122,15 @@ jobs:
xargs -I {} gh pr close {}
# push the branch to GitHub
git push origin $branch_name
git push origin "${branch_name}"
# create the PR
gh pr create --base main --head $branch_name \
--title "$pull_request_title" \
--body "Automated documentation update for $display_name" \
--label "documentation"
gh pr create \
--base=main \
--head="${branch_name}" \
--title="${pull_request_title}" \
--body="Automated documentation update for ${display_name}" \
--label="documentation"
- name: "Merge Pull Request"
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
@@ -145,9 +138,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
run: |
branch_name="${{ env.branch_name }}"
# auto-merge the PR if the build was triggered by a release. Manual builds should be reviewed by a human.
# give the PR a few seconds to be created before trying to auto-merge it
sleep 10
gh pr merge --squash $branch_name
gh pr merge --squash "${branch_name}"

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v5
- uses: actions/download-artifact@v4
with:
pattern: wheels-*

View File

@@ -31,7 +31,7 @@ jobs:
with:
repository: python/typeshed
path: typeshed
persist-credentials: true
persist-credentials: false
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -59,7 +59,7 @@ jobs:
run: |
cd ruff
git push --force origin typeshedbot/sync-typeshed
gh pr list --repo $GITHUB_REPOSITORY --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr list --repo "$GITHUB_REPOSITORY" --head typeshedbot/sync-typeshed --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "internal"
create-issue-on-failure:

12
.github/zizmor.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
#
# TODO: can we remove the ignores here so that our workflows are more secure?
rules:
dangerous-triggers:
ignore:
- pr-comment.yaml
cache-poisoning:
ignore:
- build-docker.yml
- publish-playground.yml

View File

@@ -21,3 +21,11 @@ MD014: false
MD024:
# Allow when nested under different parents e.g. CHANGELOG.md
siblings_only: true
# MD046/code-block-style
#
# Ignore this because it conflicts with the code block style used in content
# tabs of mkdocs-material which is to add a blank line after the content title.
#
# Ref: https://github.com/astral-sh/ruff/pull/15011#issuecomment-2544790854
MD046: false

View File

@@ -2,6 +2,7 @@ fail_fast: false
exclude: |
(?x)^(
.github/workflows/release.yml|
crates/red_knot_vendored/vendor/.*|
crates/red_knot_workspace/resources/.*|
crates/ruff_linter/resources/.*|
@@ -22,13 +23,12 @@ repos:
- id: validate-pyproject
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.19
rev: 0.7.21
hooks:
- id: mdformat
additional_dependencies:
- mdformat-mkdocs
- mdformat-admon
- mdformat-footnote
- mdformat-mkdocs==4.0.0
- mdformat-footnote==0.1.1
exclude: |
(?x)^(
docs/formatter/black\.md
@@ -59,7 +59,7 @@ repos:
- black==24.10.0
- repo: https://github.com/crate-ci/typos
rev: v1.28.2
rev: v1.29.4
hooks:
- id: typos
@@ -73,7 +73,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.2
rev: v0.9.1
hooks:
- id: ruff-format
- id: ruff
@@ -88,20 +88,37 @@ repos:
- id: prettier
types: [yaml]
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v0.8.0
rev: v1.0.1
hooks:
- id: zizmor
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
# (https://opensource.axo.dev/cargo-dist/)
exclude: .github/workflows/release.yml
# We could consider enabling the low-severity warnings, but they're noisy
args: [--min-severity=medium]
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.30.0
rev: 0.31.0
hooks:
- id: check-github-workflows
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
- repo: https://github.com/rhysd/actionlint
rev: v1.7.6
hooks:
- id: actionlint
stages:
# This hook is disabled by default, since it's quite slow.
# To run all hooks *including* this hook, use `uvx pre-commit run -a --hook-stage=manual`.
# To run *just* this hook, use `uvx pre-commit run -a actionlint --hook-stage=manual`.
- manual
args:
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
- "-ignore=SC2016" # another shellcheck lint: seems to have false positives?
additional_dependencies:
# actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions
# and checks these with shellcheck. This is arguably its most useful feature,
# but the integration only works if shellcheck is installed
- "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0"
ci:
skip: [cargo-fmt, dev-generate-all]

View File

@@ -1,5 +1,9 @@
# Breaking Changes
## 0.9.0
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the [changelog](./CHANGELOG.md#090) for a detailed list of changes.
## 0.8.0
- **Default to Python 3.9**

View File

@@ -1,5 +1,243 @@
# Changelog
## 0.9.1
### Preview features
- \[`pycodestyle`\] Run `too-many-newlines-at-end-of-file` on each cell in notebooks (`W391`) ([#15308](https://github.com/astral-sh/ruff/pull/15308))
- \[`ruff`\] Omit diagnostic for shadowed private function parameters in `used-dummy-variable` (`RUF052`) ([#15376](https://github.com/astral-sh/ruff/pull/15376))
### Rule changes
- \[`flake8-bugbear`\] Improve `assert-raises-exception` message (`B017`) ([#15389](https://github.com/astral-sh/ruff/pull/15389))
### Formatter
- Preserve trailing end-of line comments for the last string literal in implicitly concatenated strings ([#15378](https://github.com/astral-sh/ruff/pull/15378))
### Server
- Fix a bug where the server and client notebooks were out of sync after reordering cells ([#15398](https://github.com/astral-sh/ruff/pull/15398))
### Bug fixes
- \[`flake8-pie`\] Correctly remove wrapping parentheses (`PIE800`) ([#15394](https://github.com/astral-sh/ruff/pull/15394))
- \[`pyupgrade`\] Handle comments and multiline expressions correctly (`UP037`) ([#15337](https://github.com/astral-sh/ruff/pull/15337))
## 0.9.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.9.0) for a migration guide and overview of the changes!
### Breaking changes
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the formatter section for a detailed list of changes.
This release doesnt remove or remap any existing stable rules.
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`).
This rule has also been renamed: previously, it was called `builtin-module-shadowing`.
- [`builtin-lambda-argument-shadowing`](https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/) (`A006`)
- [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`)
- [`boolean-chained-comparison`](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/) (`PLR1716`)
- [`decimal-from-float-literal`](https://docs.astral.sh/ruff/rules/decimal-from-float-literal/) (`RUF032`)
- [`post-init-default`](https://docs.astral.sh/ruff/rules/post-init-default/) (`RUF033`)
- [`useless-if-else`](https://docs.astral.sh/ruff/rules/useless-if-else/) (`RUF034`)
The following behaviors have been stabilized:
- [`pytest-parametrize-names-wrong-type`](https://docs.astral.sh/ruff/rules/pytest-parametrize-names-wrong-type/) (`PT006`): Detect [`pytest.parametrize`](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize) calls outside decorators and calls with keyword arguments.
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`): Ignore [`pytest.importorskip`](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-importorskip) calls between import statements.
- [`mutable-dataclass-default`](https://docs.astral.sh/ruff/rules/mutable-dataclass-default/) (`RUF008`) and [`function-call-in-dataclass-default-argument`](https://docs.astral.sh/ruff/rules/function-call-in-dataclass-default-argument/) (`RUF009`): Add support for [`attrs`](https://www.attrs.org/en/stable/).
- [`bad-version-info-comparison`](https://docs.astral.sh/ruff/rules/bad-version-info-comparison/) (`PYI006`): Extend the rule to check non-stub files.
The following fixes or improvements to fixes have been stabilized:
- [`redundant-numeric-union`](https://docs.astral.sh/ruff/rules/redundant-numeric-union/) (`PYI041`)
- [`duplicate-union-members`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`)
### Formatter
This release introduces the new 2025 stable style ([#13371](https://github.com/astral-sh/ruff/issues/13371)), stabilizing the following changes:
- Format expressions in f-string elements ([#7594](https://github.com/astral-sh/ruff/issues/7594))
- Alternate quotes for strings inside f-strings ([#13860](https://github.com/astral-sh/ruff/pull/13860))
- Preserve the casing of hex codes in f-string debug expressions ([#14766](https://github.com/astral-sh/ruff/issues/14766))
- Choose the quote style for each string literal in an implicitly concatenated f-string rather than for the entire string ([#13539](https://github.com/astral-sh/ruff/pull/13539))
- Automatically join an implicitly concatenated string into a single string literal if it fits on a single line ([#9457](https://github.com/astral-sh/ruff/issues/9457))
- Remove the [`ISC001`](https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/) incompatibility warning ([#15123](https://github.com/astral-sh/ruff/pull/15123))
- Prefer parenthesizing the `assert` message over breaking the assertion expression ([#9457](https://github.com/astral-sh/ruff/issues/9457))
- Automatically parenthesize over-long `if` guards in `match` `case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513))
- More consistent formatting for `match` `case` patterns ([#6933](https://github.com/astral-sh/ruff/issues/6933))
- Avoid unnecessary parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381))
- Keep the opening parentheses on the same line as the `if` keyword for comprehensions where the condition has a leading comment ([#12282](https://github.com/astral-sh/ruff/pull/12282))
- More consistent formatting for `with` statements with a single context manager for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
- Correctly calculate the line-width for code blocks in docstrings when using `max-doc-code-line-length = "dynamic"` ([#13523](https://github.com/astral-sh/ruff/pull/13523))
### Preview features
- \[`flake8-bugbear`\] Implement `class-as-data-structure` (`B903`) ([#9601](https://github.com/astral-sh/ruff/pull/9601))
- \[`flake8-type-checking`\] Apply `quoted-type-alias` more eagerly in `TYPE_CHECKING` blocks and ignore it in stubs (`TC008`) ([#15180](https://github.com/astral-sh/ruff/pull/15180))
- \[`pylint`\] Ignore `eq-without-hash` in stub files (`PLW1641`) ([#15310](https://github.com/astral-sh/ruff/pull/15310))
- \[`pyupgrade`\] Split `UP007` into two individual rules: `UP007` for `Union` and `UP045` for `Optional` (`UP007`, `UP045`) ([#15313](https://github.com/astral-sh/ruff/pull/15313))
- \[`ruff`\] New rule that detects classes that are both an enum and a `dataclass` (`RUF049`) ([#15299](https://github.com/astral-sh/ruff/pull/15299))
- \[`ruff`\] Recode `RUF025` to `RUF037` (`RUF037`) ([#15258](https://github.com/astral-sh/ruff/pull/15258))
### Rule changes
- \[`flake8-builtins`\] Ignore [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) in stub files(`A005`) ([#15350](https://github.com/astral-sh/ruff/pull/15350))
- \[`flake8-return`\] Add support for functions returning `typing.Never` (`RET503`) ([#15298](https://github.com/astral-sh/ruff/pull/15298))
### Server
- Improve the observability by removing the need for the ["trace" value](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue) to turn on or off logging. The server logging is solely controlled using the [`logLevel` server setting](https://docs.astral.sh/ruff/editors/settings/#loglevel)
which defaults to `info`. This addresses the issue where users were notified about an error and told to consult the log, but it didnt contain any messages. ([#15232](https://github.com/astral-sh/ruff/pull/15232))
- Ignore diagnostics from other sources for code action requests ([#15373](https://github.com/astral-sh/ruff/pull/15373))
### CLI
- Improve the error message for `--config key=value` when the `key` is for a table and its a simple `value`
### Bug fixes
- \[`eradicate`\] Ignore metadata blocks directly followed by normal blocks (`ERA001`) ([#15330](https://github.com/astral-sh/ruff/pull/15330))
- \[`flake8-django`\] Recognize other magic methods (`DJ012`) ([#15365](https://github.com/astral-sh/ruff/pull/15365))
- \[`pycodestyle`\] Avoid false positives related to type aliases (`E252`) ([#15356](https://github.com/astral-sh/ruff/pull/15356))
- \[`pydocstyle`\] Avoid treating newline-separated sections as sub-sections (`D405`) ([#15311](https://github.com/astral-sh/ruff/pull/15311))
- \[`pyflakes`\] Remove call when removing final argument from `format` (`F523`) ([#15309](https://github.com/astral-sh/ruff/pull/15309))
- \[`refurb`\] Mark fix as unsafe when the right-hand side is a string (`FURB171`) ([#15273](https://github.com/astral-sh/ruff/pull/15273))
- \[`ruff`\] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) ([#15318](https://github.com/astral-sh/ruff/pull/15318))
- \[`ruff`\] Parenthesize the `int`-call argument when removing the `int` call would change semantics (`RUF046`) ([#15277](https://github.com/astral-sh/ruff/pull/15277))
## 0.8.6
### Preview features
- \[`format`\]: Preserve multiline implicit concatenated strings in docstring positions ([#15126](https://github.com/astral-sh/ruff/pull/15126))
- \[`ruff`\] Add rule to detect empty literal in deque call (`RUF025`) ([#15104](https://github.com/astral-sh/ruff/pull/15104))
- \[`ruff`\] Avoid reporting when `ndigits` is possibly negative (`RUF057`) ([#15234](https://github.com/astral-sh/ruff/pull/15234))
### Rule changes
- \[`flake8-todos`\] remove issue code length restriction (`TD003`) ([#15175](https://github.com/astral-sh/ruff/pull/15175))
- \[`pyflakes`\] Ignore errors in `@no_type_check` string annotations (`F722`, `F821`) ([#15215](https://github.com/astral-sh/ruff/pull/15215))
### CLI
- Show errors for attempted fixes only when passed `--verbose` ([#15237](https://github.com/astral-sh/ruff/pull/15237))
### Bug fixes
- \[`ruff`\] Avoid syntax error when removing int over multiple lines (`RUF046`) ([#15230](https://github.com/astral-sh/ruff/pull/15230))
- \[`pyupgrade`\] Revert "Add all PEP-585 names to `UP006` rule" ([#15250](https://github.com/astral-sh/ruff/pull/15250))
## 0.8.5
### Preview features
- \[`airflow`\] Extend names moved from core to provider (`AIR303`) ([#15145](https://github.com/astral-sh/ruff/pull/15145), [#15159](https://github.com/astral-sh/ruff/pull/15159), [#15196](https://github.com/astral-sh/ruff/pull/15196), [#15216](https://github.com/astral-sh/ruff/pull/15216))
- \[`airflow`\] Extend rule to check class attributes, methods, arguments (`AIR302`) ([#15054](https://github.com/astral-sh/ruff/pull/15054), [#15083](https://github.com/astral-sh/ruff/pull/15083))
- \[`fastapi`\] Update `FAST002` to check keyword-only arguments ([#15119](https://github.com/astral-sh/ruff/pull/15119))
- \[`flake8-type-checking`\] Disable `TC006` and `TC007` in stub files ([#15179](https://github.com/astral-sh/ruff/pull/15179))
- \[`pylint`\] Detect nested methods correctly (`PLW1641`) ([#15032](https://github.com/astral-sh/ruff/pull/15032))
- \[`ruff`\] Detect more strict-integer expressions (`RUF046`) ([#14833](https://github.com/astral-sh/ruff/pull/14833))
- \[`ruff`\] Implement `falsy-dict-get-fallback` (`RUF056`) ([#15160](https://github.com/astral-sh/ruff/pull/15160))
- \[`ruff`\] Implement `unnecessary-round` (`RUF057`) ([#14828](https://github.com/astral-sh/ruff/pull/14828))
### Rule changes
- Visit PEP 764 inline `TypedDict` keys as non-type-expressions ([#15073](https://github.com/astral-sh/ruff/pull/15073))
- \[`flake8-comprehensions`\] Skip `C416` if comprehension contains unpacking ([#14909](https://github.com/astral-sh/ruff/pull/14909))
- \[`flake8-pie`\] Allow `cast(SomeType, ...)` (`PIE796`) ([#15141](https://github.com/astral-sh/ruff/pull/15141))
- \[`flake8-simplify`\] More precise inference for dictionaries (`SIM300`) ([#15164](https://github.com/astral-sh/ruff/pull/15164))
- \[`flake8-use-pathlib`\] Catch redundant joins in `PTH201` and avoid syntax errors ([#15177](https://github.com/astral-sh/ruff/pull/15177))
- \[`pycodestyle`\] Preserve original value format (`E731`) ([#15097](https://github.com/astral-sh/ruff/pull/15097))
- \[`pydocstyle`\] Split on first whitespace character (`D403`) ([#15082](https://github.com/astral-sh/ruff/pull/15082))
- \[`pyupgrade`\] Add all PEP-585 names to `UP006` rule ([#5454](https://github.com/astral-sh/ruff/pull/5454))
### Configuration
- \[`flake8-type-checking`\] Improve flexibility of `runtime-evaluated-decorators` ([#15204](https://github.com/astral-sh/ruff/pull/15204))
- \[`pydocstyle`\] Add setting to ignore missing documentation for `*args` and `**kwargs` parameters (`D417`) ([#15210](https://github.com/astral-sh/ruff/pull/15210))
- \[`ruff`\] Add an allowlist for `unsafe-markup-use` (`RUF035`) ([#15076](https://github.com/astral-sh/ruff/pull/15076))
### Bug fixes
- Fix type subscript on older python versions ([#15090](https://github.com/astral-sh/ruff/pull/15090))
- Use `TypeChecker` for detecting `fastapi` routes ([#15093](https://github.com/astral-sh/ruff/pull/15093))
- \[`pycodestyle`\] Avoid false positives and negatives related to type parameter default syntax (`E225`, `E251`) ([#15214](https://github.com/astral-sh/ruff/pull/15214))
### Documentation
- Fix incorrect doc in `shebang-not-executable` (`EXE001`) and add git+windows solution to executable bit ([#15208](https://github.com/astral-sh/ruff/pull/15208))
- Rename rules currently not conforming to naming convention ([#15102](https://github.com/astral-sh/ruff/pull/15102))
## 0.8.4
### Preview features
- \[`airflow`\] Extend `AIR302` with additional functions and classes ([#15015](https://github.com/astral-sh/ruff/pull/15015))
- \[`airflow`\] Implement `moved-to-provider-in-3` for modules that has been moved to Airflow providers (`AIR303`) ([#14764](https://github.com/astral-sh/ruff/pull/14764))
- \[`flake8-use-pathlib`\] Extend check for invalid path suffix to include the case `"."` (`PTH210`) ([#14902](https://github.com/astral-sh/ruff/pull/14902))
- \[`perflint`\] Fix panic in `PERF401` when list variable is after the `for` loop ([#14971](https://github.com/astral-sh/ruff/pull/14971))
- \[`perflint`\] Simplify finding the loop target in `PERF401` ([#15025](https://github.com/astral-sh/ruff/pull/15025))
- \[`pylint`\] Preserve original value format (`PLR6104`) ([#14978](https://github.com/astral-sh/ruff/pull/14978))
- \[`ruff`\] Avoid false positives for `RUF027` for typing context bindings ([#15037](https://github.com/astral-sh/ruff/pull/15037))
- \[`ruff`\] Check for ambiguous pattern passed to `pytest.raises()` (`RUF043`) ([#14966](https://github.com/astral-sh/ruff/pull/14966))
### Rule changes
- \[`flake8-bandit`\] Check `S105` for annotated assignment ([#15059](https://github.com/astral-sh/ruff/pull/15059))
- \[`flake8-pyi`\] More autofixes for `redundant-none-literal` (`PYI061`) ([#14872](https://github.com/astral-sh/ruff/pull/14872))
- \[`pydocstyle`\] Skip leading whitespace for `D403` ([#14963](https://github.com/astral-sh/ruff/pull/14963))
- \[`ruff`\] Skip `SQLModel` base classes for `mutable-class-default` (`RUF012`) ([#14949](https://github.com/astral-sh/ruff/pull/14949))
### Bug
- \[`perflint`\] Parenthesize walrus expressions in autofix for `manual-list-comprehension` (`PERF401`) ([#15050](https://github.com/astral-sh/ruff/pull/15050))
### Server
- Check diagnostic refresh support from client capability which enables dynamic configuration for various editors ([#15014](https://github.com/astral-sh/ruff/pull/15014))
## 0.8.3
### Preview features
- Fix fstring formatting removing overlong implicit concatenated string in expression part ([#14811](https://github.com/astral-sh/ruff/pull/14811))
- \[`airflow`\] Add fix to remove deprecated keyword arguments (`AIR302`) ([#14887](https://github.com/astral-sh/ruff/pull/14887))
- \[`airflow`\]: Extend rule to include deprecated names for Airflow 3.0 (`AIR302`) ([#14765](https://github.com/astral-sh/ruff/pull/14765) and [#14804](https://github.com/astral-sh/ruff/pull/14804))
- \[`flake8-bugbear`\] Improve error messages for `except*` (`B025`, `B029`, `B030`, `B904`) ([#14815](https://github.com/astral-sh/ruff/pull/14815))
- \[`flake8-bugbear`\] `itertools.batched()` without explicit `strict` (`B911`) ([#14408](https://github.com/astral-sh/ruff/pull/14408))
- \[`flake8-use-pathlib`\] Dotless suffix passed to `Path.with_suffix()` (`PTH210`) ([#14779](https://github.com/astral-sh/ruff/pull/14779))
- \[`pylint`\] Include parentheses and multiple comparators in check for `boolean-chained-comparison` (`PLR1716`) ([#14781](https://github.com/astral-sh/ruff/pull/14781))
- \[`ruff`\] Do not simplify `round()` calls (`RUF046`) ([#14832](https://github.com/astral-sh/ruff/pull/14832))
- \[`ruff`\] Don't emit `used-dummy-variable` on function parameters (`RUF052`) ([#14818](https://github.com/astral-sh/ruff/pull/14818))
- \[`ruff`\] Implement `if-key-in-dict-del` (`RUF051`) ([#14553](https://github.com/astral-sh/ruff/pull/14553))
- \[`ruff`\] Mark autofix for `RUF052` as always unsafe ([#14824](https://github.com/astral-sh/ruff/pull/14824))
- \[`ruff`\] Teach autofix for `used-dummy-variable` about TypeVars etc. (`RUF052`) ([#14819](https://github.com/astral-sh/ruff/pull/14819))
### Rule changes
- \[`flake8-bugbear`\] Offer unsafe autofix for `no-explicit-stacklevel` (`B028`) ([#14829](https://github.com/astral-sh/ruff/pull/14829))
- \[`flake8-pyi`\] Skip all type definitions in `string-or-bytes-too-long` (`PYI053`) ([#14797](https://github.com/astral-sh/ruff/pull/14797))
- \[`pyupgrade`\] Do not report when a UTF-8 comment is followed by a non-UTF-8 one (`UP009`) ([#14728](https://github.com/astral-sh/ruff/pull/14728))
- \[`pyupgrade`\] Mark fixes for `convert-typed-dict-functional-to-class` and `convert-named-tuple-functional-to-class` as unsafe if they will remove comments (`UP013`, `UP014`) ([#14842](https://github.com/astral-sh/ruff/pull/14842))
### Bug fixes
- Raise syntax error for mixing `except` and `except*` ([#14895](https://github.com/astral-sh/ruff/pull/14895))
- \[`flake8-bugbear`\] Fix `B028` to allow `stacklevel` to be explicitly assigned as a positional argument ([#14868](https://github.com/astral-sh/ruff/pull/14868))
- \[`flake8-bugbear`\] Skip `B028` if `warnings.warn` is called with `*args` or `**kwargs` ([#14870](https://github.com/astral-sh/ruff/pull/14870))
- \[`flake8-comprehensions`\] Skip iterables with named expressions in `unnecessary-map` (`C417`) ([#14827](https://github.com/astral-sh/ruff/pull/14827))
- \[`flake8-pyi`\] Also remove `self` and `cls`'s annotation (`PYI034`) ([#14801](https://github.com/astral-sh/ruff/pull/14801))
- \[`flake8-pytest-style`\] Fix `pytest-parametrize-names-wrong-type` (`PT006`) to edit both `argnames` and `argvalues` if both of them are single-element tuples/lists ([#14699](https://github.com/astral-sh/ruff/pull/14699))
- \[`perflint`\] Improve autofix for `PERF401` ([#14369](https://github.com/astral-sh/ruff/pull/14369))
- \[`pylint`\] Fix `PLW1508` false positive for default string created via a mult operation ([#14841](https://github.com/astral-sh/ruff/pull/14841))
## 0.8.2
### Preview features

View File

@@ -467,7 +467,7 @@ cargo build --release && hyperfine --warmup 10 \
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e --select W505,E501"
```
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
You can run `uv venv --project ./scripts/benchmarks`, activate the venv and then run `uv sync --project ./scripts/benchmarks` to create a working environment for the
above. All reported benchmarks were computed using the versions specified by
`./scripts/benchmarks/pyproject.toml` on Python 3.11.

385
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -55,9 +55,9 @@ camino = { version = "1.1.7" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "3.0.0" }
clearscreen = { version = "4.0.0" }
codspeed-criterion-compat = { version = "2.6.0", default-features = false }
colored = { version = "2.1.0" }
colored = { version = "3.0.0" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
@@ -89,7 +89,7 @@ insta = { version = "1.35.1" }
insta-cmd = { version = "0.6.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.13.0" }
itertools = { version = "0.14.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
libc = { version = "0.2.153" }
@@ -118,7 +118,8 @@ rand = { version = "0.8.5" }
rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "254c749b02cde2fd29852a7463a33e800b771758" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -210,6 +211,9 @@ redundant_clone = "warn"
debug_assert_with_mut_call = "warn"
unused_peekable = "warn"
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
large_stack_arrays = "allow"
[profile.release]
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times

View File

@@ -116,12 +116,21 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
### Installation
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI.
Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/):
```shell
uvx ruff check # Lint all files in the current directory.
uvx ruff format # Format all files in the current directory.
```
Or install Ruff with `uv` (recommended), `pip`, or `pipx`:
```shell
# With uv.
uv add --dev ruff # to add ruff to your project
uv tool install ruff # to install ruff globally
uv tool install ruff@latest # Install Ruff globally.
uv add --dev ruff # Or add Ruff to your project.
# With pip.
pip install ruff
@@ -140,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.8.2/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.2/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.1/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -174,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.8.2
rev: v0.9.1
hooks:
# Run the linter.
- id: ruff
@@ -196,7 +205,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v1
- uses: astral-sh/ruff-action@v3
```
### Configuration<a id="configuration"></a>

View File

@@ -1,10 +1,9 @@
[files]
# https://github.com/crate-ci/typos/issues/868
extend-exclude = [
"crates/red_knot_vendored/vendor/**/*",
"**/resources/**/*",
"**/snapshots/**/*",
"crates/red_knot_workspace/src/workspace/pyproject/package_name.rs"
"crates/red_knot_vendored/vendor/**/*",
"**/resources/**/*",
"**/snapshots/**/*",
]
[default.extend-words]
@@ -21,7 +20,10 @@ Numer = "Numer" # Library name 'NumerBlox' in "Who's Using Ruff?"
[default]
extend-ignore-re = [
# Line ignore with trailing "spellchecker:disable-line"
"(?Rm)^.*#\\s*spellchecker:disable-line$",
"LICENSEs",
# Line ignore with trailing "spellchecker:disable-line"
"(?Rm)^.*#\\s*spellchecker:disable-line$",
"LICENSEs",
]
[default.extend-identifiers]
"FrIeNdLy" = "FrIeNdLy"

View File

@@ -5,22 +5,22 @@ use anyhow::{anyhow, Context};
use clap::Parser;
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use python_version::PythonVersion;
use red_knot_python_semantic::SitePackages;
use red_knot_server::run_server;
use red_knot_workspace::db::RootDatabase;
use red_knot_workspace::db::ProjectDatabase;
use red_knot_workspace::project::settings::Configuration;
use red_knot_workspace::project::ProjectMetadata;
use red_knot_workspace::watch;
use red_knot_workspace::watch::WorkspaceWatcher;
use red_knot_workspace::workspace::settings::Configuration;
use red_knot_workspace::workspace::WorkspaceMetadata;
use red_knot_workspace::watch::ProjectWatcher;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
use target_version::TargetVersion;
use crate::logging::{setup_tracing, Verbosity};
mod logging;
mod target_version;
mod python_version;
mod verbosity;
#[derive(Debug, Parser)]
@@ -34,54 +34,39 @@ struct Args {
#[command(subcommand)]
pub(crate) command: Option<Command>,
#[arg(
long,
help = "Changes the current working directory.",
long_help = "Changes the current working directory before any specified operations. This affects the workspace and configuration discovery.",
value_name = "PATH"
)]
current_directory: Option<SystemPathBuf>,
/// Run the command within the given project directory.
///
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
/// as will the project's virtual environment (`.venv`) unless the `venv-path` option is set.
///
/// Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
#[arg(long, value_name = "PROJECT")]
project: Option<SystemPathBuf>,
#[arg(
long,
help = "Path to the virtual environment the project uses",
long_help = "\
Path to the virtual environment the project uses. \
If provided, red-knot will use the `site-packages` directory of this virtual environment \
to resolve type information for the project's third-party dependencies.",
value_name = "PATH"
)]
/// Path to the virtual environment the project uses.
///
/// If provided, red-knot will use the `site-packages` directory of this virtual environment
/// to resolve type information for the project's third-party dependencies.
#[arg(long, value_name = "PATH")]
venv_path: Option<SystemPathBuf>,
#[arg(
long,
value_name = "DIRECTORY",
help = "Custom directory to use for stdlib typeshed stubs"
)]
custom_typeshed_dir: Option<SystemPathBuf>,
/// Custom directory to use for stdlib typeshed stubs.
#[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")]
typeshed: Option<SystemPathBuf>,
#[arg(
long,
value_name = "PATH",
help = "Additional path to use as a module-resolution source (can be passed multiple times)"
)]
/// Additional path to use as a module-resolution source (can be passed multiple times).
#[arg(long, value_name = "PATH")]
extra_search_path: Option<Vec<SystemPathBuf>>,
#[arg(
long,
help = "Python version to assume when resolving types",
value_name = "VERSION"
)]
target_version: Option<TargetVersion>,
/// Python version to assume when resolving types.
#[arg(long, value_name = "VERSION", alias = "target-version")]
python_version: Option<PythonVersion>,
#[clap(flatten)]
verbosity: Verbosity,
#[arg(
long,
help = "Run in watch mode by re-running whenever files change",
short = 'W'
)]
/// Run in watch mode by re-running whenever files change.
#[arg(long, short = 'W')]
watch: bool,
}
@@ -89,8 +74,8 @@ impl Args {
fn to_configuration(&self, cli_cwd: &SystemPath) -> Configuration {
let mut configuration = Configuration::default();
if let Some(target_version) = self.target_version {
configuration.target_version = Some(target_version.into());
if let Some(python_version) = self.python_version {
configuration.python_version = Some(python_version.into());
}
if let Some(venv_path) = &self.venv_path {
@@ -99,9 +84,8 @@ impl Args {
});
}
if let Some(custom_typeshed_dir) = &self.custom_typeshed_dir {
configuration.search_paths.custom_typeshed =
Some(SystemPath::absolute(custom_typeshed_dir, cli_cwd));
if let Some(typeshed) = &self.typeshed {
configuration.search_paths.typeshed = Some(SystemPath::absolute(typeshed, cli_cwd));
}
if let Some(extra_search_paths) = &self.extra_search_path {
@@ -167,15 +151,13 @@ fn run() -> anyhow::Result<ExitStatus> {
};
let cwd = args
.current_directory
.project
.as_ref()
.map(|cwd| {
if cwd.as_std_path().is_dir() {
Ok(SystemPath::absolute(cwd, &cli_base_path))
} else {
Err(anyhow!(
"Provided current-directory path `{cwd}` is not a directory"
))
Err(anyhow!("Provided project path `{cwd}` is not a directory"))
}
})
.transpose()?
@@ -183,7 +165,7 @@ fn run() -> anyhow::Result<ExitStatus> {
let system = OsSystem::new(cwd.clone());
let cli_configuration = args.to_configuration(&cwd);
let workspace_metadata = WorkspaceMetadata::discover(
let workspace_metadata = ProjectMetadata::discover(
system.current_directory(),
&system,
Some(&cli_configuration),
@@ -191,7 +173,7 @@ fn run() -> anyhow::Result<ExitStatus> {
// TODO: Use the `program_settings` to compute the key for the database's persistent
// cache and load the cache if it exists.
let mut db = RootDatabase::new(workspace_metadata, system)?;
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_configuration);
@@ -244,7 +226,7 @@ struct MainLoop {
receiver: crossbeam_channel::Receiver<MainLoopMessage>,
/// The file system watcher, if running in watch mode.
watcher: Option<WorkspaceWatcher>,
watcher: Option<ProjectWatcher>,
cli_configuration: Configuration,
}
@@ -264,21 +246,21 @@ impl MainLoop {
)
}
fn watch(mut self, db: &mut RootDatabase) -> anyhow::Result<ExitStatus> {
fn watch(mut self, db: &mut ProjectDatabase) -> anyhow::Result<ExitStatus> {
tracing::debug!("Starting watch mode");
let sender = self.sender.clone();
let watcher = watch::directory_watcher(move |event| {
sender.send(MainLoopMessage::ApplyChanges(event)).unwrap();
})?;
self.watcher = Some(WorkspaceWatcher::new(watcher, db));
self.watcher = Some(ProjectWatcher::new(watcher, db));
self.run(db);
Ok(ExitStatus::Success)
}
fn run(mut self, db: &mut RootDatabase) -> ExitStatus {
fn run(mut self, db: &mut ProjectDatabase) -> ExitStatus {
self.sender.send(MainLoopMessage::CheckWorkspace).unwrap();
let result = self.main_loop(db);
@@ -288,7 +270,7 @@ impl MainLoop {
result
}
fn main_loop(&mut self, db: &mut RootDatabase) -> ExitStatus {
fn main_loop(&mut self, db: &mut ProjectDatabase) -> ExitStatus {
// Schedule the first check.
tracing::debug!("Starting main loop");
@@ -297,10 +279,10 @@ impl MainLoop {
while let Ok(message) = self.receiver.recv() {
match message {
MainLoopMessage::CheckWorkspace => {
let db = db.snapshot();
let db = db.clone();
let sender = self.sender.clone();
// Spawn a new task that checks the workspace. This needs to be done in a separate thread
// Spawn a new task that checks the project. This needs to be done in a separate thread
// to prevent blocking the main loop here.
rayon::spawn(move || {
if let Ok(result) = db.check() {

View File

@@ -0,0 +1,68 @@
/// Enumeration of all supported Python versions
///
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum PythonVersion {
#[value(name = "3.7")]
Py37,
#[value(name = "3.8")]
Py38,
#[default]
#[value(name = "3.9")]
Py39,
#[value(name = "3.10")]
Py310,
#[value(name = "3.11")]
Py311,
#[value(name = "3.12")]
Py312,
#[value(name = "3.13")]
Py313,
}
impl PythonVersion {
const fn as_str(self) -> &'static str {
match self {
Self::Py37 => "3.7",
Self::Py38 => "3.8",
Self::Py39 => "3.9",
Self::Py310 => "3.10",
Self::Py311 => "3.11",
Self::Py312 => "3.12",
Self::Py313 => "3.13",
}
}
}
impl std::fmt::Display for PythonVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
fn from(value: PythonVersion) -> Self {
match value {
PythonVersion::Py37 => Self::PY37,
PythonVersion::Py38 => Self::PY38,
PythonVersion::Py39 => Self::PY39,
PythonVersion::Py310 => Self::PY310,
PythonVersion::Py311 => Self::PY311,
PythonVersion::Py312 => Self::PY312,
PythonVersion::Py313 => Self::PY313,
}
}
}
#[cfg(test)]
mod tests {
use crate::python_version::PythonVersion;
#[test]
fn same_default_as_python_version() {
assert_eq!(
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
red_knot_python_semantic::PythonVersion::default()
);
}
}

View File

@@ -1,62 +0,0 @@
/// Enumeration of all supported Python versions
///
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum TargetVersion {
Py37,
Py38,
#[default]
Py39,
Py310,
Py311,
Py312,
Py313,
}
impl TargetVersion {
const fn as_str(self) -> &'static str {
match self {
Self::Py37 => "py37",
Self::Py38 => "py38",
Self::Py39 => "py39",
Self::Py310 => "py310",
Self::Py311 => "py311",
Self::Py312 => "py312",
Self::Py313 => "py313",
}
}
}
impl std::fmt::Display for TargetVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
fn from(value: TargetVersion) -> Self {
match value {
TargetVersion::Py37 => Self::PY37,
TargetVersion::Py38 => Self::PY38,
TargetVersion::Py39 => Self::PY39,
TargetVersion::Py310 => Self::PY310,
TargetVersion::Py311 => Self::PY311,
TargetVersion::Py312 => Self::PY312,
TargetVersion::Py313 => Self::PY313,
}
}
}
#[cfg(test)]
mod tests {
use crate::target_version::TargetVersion;
use red_knot_python_semantic::PythonVersion;
#[test]
fn same_default_as_python_version() {
assert_eq!(
PythonVersion::from(TargetVersion::default()),
PythonVersion::default()
);
}
}

View File

@@ -5,18 +5,18 @@ use std::time::{Duration, Instant};
use anyhow::{anyhow, Context};
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
use red_knot_workspace::db::{Db, RootDatabase};
use red_knot_workspace::watch::{directory_watcher, ChangeEvent, WorkspaceWatcher};
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
use red_knot_workspace::workspace::WorkspaceMetadata;
use red_knot_workspace::db::{Db, ProjectDatabase};
use red_knot_workspace::project::settings::{Configuration, SearchPathConfiguration};
use red_knot_workspace::project::ProjectMetadata;
use red_knot_workspace::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
use ruff_db::files::{system_path_to_file, File, FileError};
use ruff_db::source::source_text;
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ruff_db::Upcast;
struct TestCase {
db: RootDatabase,
watcher: Option<WorkspaceWatcher>,
db: ProjectDatabase,
watcher: Option<ProjectWatcher>,
changes_receiver: crossbeam::channel::Receiver<Vec<ChangeEvent>>,
/// The temporary directory that contains the test files.
/// We need to hold on to it in the test case or the temp files get deleted.
@@ -26,15 +26,15 @@ struct TestCase {
}
impl TestCase {
fn workspace_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
SystemPath::absolute(relative, self.db.workspace().root(&self.db))
fn project_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
SystemPath::absolute(relative, self.db.project().root(&self.db))
}
fn root_path(&self) -> &SystemPath {
&self.root_dir
}
fn db(&self) -> &RootDatabase {
fn db(&self) -> &ProjectDatabase {
&self.db
}
@@ -150,7 +150,7 @@ impl TestCase {
) -> anyhow::Result<()> {
let program = Program::get(self.db());
let new_settings = configuration.to_settings(self.db.workspace().root(&self.db));
let new_settings = configuration.to_settings(self.db.project().root(&self.db));
self.configuration.search_paths = configuration;
program.update_search_paths(&mut self.db, &new_settings)?;
@@ -163,9 +163,8 @@ impl TestCase {
Ok(())
}
fn collect_package_files(&self, path: &SystemPath) -> Vec<File> {
let package = self.db().workspace().package(self.db(), path).unwrap();
let files = package.files(self.db());
fn collect_project_files(&self) -> Vec<File> {
let files = self.db().project().files(self.db());
let mut collected: Vec<_> = files.into_iter().collect();
collected.sort_unstable_by_key(|file| file.path(self.db()).as_system_path().unwrap());
collected
@@ -194,17 +193,17 @@ where
}
trait SetupFiles {
fn setup(self, root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()>;
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()>;
}
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
where
P: AsRef<SystemPath>,
{
fn setup(self, _root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()> {
fn setup(self, _root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
for (relative_path, content) in self {
let relative_path = relative_path.as_ref();
let absolute_path = workspace_path.join(relative_path);
let absolute_path = project_path.join(relative_path);
if let Some(parent) = absolute_path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for file `{relative_path}`")
@@ -226,8 +225,8 @@ impl<F> SetupFiles for F
where
F: FnOnce(&SystemPath, &SystemPath) -> anyhow::Result<()>,
{
fn setup(self, root_path: &SystemPath, workspace_path: &SystemPath) -> anyhow::Result<()> {
self(root_path, workspace_path)
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
self(root_path, project_path)
}
}
@@ -235,7 +234,7 @@ fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
where
F: SetupFiles,
{
setup_with_search_paths(setup_files, |_root, _workspace_path| {
setup_with_search_paths(setup_files, |_root, _project_path| {
SearchPathConfiguration::default()
})
}
@@ -265,24 +264,24 @@ where
.simplified()
.to_path_buf();
let workspace_path = root_path.join("workspace");
let project_path = root_path.join("project");
std::fs::create_dir_all(workspace_path.as_std_path())
.with_context(|| format!("Failed to create workspace directory `{workspace_path}`"))?;
std::fs::create_dir_all(project_path.as_std_path())
.with_context(|| format!("Failed to create project directory `{project_path}`"))?;
setup_files
.setup(&root_path, &workspace_path)
.setup(&root_path, &project_path)
.context("Failed to setup test files")?;
let system = OsSystem::new(&workspace_path);
let system = OsSystem::new(&project_path);
let search_paths = create_search_paths(&root_path, &workspace_path);
let search_paths = create_search_paths(&root_path, &project_path);
for path in search_paths
.extra_paths
.iter()
.flatten()
.chain(search_paths.custom_typeshed.iter())
.chain(search_paths.typeshed.iter())
.chain(search_paths.site_packages.iter().flat_map(|site_packages| {
if let SitePackages::Known(path) = site_packages {
path.as_slice()
@@ -296,19 +295,19 @@ where
}
let configuration = Configuration {
target_version: Some(PythonVersion::PY312),
python_version: Some(PythonVersion::PY312),
search_paths,
};
let workspace = WorkspaceMetadata::discover(&workspace_path, &system, Some(&configuration))?;
let project = ProjectMetadata::discover(&project_path, &system, Some(&configuration))?;
let db = RootDatabase::new(workspace, system)?;
let db = ProjectDatabase::new(project, system)?;
let (sender, receiver) = crossbeam::channel::unbounded();
let watcher = directory_watcher(move |events| sender.send(events).unwrap())
.with_context(|| "Failed to create directory watcher")?;
let watcher = WorkspaceWatcher::new(watcher, &db);
let watcher = ProjectWatcher::new(watcher, &db);
assert!(!watcher.has_errored_paths());
let test_case = TestCase {
@@ -359,12 +358,12 @@ fn update_file(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()
#[test]
fn new_file() -> anyhow::Result<()> {
let mut case = setup([("bar.py", "")])?;
let bar_path = case.workspace_path("bar.py");
let bar_path = case.project_path("bar.py");
let bar_file = case.system_file(&bar_path).unwrap();
let foo_path = case.workspace_path("foo.py");
let foo_path = case.project_path("foo.py");
assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file]);
assert_eq!(&case.collect_project_files(), &[bar_file]);
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
@@ -374,7 +373,7 @@ fn new_file() -> anyhow::Result<()> {
let foo = case.system_file(&foo_path).expect("foo.py to exist.");
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file, foo]);
assert_eq!(&case.collect_project_files(), &[bar_file, foo]);
Ok(())
}
@@ -382,12 +381,12 @@ fn new_file() -> anyhow::Result<()> {
#[test]
fn new_ignored_file() -> anyhow::Result<()> {
let mut case = setup([("bar.py", ""), (".ignore", "foo.py")])?;
let bar_path = case.workspace_path("bar.py");
let bar_path = case.project_path("bar.py");
let bar_file = case.system_file(&bar_path).unwrap();
let foo_path = case.workspace_path("foo.py");
let foo_path = case.project_path("foo.py");
assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file]);
assert_eq!(&case.collect_project_files(), &[bar_file]);
std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
@@ -396,7 +395,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
case.apply_changes(changes);
assert!(case.system_file(&foo_path).is_ok());
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file]);
assert_eq!(&case.collect_project_files(), &[bar_file]);
Ok(())
}
@@ -405,11 +404,11 @@ fn new_ignored_file() -> anyhow::Result<()> {
fn changed_file() -> anyhow::Result<()> {
let foo_source = "print('Hello, world!')";
let mut case = setup([("foo.py", foo_source)])?;
let foo_path = case.workspace_path("foo.py");
let foo_path = case.project_path("foo.py");
let foo = case.system_file(&foo_path)?;
assert_eq!(source_text(case.db(), foo).as_str(), foo_source);
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
assert_eq!(&case.collect_project_files(), &[foo]);
update_file(&foo_path, "print('Version 2')")?;
@@ -420,7 +419,7 @@ fn changed_file() -> anyhow::Result<()> {
case.apply_changes(changes);
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
assert_eq!(&case.collect_project_files(), &[foo]);
Ok(())
}
@@ -429,12 +428,12 @@ fn changed_file() -> anyhow::Result<()> {
fn deleted_file() -> anyhow::Result<()> {
let foo_source = "print('Hello, world!')";
let mut case = setup([("foo.py", foo_source)])?;
let foo_path = case.workspace_path("foo.py");
let foo_path = case.project_path("foo.py");
let foo = case.system_file(&foo_path)?;
assert!(foo.exists(case.db()));
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
assert_eq!(&case.collect_project_files(), &[foo]);
std::fs::remove_file(foo_path.as_std_path())?;
@@ -443,7 +442,7 @@ fn deleted_file() -> anyhow::Result<()> {
case.apply_changes(changes);
assert!(!foo.exists(case.db()));
assert_eq!(&case.collect_package_files(&foo_path), &[] as &[File]);
assert_eq!(&case.collect_project_files(), &[] as &[File]);
Ok(())
}
@@ -455,7 +454,7 @@ fn deleted_file() -> anyhow::Result<()> {
fn move_file_to_trash() -> anyhow::Result<()> {
let foo_source = "print('Hello, world!')";
let mut case = setup([("foo.py", foo_source)])?;
let foo_path = case.workspace_path("foo.py");
let foo_path = case.project_path("foo.py");
let trash_path = case.root_path().join(".trash");
std::fs::create_dir_all(trash_path.as_std_path())?;
@@ -463,7 +462,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
let foo = case.system_file(&foo_path)?;
assert!(foo.exists(case.db()));
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
assert_eq!(&case.collect_project_files(), &[foo]);
std::fs::rename(
foo_path.as_std_path(),
@@ -475,58 +474,50 @@ fn move_file_to_trash() -> anyhow::Result<()> {
case.apply_changes(changes);
assert!(!foo.exists(case.db()));
assert_eq!(&case.collect_package_files(&foo_path), &[] as &[File]);
assert_eq!(&case.collect_project_files(), &[] as &[File]);
Ok(())
}
/// Move a file from a non-workspace (non-watched) location into the workspace.
/// Move a file from a non-project (non-watched) location into the project.
#[test]
fn move_file_to_workspace() -> anyhow::Result<()> {
fn move_file_to_project() -> anyhow::Result<()> {
let mut case = setup([("bar.py", "")])?;
let bar_path = case.workspace_path("bar.py");
let bar_path = case.project_path("bar.py");
let bar = case.system_file(&bar_path).unwrap();
let foo_path = case.root_path().join("foo.py");
std::fs::write(foo_path.as_std_path(), "")?;
let foo_in_workspace_path = case.workspace_path("foo.py");
let foo_in_project = case.project_path("foo.py");
assert!(case.system_file(&foo_path).is_ok());
assert_eq!(&case.collect_package_files(&bar_path), &[bar]);
assert!(case
.db()
.workspace()
.package(case.db(), &foo_path)
.is_none());
assert_eq!(&case.collect_project_files(), &[bar]);
std::fs::rename(foo_path.as_std_path(), foo_in_workspace_path.as_std_path())?;
std::fs::rename(foo_path.as_std_path(), foo_in_project.as_std_path())?;
let changes = case.stop_watch(event_for_file("foo.py"));
case.apply_changes(changes);
let foo_in_workspace = case.system_file(&foo_in_workspace_path)?;
let foo_in_project = case.system_file(&foo_in_project)?;
assert!(foo_in_workspace.exists(case.db()));
assert_eq!(
&case.collect_package_files(&foo_in_workspace_path),
&[bar, foo_in_workspace]
);
assert!(foo_in_project.exists(case.db()));
assert_eq!(&case.collect_project_files(), &[bar, foo_in_project]);
Ok(())
}
/// Rename a workspace file.
/// Rename a project file.
#[test]
fn rename_file() -> anyhow::Result<()> {
let mut case = setup([("foo.py", "")])?;
let foo_path = case.workspace_path("foo.py");
let bar_path = case.workspace_path("bar.py");
let foo_path = case.project_path("foo.py");
let bar_path = case.project_path("bar.py");
let foo = case.system_file(&foo_path)?;
assert_eq!(case.collect_package_files(&foo_path), [foo]);
assert_eq!(case.collect_project_files(), [foo]);
std::fs::rename(foo_path.as_std_path(), bar_path.as_std_path())?;
@@ -539,15 +530,15 @@ fn rename_file() -> anyhow::Result<()> {
let bar = case.system_file(&bar_path)?;
assert!(bar.exists(case.db()));
assert_eq!(case.collect_package_files(&foo_path), [bar]);
assert_eq!(case.collect_project_files(), [bar]);
Ok(())
}
#[test]
fn directory_moved_to_workspace() -> anyhow::Result<()> {
fn directory_moved_to_project() -> anyhow::Result<()> {
let mut case = setup([("bar.py", "import sub.a")])?;
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
let bar = case.system_file(case.project_path("bar.py")).unwrap();
let sub_original_path = case.root_path().join("sub");
let init_original_path = sub_original_path.join("__init__.py");
@@ -565,12 +556,9 @@ fn directory_moved_to_workspace() -> anyhow::Result<()> {
);
assert_eq!(sub_a_module, None);
assert_eq!(
case.collect_package_files(&case.workspace_path("bar.py")),
&[bar]
);
assert_eq!(case.collect_project_files(), &[bar]);
let sub_new_path = case.workspace_path("sub");
let sub_new_path = case.project_path("sub");
std::fs::rename(sub_original_path.as_std_path(), sub_new_path.as_std_path())
.with_context(|| "Failed to move sub directory")?;
@@ -592,10 +580,7 @@ fn directory_moved_to_workspace() -> anyhow::Result<()> {
)
.is_some());
assert_eq!(
case.collect_package_files(&case.workspace_path("bar.py")),
&[bar, init_file, a_file]
);
assert_eq!(case.collect_project_files(), &[bar, init_file, a_file]);
Ok(())
}
@@ -607,7 +592,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
("sub/__init__.py", ""),
("sub/a.py", ""),
])?;
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(resolve_module(
case.db().upcast(),
@@ -615,7 +600,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
)
.is_some());
let sub_path = case.workspace_path("sub");
let sub_path = case.project_path("sub");
let init_file = case
.system_file(sub_path.join("__init__.py"))
.expect("__init__.py to exist");
@@ -623,10 +608,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
.system_file(sub_path.join("a.py"))
.expect("a.py to exist");
assert_eq!(
case.collect_package_files(&case.workspace_path("bar.py")),
&[bar, init_file, a_file]
);
assert_eq!(case.collect_project_files(), &[bar, init_file, a_file]);
std::fs::create_dir(case.root_path().join(".trash").as_std_path())?;
let trashed_sub = case.root_path().join(".trash/sub");
@@ -647,10 +629,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
assert!(!init_file.exists(case.db()));
assert!(!a_file.exists(case.db()));
assert_eq!(
case.collect_package_files(&case.workspace_path("bar.py")),
&[bar]
);
assert_eq!(case.collect_project_files(), &[bar]);
Ok(())
}
@@ -663,7 +642,7 @@ fn directory_renamed() -> anyhow::Result<()> {
("sub/a.py", ""),
])?;
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(resolve_module(
case.db().upcast(),
@@ -676,7 +655,7 @@ fn directory_renamed() -> anyhow::Result<()> {
)
.is_none());
let sub_path = case.workspace_path("sub");
let sub_path = case.project_path("sub");
let sub_init = case
.system_file(sub_path.join("__init__.py"))
.expect("__init__.py to exist");
@@ -684,14 +663,11 @@ fn directory_renamed() -> anyhow::Result<()> {
.system_file(sub_path.join("a.py"))
.expect("a.py to exist");
assert_eq!(
case.collect_package_files(&sub_path),
&[bar, sub_init, sub_a]
);
assert_eq!(case.collect_project_files(), &[bar, sub_init, sub_a]);
let foo_baz = case.workspace_path("foo/baz");
let foo_baz = case.project_path("foo/baz");
std::fs::create_dir(case.workspace_path("foo").as_std_path())?;
std::fs::create_dir(case.project_path("foo").as_std_path())?;
std::fs::rename(sub_path.as_std_path(), foo_baz.as_std_path())
.with_context(|| "Failed to move the sub directory")?;
@@ -730,7 +706,7 @@ fn directory_renamed() -> anyhow::Result<()> {
assert!(foo_baz_a.exists(case.db()));
assert_eq!(
case.collect_package_files(&sub_path),
case.collect_project_files(),
&[bar, foo_baz_init, foo_baz_a]
);
@@ -745,7 +721,7 @@ fn directory_deleted() -> anyhow::Result<()> {
("sub/a.py", ""),
])?;
let bar = case.system_file(case.workspace_path("bar.py")).unwrap();
let bar = case.system_file(case.project_path("bar.py")).unwrap();
assert!(resolve_module(
case.db().upcast(),
@@ -753,7 +729,7 @@ fn directory_deleted() -> anyhow::Result<()> {
)
.is_some());
let sub_path = case.workspace_path("sub");
let sub_path = case.project_path("sub");
let init_file = case
.system_file(sub_path.join("__init__.py"))
@@ -761,10 +737,7 @@ fn directory_deleted() -> anyhow::Result<()> {
let a_file = case
.system_file(sub_path.join("a.py"))
.expect("a.py to exist");
assert_eq!(
case.collect_package_files(&sub_path),
&[bar, init_file, a_file]
);
assert_eq!(case.collect_project_files(), &[bar, init_file, a_file]);
std::fs::remove_dir_all(sub_path.as_std_path())
.with_context(|| "Failed to remove the sub directory")?;
@@ -782,20 +755,20 @@ fn directory_deleted() -> anyhow::Result<()> {
assert!(!init_file.exists(case.db()));
assert!(!a_file.exists(case.db()));
assert_eq!(case.collect_package_files(&sub_path), &[bar]);
assert_eq!(case.collect_project_files(), &[bar]);
Ok(())
}
#[test]
fn search_path() -> anyhow::Result<()> {
let mut case = setup_with_search_paths(
[("bar.py", "import sub.a")],
|root_path, _workspace_path| SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
..SearchPathConfiguration::default()
},
)?;
let mut case =
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, _project_path| {
SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
..SearchPathConfiguration::default()
}
})?;
let site_packages = case.root_path().join("site_packages");
@@ -812,8 +785,8 @@ fn search_path() -> anyhow::Result<()> {
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
assert_eq!(
case.collect_package_files(&case.workspace_path("bar.py")),
&[case.system_file(case.workspace_path("bar.py")).unwrap()]
case.collect_project_files(),
&[case.system_file(case.project_path("bar.py")).unwrap()]
);
Ok(())
@@ -823,7 +796,7 @@ fn search_path() -> anyhow::Result<()> {
fn add_search_path() -> anyhow::Result<()> {
let mut case = setup([("bar.py", "import sub.a")])?;
let site_packages = case.workspace_path("site_packages");
let site_packages = case.project_path("site_packages");
std::fs::create_dir_all(site_packages.as_std_path())?;
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_none());
@@ -848,13 +821,13 @@ fn add_search_path() -> anyhow::Result<()> {
#[test]
fn remove_search_path() -> anyhow::Result<()> {
let mut case = setup_with_search_paths(
[("bar.py", "import sub.a")],
|root_path, _workspace_path| SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
..SearchPathConfiguration::default()
},
)?;
let mut case =
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, _project_path| {
SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
..SearchPathConfiguration::default()
}
})?;
// Remove site packages from the search path settings.
let site_packages = case.root_path().join("site_packages");
@@ -876,8 +849,8 @@ fn remove_search_path() -> anyhow::Result<()> {
#[test]
fn changed_versions_file() -> anyhow::Result<()> {
let mut case = setup_with_search_paths(
|root_path: &SystemPath, workspace_path: &SystemPath| {
std::fs::write(workspace_path.join("bar.py").as_std_path(), "import sub.a")?;
|root_path: &SystemPath, project_path: &SystemPath| {
std::fs::write(project_path.join("bar.py").as_std_path(), "import sub.a")?;
std::fs::create_dir_all(root_path.join("typeshed/stdlib").as_std_path())?;
std::fs::write(root_path.join("typeshed/stdlib/VERSIONS").as_std_path(), "")?;
std::fs::write(
@@ -887,8 +860,8 @@ fn changed_versions_file() -> anyhow::Result<()> {
Ok(())
},
|root_path, _workspace_path| SearchPathConfiguration {
custom_typeshed: Some(root_path.join("typeshed")),
|root_path, _project_path| SearchPathConfiguration {
typeshed: Some(root_path.join("typeshed")),
..SearchPathConfiguration::default()
},
)?;
@@ -915,11 +888,11 @@ fn changed_versions_file() -> anyhow::Result<()> {
Ok(())
}
/// Watch a workspace that contains two files where one file is a hardlink to another.
/// Watch a project that contains two files where one file is a hardlink to another.
///
/// Setup:
/// ```text
/// - workspace
/// - project
/// |- foo.py
/// |- bar.py (hard link to foo.py)
/// ```
@@ -935,22 +908,22 @@ fn changed_versions_file() -> anyhow::Result<()> {
/// I haven't found any documentation that states the notification behavior on Windows but what
/// we're seeing is that Windows only emits a single event, similar to Linux.
#[test]
fn hard_links_in_workspace() -> anyhow::Result<()> {
let mut case = setup(|_root: &SystemPath, workspace: &SystemPath| {
let foo_path = workspace.join("foo.py");
fn hard_links_in_project() -> anyhow::Result<()> {
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
let foo_path = project.join("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = workspace.join("bar.py");
let bar_path = project.join("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
Ok(())
})?;
let foo_path = case.workspace_path("foo.py");
let foo_path = case.project_path("foo.py");
let foo = case.system_file(&foo_path).unwrap();
let bar_path = case.workspace_path("bar.py");
let bar_path = case.project_path("bar.py");
let bar = case.system_file(&bar_path).unwrap();
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
@@ -973,12 +946,12 @@ fn hard_links_in_workspace() -> anyhow::Result<()> {
Ok(())
}
/// Watch a workspace that contains one file that is a hardlink to a file outside the workspace.
/// Watch a project that contains one file that is a hardlink to a file outside the project.
///
/// Setup:
/// ```text
/// - foo.py
/// - workspace
/// - project
/// |- bar.py (hard link to /foo.py)
/// ```
///
@@ -996,7 +969,7 @@ fn hard_links_in_workspace() -> anyhow::Result<()> {
/// [source](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw)
///
/// My interpretation of this is that Windows doesn't support observing changes made to
/// hard linked files outside the workspace.
/// hard linked files outside the project.
#[test]
#[cfg_attr(
target_os = "linux",
@@ -1006,13 +979,13 @@ fn hard_links_in_workspace() -> anyhow::Result<()> {
target_os = "windows",
ignore = "windows doesn't support observing changes to hard linked files."
)]
fn hard_links_to_target_outside_workspace() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, workspace: &SystemPath| {
fn hard_links_to_target_outside_project() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
let foo_path = root.join("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = workspace.join("bar.py");
let bar_path = project.join("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
@@ -1021,7 +994,7 @@ fn hard_links_to_target_outside_workspace() -> anyhow::Result<()> {
let foo_path = case.root_path().join("foo.py");
let foo = case.system_file(&foo_path).unwrap();
let bar_path = case.workspace_path("bar.py");
let bar_path = case.project_path("bar.py");
let bar = case.system_file(&bar_path).unwrap();
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
@@ -1044,13 +1017,13 @@ mod unix {
//! Tests that make use of unix specific file-system features.
use super::*;
/// Changes the metadata of the only file in the workspace.
/// Changes the metadata of the only file in the project.
#[test]
fn changed_metadata() -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut case = setup([("foo.py", "")])?;
let foo_path = case.workspace_path("foo.py");
let foo_path = case.project_path("foo.py");
let foo = case.system_file(&foo_path)?;
assert_eq!(
@@ -1086,14 +1059,14 @@ mod unix {
Ok(())
}
/// A workspace path is a symlink to a file outside the workspace.
/// A project path is a symlink to a file outside the project.
///
/// Setup:
/// ```text
/// - bar
/// |- baz.py
///
/// - workspace
/// - project
/// |- bar -> /bar
/// ```
///
@@ -1115,7 +1088,7 @@ mod unix {
ignore = "FSEvents doesn't emit change events for symlinked directories outside of the watched paths."
)]
fn symlink_target_outside_watched_paths() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, workspace: &SystemPath| {
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let link_target = root.join("bar");
std::fs::create_dir_all(link_target.as_std_path())
@@ -1124,8 +1097,8 @@ mod unix {
std::fs::write(baz_original.as_std_path(), "def baz(): ...")
.context("Failed to write link target file")?;
// Create a symlink inside the workspace
let bar = workspace.join("bar");
// Create a symlink inside the project
let bar = project.join("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
.context("Failed to create symlink to bar package")?;
@@ -1137,7 +1110,7 @@ mod unix {
&ModuleName::new_static("bar.baz").unwrap(),
)
.expect("Expected bar.baz to exist in site-packages.");
let baz_workspace = case.workspace_path("bar/baz.py");
let baz_project = case.project_path("bar/baz.py");
assert_eq!(
source_text(case.db(), baz.file()).as_str(),
@@ -1145,7 +1118,7 @@ mod unix {
);
assert_eq!(
baz.file().path(case.db()).as_system_path(),
Some(&*baz_workspace)
Some(&*baz_project)
);
let baz_original = case.root_path().join("bar/baz.py");
@@ -1164,7 +1137,7 @@ mod unix {
);
// Write to the symlink source.
update_file(baz_workspace, "def baz(): print('Version 3')")
update_file(baz_project, "def baz(): print('Version 3')")
.context("Failed to update bar/baz.py")?;
let changes = case.stop_watch(event_for_file("baz.py"));
@@ -1179,14 +1152,14 @@ mod unix {
Ok(())
}
/// Workspace contains a symlink to another directory inside the workspace.
/// Project contains a symlink to another directory inside the project.
/// Changes to files in the symlinked directory should be reflected
/// to all files.
///
/// Setup:
/// ```text
/// - workspace
/// | - bar -> /workspace/patched/bar
/// - project
/// | - bar -> /project/patched/bar
/// |
/// | - patched
/// | |-- bar
@@ -1195,10 +1168,10 @@ mod unix {
/// |-- foo.py
/// ```
#[test]
fn symlink_inside_workspace() -> anyhow::Result<()> {
let mut case = setup(|_root: &SystemPath, workspace: &SystemPath| {
fn symlink_inside_project() -> anyhow::Result<()> {
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let link_target = workspace.join("patched/bar");
let link_target = project.join("patched/bar");
std::fs::create_dir_all(link_target.as_std_path())
.context("Failed to create link target directory")?;
let baz_original = link_target.join("baz.py");
@@ -1206,8 +1179,8 @@ mod unix {
.context("Failed to write link target file")?;
// Create a symlink inside site-packages
let bar_in_workspace = workspace.join("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_workspace.as_std_path())
let bar_in_project = project.join("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_project.as_std_path())
.context("Failed to create symlink to bar package")?;
Ok(())
@@ -1218,9 +1191,9 @@ mod unix {
&ModuleName::new_static("bar.baz").unwrap(),
)
.expect("Expected bar.baz to exist in site-packages.");
let bar_baz = case.workspace_path("bar/baz.py");
let bar_baz = case.project_path("bar/baz.py");
let patched_bar_baz = case.workspace_path("patched/bar/baz.py");
let patched_bar_baz = case.project_path("patched/bar/baz.py");
let patched_bar_baz_file = case.system_file(&patched_bar_baz).unwrap();
assert_eq!(
@@ -1279,7 +1252,7 @@ mod unix {
/// - site-packages
/// | - bar/baz.py
///
/// - workspace
/// - project
/// |-- .venv/lib/python3.12/site-packages -> /site-packages
/// |
/// |-- foo.py
@@ -1287,7 +1260,7 @@ mod unix {
#[test]
fn symlinked_module_search_path() -> anyhow::Result<()> {
let mut case = setup_with_search_paths(
|root: &SystemPath, workspace: &SystemPath| {
|root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let site_packages = root.join("site-packages");
let bar = site_packages.join("bar");
@@ -1298,7 +1271,7 @@ mod unix {
.context("Failed to write baz.py")?;
// Symlink the site packages in the venv to the global site packages
let venv_site_packages = workspace.join(".venv/lib/python3.12/site-packages");
let venv_site_packages = project.join(".venv/lib/python3.12/site-packages");
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
.context("Failed to create .venv directory")?;
std::os::unix::fs::symlink(
@@ -1309,9 +1282,9 @@ mod unix {
Ok(())
},
|_root, workspace| SearchPathConfiguration {
|_root, project| SearchPathConfiguration {
site_packages: Some(SitePackages::Known(vec![
workspace.join(".venv/lib/python3.12/site-packages")
project.join(".venv/lib/python3.12/site-packages")
])),
..SearchPathConfiguration::default()
},
@@ -1323,7 +1296,7 @@ mod unix {
)
.expect("Expected bar.baz to exist in site-packages.");
let baz_site_packages_path =
case.workspace_path(".venv/lib/python3.12/site-packages/bar/baz.py");
case.project_path(".venv/lib/python3.12/site-packages/bar/baz.py");
let baz_site_packages = case.system_file(&baz_site_packages_path).unwrap();
let baz_original = case.root_path().join("site-packages/bar/baz.py");
let baz_original_file = case.system_file(&baz_original).unwrap();
@@ -1372,13 +1345,15 @@ mod unix {
}
#[test]
fn nested_packages_delete_root() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, workspace_root: &SystemPath| {
fn nested_projects_delete_root() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, project_root: &SystemPath| {
std::fs::write(
workspace_root.join("pyproject.toml").as_std_path(),
project_root.join("pyproject.toml").as_std_path(),
r#"
[project]
name = "inner"
[tool.knot]
"#,
)?;
@@ -1387,120 +1362,24 @@ fn nested_packages_delete_root() -> anyhow::Result<()> {
r#"
[project]
name = "outer"
[tool.knot]
"#,
)?;
Ok(())
})?;
assert_eq!(
case.db().workspace().root(case.db()),
&*case.workspace_path("")
);
assert_eq!(case.db().project().root(case.db()), &*case.project_path(""));
std::fs::remove_file(case.workspace_path("pyproject.toml").as_std_path())?;
std::fs::remove_file(case.project_path("pyproject.toml").as_std_path())?;
let changes = case.stop_watch(ChangeEvent::is_deleted);
case.apply_changes(changes);
// It should now pick up the outer workspace.
assert_eq!(case.db().workspace().root(case.db()), case.root_path());
Ok(())
}
#[test]
fn added_package() -> anyhow::Result<()> {
let mut case = setup([
(
"pyproject.toml",
r#"
[project]
name = "inner"
[tool.knot.workspace]
members = ["packages/*"]
"#,
),
(
"packages/a/pyproject.toml",
r#"
[project]
name = "a"
"#,
),
])?;
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
std::fs::create_dir(case.workspace_path("packages/b").as_std_path())
.context("failed to create folder for package 'b'")?;
// It seems that the file watcher won't pick up on file changes shortly after the folder
// was created... I suspect this is because most file watchers don't support recursive
// file watching. Instead, file-watching libraries manually implement recursive file watching
// by setting a watcher for each directory. But doing this obviously "lags" behind.
case.take_watch_changes();
std::fs::write(
case.workspace_path("packages/b/pyproject.toml")
.as_std_path(),
r#"
[project]
name = "b"
"#,
)
.context("failed to write pyproject.toml for package b")?;
let changes = case.stop_watch(event_for_file("pyproject.toml"));
case.apply_changes(changes);
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
Ok(())
}
#[test]
fn removed_package() -> anyhow::Result<()> {
let mut case = setup([
(
"pyproject.toml",
r#"
[project]
name = "inner"
[tool.knot.workspace]
members = ["packages/*"]
"#,
),
(
"packages/a/pyproject.toml",
r#"
[project]
name = "a"
"#,
),
(
"packages/b/pyproject.toml",
r#"
[project]
name = "b"
"#,
),
])?;
assert_eq!(case.db().workspace().packages(case.db()).len(), 3);
std::fs::remove_dir_all(case.workspace_path("packages/b").as_std_path())
.context("failed to remove package 'b'")?;
let changes = case.stop_watch(ChangeEvent::is_deleted);
case.apply_changes(changes);
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
// It should now pick up the outer project.
assert_eq!(case.db().project().root(case.db()), case.root_path());
Ok(())
}

View File

@@ -13,18 +13,21 @@ license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_index = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_trivia = { workspace = true }
anyhow = { workspace = true }
bitflags = { workspace = true }
camino = { workspace = true }
compact_str = { workspace = true }
countme = { workspace = true }
drop_bomb = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
ordermap = { workspace = true }
@@ -52,5 +55,8 @@ tempfile = { workspace = true }
quickcheck = { version = "1.0.3", default-features = false }
quickcheck_macros = { version = "1.0.0" }
[features]
serde = ["ruff_db/serde", "dep:serde"]
[lints]
workspace = true

View File

@@ -0,0 +1,94 @@
# `Annotated`
`Annotated` attaches arbitrary metadata to a given type.
## Usages
`Annotated[T, ...]` is equivalent to `T`: All metadata arguments are simply ignored.
```py
from typing_extensions import Annotated
def _(x: Annotated[int, "foo"]):
reveal_type(x) # revealed: int
def _(x: Annotated[int, lambda: 0 + 1 * 2 // 3, _(4)]):
reveal_type(x) # revealed: int
def _(x: Annotated[int, "arbitrary", "metadata", "elements", "are", "fine"]):
reveal_type(x) # revealed: int
def _(x: Annotated[tuple[str, int], bytes]):
reveal_type(x) # revealed: tuple[str, int]
```
## Parameterization
It is invalid to parameterize `Annotated` with less than two arguments.
```py
from typing_extensions import Annotated
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
def _(x: Annotated):
reveal_type(x) # revealed: Unknown
def _(flag: bool):
if flag:
X = Annotated
else:
X = bool
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
def f(y: X):
reveal_type(y) # revealed: Unknown | bool
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
def _(x: Annotated | bool):
reveal_type(x) # revealed: Unknown | bool
# error: [invalid-type-form]
def _(x: Annotated[()]):
reveal_type(x) # revealed: Unknown
# error: [invalid-type-form]
def _(x: Annotated[int]):
# `Annotated[T]` is invalid and will raise an error at runtime,
# but we treat it the same as `T` to provide better diagnostics later on.
# The subscription itself is still reported, regardless.
# Same for the `(int,)` form below.
reveal_type(x) # revealed: int
# error: [invalid-type-form]
def _(x: Annotated[(int,)]):
reveal_type(x) # revealed: int
```
## Inheritance
### Correctly parameterized
Inheriting from `Annotated[T, ...]` is equivalent to inheriting from `T` itself.
```py
from typing_extensions import Annotated
# TODO: False positive
# error: [invalid-base]
class C(Annotated[int, "foo"]): ...
# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]`
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
```
### Not parameterized
```py
from typing_extensions import Annotated
# At runtime, this is an error.
# error: [invalid-base]
class C(Annotated): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
```

View File

@@ -34,8 +34,7 @@ If you define your own class named `Any`, using that in a type expression refers
isn't a spelling of the Any type.
```py
class Any:
pass
class Any: ...
x: Any
@@ -59,8 +58,7 @@ assignable to `int`.
```py
from typing import Any
class Subclass(Any):
pass
class Subclass(Any): ...
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
@@ -68,8 +66,18 @@ x: Subclass = 1 # error: [invalid-assignment]
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
def f() -> Subclass:
pass
reveal_type(f()) # revealed: Subclass
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
## Invalid
`Any` cannot be parameterized:
```py
from typing import Any
# error: [invalid-type-form] "Type `typing.Any` expected no type parameter"
def f(x: Any[int]):
reveal_type(x) # revealed: Unknown
```

View File

@@ -0,0 +1,153 @@
# Literal
<https://typing.readthedocs.io/en/latest/spec/literal.html#literals>
## Parameterization
```py
from typing import Literal
from enum import Enum
mode: Literal["w", "r"]
a1: Literal[26]
a2: Literal[0x1A]
a3: Literal[-4]
a4: Literal["hello world"]
a5: Literal[b"hello world"]
a6: Literal[True]
a7: Literal[None]
a8: Literal[Literal[1]]
class Color(Enum):
RED = 0
GREEN = 1
BLUE = 2
b1: Literal[Color.RED]
def f():
reveal_type(mode) # revealed: Literal["w", "r"]
reveal_type(a1) # revealed: Literal[26]
reveal_type(a2) # revealed: Literal[26]
reveal_type(a3) # revealed: Literal[-4]
reveal_type(a4) # revealed: Literal["hello world"]
reveal_type(a5) # revealed: Literal[b"hello world"]
reveal_type(a6) # revealed: Literal[True]
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: Literal[0]
# error: [invalid-type-form]
invalid1: Literal[3 + 4]
# error: [invalid-type-form]
invalid2: Literal[4 + 3j]
# error: [invalid-type-form]
invalid3: Literal[(3, 4)]
hello = "hello"
invalid4: Literal[
1 + 2, # error: [invalid-type-form]
"foo",
hello, # error: [invalid-type-form]
(1, 2, 3), # error: [invalid-type-form]
]
```
## Shortening unions of literals
When a Literal is parameterized with more than one value, its treated as exactly to equivalent to
the union of those types.
```py
from typing import Literal
def x(
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
a2: Literal["w"] | Literal["r"],
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
):
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(a2) # revealed: Literal["w", "r"]
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
```
## Display of heterogeneous unions of literals
```py
from typing import Literal, Union
def foo(x: int) -> int:
return x + 1
def bar(s: str) -> str:
return s
class A: ...
class B: ...
def union_example(
x: Union[
# unknown type
# error: [unresolved-reference]
y,
Literal[-1],
Literal["A"],
Literal[b"A"],
Literal[b"\x00"],
Literal[b"\x07"],
Literal[0],
Literal[1],
Literal["B"],
Literal["foo"],
Literal["bar"],
Literal["B"],
Literal[True],
None,
]
):
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
```
## Detecting Literal outside typing and typing_extensions
Only Literal that is defined in typing and typing_extension modules is detected as the special
Literal.
```pyi path=other.pyi
from typing import _SpecialForm
Literal: _SpecialForm
```
```py
from other import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: @Todo(generics)
```
## Detecting typing_extensions.Literal
```py
from typing_extensions import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: Literal[26]
```
## Invalid
```py
from typing import Literal
# error: [invalid-type-form] "`Literal` requires at least one argument when used in a type expression"
def _(x: Literal):
reveal_type(x) # revealed: Unknown
```

View File

@@ -27,19 +27,19 @@ def f():
```py
from typing_extensions import Literal, LiteralString
bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
bad_union: Literal["hello", LiteralString] # error: [invalid-type-form]
bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
```
### Parametrized
### Parameterized
`LiteralString` cannot be parametrized.
`LiteralString` cannot be parameterized.
```py
from typing_extensions import LiteralString
a: LiteralString[str] # error: [invalid-type-parameter]
b: LiteralString["foo"] # error: [invalid-type-parameter]
a: LiteralString[str] # error: [invalid-type-form]
b: LiteralString["foo"] # error: [invalid-type-form]
```
### As a base class
@@ -73,12 +73,12 @@ qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(call todo)
reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types)
template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(call todo)
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
```
### Assignability
@@ -89,28 +89,26 @@ vice versa.
```py
from typing_extensions import Literal, LiteralString
def coinflip() -> bool:
return True
def _(flag: bool):
foo_1: Literal["foo"] = "foo"
bar_1: LiteralString = foo_1 # fine
foo_1: Literal["foo"] = "foo"
bar_1: LiteralString = foo_1 # fine
foo_2 = "foo" if flag else "bar"
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
bar_2: LiteralString = foo_2 # fine
foo_2 = "foo" if coinflip() else "bar"
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
bar_2: LiteralString = foo_2 # fine
foo_3: LiteralString = "foo" * 1_000_000_000
bar_3: str = foo_2 # fine
foo_3: LiteralString = "foo" * 1_000_000_000
bar_3: str = foo_2 # fine
baz_1: str = str()
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
baz_1: str = str()
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
baz_2: LiteralString = "baz" * 1_000_000_000
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
baz_2: LiteralString = "baz" * 1_000_000_000
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
baz_3 = "foo" if coinflip() else 1
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
baz_3 = "foo" if flag else 1
reveal_type(baz_3) # revealed: Literal["foo", 1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
```
### Narrowing
@@ -137,7 +135,7 @@ if "" < lorem == "ipsum":
```toml
[environment]
target-version = "3.11"
python-version = "3.11"
```
```py

View File

@@ -21,7 +21,7 @@ reveal_type(stop())
```py
from typing_extensions import NoReturn, Never, Any
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
# error: [invalid-type-form] "Type `typing.Never` expected no type parameter"
x: Never[int]
a1: NoReturn
a2: Never
@@ -47,18 +47,29 @@ def f():
## `typing.Never`
`typing.Never` is only available in Python 3.11 and later:
`typing.Never` is only available in Python 3.11 and later.
### Python 3.11
```toml
[environment]
target-version = "3.11"
python-version = "3.11"
```
```py
from typing import Never
x: Never
def f():
reveal_type(x) # revealed: Never
reveal_type(Never) # revealed: typing.Never
```
### Python 3.10
```toml
[environment]
python-version = "3.10"
```
```py
# error: [unresolved-import]
from typing import Never
```

View File

@@ -0,0 +1,127 @@
# Typing-module aliases to other stdlib classes
The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but
still need to be supported by a type checker.
## Correspondence
All of the following symbols can be mapped one-to-one with the actual type:
```py
import typing
def f(
list_bare: typing.List,
list_parametrized: typing.List[int],
dict_bare: typing.Dict,
dict_parametrized: typing.Dict[int, str],
set_bare: typing.Set,
set_parametrized: typing.Set[int],
frozen_set_bare: typing.FrozenSet,
frozen_set_parametrized: typing.FrozenSet[str],
chain_map_bare: typing.ChainMap,
chain_map_parametrized: typing.ChainMap[int],
counter_bare: typing.Counter,
counter_parametrized: typing.Counter[int],
default_dict_bare: typing.DefaultDict,
default_dict_parametrized: typing.DefaultDict[str, int],
deque_bare: typing.Deque,
deque_parametrized: typing.Deque[str],
ordered_dict_bare: typing.OrderedDict,
ordered_dict_parametrized: typing.OrderedDict[int, str],
):
reveal_type(list_bare) # revealed: list
reveal_type(list_parametrized) # revealed: list
reveal_type(dict_bare) # revealed: dict
reveal_type(dict_parametrized) # revealed: dict
reveal_type(set_bare) # revealed: set
reveal_type(set_parametrized) # revealed: set
reveal_type(frozen_set_bare) # revealed: frozenset
reveal_type(frozen_set_parametrized) # revealed: frozenset
reveal_type(chain_map_bare) # revealed: ChainMap
reveal_type(chain_map_parametrized) # revealed: ChainMap
reveal_type(counter_bare) # revealed: Counter
reveal_type(counter_parametrized) # revealed: Counter
reveal_type(default_dict_bare) # revealed: defaultdict
reveal_type(default_dict_parametrized) # revealed: defaultdict
reveal_type(deque_bare) # revealed: deque
reveal_type(deque_parametrized) # revealed: deque
reveal_type(ordered_dict_bare) # revealed: OrderedDict
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict
```
## Inheritance
The aliases can be inherited from. Some of these are still partially or wholly TODOs.
```py
import typing
####################
### Built-ins
class ListSubclass(typing.List): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[ListSubclass], Literal[list], Unknown, Literal[object]]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[SetSubclass], Literal[set], Unknown, Literal[object]]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
# TODO: should have `Generic`, should not have `Unknown`
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
reveal_type(FrozenSetSubclass.__mro__)
####################
### `collections`
class ChainMapSubclass(typing.ChainMap): ...
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
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]]
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]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# TODO: Should be (DequeSubclass, deque, MutableSequence, Sequence, Reversible, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Unknown, Literal[object]]
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]]
reveal_type(OrderedDictSubclass.__mro__)
```

View File

@@ -3,75 +3,56 @@
## Simple
```py
def f() -> "int":
return 1
reveal_type(f()) # revealed: int
def f(v: "int"):
reveal_type(v) # revealed: int
```
## Nested
```py
def f() -> "'int'":
return 1
reveal_type(f()) # revealed: int
def f(v: "'int'"):
reveal_type(v) # revealed: int
```
## Type expression
```py
def f1() -> "int | str":
return 1
def f2() -> "tuple[int, str]":
return 1
reveal_type(f1()) # revealed: int | str
reveal_type(f2()) # revealed: tuple[int, str]
def f1(v: "int | str", w: "tuple[int, str]"):
reveal_type(v) # revealed: int | str
reveal_type(w) # revealed: tuple[int, str]
```
## Partial
```py
def f() -> tuple[int, "str"]:
return 1
reveal_type(f()) # revealed: tuple[int, str]
def f(v: tuple[int, "str"]):
reveal_type(v) # revealed: tuple[int, str]
```
## Deferred
```py
def f() -> "Foo":
return Foo()
def f(v: "Foo"):
reveal_type(v) # revealed: Foo
class Foo:
pass
reveal_type(f()) # revealed: Foo
class Foo: ...
```
## Deferred (undefined)
```py
# error: [unresolved-reference]
def f() -> "Foo":
pass
reveal_type(f()) # revealed: Unknown
def f(v: "Foo"):
reveal_type(v) # revealed: Unknown
```
## Partial deferred
```py
def f() -> int | "Foo":
return 1
def f(v: int | "Foo"):
reveal_type(v) # revealed: int | Foo
class Foo:
pass
reveal_type(f()) # revealed: int | Foo
class Foo: ...
```
## `typing.Literal`
@@ -79,65 +60,43 @@ reveal_type(f()) # revealed: int | Foo
```py
from typing import Literal
def f1() -> Literal["Foo", "Bar"]:
return "Foo"
def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'):
reveal_type(v) # revealed: Literal["Foo", "Bar"]
reveal_type(w) # revealed: Literal["Foo", "Bar"]
def f2() -> 'Literal["Foo", "Bar"]':
return "Foo"
class Foo:
pass
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
class Foo: ...
```
## Various string kinds
```py
# error: [annotation-raw-string] "Type expressions cannot use raw string literal"
def f1() -> r"int":
return 1
# error: [annotation-f-string] "Type expressions cannot use f-strings"
def f2() -> f"int":
return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
def f3() -> b"int":
return 1
def f4() -> "int":
return 1
# error: [annotation-implicit-concat] "Type expressions cannot span multiple string literals"
def f5() -> "in" "t":
return 1
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
def f6() -> "\N{LATIN SMALL LETTER I}nt":
return 1
# error: [annotation-escape-character] "Type expressions cannot contain escape characters"
def f7() -> "\x69nt":
return 1
def f8() -> """int""":
return 1
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
def f9() -> "b'int'":
return 1
reveal_type(f1()) # revealed: Unknown
reveal_type(f2()) # revealed: Unknown
reveal_type(f3()) # revealed: Unknown
reveal_type(f4()) # revealed: int
reveal_type(f5()) # revealed: Unknown
reveal_type(f6()) # revealed: Unknown
reveal_type(f7()) # revealed: Unknown
reveal_type(f8()) # revealed: int
reveal_type(f9()) # revealed: Unknown
def f1(
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
a: r"int",
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
b: f"int",
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
c: b"int",
d: "int",
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
e: "in" "t",
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
f: "\N{LATIN SMALL LETTER I}nt",
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
g: "\x69nt",
h: """int""",
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
i: "b'int'",
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: int
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
reveal_type(g) # revealed: Unknown
reveal_type(h) # revealed: int
reveal_type(i) # revealed: Unknown
```
## Various string kinds in `typing.Literal`
@@ -145,10 +104,8 @@ reveal_type(f9()) # revealed: Unknown
```py
from typing import Literal
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]:
return "normal"
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
```
## Class variables
@@ -175,8 +132,7 @@ c: "Foo"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
d: "Foo" = 1
class Foo:
pass
class Foo: ...
c = Foo()
@@ -208,9 +164,9 @@ i: "{i for i in range(5)}"
j: "{i: i for i in range(5)}"
k: "(i for i in range(5))"
l: "await 1"
# error: [forward-annotation-syntax-error]
# error: [invalid-syntax-in-forward-annotation]
m: "yield 1"
# error: [forward-annotation-syntax-error]
# error: [invalid-syntax-in-forward-annotation]
n: "yield from 1"
o: "1 < 2"
p: "call()"

View File

@@ -0,0 +1,71 @@
# Unsupported special forms
## Not yet supported
Several special forms are unsupported by red-knot currently. However, we also don't emit
false-positive errors if you use one in an annotation:
```py
from typing_extensions import Self, TypeVarTuple, Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, TypeAlias, Callable, TypeVar
P = ParamSpec("P")
Ts = TypeVarTuple("Ts")
R_co = TypeVar("R_co", covariant=True)
Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
# TODO: should understand the annotation
reveal_type(args) # revealed: tuple
reveal_type(Alias) # revealed: @Todo(Unsupported or invalid type in a type expression)
def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
# TODO: should understand the annotation
reveal_type(args) # revealed: tuple
# TODO: should understand the annotation
reveal_type(kwargs) # revealed: dict
return callback(42, *args, **kwargs)
class Foo:
def method(self, x: Self):
reveal_type(x) # revealed: @Todo(Unsupported or invalid type in a type expression)
```
## Inheritance
You can't inherit from most of these. `typing.Callable` is an exception.
```py
from typing import Callable
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
class A(Self): ... # error: [invalid-base]
class B(Unpack): ... # error: [invalid-base]
class C(TypeGuard): ... # error: [invalid-base]
class D(TypeIs): ... # error: [invalid-base]
class E(Concatenate): ... # error: [invalid-base]
class F(Callable): ...
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
```
## Subscriptability
Some of these are not subscriptable:
```py
from typing_extensions import Self, TypeAlias
X: TypeAlias[T] = int # error: [invalid-type-form]
class Foo[T]:
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
def method(self: Self[int]) -> Self[int]:
reveal_type(self) # revealed: Unknown
```

View File

@@ -0,0 +1,37 @@
# Unsupported type qualifiers
## Not yet supported
Several type qualifiers are unsupported by red-knot currently. However, we also don't emit
false-positive errors if you use one in an annotation:
```py
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly, TypedDict
X: Final = 42
Y: Final[int] = 42
class Foo:
A: ClassVar[int] = 42
# TODO: `TypedDict` is actually valid as a base
# error: [invalid-base]
class Bar(TypedDict):
x: Required[int]
y: NotRequired[str]
z: ReadOnly[bytes]
```
## Inheritance
You can't inherit from a type qualifier.
```py
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
class A(Final): ... # error: [invalid-base]
class B(ClassVar): ... # error: [invalid-base]
class C(Required): ... # error: [invalid-base]
class D(NotRequired): ... # error: [invalid-base]
class E(ReadOnly): ... # error: [invalid-base]
```

View File

@@ -33,8 +33,6 @@ b: tuple[int] = (42,)
c: tuple[str, int] = ("42", 42)
d: tuple[tuple[str, str], tuple[int, int]] = (("foo", "foo"), (42, 42))
e: tuple[str, ...] = ()
# TODO: we should not emit this error
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
f: tuple[str, *tuple[int, ...], bytes] = ("42", b"42")
g: tuple[str, Unpack[tuple[int, ...]], bytes] = ("42", b"42")
h: tuple[list[int], list[int]] = ([], [])
@@ -78,20 +76,10 @@ c: tuple[str | int, str] = ([], "foo")
## PEP-604 annotations are supported
```py
def foo() -> str | int | None:
return None
reveal_type(foo()) # revealed: str | int | None
def bar() -> str | str | None:
return None
reveal_type(bar()) # revealed: str | None
def baz() -> str | str:
return "Hello, world!"
reveal_type(baz()) # revealed: str
def foo(v: str | int | None, w: str | str | None, x: str | str):
reveal_type(v) # revealed: str | int | None
reveal_type(w) # revealed: str | None
reveal_type(x) # revealed: str
```
## Attribute expressions in type annotations are understood
@@ -118,8 +106,7 @@ from __future__ import annotations
x: Foo
class Foo:
pass
class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
@@ -130,9 +117,15 @@ reveal_type(x) # revealed: Foo
```pyi path=main.pyi
x: Foo
class Foo:
pass
class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
```
## Annotated assignments in stub files are inferred correctly
```pyi path=main.pyi
x: int = 1
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -40,143 +40,125 @@ class C:
return 42
x = C()
# error: [invalid-argument-type]
x -= 1
# TODO: should error, once operand type check is implemented
reveal_type(x) # revealed: int
```
## Method union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
if flag:
def __iadd__(self, other: int) -> str:
return "Hello, world!"
else:
def __iadd__(self, other: int) -> int:
return 42
flag = bool_instance()
f = Foo()
f += 12
class Foo:
if bool_instance():
def __iadd__(self, other: int) -> str:
return "Hello, world!"
else:
def __iadd__(self, other: int) -> int:
return 42
f = Foo()
f += 12
reveal_type(f) # revealed: str | int
reveal_type(f) # revealed: str | int
```
## Partially bound `__iadd__`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
if flag:
def __iadd__(self, other: str) -> int:
return 42
class Foo:
if bool_instance():
def __iadd__(self, other: str) -> int:
return 42
f = Foo()
f = Foo()
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
# that `Foo.__iadd__` may be unbound as additional context.
f += "Hello, world!"
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
# that `Foo.__iadd__` may be unbound as additional context.
f += "Hello, world!"
reveal_type(f) # revealed: int | Unknown
reveal_type(f) # revealed: int | Unknown
```
## Partially bound with `__add__`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
def __add__(self, other: str) -> str:
return "Hello, world!"
if flag:
def __iadd__(self, other: str) -> int:
return 42
class Foo:
def __add__(self, other: str) -> str:
return "Hello, world!"
if bool_instance():
def __iadd__(self, other: str) -> int:
return 42
f = Foo()
f += "Hello, world!"
f = Foo()
f += "Hello, world!"
reveal_type(f) # revealed: int | str
reveal_type(f) # revealed: int | str
```
## Partially bound target union
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if flag1:
def __iadd__(self, other: int) -> int:
return 42
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if bool_instance():
def __iadd__(self, other: int) -> int:
return 42
if flag2:
f = Foo()
else:
f = 42.0
f += 12
if bool_instance():
f = Foo()
else:
f = 42.0
f += 12
reveal_type(f) # revealed: int | str | float
reveal_type(f) # revealed: int | str | float
```
## Target union
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class Foo:
def __iadd__(self, other: int) -> str:
return "Hello, world!"
flag = bool_instance()
if flag:
f = Foo()
else:
f = 42.0
f += 12
class Foo:
def __iadd__(self, other: int) -> str:
return "Hello, world!"
if flag:
f = Foo()
else:
f = 42.0
f += 12
reveal_type(f) # revealed: str | float
reveal_type(f) # revealed: str | float
```
## Partially bound target union with `__add__`
```py
def bool_instance() -> bool:
return True
def f(flag: bool, flag2: bool):
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if flag:
def __iadd__(self, other: int) -> int:
return 42
flag = bool_instance()
class Bar:
def __add__(self, other: int) -> bytes:
return b"Hello, world!"
class Foo:
def __add__(self, other: int) -> str:
return "Hello, world!"
if bool_instance():
def __iadd__(self, other: int) -> int:
return 42
def __iadd__(self, other: int) -> float:
return 42.0
class Bar:
def __add__(self, other: int) -> bytes:
return b"Hello, world!"
if flag2:
f = Foo()
else:
f = Bar()
f += 12
def __iadd__(self, other: int) -> float:
return 42.0
if flag:
f = Foo()
else:
f = Bar()
f += 12
reveal_type(f) # revealed: int | str | float
reveal_type(f) # revealed: int | str | float
```

View File

@@ -1,29 +1,27 @@
# Class attributes
# Attributes
Tests for attribute access on various kinds of types.
## Union of attributes
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
class C1:
x = 1
else:
class C1:
x = 2
class C2:
def _(flag: bool):
if flag:
x = 3
else:
x = 4
class C1:
x = 1
reveal_type(C1.x) # revealed: Literal[1, 2]
reveal_type(C2.x) # revealed: Literal[3, 4]
else:
class C1:
x = 2
class C2:
if flag:
x = 3
else:
x = 4
reveal_type(C1.x) # revealed: Literal[1, 2]
reveal_type(C2.x) # revealed: Literal[3, 4]
```
## Inherited attributes
@@ -68,24 +66,19 @@ reveal_type(A.X) # revealed: Literal[42]
In this example, the `x` attribute is not defined in the `C2` element of the union:
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class C1:
x = 1
class C1:
x = 1
class C2: ...
class C2: ...
class C3:
x = 3
class C3:
x = 3
C = C1 if flag1 else C2 if flag2 else C3
flag1 = bool_instance()
flag2 = bool_instance()
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 3]
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 3]
```
### Possibly-unbound within a class
@@ -94,26 +87,21 @@ We raise the same diagnostic if the attribute is possibly-unbound in at least on
union:
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag1: bool, flag2: bool):
class C1:
x = 1
class C1:
x = 1
class C2:
if flag:
x = 2
class C2:
if bool_instance():
x = 2
class C3:
x = 3
class C3:
x = 3
C = C1 if flag1 else C2 if flag2 else C3
flag1 = bool_instance()
flag2 = bool_instance()
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 2, 3]
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
reveal_type(C.x) # revealed: Literal[1, 2, 3]
```
## Unions with all paths unbound
@@ -121,16 +109,115 @@ reveal_type(C.x) # revealed: Literal[1, 2, 3]
If the symbol is unbound in all elements of the union, we detect that:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class C1: ...
class C2: ...
C = C1 if flag else C2
class C1: ...
class C2: ...
flag = bool_instance()
C = C1 if flag else C2
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown
```
## Objects of all types have a `__class__` method
```py
import typing_extensions
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
a = 42
reveal_type(a.__class__) # revealed: Literal[int]
b = "42"
reveal_type(b.__class__) # revealed: Literal[str]
c = b"42"
reveal_type(c.__class__) # revealed: Literal[bytes]
d = True
reveal_type(d.__class__) # revealed: Literal[bool]
e = (42, 42)
reveal_type(e.__class__) # revealed: Literal[tuple]
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(a.__class__) # revealed: type[int]
reveal_type(b.__class__) # revealed: Literal[str]
reveal_type(c.__class__) # revealed: type[int] | type[str]
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
# It would be incorrect to infer `Literal[type]` here,
# as `c` could be some subclass of `str` with a custom metaclass.
# All we know is that the metaclass must be a (non-strict) subclass of `type`.
reveal_type(d.__class__) # revealed: type[type]
reveal_type(f.__class__) # revealed: Literal[FunctionType]
class Foo: ...
reveal_type(Foo.__class__) # revealed: Literal[type]
```
## Function-literal attributes
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
functions are instances of that class:
```py path=a.py
def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(instance attributes)
reveal_type(f.__kwdefaults__) # revealed: @Todo(instance attributes)
```
Some attributes are special-cased, however:
```py path=b.py
def f(): ...
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
```
## Int-literal attributes
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
integers are instances of that class:
```py path=a.py
reveal_type((2).bit_length) # revealed: @Todo(instance attributes)
reveal_type((2).denominator) # revealed: @Todo(instance attributes)
```
Some attributes are special-cased, however:
```py path=b.py
reveal_type((2).numerator) # revealed: Literal[2]
reveal_type((2).real) # revealed: Literal[2]
```
## Literal `bool` attributes
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
bols are instances of that class:
```py path=a.py
reveal_type(True.__and__) # revealed: @Todo(instance attributes)
reveal_type(False.__or__) # revealed: @Todo(instance attributes)
```
Some attributes are special-cased, however:
```py path=b.py
reveal_type(True.numerator) # revealed: Literal[1]
reveal_type(False.real) # revealed: Literal[0]
```
## Bytes-literal attributes
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
```py
reveal_type(b"foo".join) # revealed: @Todo(instance attributes)
reveal_type(b"foo".endswith) # revealed: @Todo(instance attributes)
```

View File

@@ -46,3 +46,50 @@ reveal_type(a | b) # revealed: Literal[True]
reveal_type(b | a) # revealed: Literal[True]
reveal_type(b | b) # revealed: Literal[False]
```
## Arithmetic with a variable
```py
a = True
b = False
def lhs_is_int(x: int):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_int(x: int):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def lhs_is_bool(x: bool):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_bool(x: bool):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def both_are_bool(x: bool, y: bool):
reveal_type(x + y) # revealed: int
reveal_type(x - y) # revealed: int
reveal_type(x * y) # revealed: int
reveal_type(x // y) # revealed: int
reveal_type(x / y) # revealed: float
reveal_type(x % y) # revealed: int
```

View File

@@ -0,0 +1,27 @@
# Binary operations on classes
## Union of two classes
Unioning two classes via the `|` operator is only available in Python 3.10 and later.
```toml
[environment]
python-version = "3.10"
```
```py
class A: ...
class B: ...
reveal_type(A | B) # revealed: UnionType
```
## Union of two classes (prior to 3.10)
```py
class A: ...
class B: ...
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
reveal_type(A | B) # revealed: Unknown
```

View File

@@ -0,0 +1,371 @@
# Custom binary operations
## Class instances
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
def __sub__(self, other) -> Literal["-"]:
return "-"
def __mul__(self, other) -> Literal["*"]:
return "*"
def __matmul__(self, other) -> Literal["@"]:
return "@"
def __truediv__(self, other) -> Literal["/"]:
return "/"
def __mod__(self, other) -> Literal["%"]:
return "%"
def __pow__(self, other) -> Literal["**"]:
return "**"
def __lshift__(self, other) -> Literal["<<"]:
return "<<"
def __rshift__(self, other) -> Literal[">>"]:
return ">>"
def __or__(self, other) -> Literal["|"]:
return "|"
def __xor__(self, other) -> Literal["^"]:
return "^"
def __and__(self, other) -> Literal["&"]:
return "&"
def __floordiv__(self, other) -> Literal["//"]:
return "//"
class Sub(Yes): ...
class No: ...
# Yes implements all of the dunder methods.
reveal_type(Yes() + Yes()) # revealed: Literal["+"]
reveal_type(Yes() - Yes()) # revealed: Literal["-"]
reveal_type(Yes() * Yes()) # revealed: Literal["*"]
reveal_type(Yes() @ Yes()) # revealed: Literal["@"]
reveal_type(Yes() / Yes()) # revealed: Literal["/"]
reveal_type(Yes() % Yes()) # revealed: Literal["%"]
reveal_type(Yes() ** Yes()) # revealed: Literal["**"]
reveal_type(Yes() << Yes()) # revealed: Literal["<<"]
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"]
reveal_type(Yes() | Yes()) # revealed: Literal["|"]
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"]
reveal_type(Yes() & Yes()) # revealed: Literal["&"]
reveal_type(Yes() // Yes()) # revealed: Literal["//"]
# Sub inherits Yes's implementation of the dunder methods.
reveal_type(Sub() + Sub()) # revealed: Literal["+"]
reveal_type(Sub() - Sub()) # revealed: Literal["-"]
reveal_type(Sub() * Sub()) # revealed: Literal["*"]
reveal_type(Sub() @ Sub()) # revealed: Literal["@"]
reveal_type(Sub() / Sub()) # revealed: Literal["/"]
reveal_type(Sub() % Sub()) # revealed: Literal["%"]
reveal_type(Sub() ** Sub()) # revealed: Literal["**"]
reveal_type(Sub() << Sub()) # revealed: Literal["<<"]
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"]
reveal_type(Sub() | Sub()) # revealed: Literal["|"]
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"]
reveal_type(Sub() & Sub()) # revealed: Literal["&"]
reveal_type(Sub() // Sub()) # revealed: Literal["//"]
# No does not implement any of the dunder methods.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`"
reveal_type(No() + No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`"
reveal_type(No() - No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`"
reveal_type(No() * No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`"
reveal_type(No() @ No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`"
reveal_type(No() / No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`"
reveal_type(No() % No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`"
reveal_type(No() ** No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`"
reveal_type(No() << No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`"
reveal_type(No() >> No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`"
reveal_type(No() | No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`"
reveal_type(No() ^ No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`"
reveal_type(No() & No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`"
reveal_type(No() // No()) # revealed: Unknown
# Yes does not implement any of the reflected dunder methods.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() + Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() - Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() * Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() @ Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() / Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() % Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() ** Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() << Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() >> Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() | Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() ^ Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() & Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() // Yes()) # revealed: Unknown
```
## Subclass reflections override superclass dunders
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
def __sub__(self, other) -> Literal["-"]:
return "-"
def __mul__(self, other) -> Literal["*"]:
return "*"
def __matmul__(self, other) -> Literal["@"]:
return "@"
def __truediv__(self, other) -> Literal["/"]:
return "/"
def __mod__(self, other) -> Literal["%"]:
return "%"
def __pow__(self, other) -> Literal["**"]:
return "**"
def __lshift__(self, other) -> Literal["<<"]:
return "<<"
def __rshift__(self, other) -> Literal[">>"]:
return ">>"
def __or__(self, other) -> Literal["|"]:
return "|"
def __xor__(self, other) -> Literal["^"]:
return "^"
def __and__(self, other) -> Literal["&"]:
return "&"
def __floordiv__(self, other) -> Literal["//"]:
return "//"
class Sub(Yes):
def __radd__(self, other) -> Literal["r+"]:
return "r+"
def __rsub__(self, other) -> Literal["r-"]:
return "r-"
def __rmul__(self, other) -> Literal["r*"]:
return "r*"
def __rmatmul__(self, other) -> Literal["r@"]:
return "r@"
def __rtruediv__(self, other) -> Literal["r/"]:
return "r/"
def __rmod__(self, other) -> Literal["r%"]:
return "r%"
def __rpow__(self, other) -> Literal["r**"]:
return "r**"
def __rlshift__(self, other) -> Literal["r<<"]:
return "r<<"
def __rrshift__(self, other) -> Literal["r>>"]:
return "r>>"
def __ror__(self, other) -> Literal["r|"]:
return "r|"
def __rxor__(self, other) -> Literal["r^"]:
return "r^"
def __rand__(self, other) -> Literal["r&"]:
return "r&"
def __rfloordiv__(self, other) -> Literal["r//"]:
return "r//"
class No:
def __radd__(self, other) -> Literal["r+"]:
return "r+"
def __rsub__(self, other) -> Literal["r-"]:
return "r-"
def __rmul__(self, other) -> Literal["r*"]:
return "r*"
def __rmatmul__(self, other) -> Literal["r@"]:
return "r@"
def __rtruediv__(self, other) -> Literal["r/"]:
return "r/"
def __rmod__(self, other) -> Literal["r%"]:
return "r%"
def __rpow__(self, other) -> Literal["r**"]:
return "r**"
def __rlshift__(self, other) -> Literal["r<<"]:
return "r<<"
def __rrshift__(self, other) -> Literal["r>>"]:
return "r>>"
def __ror__(self, other) -> Literal["r|"]:
return "r|"
def __rxor__(self, other) -> Literal["r^"]:
return "r^"
def __rand__(self, other) -> Literal["r&"]:
return "r&"
def __rfloordiv__(self, other) -> Literal["r//"]:
return "r//"
# Subclass reflected dunder methods take precedence over the superclass's regular dunders.
reveal_type(Yes() + Sub()) # revealed: Literal["r+"]
reveal_type(Yes() - Sub()) # revealed: Literal["r-"]
reveal_type(Yes() * Sub()) # revealed: Literal["r*"]
reveal_type(Yes() @ Sub()) # revealed: Literal["r@"]
reveal_type(Yes() / Sub()) # revealed: Literal["r/"]
reveal_type(Yes() % Sub()) # revealed: Literal["r%"]
reveal_type(Yes() ** Sub()) # revealed: Literal["r**"]
reveal_type(Yes() << Sub()) # revealed: Literal["r<<"]
reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"]
reveal_type(Yes() | Sub()) # revealed: Literal["r|"]
reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"]
reveal_type(Yes() & Sub()) # revealed: Literal["r&"]
reveal_type(Yes() // Sub()) # revealed: Literal["r//"]
# But for an unrelated class, the superclass regular dunders are used.
reveal_type(Yes() + No()) # revealed: Literal["+"]
reveal_type(Yes() - No()) # revealed: Literal["-"]
reveal_type(Yes() * No()) # revealed: Literal["*"]
reveal_type(Yes() @ No()) # revealed: Literal["@"]
reveal_type(Yes() / No()) # revealed: Literal["/"]
reveal_type(Yes() % No()) # revealed: Literal["%"]
reveal_type(Yes() ** No()) # revealed: Literal["**"]
reveal_type(Yes() << No()) # revealed: Literal["<<"]
reveal_type(Yes() >> No()) # revealed: Literal[">>"]
reveal_type(Yes() | No()) # revealed: Literal["|"]
reveal_type(Yes() ^ No()) # revealed: Literal["^"]
reveal_type(Yes() & No()) # revealed: Literal["&"]
reveal_type(Yes() // No()) # revealed: Literal["//"]
```
## Classes
Dunder methods defined in a class are available to instances of that class, but not to the class
itself. (For these operators to work on the class itself, they would have to be defined on the
class's type, i.e. `type`.)
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
class Sub(Yes): ...
class No: ...
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
reveal_type(Yes + Yes) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
reveal_type(Sub + Sub) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
reveal_type(No + No) # revealed: Unknown
```
## Subclass
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
class Sub(Yes): ...
class No: ...
def yes() -> type[Yes]:
return Yes
def sub() -> type[Sub]:
return Sub
def no() -> type[No]:
return No
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`"
reveal_type(yes() + yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`"
reveal_type(sub() + sub()) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`"
reveal_type(no() + no()) # revealed: Unknown
```
## Function literals
```py
def f():
pass
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f + f) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f - f) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f * f) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f @ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f / f) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f % f) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f**f) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f << f) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f >> f) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f | f) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f ^ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f & f) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f // f) # revealed: Unknown
```

View File

@@ -281,20 +281,12 @@ reveal_type(42 + 4.2) # revealed: int
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3 + 3j) # revealed: int
def returns_int() -> int:
return 42
def _(x: bool, y: int):
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: float
def returns_bool() -> bool:
return True
x = returns_bool()
y = returns_int()
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
```
## With literal types

View File

@@ -9,6 +9,34 @@ reveal_type(3 * -1) # revealed: Literal[-3]
reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
# formal parameter type.
reveal_type(2 + "f") # revealed: int
def lhs(x: int):
reveal_type(x + 1) # revealed: int
reveal_type(x - 4) # revealed: int
reveal_type(x * -1) # revealed: int
reveal_type(x // 3) # revealed: int
reveal_type(x / 3) # revealed: float
reveal_type(x % 3) # revealed: int
def rhs(x: int):
reveal_type(2 + x) # revealed: int
reveal_type(3 - x) # revealed: int
reveal_type(3 * x) # revealed: int
reveal_type(-3 // x) # revealed: int
reveal_type(-3 / x) # revealed: float
reveal_type(5 % x) # revealed: int
def both(x: int):
reveal_type(x + x) # revealed: int
reveal_type(x - x) # revealed: int
reveal_type(x * x) # revealed: int
reveal_type(x // x) # revealed: int
reveal_type(x / x) # revealed: float
reveal_type(x % x) # revealed: int
```
## Power
@@ -21,6 +49,11 @@ largest_u32 = 4_294_967_295
reveal_type(2**2) # revealed: Literal[4]
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: @Todo(return type)
reveal_type(2**x) # revealed: @Todo(return type)
reveal_type(x**x) # revealed: @Todo(return type)
```
## Division by Zero

View File

@@ -7,72 +7,61 @@ Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand
evaluated.
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag or (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if bool_instance() or (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if bool_instance() and (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if flag and (x := 1):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
```
## First expression is always evaluated
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if (x := 1) or flag:
reveal_type(x) # revealed: Literal[1]
if (x := 1) or bool_instance():
reveal_type(x) # revealed: Literal[1]
if (x := 1) and bool_instance():
reveal_type(x) # revealed: Literal[1]
if (x := 1) and flag:
reveal_type(x) # revealed: Literal[1]
```
## Statically known truthiness
```py
if True or (x := 1):
# TODO: infer that the second arm is never executed, and raise `unresolved-reference`.
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
# error: [unresolved-reference]
reveal_type(x) # revealed: Unknown
if True and (x := 1):
# TODO: infer that the second arm is always executed, do not raise a diagnostic
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
```
## Later expressions can always use variables from earlier expressions
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1]
# error: [unresolved-reference]
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unknown
# error: [unresolved-reference]
flag or reveal_type(y) or (y := 1) # revealed: Unknown
```
## Nested expressions
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
if flag1 or ((x := 1) and flag2):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if ((y := 1) and flag1) or flag2:
reveal_type(y) # revealed: Literal[1]
if bool_instance() or ((x := 1) and bool_instance()):
# error: [possibly-unresolved-reference]
reveal_type(x) # revealed: Literal[1]
if ((y := 1) and bool_instance()) or bool_instance():
reveal_type(y) # revealed: Literal[1]
# error: [possibly-unresolved-reference]
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Literal[1]
# error: [possibly-unresolved-reference]
reveal_type(z) # revealed: Literal[1]
if (flag1 and (z := 1)) or reveal_type(z): # revealed: Literal[1]
# error: [possibly-unresolved-reference]
reveal_type(z) # revealed: Literal[1]
```

View File

@@ -0,0 +1,209 @@
# Boundness and declaredness: public uses
This document demonstrates how type-inference and diagnostics works for *public* uses of a symbol,
that is, a use of a symbol from another scope. If a symbol has a declared type in its local scope
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
We test the whole matrix of possible boundness and declaredness states. The current behavior is
summarized in the following table, while the tests below demonstrate each case. Note that some of
this behavior is questionable and might change in the future. See the TODOs in `symbol_by_id`
(`types.rs`) and [this issue](https://github.com/astral-sh/ruff/issues/14297) for more information.
In particular, we should raise errors in the "possibly-undeclared-and-unbound" as well as the
"undeclared-and-possibly-unbound" cases (marked with a "?").
| **Public type** | declared | possibly-undeclared | undeclared |
| ---------------- | ------------ | -------------------------- | ------------ |
| bound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
| unbound | `T_declared` | `T_declared` | `Unknown` |
| **Diagnostic** | declared | possibly-undeclared | undeclared |
| ---------------- | -------- | ------------------------- | ------------------- |
| bound | | | |
| possibly-unbound | | `possibly-unbound-import` | ? |
| unbound | | ? | `unresolved-import` |
## Declared
### Declared and bound
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type
(`Literal[1]`), or a conflicting inferred type (`Literal[2]`):
```py path=mod.py
x: int = 1
# error: [invalid-assignment]
y: str = 2
```
```py
from mod import x, y
reveal_type(x) # revealed: int
reveal_type(y) # revealed: str
```
### Declared and possibly unbound
If a symbol is declared and *possibly* unbound, we trust that other module and use the declared type
without raising an error.
```py path=mod.py
def flag() -> bool: ...
x: int
y: str
if flag:
x = 1
# error: [invalid-assignment]
y = 2
```
```py
from mod import x, y
reveal_type(x) # revealed: int
reveal_type(y) # revealed: str
```
### Declared and unbound
Similarly, if a symbol is declared but unbound, we do not raise an error. We trust that this symbol
is available somehow and simply use the declared type.
```py path=mod.py
x: int
```
```py
from mod import x
reveal_type(x) # revealed: int
```
## Possibly undeclared
### Possibly undeclared and bound
If a symbol is possibly undeclared but definitely bound, we use the union of the declared and
inferred types:
```py path=mod.py
from typing import Any
def flag() -> bool: ...
x = 1
y = 2
if flag():
x: Any
# error: [invalid-declaration]
y: str
```
```py
from mod import x, y
reveal_type(x) # revealed: Literal[1] | Any
reveal_type(y) # revealed: Literal[2] | Unknown
```
### Possibly undeclared and possibly unbound
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and
inferred types. This case is interesting because the "possibly declared" definition might not be the
same as the "possibly bound" definition (symbol `y`). Note that we raise a `possibly-unbound-import`
error for both `x` and `y`:
```py path=mod.py
def flag() -> bool: ...
if flag():
x: Any = 1
y = 2
else:
y: str
```
```py
# error: [possibly-unbound-import]
# error: [possibly-unbound-import]
from mod import x, y
reveal_type(x) # revealed: Literal[1] | Any
reveal_type(y) # revealed: Literal[2] | str
```
### Possibly undeclared and unbound
If a symbol is possibly undeclared and definitely unbound, we currently do not raise an error. This
seems inconsistent when compared to the case just above.
```py path=mod.py
def flag() -> bool: ...
if flag():
x: int
```
```py
# TODO: this should raise an error. Once we fix this, update the section description and the table
# on top of this document.
from mod import x
reveal_type(x) # revealed: int
```
## Undeclared
### Undeclared but bound
We use the inferred type as the public type, if a symbol has no declared type.
```py path=mod.py
x = 1
```
```py
from mod import x
reveal_type(x) # revealed: Literal[1]
```
### Undeclared and possibly unbound
If a symbol is undeclared and *possibly* unbound, we currently do not raise an error. This seems
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case.
```py path=mod.py
def flag() -> bool: ...
if flag:
x = 1
```
```py
# TODO: this should raise an error. Once we fix this, update the section description and the table
# on top of this document.
from mod import x
reveal_type(x) # revealed: Literal[1]
```
### Undeclared and unbound
If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
```py path=mod.py
if False:
x: int = 1
```
```py
# error: [unresolved-import]
from mod import x
reveal_type(x) # revealed: Unknown
```

View File

@@ -22,29 +22,27 @@ reveal_type(b) # revealed: Unknown
## Possibly unbound `__call__` method
```py
def flag() -> bool: ...
def _(flag: bool):
class PossiblyNotCallable:
if flag:
def __call__(self) -> int: ...
class PossiblyNotCallable:
if flag():
def __call__(self) -> int: ...
a = PossiblyNotCallable()
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
reveal_type(result) # revealed: int
a = PossiblyNotCallable()
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
reveal_type(result) # revealed: int
```
## Possibly unbound callable
```py
def flag() -> bool: ...
def _(flag: bool):
if flag:
class PossiblyUnbound:
def __call__(self) -> int: ...
if flag():
class PossiblyUnbound:
def __call__(self) -> int: ...
# error: [possibly-unresolved-reference]
a = PossiblyUnbound()
reveal_type(a()) # revealed: int
# error: [possibly-unresolved-reference]
a = PossiblyUnbound()
reveal_type(a()) # revealed: int
```
## Non-callable `__call__`
@@ -61,15 +59,43 @@ reveal_type(a()) # revealed: Unknown
## Possibly non-callable `__call__`
```py
def flag() -> bool: ...
def _(flag: bool):
class NonCallable:
if flag:
__call__ = 1
else:
def __call__(self) -> int: ...
class NonCallable:
if flag():
__call__ = 1
else:
def __call__(self) -> int: ...
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
```
## Call binding errors
### Wrong argument type
```py
class C:
def __call__(self, x: int) -> int:
return 1
c = C()
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`"
reveal_type(c("foo")) # revealed: int
```
### Wrong argument type on `self`
```py
class C:
# TODO this definition should also be an error; `C` must be assignable to type of `self`
def __call__(self: int) -> int:
return 1
c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
reveal_type(c()) # revealed: int
```

View File

@@ -57,12 +57,276 @@ x = nonsense() # error: "Object of type `Literal[123]` is not callable"
## Potentially unbound function
```py
def flag() -> bool: ...
if flag():
def foo() -> int:
return 42
# error: [possibly-unresolved-reference]
reveal_type(foo()) # revealed: int
def _(flag: bool):
if flag:
def foo() -> int:
return 42
# error: [possibly-unresolved-reference]
reveal_type(foo()) # revealed: int
```
## Wrong argument type
### Positional argument, positional-or-keyword parameter
```py
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, positional-only parameter
```py
def f(x: int, /) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, variadic parameter
```py
def f(*args: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `*args` of function `f`; expected type `int`"
reveal_type(f("foo")) # revealed: int
```
### Keyword argument, positional-or-keyword parameter
```py
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keyword-only parameter
```py
def f(*, x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keywords parameter
```py
def f(**kwargs: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `**kwargs` of function `f`; expected type `int`"
reveal_type(f(x="foo")) # revealed: int
```
### Correctly match keyword out-of-order
```py
def f(x: int = 1, y: str = "foo") -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal[2]` cannot be assigned to parameter `y` of function `f`; expected type `str`"
# error: 20 [invalid-argument-type] "Object of type `Literal["bar"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
reveal_type(f(y=2, x="bar")) # revealed: int
```
## Too many positional arguments
### One too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
reveal_type(f("foo")) # revealed: int
```
### Two too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
reveal_type(f("foo", "bar")) # revealed: int
```
### No too-many-positional if variadic is taken
```py
def f(*args: int) -> int:
return 1
reveal_type(f(1, 2, 3)) # revealed: int
```
### Multiple keyword arguments map to keyword variadic parameter
```py
def f(**kwargs: int) -> int:
return 1
reveal_type(f(foo=1, bar=2)) # revealed: int
```
## Missing arguments
### No defaults or variadic
```py
def f(x: int) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### With default
```py
def f(x: int, y: str = "foo") -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Defaulted argument is not required
```py
def f(x: int = 1) -> int:
return 1
reveal_type(f()) # revealed: int
```
### With variadic
```py
def f(x: int, *y: str) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Variadic argument is not required
```py
def f(*args: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Keywords argument is not required
```py
def f(**kwargs: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Multiple
```py
def f(x: int, y: int) -> int:
return 1
# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
reveal_type(f()) # revealed: int
```
## Unknown argument
```py
def f(x: int) -> int:
return 1
# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
reveal_type(f(x=1, y=2)) # revealed: int
```
## Parameter already assigned
```py
def f(x: int) -> int:
return 1
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2)) # revealed: int
```
## Special functions
Some functions require special handling in type inference. Here, we make sure that we still emit
proper diagnostics in case of missing or superfluous arguments.
### `reveal_type`
```py
from typing_extensions import reveal_type
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type() # revealed: Unknown
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2) # revealed: Literal[1]
```
### `static_assert`
```py
from knot_extensions import static_assert
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
# error: [static-assert-error]
static_assert()
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
static_assert(True, 2, 3)
```
### `len`
```py
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
len()
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
len([], 1)
```
### Type API predicates
```py
from knot_extensions import is_subtype_of, is_fully_static
# error: [missing-argument]
is_subtype_of()
# error: [missing-argument]
is_subtype_of(int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int, int)
# error: [missing-argument]
is_fully_static()
# error: [too-many-positional-arguments]
is_fully_static(int, int)
```

View File

@@ -0,0 +1,44 @@
# Invalid signatures
## Multiple arguments with the same name
We always map a keyword argument to the first parameter of that name.
```py
# error: [invalid-syntax] "Duplicate parameter "x""
def f(x: int, x: str) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2)) # revealed: int
```
## Positional after non-positional
When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them
to the valid order:
```py
# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter"
def f(**kw: int, x: str) -> int:
return 1
# error: 15 [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `str`"
reveal_type(f(1)) # revealed: int
```
## Non-defaulted after defaulted
We emit a syntax diagnostic for this, but it doesn't cause any problems for binding.
```py
# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default"
def f(x: int = 1, y: str) -> int:
return 1
reveal_type(f(y="foo")) # revealed: int
# error: [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
# error: [missing-argument] "No argument provided for required parameter `y` of function `f`"
reveal_type(f("foo")) # revealed: int
```

View File

@@ -3,22 +3,14 @@
## Union of return types
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
def f() -> int:
return 1
else:
def f() -> str:
return "foo"
reveal_type(f()) # revealed: int | str
def _(flag: bool):
if flag:
def f() -> int:
return 1
else:
def f() -> str:
return "foo"
reveal_type(f()) # revealed: int | str
```
## Calling with an unknown union
@@ -26,13 +18,10 @@ reveal_type(f()) # revealed: int | str
```py
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
def f() -> int:
return 1
@@ -44,20 +33,14 @@ reveal_type(f()) # revealed: Unknown | int
Calling a union with a non-callable element should emit a diagnostic.
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
if flag:
f = 1
else:
def f() -> int:
return 1
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
reveal_type(x) # revealed: Unknown | int
def _(flag: bool):
if flag:
f = 1
else:
def f() -> int:
return 1
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
reveal_type(x) # revealed: Unknown | int
```
## Multiple non-callable elements in a union
@@ -65,23 +48,17 @@ reveal_type(x) # revealed: Unknown | int
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
if flag:
f = 1
elif flag2:
f = "foo"
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
def _(flag: bool, flag2: bool):
if flag:
f = 1
elif flag2:
f = "foo"
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
```
## All non-callable union elements
@@ -89,16 +66,12 @@ reveal_type(f())
Calling a union with no callable elements can emit a simpler diagnostic.
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
f = 1
else:
f = "foo"
flag = bool_instance()
if flag:
f = 1
else:
f = "foo"
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
reveal_type(x) # revealed: Unknown
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```

View File

@@ -3,38 +3,31 @@
```py
class A: ...
def get_a() -> A: ...
def get_object() -> object: ...
def _(a1: A, a2: A, o: object):
n1 = None
n2 = None
a1 = get_a()
a2 = get_a()
reveal_type(a1 is a1) # revealed: bool
reveal_type(a1 is a2) # revealed: bool
n1 = None
n2 = None
reveal_type(n1 is n1) # revealed: Literal[True]
reveal_type(n1 is n2) # revealed: Literal[True]
o = get_object()
reveal_type(a1 is n1) # revealed: Literal[False]
reveal_type(n1 is a1) # revealed: Literal[False]
reveal_type(a1 is a1) # revealed: bool
reveal_type(a1 is a2) # revealed: bool
reveal_type(a1 is o) # revealed: bool
reveal_type(n1 is o) # revealed: bool
reveal_type(n1 is n1) # revealed: Literal[True]
reveal_type(n1 is n2) # revealed: Literal[True]
reveal_type(a1 is not a1) # revealed: bool
reveal_type(a1 is not a2) # revealed: bool
reveal_type(a1 is n1) # revealed: Literal[False]
reveal_type(n1 is a1) # revealed: Literal[False]
reveal_type(n1 is not n1) # revealed: Literal[False]
reveal_type(n1 is not n2) # revealed: Literal[False]
reveal_type(a1 is o) # revealed: bool
reveal_type(n1 is o) # revealed: bool
reveal_type(a1 is not n1) # revealed: Literal[True]
reveal_type(n1 is not a1) # revealed: Literal[True]
reveal_type(a1 is not a1) # revealed: bool
reveal_type(a1 is not a2) # revealed: bool
reveal_type(n1 is not n1) # revealed: Literal[False]
reveal_type(n1 is not n2) # revealed: Literal[False]
reveal_type(a1 is not n1) # revealed: Literal[True]
reveal_type(n1 is not a1) # revealed: Literal[True]
reveal_type(a1 is not o) # revealed: bool
reveal_type(n1 is not o) # revealed: bool
reveal_type(a1 is not o) # revealed: bool
reveal_type(n1 is not o) # revealed: bool
```

View File

@@ -312,17 +312,9 @@ reveal_type(1 <= 2j) # revealed: bool
reveal_type(1 > 2j) # revealed: bool
reveal_type(1 >= 2j) # revealed: bool
def bool_instance() -> bool:
return True
def int_instance() -> int:
return 42
x = bool_instance()
y = int_instance()
reveal_type(x < y) # revealed: bool
reveal_type(y < x) # revealed: bool
reveal_type(4.2 < x) # revealed: bool
reveal_type(x < 4.2) # revealed: bool
def f(x: bool, y: int):
reveal_type(x < y) # revealed: bool
reveal_type(y < x) # revealed: bool
reveal_type(4.2 < x) # revealed: bool
reveal_type(x < 4.2) # revealed: bool
```

View File

@@ -20,10 +20,8 @@ reveal_type(1 <= "" and 0 < 1) # revealed: bool
```py
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
def int_instance() -> int:
return 42
reveal_type(1 == int_instance()) # revealed: bool
reveal_type(9 < int_instance()) # revealed: bool
reveal_type(int_instance() < int_instance()) # revealed: bool
def _(a: int, b: int):
reveal_type(1 == a) # revealed: bool
reveal_type(9 < a) # revealed: bool
reveal_type(a < b) # revealed: bool
```

View File

@@ -14,21 +14,19 @@ class Child1(Base):
class Child2(Base): ...
def get_base() -> Base: ...
def _(x: Base):
c1 = Child1()
x = get_base()
c1 = Child1()
# Create an intersection type through narrowing:
if isinstance(x, Child1):
if isinstance(x, Child2):
reveal_type(x) # revealed: Child1 & Child2
# Create an intersection type through narrowing:
if isinstance(x, Child1):
if isinstance(x, Child2):
reveal_type(x) # revealed: Child1 & Child2
reveal_type(x == 1) # revealed: Literal[True]
reveal_type(x == 1) # revealed: Literal[True]
# Other comparison operators fall back to the base type:
reveal_type(x > 1) # revealed: bool
reveal_type(x is c1) # revealed: bool
# Other comparison operators fall back to the base type:
reveal_type(x > 1) # revealed: bool
reveal_type(x is c1) # revealed: bool
```
## Negative contributions
@@ -73,18 +71,15 @@ if x != "abc":
#### Integers
```py
def get_int() -> int: ...
def _(x: int):
if x != 1:
reveal_type(x) # revealed: int & ~Literal[1]
x = get_int()
reveal_type(x != 1) # revealed: Literal[True]
reveal_type(x != 2) # revealed: bool
if x != 1:
reveal_type(x) # revealed: int & ~Literal[1]
reveal_type(x != 1) # revealed: Literal[True]
reveal_type(x != 2) # revealed: bool
reveal_type(x == 1) # revealed: Literal[False]
reveal_type(x == 2) # revealed: bool
reveal_type(x == 1) # revealed: Literal[False]
reveal_type(x == 2) # revealed: bool
```
### Identity comparisons
@@ -92,18 +87,15 @@ if x != 1:
```py
class A: ...
def get_object() -> object: ...
def _(o: object):
a = A()
n = None
o = object()
if o is not None:
reveal_type(o) # revealed: object & ~None
a = A()
n = None
if o is not None:
reveal_type(o) # revealed: object & ~None
reveal_type(o is n) # revealed: Literal[False]
reveal_type(o is not n) # revealed: Literal[True]
reveal_type(o is n) # revealed: Literal[False]
reveal_type(o is not n) # revealed: Literal[True]
```
## Diagnostics
@@ -119,16 +111,13 @@ class Container:
class NonContainer: ...
def get_object() -> object: ...
def _(x: object):
if isinstance(x, Container):
if isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & NonContainer
x = get_object()
if isinstance(x, Container):
if isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & NonContainer
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
reveal_type(2 in x) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
reveal_type(2 in x) # revealed: bool
```
### Unsupported operators for negative contributions
@@ -142,14 +131,11 @@ class Container:
class NonContainer: ...
def get_object() -> object: ...
def _(x: object):
if isinstance(x, Container):
if not isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & ~NonContainer
x = get_object()
if isinstance(x, Container):
if not isinstance(x, NonContainer):
reveal_type(x) # revealed: Container & ~NonContainer
# No error here!
reveal_type(2 in x) # revealed: bool
# No error here!
reveal_type(2 in x) # revealed: bool
```

View File

@@ -31,10 +31,10 @@ class C:
def __lt__(self, other) -> C: ...
x = A() < B() < C()
reveal_type(x) # revealed: A | B
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: bool | A
reveal_type(y) # revealed: Literal[False] | A
z = 10 < 0 < A() < B() < C()
reveal_type(z) # revealed: Literal[False]

View File

@@ -3,18 +3,17 @@
## String literals
```py
def str_instance() -> str: ...
def _(x: str):
reveal_type("abc" == "abc") # revealed: Literal[True]
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
reveal_type("abc" in "ab cd") # revealed: Literal[False]
reveal_type("" not in "hello") # revealed: Literal[False]
reveal_type("--" is "--") # revealed: bool
reveal_type("A" is "B") # revealed: Literal[False]
reveal_type("--" is not "--") # revealed: bool
reveal_type("A" is not "B") # revealed: Literal[True]
reveal_type(x < "...") # revealed: bool
reveal_type("abc" == "abc") # revealed: Literal[True]
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
reveal_type("abc" in "ab cd") # revealed: Literal[False]
reveal_type("" not in "hello") # revealed: Literal[False]
reveal_type("--" is "--") # revealed: bool
reveal_type("A" is "B") # revealed: Literal[False]
reveal_type("--" is not "--") # revealed: bool
reveal_type("A" is not "B") # revealed: Literal[True]
reveal_type(str_instance() < "...") # revealed: bool
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
```

View File

@@ -58,28 +58,23 @@ reveal_type(c >= d) # revealed: Literal[True]
#### Results with Ambiguity
```py
def bool_instance() -> bool:
return True
def _(x: bool, y: int):
a = (x,)
b = (y,)
def int_instance() -> int:
return 42
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
a = (bool_instance(),)
b = (int_instance(),)
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
```
#### Comparison Unsupported
@@ -197,7 +192,7 @@ reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
> Example: `(<int instance>, "foo") == (<int instance>, "bar")`
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
@@ -208,42 +203,38 @@ In contrast, with operators like `<` and `>`, the comparison must consider each
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
```py
def str_instance() -> str:
return "hello"
def _(x: str, y: int):
reveal_type("foo" == "bar") # revealed: Literal[False]
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
reveal_type((y, "foo") == (y, "bar")) # revealed: Literal[False]
def int_instance() -> int:
return 42
a = (x, y, "foo")
reveal_type("foo" == "bar") # revealed: Literal[False]
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False]
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
a = (str_instance(), int_instance(), "foo")
b = (x, y, "bar")
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
b = (str_instance(), int_instance(), "bar")
c = (x, y, "foo", "different_length")
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
c = (str_instance(), int_instance(), "foo", "different_length")
reveal_type(a == c) # revealed: Literal[False]
reveal_type(a != c) # revealed: Literal[True]
reveal_type(a < c) # revealed: bool
reveal_type(a <= c) # revealed: bool
reveal_type(a > c) # revealed: bool
reveal_type(a >= c) # revealed: bool
reveal_type(a == c) # revealed: Literal[False]
reveal_type(a != c) # revealed: Literal[True]
reveal_type(a < c) # revealed: bool
reveal_type(a <= c) # revealed: bool
reveal_type(a > c) # revealed: bool
reveal_type(a >= c) # revealed: bool
```
#### Error Propagation
@@ -252,42 +243,36 @@ Errors occurring within a tuple comparison should propagate outward. However, if
comparison can clearly conclude before encountering an error, the error should not be raised.
```py
def int_instance() -> int:
return 42
def _(n: int, s: str):
class A: ...
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
A() < A()
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
A() <= A()
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
A() > A()
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
A() >= A()
def str_instance() -> str:
return "hello"
a = (0, n, A())
class A: ...
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a < a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a <= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a > a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a >= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
A() < A()
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
A() <= A()
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
A() > A()
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
A() >= A()
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
# and should terminate immediately.
b = (99999, n, A())
a = (0, int_instance(), A())
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a < a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a <= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a > a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
reveal_type(a >= a) # revealed: Unknown
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
# and should terminate immediately.
b = (99999, int_instance(), A())
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
```
### Membership Test Comparisons
@@ -295,22 +280,20 @@ reveal_type(a >= b) # revealed: Literal[False]
"Membership Test Comparisons" refers to the operators `in` and `not in`.
```py
def int_instance() -> int:
return 42
def _(n: int):
a = (1, 2)
b = ((3, 4), (1, 2))
c = ((1, 2, 3), (4, 5, 6))
d = ((n, n), (n, n))
a = (1, 2)
b = ((3, 4), (1, 2))
c = ((1, 2, 3), (4, 5, 6))
d = ((int_instance(), int_instance()), (int_instance(), int_instance()))
reveal_type(a in b) # revealed: Literal[True]
reveal_type(a not in b) # revealed: Literal[False]
reveal_type(a in b) # revealed: Literal[True]
reveal_type(a not in b) # revealed: Literal[False]
reveal_type(a in c) # revealed: Literal[False]
reveal_type(a not in c) # revealed: Literal[True]
reveal_type(a in c) # revealed: Literal[False]
reveal_type(a not in c) # revealed: Literal[True]
reveal_type(a in d) # revealed: bool
reveal_type(a not in d) # revealed: bool
reveal_type(a in d) # revealed: bool
reveal_type(a not in d) # revealed: bool
```
### Identity Comparisons

View File

@@ -5,49 +5,46 @@
Comparisons on union types need to consider all possible cases:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
one_or_two = 1 if flag else 2
flag = bool_instance()
one_or_two = 1 if flag else 2
reveal_type(one_or_two <= 2) # revealed: Literal[True]
reveal_type(one_or_two <= 1) # revealed: bool
reveal_type(one_or_two <= 0) # revealed: Literal[False]
reveal_type(one_or_two <= 2) # revealed: Literal[True]
reveal_type(one_or_two <= 1) # revealed: bool
reveal_type(one_or_two <= 0) # revealed: Literal[False]
reveal_type(2 >= one_or_two) # revealed: Literal[True]
reveal_type(1 >= one_or_two) # revealed: bool
reveal_type(0 >= one_or_two) # revealed: Literal[False]
reveal_type(2 >= one_or_two) # revealed: Literal[True]
reveal_type(1 >= one_or_two) # revealed: bool
reveal_type(0 >= one_or_two) # revealed: Literal[False]
reveal_type(one_or_two < 1) # revealed: Literal[False]
reveal_type(one_or_two < 2) # revealed: bool
reveal_type(one_or_two < 3) # revealed: Literal[True]
reveal_type(one_or_two < 1) # revealed: Literal[False]
reveal_type(one_or_two < 2) # revealed: bool
reveal_type(one_or_two < 3) # revealed: Literal[True]
reveal_type(one_or_two > 0) # revealed: Literal[True]
reveal_type(one_or_two > 1) # revealed: bool
reveal_type(one_or_two > 2) # revealed: Literal[False]
reveal_type(one_or_two > 0) # revealed: Literal[True]
reveal_type(one_or_two > 1) # revealed: bool
reveal_type(one_or_two > 2) # revealed: Literal[False]
reveal_type(one_or_two == 3) # revealed: Literal[False]
reveal_type(one_or_two == 1) # revealed: bool
reveal_type(one_or_two == 3) # revealed: Literal[False]
reveal_type(one_or_two == 1) # revealed: bool
reveal_type(one_or_two != 3) # revealed: Literal[True]
reveal_type(one_or_two != 1) # revealed: bool
reveal_type(one_or_two != 3) # revealed: Literal[True]
reveal_type(one_or_two != 1) # revealed: bool
a_or_ab = "a" if flag else "ab"
a_or_ab = "a" if flag else "ab"
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
reveal_type("a" in a_or_ab) # revealed: Literal[True]
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
reveal_type("a" in a_or_ab) # revealed: Literal[True]
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
reveal_type("b" in a_or_ab) # revealed: bool
reveal_type("b" not in a_or_ab) # revealed: bool
reveal_type("b" in a_or_ab) # revealed: bool
reveal_type("b" not in a_or_ab) # revealed: bool
one_or_none = 1 if flag else None
one_or_none = 1 if flag else None
reveal_type(one_or_none is None) # revealed: bool
reveal_type(one_or_none is not None) # revealed: bool
reveal_type(one_or_none is None) # revealed: bool
reveal_type(one_or_none is not None) # revealed: bool
```
## Union on both sides of the comparison
@@ -56,18 +53,15 @@ With unions on both sides, we need to consider the full cross product of options
resulting (union) type:
```py
def bool_instance() -> bool:
return True
def _(flag_s: bool, flag_l: bool):
small = 1 if flag_s else 2
large = 2 if flag_l else 3
flag_s, flag_l = bool_instance(), bool_instance()
small = 1 if flag_s else 2
large = 2 if flag_l else 3
reveal_type(small <= large) # revealed: Literal[True]
reveal_type(small >= large) # revealed: bool
reveal_type(small <= large) # revealed: Literal[True]
reveal_type(small >= large) # revealed: bool
reveal_type(small < large) # revealed: bool
reveal_type(small > large) # revealed: Literal[False]
reveal_type(small < large) # revealed: bool
reveal_type(small > large) # revealed: Literal[False]
```
## Unsupported operations
@@ -77,12 +71,9 @@ back to `bool` for the result type instead of trying to infer something more pre
(supported) variants:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = [1, 2] if flag else 1
flag = bool_instance()
x = [1, 2] if flag else 1
result = 1 in x # error: "Operator `in` is not supported"
reveal_type(result) # revealed: bool
result = 1 in x # error: "Operator `in` is not supported"
reveal_type(result) # revealed: bool
```

View File

@@ -1,42 +1,38 @@
# Comparison: Unsupported operators
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag1: bool, flag2: bool):
class A: ...
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
class A: ...
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
reveal_type(b) # revealed: bool
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `object` and `int`")
c = object() < 5
# TODO: should be Unknown, once operand type check is implemented
reveal_type(c) # revealed: bool
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
reveal_type(b) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `int` and `object`")
d = 5 < object()
# TODO: should be Unknown, once operand type check is implemented
reveal_type(d) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `object` and `int`")
c = object() < 5
# TODO: should be Unknown, once operand type check is implemented
reveal_type(c) # revealed: bool
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `int` and `object`")
d = 5 < object()
# TODO: should be Unknown, once operand type check is implemented
reveal_type(d) # revealed: bool
# TODO: should error, need to check if __lt__ signature is valid for right operand
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
f = (1, 2) < (1, "hello")
# TODO: should be Unknown, once operand type check is implemented
reveal_type(f) # revealed: bool
flag = bool_instance()
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool
# TODO: should error, need to check if __lt__ signature is valid for right operand
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
f = (1, 2) < (1, "hello")
# TODO: should be Unknown, once operand type check is implemented
reveal_type(f) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
g = (bool_instance(), A()) < (bool_instance(), A())
reveal_type(g) # revealed: Unknown
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
g = (flag1, A()) < (flag2, A())
reveal_type(g) # revealed: Unknown
```

View File

@@ -3,47 +3,35 @@
## Simple if-expression
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else 2
reveal_type(x) # revealed: Literal[1, 2]
def _(flag: bool):
x = 1 if flag else 2
reveal_type(x) # revealed: Literal[1, 2]
```
## If-expression with walrus operator
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
y = 0
z = 0
x = (y := 1) if flag else (z := 2)
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(y) # revealed: Literal[0, 1]
reveal_type(z) # revealed: Literal[0, 2]
def _(flag: bool):
y = 0
z = 0
x = (y := 1) if flag else (z := 2)
reveal_type(x) # revealed: Literal[1, 2]
reveal_type(y) # revealed: Literal[0, 1]
reveal_type(z) # revealed: Literal[0, 2]
```
## Nested if-expression
```py
def bool_instance() -> bool:
return True
flag, flag2 = bool_instance(), bool_instance()
x = 1 if flag else 2 if flag2 else 3
reveal_type(x) # revealed: Literal[1, 2, 3]
def _(flag: bool, flag2: bool):
x = 1 if flag else 2 if flag2 else 3
reveal_type(x) # revealed: Literal[1, 2, 3]
```
## None
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1 if flag else None
reveal_type(x) # revealed: Literal[1] | None
def _(flag: bool):
x = 1 if flag else None
reveal_type(x) # revealed: Literal[1] | None
```

View File

@@ -3,128 +3,147 @@
## Simple if
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
y = 1
y = 2
flag = bool_instance()
y = 1
y = 2
if flag:
y = 3
if flag:
y = 3
reveal_type(y) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[2, 3]
```
## Simple if-elif-else
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 1
y = 2
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:
y = 3
elif flag2:
y = 4
else:
r = y
y = 5
s = y
x = y
if flag:
y = 3
elif flag2:
y = 4
else:
r = y
y = 5
s = y
x = y
reveal_type(x) # revealed: Literal[3, 4, 5]
reveal_type(x) # revealed: Literal[3, 4, 5]
# revealed: Literal[2]
# error: [possibly-unresolved-reference]
reveal_type(r)
# revealed: Literal[2]
# error: [possibly-unresolved-reference]
reveal_type(r)
# revealed: Literal[5]
# error: [possibly-unresolved-reference]
reveal_type(s)
# revealed: Literal[5]
# error: [possibly-unresolved-reference]
reveal_type(s)
```
## Single symbol across if-elif-else
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
if flag:
y = 1
elif flag2:
y = 2
else:
y = 3
flag, flag2 = bool_instance(), bool_instance()
if flag:
y = 1
elif flag2:
y = 2
else:
y = 3
reveal_type(y) # revealed: Literal[1, 2, 3]
reveal_type(y) # revealed: Literal[1, 2, 3]
```
## if-elif-else without else assignment
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 0
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
if flag:
y = 1
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
```
## if-elif-else with intervening assignment
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 0
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
y = 1
z = 3
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
if flag:
y = 1
z = 3
elif flag2:
y = 2
else:
pass
reveal_type(y) # revealed: Literal[0, 1, 2]
```
## Nested if statement
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 0
flag, flag2 = bool_instance(), bool_instance()
y = 0
if flag:
if flag2:
y = 1
reveal_type(y) # revealed: Literal[0, 1]
if flag:
if flag2:
y = 1
reveal_type(y) # revealed: Literal[0, 1]
```
## if-elif without else
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag2: bool):
y = 1
y = 2
flag, flag2 = bool_instance(), bool_instance()
y = 1
y = 2
if flag:
y = 3
elif flag2:
y = 4
if flag:
y = 3
elif flag2:
y = 4
reveal_type(y) # revealed: Literal[2, 3, 4]
reveal_type(y) # revealed: Literal[2, 3, 4]
```
## if-elif with assignment expressions in tests
```py
def check(x: int) -> bool:
return bool(x)
if check(x := 1):
x = 2
elif check(x := 3):
x = 4
reveal_type(x) # revealed: Literal[2, 3, 4]
```
## constraints apply to later test expressions
```py
def check(x) -> bool:
return bool(x)
def _(flag: bool):
x = 1 if flag else None
y = 0
if x is None:
pass
elif check(y := x):
pass
reveal_type(y) # revealed: Literal[0, 1]
```

View File

@@ -3,39 +3,43 @@
## With wildcard
```py
match 0:
case 1:
y = 2
case _:
y = 3
def _(target: int):
match target:
case 1:
y = 2
case _:
y = 3
reveal_type(y) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[2, 3]
```
## Without wildcard
```py
match 0:
case 1:
y = 2
case 2:
y = 3
def _(target: int):
match target:
case 1:
y = 2
case 2:
y = 3
# revealed: Literal[2, 3]
# error: [possibly-unresolved-reference]
reveal_type(y)
# revealed: Literal[2, 3]
# error: [possibly-unresolved-reference]
reveal_type(y)
```
## Basic match
```py
y = 1
y = 2
match 0:
case 1:
y = 3
case 2:
y = 4
def _(target: int):
y = 1
y = 2
reveal_type(y) # revealed: Literal[2, 3, 4]
match target:
case 1:
y = 3
case 2:
y = 4
reveal_type(y) # revealed: Literal[2, 3, 4]
```

View File

@@ -10,42 +10,66 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
## Incompatible declarations
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
x: str
else:
x: int
flag = bool_instance()
if flag:
x: str
else:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
```
## Partial declarations
## Incompatible declarations for 2 (out of 3) types
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
if flag1:
x: str
elif flag2:
x: int
flag = bool_instance()
if flag:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
# Here, the declared type for `x` is `int | str | Unknown`.
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
```
## Incompatible declarations with bad assignment
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
if flag:
x: str
else:
x: int
flag = bool_instance()
if flag:
x: str
else:
x: int
# error: [conflicting-declarations]
# error: [invalid-assignment]
x = b"foo"
# error: [conflicting-declarations]
# error: [invalid-assignment]
x = b"foo"
```
## No errors
Currently, we avoid raising the conflicting-declarations for the following cases:
### Partial declarations
```py
def _(flag: bool):
if flag:
x: int
x = 1
```
### Partial declarations in try-except
Refer to <https://github.com/astral-sh/ruff/issues/13966>
```py
def _():
try:
x: int = 1
except:
x = 2
x = 3
```

View File

@@ -0,0 +1,142 @@
# `assert_type`
## Basic
```py
from typing_extensions import assert_type
def _(x: int):
assert_type(x, int) # fine
assert_type(x, str) # error: [type-assertion-failure]
```
## Narrowing
The asserted type is checked against the inferred type, not the declared type.
```toml
[environment]
python-version = "3.10"
```
```py
from typing_extensions import assert_type
def _(x: int | str):
if isinstance(x, int):
reveal_type(x) # revealed: int
assert_type(x, int) # fine
```
## Equivalence
The actual type must match the asserted type precisely.
```py
from typing import Any, Type, Union
from typing_extensions import assert_type
# Subtype does not count
def _(x: bool):
assert_type(x, int) # error: [type-assertion-failure]
def _(a: type[int], b: type[Any]):
assert_type(a, type[Any]) # error: [type-assertion-failure]
assert_type(b, type[int]) # error: [type-assertion-failure]
# The expression constructing the type is not taken into account
def _(a: type[int]):
assert_type(a, Type[int]) # fine
```
## Gradual types
```py
from typing import Any
from typing_extensions import Literal, assert_type
from knot_extensions import Unknown
# Any and Unknown are considered equivalent
def _(a: Unknown, b: Any):
reveal_type(a) # revealed: Unknown
assert_type(a, Any) # fine
reveal_type(b) # revealed: Any
assert_type(b, Unknown) # fine
def _(a: type[Unknown], b: type[Any]):
reveal_type(a) # revealed: type[Unknown]
assert_type(a, type[Any]) # fine
reveal_type(b) # revealed: type[Any]
assert_type(b, type[Unknown]) # fine
```
## Tuples
Tuple types with the same elements are the same.
```py
from typing_extensions import assert_type
from knot_extensions import Unknown
def _(a: tuple[int, str, bytes]):
assert_type(a, tuple[int, str, bytes]) # fine
assert_type(a, tuple[int, str]) # error: [type-assertion-failure]
assert_type(a, tuple[int, str, bytes, None]) # error: [type-assertion-failure]
assert_type(a, tuple[int, bytes, str]) # error: [type-assertion-failure]
def _(a: tuple[Any, ...], b: tuple[Unknown, ...]):
assert_type(a, tuple[Any, ...]) # fine
assert_type(a, tuple[Unknown, ...]) # fine
assert_type(b, tuple[Unknown, ...]) # fine
assert_type(b, tuple[Any, ...]) # fine
```
## Unions
Unions with the same elements are the same, regardless of order.
```toml
[environment]
python-version = "3.10"
```
```py
from typing_extensions import assert_type
def _(a: str | int):
assert_type(a, str | int) # fine
# TODO: Order-independent union handling in type equivalence
assert_type(a, int | str) # error: [type-assertion-failure]
```
## Intersections
Intersections are the same when their positive and negative parts are respectively the same,
regardless of order.
```py
from typing_extensions import assert_type
from knot_extensions import Intersection, Not
class A: ...
class B: ...
class C: ...
class D: ...
def _(a: A):
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
reveal_type(a) # revealed: A & B & ~C & ~D
assert_type(a, Intersection[A, B, Not[C], Not[D]]) # fine
# TODO: Order-independent intersection handling in type equivalence
assert_type(a, Intersection[B, A, Not[D], Not[C]]) # error: [type-assertion-failure]
```

View File

@@ -0,0 +1,27 @@
# `cast`
`cast()` takes two arguments, one type and one value, and returns a value of the given type.
The (inferred) type of the value and the given type do not need to have any correlation.
```py
from typing import Literal, cast
reveal_type(True) # revealed: Literal[True]
reveal_type(cast(str, True)) # revealed: str
reveal_type(cast("str", True)) # revealed: str
reveal_type(cast(int | str, 1)) # revealed: int | str
# error: [invalid-type-form]
reveal_type(cast(Literal, True)) # revealed: Unknown
# TODO: These should be errors
cast(1)
cast(str)
cast(str, b"ar", "foo")
# TODO: Either support keyword arguments properly,
# or give a comprehensible error message saying they're unsupported
cast(val="foo", typ=int) # error: [unresolved-reference] "Name `foo` used when not defined"
```

View File

@@ -49,12 +49,124 @@ def foo(
try:
help()
except x as e:
# TODO: should be `AttributeError`
reveal_type(e) # revealed: @Todo(exception type)
reveal_type(e) # revealed: AttributeError
except y as f:
# TODO: should be `OSError | RuntimeError`
reveal_type(f) # revealed: @Todo(exception type)
reveal_type(f) # revealed: OSError | RuntimeError
except z as g:
# TODO: should be `BaseException`
reveal_type(g) # revealed: @Todo(exception type)
reveal_type(g) # revealed: @Todo(full tuple[...] support)
```
## Invalid exception handlers
```py
try:
pass
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
except 3 as e:
reveal_type(e) # revealed: Unknown
try:
pass
# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
except (ValueError, OSError, "foo", b"bar") as e:
reveal_type(e) # revealed: ValueError | OSError | Unknown
def foo(
x: type[str],
y: tuple[type[OSError], type[RuntimeError], int],
z: tuple[type[str], ...],
):
try:
help()
# error: [invalid-exception-caught]
except x as e:
reveal_type(e) # revealed: Unknown
# error: [invalid-exception-caught]
except y as f:
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
except z as g:
# TODO: should emit a diagnostic here:
reveal_type(g) # revealed: @Todo(full tuple[...] support)
```
## Object raised is not an exception
```py
try:
raise AttributeError() # fine
except:
...
try:
raise FloatingPointError # fine
except:
...
try:
raise 1 # error: [invalid-raise]
except:
...
try:
raise int # error: [invalid-raise]
except:
...
def _(e: Exception | type[Exception]):
raise e # fine
def _(e: Exception | type[Exception] | None):
raise e # error: [invalid-raise]
```
## Exception cause is not an exception
```py
try:
raise EOFError() from GeneratorExit # fine
except:
...
try:
raise StopIteration from MemoryError() # fine
except:
...
try:
raise BufferError() from None # fine
except:
...
try:
raise ZeroDivisionError from False # error: [invalid-raise]
except:
...
try:
raise SystemExit from bool() # error: [invalid-raise]
except:
...
try:
raise
except KeyboardInterrupt as e: # fine
reveal_type(e) # revealed: KeyboardInterrupt
raise LookupError from e # fine
try:
raise
except int as e: # error: [invalid-exception-caught]
reveal_type(e) # revealed: Unknown
raise KeyError from e
def _(e: Exception | type[Exception]):
raise ModuleNotFoundError from e # fine
def _(e: Exception | type[Exception] | None):
raise IndexError from e # fine
def _(e: int | None):
raise IndexError from e # error: [invalid-raise]
```

View File

@@ -1,30 +1,66 @@
# Except star
# `except*`
## Except\* with BaseException
`except*` is only available in Python 3.11 and later:
```toml
[environment]
python-version = "3.11"
```
## `except*` with `BaseException`
```py
try:
help()
except* BaseException as e:
# TODO: should be `BaseExceptionGroup[BaseException]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```
## Except\* with specific exception
## `except*` with specific exception
```py
try:
help()
except* OSError as e:
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
# (needs homogenous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```
## Except\* with multiple exceptions
## `except*` with multiple exceptions
```py
try:
help()
except* (TypeError, AttributeError) as e:
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
# (needs homogenous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
```
## `except*` with mix of `Exception`s and `BaseException`s
```py
try:
help()
except* (KeyboardInterrupt, AttributeError) as e:
# TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```
## Invalid `except*` handlers
```py
try:
help()
except* 3 as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
try:
help()
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
```

View File

@@ -9,5 +9,4 @@ try:
print
except as e: # error: [invalid-syntax]
reveal_type(e) # revealed: Unknown
```

View File

@@ -3,26 +3,25 @@
## Boundness
```py
def flag() -> bool: ...
def _(flag: bool):
class A:
always_bound = 1
class A:
always_bound = 1
if flag:
union = 1
else:
union = "abc"
if flag():
union = 1
else:
union = "abc"
if flag:
possibly_unbound = "abc"
if flag():
possibly_unbound = "abc"
reveal_type(A.always_bound) # revealed: Literal[1]
reveal_type(A.always_bound) # revealed: Literal[1]
reveal_type(A.union) # revealed: Literal[1, "abc"]
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
reveal_type(A.non_existent) # revealed: Unknown
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
reveal_type(A.non_existent) # revealed: Unknown
```

View File

@@ -3,54 +3,45 @@
## OR
```py
def foo() -> str:
pass
reveal_type(True or False) # revealed: Literal[True]
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
reveal_type("" or "y" or "z") # revealed: Literal["y"]
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 | Literal[False]
reveal_type(foo() or True) # revealed: str | Literal[True]
def _(foo: str):
reveal_type(True or False) # revealed: Literal[True]
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
reveal_type("" or "y" or "z") # revealed: Literal["y"]
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]
```
## AND
```py
def foo() -> str:
pass
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo() and False) # revealed: str | Literal[False]
reveal_type(foo() and True) # revealed: str | 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[""]
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("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]
```
## Simple function calls to bool
```py
def returns_bool() -> bool:
return True
def _(flag: bool):
if flag:
x = True
else:
x = False
if returns_bool():
x = True
else:
x = False
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: bool
```
## Complex
```py
def foo() -> str:
pass
reveal_type("x" and "y" or "z") # revealed: Literal["y"]
reveal_type("x" or "y" and "z") # revealed: Literal["x"]
reveal_type("" and "y" or "z") # revealed: Literal["z"]

View File

@@ -3,13 +3,11 @@
## Union
```py
def bool_instance() -> bool:
return True
reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2]
def _(flag: bool):
reveal_type(1 if flag else 2) # revealed: Literal[1, 2]
```
## Statically known branches
## Statically known conditions in if-expressions
```py
reveal_type(1 if True else 2) # revealed: Literal[1]
@@ -30,14 +28,12 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2]
The test inside an if expression should not affect code outside of the expression.
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x: Literal[42, "hello"] = 42 if flag else "hello"
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
reveal_type(x) # revealed: Literal[42, "hello"]
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
_ = ... if isinstance(x, str) else ...
_ = ... if isinstance(x, str) else ...
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
reveal_type(x) # revealed: Literal[42, "hello"]
```

View File

@@ -119,7 +119,7 @@ class ZeroOrStr:
reveal_type(len(Zero())) # revealed: Literal[0]
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0]
# TODO: Emit a diagnostic
reveal_type(len(OneOrFoo())) # revealed: int
@@ -211,8 +211,7 @@ reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
### No `__len__`
```py
class NoDunderLen:
pass
class NoDunderLen: ...
# TODO: Emit a diagnostic
reveal_type(len(NoDunderLen())) # revealed: int

View File

@@ -0,0 +1,31 @@
# Tests for the `@typing(_extensions).final` decorator
## Cannot subclass
Don't do this:
```py
import typing_extensions
from typing import final
@final
class A: ...
class B(A): ... # error: 9 [subclass-of-final-class] "Class `B` cannot inherit from final class `A`"
@typing_extensions.final
class C: ...
class D(C): ... # error: [subclass-of-final-class]
class E: ...
class F: ...
class G: ...
# fmt: off
class H(
E,
F,
A, # error: [subclass-of-final-class]
G,
): ...
```

View File

@@ -73,7 +73,7 @@ def f[T]():
A typevar with less than two constraints emits a diagnostic:
```py
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
def f[T: (int,)]():
pass
```

View File

@@ -25,3 +25,82 @@ reveal_type(D) # revealed: Literal[C]
```py path=b.py
class C: ...
```
## Nested
```py
import a.b
reveal_type(a.b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
## Deeply nested
```py
import a.b.c
reveal_type(a.b.c.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b/__init__.py
```
```py path=a/b/c.py
class C: ...
```
## Nested with rename
```py
import a.b as b
reveal_type(b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
## Deeply nested with rename
```py
import a.b.c as c
reveal_type(c.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b/__init__.py
```
```py path=a/b/c.py
class C: ...
```
## Unresolvable submodule imports
```py
# Topmost component resolvable, submodule not resolvable:
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
# Topmost component unresolvable:
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
```
```py path=a/__init__.py
```

View File

@@ -3,6 +3,6 @@
```py
import builtins
x = builtins.copyright
reveal_type(x) # revealed: Literal[copyright]
x = builtins.chr
reveal_type(x) # revealed: Literal[chr]
```

View File

@@ -3,11 +3,10 @@
## Maybe unbound
```py path=maybe_unbound.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
y = 3
x = y # error: [possibly-unresolved-reference]
@@ -31,13 +30,12 @@ reveal_type(y) # revealed: Literal[3]
## Maybe unbound annotated
```py path=maybe_unbound_annotated.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
y: int = 3
x = y # error: [possibly-unresolved-reference]
# revealed: Literal[3]
@@ -63,10 +61,10 @@ reveal_type(y) # revealed: int
Importing a possibly undeclared name still gives us its declared type:
```py path=maybe_undeclared.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
if bool_instance():
if coinflip():
x: int
```
@@ -83,14 +81,12 @@ def f(): ...
```
```py path=b.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
from c import f
else:
def f(): ...
```
@@ -111,11 +107,10 @@ x: int
```
```py path=b.py
def bool_instance() -> bool:
def coinflip() -> bool:
return True
flag = bool_instance()
if flag:
if coinflip():
from c import x
else:
x = 1

View File

@@ -0,0 +1,75 @@
# Conflicting attributes and submodules
## Via import
```py
import a.b
reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
b = 42
```
```py path=a/b.py
```
## Via from/import
```py
from a import b
reveal_type(b) # revealed: Literal[42]
```
```py path=a/__init__.py
b = 42
```
```py path=a/b.py
```
## Via both
```py
import a.b
from a import b
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
b = 42
```
```py path=a/b.py
```
## Via both (backwards)
In this test, we infer a different type for `b` than the runtime behavior of the Python interpreter.
The interpreter will not load the submodule `a.b` during the `from a import b` statement, since `a`
contains a non-module attribute named `b`. (See the [definition][from-import] of a `from...import`
statement for details.) However, because our import tracking is flow-insensitive, we will see that
`a.b` is imported somewhere in the file, and therefore assume that the `from...import` statement
sees the submodule as the value of `b` instead of the integer.
```py
from a import b
import a.b
# Python would say `Literal[42]` for `b`
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'>
```
```py path=a/__init__.py
b = 42
```
```py path=a/b.py
```
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement

View File

@@ -7,3 +7,25 @@ from import bar # error: [invalid-syntax]
reveal_type(bar) # revealed: Unknown
```
## Invalid nested module import
TODO: This is correctly flagged as an error, but we could clean up the diagnostics that we report.
```py
# TODO: No second diagnostic
# error: [invalid-syntax] "Expected ',', found '.'"
# error: [unresolved-import] "Module `a` has no member `c`"
from a import b.c
# TODO: Should these be inferred as Unknown?
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.c) # revealed: Literal[1]
```
```py path=a/__init__.py
```
```py path=a/b.py
c = 1
```

View File

@@ -121,23 +121,44 @@ X = 42
```
```py path=package/bar.py
# TODO: support submodule imports
from . import foo # error: [unresolved-import]
from . import foo
y = foo.X
# TODO: should be `Literal[42]`
reveal_type(y) # revealed: Unknown
reveal_type(foo.X) # revealed: Literal[42]
```
## Non-existent + bare to module
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
nor an attribute of `package`.
```py path=package/__init__.py
```
```py path=package/bar.py
# TODO: support submodule imports
from . import foo # error: [unresolved-import]
reveal_type(foo) # revealed: Unknown
```
## Import submodule from self
We don't currently consider `from...import` statements when building up the `imported_modules` set
in the semantic index. When accessing an attribute of a module, we only consider it a potential
submodule when that submodule name appears in the `imported_modules` set. That means that submodules
that are imported via `from...import` are not visible to our type inference if you also access that
submodule via the attribute on its parent package.
```py path=package/__init__.py
```
```py path=package/foo.py
X = 42
```
```py path=package/bar.py
from . import foo
import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
reveal_type(package.foo.X) # revealed: Unknown
```

View File

@@ -0,0 +1,100 @@
# Tracking imported modules
These tests depend on how we track which modules have been imported. There are currently two
characteristics of our module tracking that can lead to inaccuracies:
- Imports are tracked on a per-file basis. At runtime, importing a submodule in one file makes that
submodule globally available via any reference to the containing package. We will flag an error
if a file tries to access a submodule without there being an import of that submodule _in that
same file_.
This is a purposeful decision, and not one we plan to change. If a module wants to re-export some
other module that it imports, there are ways to do that (tested below) that are blessed by the
typing spec and that are visible to our file-scoped import tracking.
- Imports are tracked flow-insensitively: submodule accesses are allowed and resolved if that
submodule is imported _anywhere in the file_. This handles the common case where all imports are
grouped at the top of the file, and is easiest to implement. We might revisit this decision and
track submodule imports flow-sensitively, in which case we will have to update the assertions in
some of these tests.
## Import submodule later in file
This test highlights our flow-insensitive analysis, since we access the `a.b` submodule before it
has been imported.
```py
import a
# Would be an error with flow-sensitive tracking
reveal_type(a.b.C) # revealed: Literal[C]
import a.b
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
## Rename a re-export
This test highlights how import tracking is local to each file, but specifically to the file where a
containing module is first referenced. This allows the main module to see that `q.a` contains a
submodule `b`, even though `a.b` is never imported in the main module.
```py
from q import a, b
reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.C) # revealed: Literal[C]
reveal_type(a.b) # revealed: <module 'a.b'>
reveal_type(a.b.C) # revealed: Literal[C]
```
```py path=a/__init__.py
```
```py path=a/b.py
class C: ...
```
```py path=q.py
import a as a
import a.b as b
```
## Attribute overrides submodule
Technically, either a submodule or a non-module attribute could shadow the other, depending on the
ordering of when the submodule is loaded relative to the parent module's `__init__.py` file being
evaluated. We have chosen to always have the submodule take priority. (This matches pyright's
current behavior, and opposite of mypy's current behavior.)
```py
import sub.b
import attr.b
# In the Python interpreter, `attr.b` is Literal[1]
reveal_type(sub.b) # revealed: <module 'sub.b'>
reveal_type(attr.b) # revealed: <module 'attr.b'>
```
```py path=sub/__init__.py
b = 1
```
```py path=sub/b.py
```
```py path=attr/__init__.py
from . import b as _
b = 1
```
```py path=attr/b.py
```

View File

@@ -0,0 +1,825 @@
# Intersection types
## Introduction
This test suite covers certain properties of intersection types and makes sure that we can apply
various simplification strategies. We use `Intersection` (`&`) and `Not` (`~`) to construct
intersection types (note that we display negative contributions at the end; the order does not
matter):
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
def _(
i1: Intersection[P, Q],
i2: Intersection[P, Not[Q]],
i3: Intersection[Not[P], Q],
i4: Intersection[Not[P], Not[Q]],
) -> None:
reveal_type(i1) # revealed: P & Q
reveal_type(i2) # revealed: P & ~Q
reveal_type(i3) # revealed: Q & ~P
reveal_type(i4) # revealed: ~P & ~Q
```
## Notation
Throughout this document, we use the following types as representatives for certain equivalence
classes.
### Non-disjoint types
We use `P`, `Q`, `R`, … to denote types that are non-disjoint:
```py
from knot_extensions import static_assert, is_disjoint_from
class P: ...
class Q: ...
class R: ...
static_assert(not is_disjoint_from(P, Q))
static_assert(not is_disjoint_from(P, R))
static_assert(not is_disjoint_from(Q, R))
```
Although `P` is not a subtype of `Q` and `Q` is not a subtype of `P`, the two types are not disjoint
because it would be possible to create a class `S` that inherits from both `P` and `Q` using
multiple inheritance. An instance of `S` would be a member of the `P` type _and_ the `Q` type.
### Disjoint types
We use `Literal[1]`, `Literal[2]`, … as examples of pairwise-disjoint types, and `int` as a joint
supertype of these:
```py
from knot_extensions import static_assert, is_disjoint_from, is_subtype_of
from typing import Literal
static_assert(is_disjoint_from(Literal[1], Literal[2]))
static_assert(is_disjoint_from(Literal[1], Literal[3]))
static_assert(is_disjoint_from(Literal[2], Literal[3]))
static_assert(is_subtype_of(Literal[1], int))
static_assert(is_subtype_of(Literal[2], int))
static_assert(is_subtype_of(Literal[3], int))
```
### Subtypes
Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of (proper) subtypes:
```py
from knot_extensions import static_assert, is_subtype_of, is_disjoint_from
class A: ...
class B(A): ...
class C(B): ...
static_assert(is_subtype_of(B, A))
static_assert(is_subtype_of(C, B))
static_assert(is_subtype_of(C, A))
static_assert(not is_subtype_of(A, B))
static_assert(not is_subtype_of(B, C))
static_assert(not is_subtype_of(A, C))
class B1(A): ...
class B2(A): ...
static_assert(is_subtype_of(B1, A))
static_assert(is_subtype_of(B2, A))
static_assert(not is_subtype_of(A, B1))
static_assert(not is_subtype_of(A, B2))
static_assert(not is_subtype_of(B1, B2))
static_assert(not is_subtype_of(B2, B1))
```
## Structural properties
This section covers structural properties of intersection types and documents some decisions on how
to represent mixtures of intersections and unions.
### Single-element intersections
If we have an intersection with a single element, we can simplify to that element. Similarly, we
show an intersection with a single negative contribution as just the negation of that element.
```py
from knot_extensions import Intersection, Not
class P: ...
def _(
i1: Intersection[P],
i2: Intersection[Not[P]],
) -> None:
reveal_type(i1) # revealed: P
reveal_type(i2) # revealed: ~P
```
### Flattening of nested intersections
We eagerly flatten nested intersections types.
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
class S: ...
def positive_contributions(
i1: Intersection[P, Intersection[Q, R]],
i2: Intersection[Intersection[P, Q], R],
) -> None:
reveal_type(i1) # revealed: P & Q & R
reveal_type(i2) # revealed: P & Q & R
def negative_contributions(
i1: Intersection[Not[P], Intersection[Not[Q], Not[R]]],
i2: Intersection[Intersection[Not[P], Not[Q]], Not[R]],
) -> None:
reveal_type(i1) # revealed: ~P & ~Q & ~R
reveal_type(i2) # revealed: ~P & ~Q & ~R
def mixed(
i1: Intersection[P, Intersection[Not[Q], R]],
i2: Intersection[Intersection[P, Not[Q]], R],
i3: Intersection[Not[P], Intersection[Q, Not[R]]],
i4: Intersection[Intersection[Q, Not[R]], Not[P]],
) -> None:
reveal_type(i1) # revealed: P & R & ~Q
reveal_type(i2) # revealed: P & R & ~Q
reveal_type(i3) # revealed: Q & ~P & ~R
reveal_type(i4) # revealed: Q & ~R & ~P
def multiple(
i1: Intersection[Intersection[P, Q], Intersection[R, S]],
):
reveal_type(i1) # revealed: P & Q & R & S
def nested(
i1: Intersection[Intersection[Intersection[P, Q], R], S],
i2: Intersection[P, Intersection[Q, Intersection[R, S]]],
):
reveal_type(i1) # revealed: P & Q & R & S
reveal_type(i2) # revealed: P & Q & R & S
```
### Union of intersections
We always normalize our representation to a _union of intersections_, so when we add a _union to an
intersection_, we distribute the union over the respective elements:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
class S: ...
def _(
i1: Intersection[P, Q | R | S],
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
def simplifications_for_same_elements(
i1: Intersection[P, Q | P],
i2: Intersection[Q, P | Q],
i3: Intersection[P | Q, Q | R],
i4: Intersection[P | Q, P | Q],
i5: Intersection[P | Q, Q | P],
) -> None:
# P & (Q | P)
# = P & Q | P & P
# = P & Q | P
# = P
# (because P is a supertype of P & Q)
reveal_type(i1) # revealed: P
# similar here:
reveal_type(i2) # revealed: Q
# (P | Q) & (Q | R)
# = P & Q | P & R | Q & Q | Q & R
# = 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
# (P | Q) & (P | Q)
# = P & P | P & Q | Q & P | Q & Q
# = P | P & Q | Q
# = P | Q
reveal_type(i4) # revealed: P | Q
```
### Negation distributes over union
Distribution also applies to a negation operation. This is a manifestation of one of
[De Morgan's laws], namely `~(P | Q) = ~P & ~Q`:
```py
from knot_extensions import Not
from typing import Literal
class P: ...
class Q: ...
class R: ...
def _(i1: Not[P | Q], i2: Not[P | Q | R]) -> None:
reveal_type(i1) # revealed: ~P & ~Q
reveal_type(i2) # revealed: ~P & ~Q & ~R
def example_literals(i: Not[Literal[1, 2]]) -> None:
reveal_type(i) # revealed: ~Literal[1] & ~Literal[2]
```
### Negation of intersections
The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
def _(
i1: Not[Intersection[P, Q]],
i2: Not[Intersection[P, Q, R]],
) -> None:
reveal_type(i1) # revealed: ~P | ~Q
reveal_type(i2) # revealed: ~P | ~Q | ~R
```
### `Never` is dual to `object`
`Never` represents the empty set of values, while `object` represents the set of all values, so
`~Never` is equivalent to `object`, and `~object` is equivalent to `Never`. This is a manifestation
of the [complement laws] of set theory.
```py
from knot_extensions import Intersection, Not
from typing_extensions import Never
def _(
not_never: Not[Never],
not_object: Not[object],
) -> None:
reveal_type(not_never) # revealed: object
reveal_type(not_object) # revealed: Never
```
### Intersection of a type and its negation
Continuing with more [complement laws], if we see both `P` and `~P` in an intersection, we can
simplify to `Never`, even in the presence of other types:
```py
from knot_extensions import Intersection, Not
from typing import Any
class P: ...
class Q: ...
def _(
i1: Intersection[P, Not[P]],
i2: Intersection[Not[P], P],
i3: Intersection[P, Q, Not[P]],
i4: Intersection[Not[P], Q, P],
i5: Intersection[P, Any, Not[P]],
i6: Intersection[Not[P], Any, P],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
```
### Union of a type and its negation
Similarly, if we have both `P` and `~P` in a _union_, we can simplify that to `object`.
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
def _(
i1: P | Not[P],
i2: Not[P] | P,
i3: P | Q | Not[P],
i4: Not[P] | Q | P,
) -> None:
reveal_type(i1) # revealed: object
reveal_type(i2) # revealed: object
reveal_type(i3) # revealed: object
reveal_type(i4) # revealed: object
```
### Negation is an involution
The final of the [complement laws] states that negating twice is equivalent to not negating at all:
```py
from knot_extensions import Not
class P: ...
def _(
i1: Not[P],
i2: Not[Not[P]],
i3: Not[Not[Not[P]]],
i4: Not[Not[Not[Not[P]]]],
) -> None:
reveal_type(i1) # revealed: ~P
reveal_type(i2) # revealed: P
reveal_type(i3) # revealed: ~P
reveal_type(i4) # revealed: P
```
## Simplification strategies
In this section, we present various simplification strategies that go beyond the structure of the
representation.
### `Never` in intersections
If we intersect with `Never`, we can simplify the whole intersection to `Never`, even if there are
dynamic types involved:
```py
from knot_extensions import Intersection, Not
from typing_extensions import Never, Any
class P: ...
class Q: ...
def _(
i1: Intersection[P, Never],
i2: Intersection[Never, P],
i3: Intersection[Any, Never],
i4: Intersection[Never, Not[Any]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
```
### Simplifications using disjointness
#### Positive contributions
If we intersect disjoint types, we can simplify to `Never`, even in the presence of other types:
```py
from knot_extensions import Intersection, Not
from typing import Literal, Any
class P: ...
def _(
i01: Intersection[Literal[1], Literal[2]],
i02: Intersection[Literal[2], Literal[1]],
i03: Intersection[Literal[1], Literal[2], P],
i04: Intersection[Literal[1], P, Literal[2]],
i05: Intersection[P, Literal[1], Literal[2]],
i06: Intersection[Literal[1], Literal[2], Any],
i07: Intersection[Literal[1], Any, Literal[2]],
i08: Intersection[Any, Literal[1], Literal[2]],
) -> None:
reveal_type(i01) # revealed: Never
reveal_type(i02) # revealed: Never
reveal_type(i03) # revealed: Never
reveal_type(i04) # revealed: Never
reveal_type(i05) # revealed: Never
reveal_type(i06) # revealed: Never
reveal_type(i07) # revealed: Never
reveal_type(i08) # revealed: Never
# `bool` is final and can not be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which
# is disjoint from `type[str]`:
def example_type_bool_type_str(
i: Intersection[type[bool], type[str]],
) -> None:
reveal_type(i) # revealed: Never
```
#### Positive and negative contributions
If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can remove the negative
contribution `~Y`, as `~Y` must fully contain the positive contribution `X` as a subtype:
```py
from knot_extensions import Intersection, Not
from typing import Literal
def _(
i1: Intersection[Literal[1], Not[Literal[2]]],
i2: Intersection[Not[Literal[2]], Literal[1]],
i3: Intersection[Literal[1], Not[Literal[2]], int],
i4: Intersection[Literal[1], int, Not[Literal[2]]],
i5: Intersection[int, Literal[1], Not[Literal[2]]],
) -> None:
reveal_type(i1) # revealed: Literal[1]
reveal_type(i2) # revealed: Literal[1]
reveal_type(i3) # revealed: Literal[1]
reveal_type(i4) # revealed: Literal[1]
reveal_type(i5) # revealed: Literal[1]
# None is disjoint from int, so this simplification applies here
def example_none(
i1: Intersection[int, Not[None]],
i2: Intersection[Not[None], int],
) -> None:
reveal_type(i1) # revealed: int
reveal_type(i2) # revealed: int
```
### Simplifications using subtype relationships
#### Positive type and positive subtype
Subtypes are contained within their supertypes, so we can simplify intersections by removing
superfluous supertypes:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i01: Intersection[A, B],
i02: Intersection[B, A],
i03: Intersection[A, C],
i04: Intersection[C, A],
i05: Intersection[B, C],
i06: Intersection[C, B],
i07: Intersection[A, B, C],
i08: Intersection[C, B, A],
i09: Intersection[B, C, A],
i10: Intersection[A, B, Unrelated],
i11: Intersection[B, A, Unrelated],
i12: Intersection[B, Unrelated, A],
i13: Intersection[A, Unrelated, B],
i14: Intersection[Unrelated, A, B],
i15: Intersection[Unrelated, B, A],
i16: Intersection[A, B, Any],
i17: Intersection[B, A, Any],
i18: Intersection[B, Any, A],
i19: Intersection[A, Any, B],
i20: Intersection[Any, A, B],
i21: Intersection[Any, B, A],
) -> None:
reveal_type(i01) # revealed: B
reveal_type(i02) # revealed: B
reveal_type(i03) # revealed: C
reveal_type(i04) # revealed: C
reveal_type(i05) # revealed: C
reveal_type(i06) # revealed: C
reveal_type(i07) # revealed: C
reveal_type(i08) # revealed: C
reveal_type(i09) # revealed: C
reveal_type(i10) # revealed: B & Unrelated
reveal_type(i11) # revealed: B & Unrelated
reveal_type(i12) # revealed: B & Unrelated
reveal_type(i13) # revealed: Unrelated & B
reveal_type(i14) # revealed: Unrelated & B
reveal_type(i15) # revealed: Unrelated & B
reveal_type(i16) # revealed: B & Any
reveal_type(i17) # revealed: B & Any
reveal_type(i18) # revealed: B & Any
reveal_type(i19) # revealed: Any & B
reveal_type(i20) # revealed: Any & B
reveal_type(i21) # revealed: Any & B
```
#### Negative type and negative subtype
For negative contributions, this property is reversed. Here we can remove superfluous _subtypes_:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i01: Intersection[Not[B], Not[A]],
i02: Intersection[Not[A], Not[B]],
i03: Intersection[Not[A], Not[C]],
i04: Intersection[Not[C], Not[A]],
i05: Intersection[Not[B], Not[C]],
i06: Intersection[Not[C], Not[B]],
i07: Intersection[Not[A], Not[B], Not[C]],
i08: Intersection[Not[C], Not[B], Not[A]],
i09: Intersection[Not[B], Not[C], Not[A]],
i10: Intersection[Not[B], Not[A], Unrelated],
i11: Intersection[Not[A], Not[B], Unrelated],
i12: Intersection[Not[A], Unrelated, Not[B]],
i13: Intersection[Not[B], Unrelated, Not[A]],
i14: Intersection[Unrelated, Not[A], Not[B]],
i15: Intersection[Unrelated, Not[B], Not[A]],
i16: Intersection[Not[B], Not[A], Any],
i17: Intersection[Not[A], Not[B], Any],
i18: Intersection[Not[A], Any, Not[B]],
i19: Intersection[Not[B], Any, Not[A]],
i20: Intersection[Any, Not[A], Not[B]],
i21: Intersection[Any, Not[B], Not[A]],
) -> None:
reveal_type(i01) # revealed: ~A
reveal_type(i02) # revealed: ~A
reveal_type(i03) # revealed: ~A
reveal_type(i04) # revealed: ~A
reveal_type(i05) # revealed: ~B
reveal_type(i06) # revealed: ~B
reveal_type(i07) # revealed: ~A
reveal_type(i08) # revealed: ~A
reveal_type(i09) # revealed: ~A
reveal_type(i10) # revealed: Unrelated & ~A
reveal_type(i11) # revealed: Unrelated & ~A
reveal_type(i12) # revealed: Unrelated & ~A
reveal_type(i13) # revealed: Unrelated & ~A
reveal_type(i14) # revealed: Unrelated & ~A
reveal_type(i15) # revealed: Unrelated & ~A
reveal_type(i16) # revealed: Any & ~A
reveal_type(i17) # revealed: Any & ~A
reveal_type(i18) # revealed: Any & ~A
reveal_type(i19) # revealed: Any & ~A
reveal_type(i20) # revealed: Any & ~A
reveal_type(i21) # revealed: Any & ~A
```
#### Negative type and multiple negative subtypes
If there are multiple negative subtypes, all of them can be removed:
```py
from knot_extensions import Intersection, Not
class A: ...
class B1(A): ...
class B2(A): ...
def _(
i1: Intersection[Not[A], Not[B1], Not[B2]],
i2: Intersection[Not[A], Not[B2], Not[B1]],
i3: Intersection[Not[B1], Not[A], Not[B2]],
i4: Intersection[Not[B1], Not[B2], Not[A]],
i5: Intersection[Not[B2], Not[A], Not[B1]],
i6: Intersection[Not[B2], Not[B1], Not[A]],
) -> None:
reveal_type(i1) # revealed: ~A
reveal_type(i2) # revealed: ~A
reveal_type(i3) # revealed: ~A
reveal_type(i4) # revealed: ~A
reveal_type(i5) # revealed: ~A
reveal_type(i6) # revealed: ~A
```
#### Negative type and positive subtype
When `A` is a supertype of `B`, its negation `~A` is disjoint from `B`, so we can simplify the
intersection to `Never`:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i1: Intersection[Not[A], B],
i2: Intersection[B, Not[A]],
i3: Intersection[Not[A], C],
i4: Intersection[C, Not[A]],
i5: Intersection[Unrelated, Not[A], B],
i6: Intersection[B, Not[A], Not[Unrelated]],
i7: Intersection[Any, Not[A], B],
i8: Intersection[B, Not[A], Not[Any]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
reveal_type(i7) # revealed: Never
reveal_type(i8) # revealed: Never
```
### Simplifications of `bool`, `AlwaysTruthy` and `AlwaysFalsy`
In general, intersections with `AlwaysTruthy` and `AlwaysFalsy` cannot be simplified. Naively, you
might think that `int & AlwaysFalsy` could simplify to `Literal[0]`, but this is not the case: for
example, the `False` constant inhabits the type `int & AlwaysFalsy` (due to the fact that
`False.__class__` is `bool` at runtime, and `bool` subclasses `int`), but `False` does not inhabit
the type `Literal[0]`.
Nonetheless, intersections of `AlwaysFalsy` or `AlwaysTruthy` with `bool` _can_ be simplified, due
to the fact that `bool` is a `@final` class at runtime that cannot be subclassed.
```py
from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy
class P: ...
def f(
a: Intersection[bool, AlwaysTruthy],
b: Intersection[bool, AlwaysFalsy],
c: Intersection[bool, Not[AlwaysTruthy]],
d: Intersection[bool, Not[AlwaysFalsy]],
e: Intersection[bool, AlwaysTruthy, P],
f: Intersection[bool, AlwaysFalsy, P],
g: Intersection[bool, Not[AlwaysTruthy], P],
h: Intersection[bool, Not[AlwaysFalsy], P],
):
reveal_type(a) # revealed: Literal[True]
reveal_type(b) # revealed: Literal[False]
reveal_type(c) # revealed: Literal[False]
reveal_type(d) # revealed: Literal[True]
# `bool & AlwaysTruthy & P` -> `Literal[True] & P` -> `Never`
reveal_type(e) # revealed: Never
reveal_type(f) # revealed: Never
reveal_type(g) # revealed: Never
reveal_type(h) # revealed: Never
```
## Simplification of `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy`
Similarly, intersections between `LiteralString`, `AlwaysTruthy` and `AlwaysFalsy` can be
simplified, due to the fact that a `LiteralString` inhabitant is known to have `__class__` set to
exactly `str` (and not a subclass of `str`):
```py
from knot_extensions import Intersection, Not, AlwaysTruthy, AlwaysFalsy
from typing_extensions import LiteralString
def f(
a: Intersection[LiteralString, AlwaysTruthy],
b: Intersection[LiteralString, AlwaysFalsy],
c: Intersection[LiteralString, Not[AlwaysTruthy]],
d: Intersection[LiteralString, Not[AlwaysFalsy]],
e: Intersection[AlwaysFalsy, LiteralString],
f: Intersection[Not[AlwaysTruthy], LiteralString],
):
reveal_type(a) # revealed: LiteralString & ~Literal[""]
reveal_type(b) # revealed: Literal[""]
reveal_type(c) # revealed: Literal[""]
reveal_type(d) # revealed: LiteralString & ~Literal[""]
reveal_type(e) # revealed: Literal[""]
reveal_type(f) # revealed: Literal[""]
```
## Addition of a type to an intersection with many non-disjoint types
This slightly strange-looking test is a regression test for a mistake that was nearly made in a PR:
<https://github.com/astral-sh/ruff/pull/15475#discussion_r1915041987>.
```py
from knot_extensions import AlwaysFalsy, Intersection, Unknown
from typing_extensions import Literal
def _(x: Intersection[str, Unknown, AlwaysFalsy, Literal[""]]):
reveal_type(x) # revealed: Unknown & Literal[""]
```
## Non fully-static types
### Negation of dynamic types
`Any` represents the dynamic type, an unknown set of runtime values. The negation of that, `~Any`,
is still an unknown set of runtime values, so `~Any` is equivalent to `Any`. We therefore eagerly
simplify `~Any` to `Any` in intersections. The same applies to `Unknown`.
```py
from knot_extensions import Intersection, Not, Unknown
from typing_extensions import Any, Never
class P: ...
def any(
i1: Not[Any],
i2: Intersection[P, Not[Any]],
i3: Intersection[Never, Not[Any]],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: P & Any
reveal_type(i3) # revealed: Never
def unknown(
i1: Not[Unknown],
i2: Intersection[P, Not[Unknown]],
i3: Intersection[Never, Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: P & Unknown
reveal_type(i3) # revealed: Never
```
### Collapsing of multiple `Any`/`Unknown` contributions
The intersection of an unknown set of runtime values with (another) unknown set of runtime values is
still an unknown set of runtime values:
```py
from knot_extensions import Intersection, Not, Unknown
from typing_extensions import Any
class P: ...
def any(
i1: Intersection[Any, Any],
i2: Intersection[P, Any, Any],
i3: Intersection[Any, P, Any],
i4: Intersection[Any, Any, P],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: P & Any
reveal_type(i3) # revealed: Any & P
reveal_type(i4) # revealed: Any & P
def unknown(
i1: Intersection[Unknown, Unknown],
i2: Intersection[P, Unknown, Unknown],
i3: Intersection[Unknown, P, Unknown],
i4: Intersection[Unknown, Unknown, P],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: P & Unknown
reveal_type(i3) # revealed: Unknown & P
reveal_type(i4) # revealed: Unknown & P
```
### No self-cancellation
Dynamic types do not cancel each other out. Intersecting an unknown set of values with the negation
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
```py
from knot_extensions import Intersection, Not, Unknown
def any(
i1: Intersection[Any, Not[Any]],
i2: Intersection[Not[Any], Any],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: Any
def unknown(
i1: Intersection[Unknown, Not[Unknown]],
i2: Intersection[Not[Unknown], Unknown],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: Unknown
```
### Mixed dynamic types
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
```py
from knot_extensions import Intersection, Not, Unknown
def mixed(
i1: Intersection[Any, Unknown],
i2: Intersection[Any, Not[Unknown]],
i3: Intersection[Not[Any], Unknown],
i4: Intersection[Not[Any], Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Any & Unknown
reveal_type(i2) # revealed: Any & Unknown
reveal_type(i3) # revealed: Any & Unknown
reveal_type(i4) # revealed: Any & Unknown
```
[complement laws]: https://en.wikipedia.org/wiki/Complement_(set_theory)
[de morgan's laws]: https://en.wikipedia.org/wiki/De_Morgan%27s_laws

View File

@@ -0,0 +1,52 @@
# Known constants
## `typing.TYPE_CHECKING`
This constant is `True` when in type-checking mode, `False` otherwise. The symbol is defined to be
`False` at runtime. In typeshed, it is annotated as `bool`. This test makes sure that we infer
`Literal[True]` for it anyways.
### Basic
```py
from typing import TYPE_CHECKING
import typing
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
reveal_type(typing.TYPE_CHECKING) # revealed: Literal[True]
```
### Aliased
Make sure that we still infer the correct type if the constant has been given a different name:
```py
from typing import TYPE_CHECKING as TC
reveal_type(TC) # revealed: Literal[True]
```
### Must originate from `typing`
Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants
with the same name:
```py path=constants.py
TYPE_CHECKING: bool = False
```
```py
from constants import TYPE_CHECKING
reveal_type(TYPE_CHECKING) # revealed: bool
```
### `typing_extensions` re-export
This should behave in the same way as `typing.TYPE_CHECKING`:
```py
from typing_extensions import TYPE_CHECKING
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
```

View File

@@ -1,7 +1,23 @@
# Ellipsis literals
## Simple
## Python 3.9
```toml
[environment]
python-version = "3.9"
```
```py
reveal_type(...) # revealed: EllipsisType | ellipsis
reveal_type(...) # revealed: ellipsis
```
## Python 3.10
```toml
[environment]
python-version = "3.10"
```
```py
reveal_type(...) # revealed: EllipsisType
```

View File

@@ -1,93 +0,0 @@
# Literal
<https://typing.readthedocs.io/en/latest/spec/literal.html#literals>
## Parameterization
```py
from typing import Literal
from enum import Enum
mode: Literal["w", "r"]
mode2: Literal["w"] | Literal["r"]
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
a1: Literal[26]
a2: Literal[0x1A]
a3: Literal[-4]
a4: Literal["hello world"]
a5: Literal[b"hello world"]
a6: Literal[True]
a7: Literal[None]
a8: Literal[Literal[1]]
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
class Color(Enum):
RED = 0
GREEN = 1
BLUE = 2
b1: Literal[Color.RED]
def f():
reveal_type(mode) # revealed: Literal["w", "r"]
reveal_type(mode2) # revealed: Literal["w", "r"]
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
reveal_type(a1) # revealed: Literal[26]
reveal_type(a2) # revealed: Literal[26]
reveal_type(a3) # revealed: Literal[-4]
reveal_type(a4) # revealed: Literal["hello world"]
reveal_type(a5) # revealed: Literal[b"hello world"]
reveal_type(a6) # revealed: Literal[True]
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: Literal[0]
# error: [invalid-literal-parameter]
invalid1: Literal[3 + 4]
# error: [invalid-literal-parameter]
invalid2: Literal[4 + 3j]
# error: [invalid-literal-parameter]
invalid3: Literal[(3, 4)]
hello = "hello"
invalid4: Literal[
1 + 2, # error: [invalid-literal-parameter]
"foo",
hello, # error: [invalid-literal-parameter]
(1, 2, 3), # error: [invalid-literal-parameter]
]
```
## Detecting Literal outside typing and typing_extensions
Only Literal that is defined in typing and typing_extension modules is detected as the special
Literal.
```pyi path=other.pyi
from typing import _SpecialForm
Literal: _SpecialForm
```
```py
from other import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: @Todo(generics)
```
## Detecting typing_extensions.Literal
```py
from typing_extensions import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: Literal[26]
```

View File

@@ -98,7 +98,7 @@ reveal_type(x)
for x in (1, "a", b"foo"):
pass
# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
# revealed: Literal[1, "a", b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
```
@@ -106,23 +106,19 @@ reveal_type(x)
## With non-callable iterator
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class NotIterable:
if flag:
__iter__ = 1
else:
__iter__ = None
flag = bool_instance()
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
class NotIterable:
if flag:
__iter__ = 1
else:
__iter__ = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
pass
# revealed: Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
# revealed: Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## Invalid iterable
@@ -160,13 +156,9 @@ class Test2:
def __iter__(self) -> TestIter:
return TestIter()
def bool_instance() -> bool:
return True
flag = bool_instance()
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
```
## Union type as iterator
@@ -215,13 +207,9 @@ class Test2:
def __iter__(self) -> TestIter3 | TestIter4:
return TestIter3()
def bool_instance() -> bool:
return True
flag = bool_instance()
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
```
## Union type as iterable where one union element has no `__iter__` method
@@ -235,12 +223,10 @@ class Test:
def __iter__(self) -> TestIter:
return TestIter()
def coinflip() -> bool:
return True
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
for x in Test() if coinflip() else 42:
reveal_type(x) # revealed: int
def _(flag: bool):
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
for x in Test() if flag else 42:
reveal_type(x) # revealed: int
```
## Union type as iterable where one union element has invalid `__iter__` method
@@ -258,12 +244,10 @@ class Test2:
def __iter__(self) -> int:
return 42
def coinflip() -> bool:
return True
# error: "Object of type `Test | Test2` is not iterable"
for x in Test() if coinflip() else Test2():
reveal_type(x) # revealed: Unknown
def _(flag: bool):
# error: "Object of type `Test | Test2` is not iterable"
for x in Test() if flag else Test2():
reveal_type(x) # revealed: Unknown
```
## Union type as iterator where one union element has no `__next__` method

View File

@@ -1,59 +1,50 @@
# While loops
## Basic While Loop
## Basic `while` loop
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1
while flag:
x = 2
flag = bool_instance()
x = 1
while flag:
x = 2
reveal_type(x) # revealed: Literal[1, 2]
```
## While with else (no break)
```py
def bool_instance() -> bool:
return True
flag = bool_instance()
x = 1
while flag:
x = 2
else:
reveal_type(x) # revealed: Literal[1, 2]
x = 3
reveal_type(x) # revealed: Literal[3]
```
## While with Else (may break)
## `while` with `else` (no `break`)
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1
while flag:
x = 2
else:
reveal_type(x) # revealed: Literal[1, 2]
x = 3
flag, flag2 = bool_instance(), bool_instance()
x = 1
y = 0
while flag:
x = 2
if flag2:
y = 4
break
else:
y = x
x = 3
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[1, 2, 4]
reveal_type(x) # revealed: Literal[3]
```
## Nested while loops
## `while` with `else` (may `break`)
```py
def _(flag: bool, flag2: bool):
x = 1
y = 0
while flag:
x = 2
if flag2:
y = 4
break
else:
y = x
x = 3
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[4, 1, 2]
```
## Nested `while` loops
```py
def flag() -> bool:
@@ -78,3 +69,50 @@ else:
reveal_type(x) # revealed: Literal[3, 4, 5]
```
## Boundness
Make sure that the boundness information is correctly tracked in `while` loop control flow.
### Basic `while` loop
```py
def _(flag: bool):
while flag:
x = 1
# error: [possibly-unresolved-reference]
x
```
### `while` with `else` (no `break`)
```py
def _(flag: bool):
while flag:
y = 1
else:
x = 1
# no error, `x` is always bound
x
# error: [possibly-unresolved-reference]
y
```
### `while` with `else` (may `break`)
```py
def _(flag: bool, flag2: bool):
while flag:
x = 1
if flag2:
break
else:
y = 1
# error: [possibly-unresolved-reference]
x
# error: [possibly-unresolved-reference]
y
```

View File

@@ -5,7 +5,7 @@ The following configuration will be attached to the *root* section (without any
```toml
[environment]
target-version = "3.10"
python-version = "3.10"
```
# Basic
@@ -34,7 +34,7 @@ Here, we make sure that we can overwrite the global configuration in a child sec
```toml
[environment]
target-version = "3.11"
python-version = "3.11"
```
```py
@@ -55,7 +55,7 @@ Children in this section should all use the section configuration:
```toml
[environment]
target-version = "3.12"
python-version = "3.12"
```
## Child

View File

@@ -68,7 +68,7 @@ class B(metaclass=M2): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
class C(A, B): ...
reveal_type(C.__class__) # revealed: Unknown
reveal_type(C.__class__) # revealed: type[Unknown]
```
## Conflict (2)
@@ -85,7 +85,7 @@ class A(metaclass=M1): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship"
class B(A, metaclass=M2): ...
reveal_type(B.__class__) # revealed: Unknown
reveal_type(B.__class__) # revealed: type[Unknown]
```
## Common metaclass
@@ -129,7 +129,7 @@ class C(metaclass=M12): ...
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
class D(A, B, C): ...
reveal_type(D.__class__) # revealed: Unknown
reveal_type(D.__class__) # revealed: type[Unknown]
```
## Unknown
@@ -170,8 +170,35 @@ def f(*args, **kwargs) -> int: ...
class A(metaclass=f): ...
# TODO should be `type[int]`
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
# TODO: Should be `int`
reveal_type(A) # revealed: Literal[A]
reveal_type(A.__class__) # revealed: type[int]
def _(n: int):
# error: [invalid-metaclass]
class B(metaclass=n): ...
# TODO: Should be `Unknown`
reveal_type(B) # revealed: Literal[B]
reveal_type(B.__class__) # revealed: type[Unknown]
def _(flag: bool):
m = f if flag else 42
# error: [invalid-metaclass]
class C(metaclass=m): ...
# TODO: Should be `int | Unknown`
reveal_type(C) # revealed: Literal[C]
reveal_type(C.__class__) # revealed: type[Unknown]
class SignatureMismatch: ...
# TODO: Emit a diagnostic
class D(metaclass=SignatureMismatch): ...
# TODO: Should be `Unknown`
reveal_type(D) # revealed: Literal[D]
# TODO: Should be `type[Unknown]`
reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
```
## Cyclic
@@ -179,11 +206,11 @@ reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
```py path=a.pyi
class A(B): ... # error: [cyclic-class-def]
class B(C): ... # error: [cyclic-class-def]
class C(A): ... # error: [cyclic-class-def]
class A(B): ... # error: [cyclic-class-definition]
class B(C): ... # error: [cyclic-class-definition]
class C(A): ... # error: [cyclic-class-definition]
reveal_type(A.__class__) # revealed: Unknown
reveal_type(A.__class__) # revealed: type[Unknown]
```
## PEP 695 generic
@@ -194,3 +221,26 @@ class A[T: str](metaclass=M): ...
reveal_type(A.__class__) # revealed: Literal[M]
```
## Metaclasses of metaclasses
```py
class Foo(type): ...
class Bar(type, metaclass=Foo): ...
class Baz(type, metaclass=Bar): ...
class Spam(metaclass=Baz): ...
reveal_type(Spam.__class__) # revealed: Literal[Baz]
reveal_type(Spam.__class__.__class__) # revealed: Literal[Bar]
reveal_type(Spam.__class__.__class__.__class__) # revealed: Literal[Foo]
def test(x: Spam):
reveal_type(x.__class__) # revealed: type[Spam]
reveal_type(x.__class__.__class__) # revealed: type[Baz]
reveal_type(x.__class__.__class__.__class__) # revealed: type[Bar]
reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Foo]
reveal_type(x.__class__.__class__.__class__.__class__.__class__) # revealed: type[type]
# revealed: type[type]
reveal_type(x.__class__.__class__.__class__.__class__.__class__.__class__.__class__.__class__)
```

View File

@@ -348,14 +348,14 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
These are invalid, but we need to be able to handle them gracefully without panicking.
```py path=a.pyi
class Foo(Foo): ... # error: [cyclic-class-def]
class Foo(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo) # revealed: Literal[Foo]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
class Bar: ...
class Baz: ...
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-definition]
reveal_type(Boz) # revealed: Literal[Boz]
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
@@ -366,9 +366,9 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
These are similarly unlikely, but we still shouldn't crash:
```py path=a.pyi
class Foo(Bar): ... # error: [cyclic-class-def]
class Bar(Baz): ... # error: [cyclic-class-def]
class Baz(Foo): ... # error: [cyclic-class-def]
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
@@ -379,9 +379,9 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
```py path=a.pyi
class Spam: ...
class Foo(Bar): ... # error: [cyclic-class-def]
class Bar(Baz): ... # error: [cyclic-class-def]
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo, Spam): ... # error: [cyclic-class-definition]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
@@ -391,16 +391,16 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
## Classes with cycles in their MRO, and a sub-graph
```py path=a.pyi
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
class Bar(Foo): ...
# TODO: can we avoid emitting the errors for these?
# The classes have cyclic superclasses,
# but are not themselves cyclic...
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
class Spam(Baz): ... # error: [cyclic-class-def]
class Baz(Bar, BarCycle): ... # error: [cyclic-class-definition]
class Spam(Baz): ... # error: [cyclic-class-definition]
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]

View File

@@ -1,32 +1,32 @@
## Narrowing for `bool(..)` checks
```py
def flag() -> bool: ...
def _(flag: bool):
x = 1 if flag() else None
x = 1 if flag else None
# valid invocation, positive
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None):
reveal_type(x) # revealed: Literal[1]
# valid invocation, negative
reveal_type(x) # revealed: Literal[1] | None
if not bool(x is not None):
reveal_type(x) # revealed: None
# no args/narrowing
reveal_type(x) # revealed: Literal[1] | None
if not bool():
# valid invocation, positive
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None):
reveal_type(x) # revealed: Literal[1]
# invalid invocation, too many positional args
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, 5): # TODO diagnostic
# valid invocation, negative
reveal_type(x) # revealed: Literal[1] | None
if not bool(x is not None):
reveal_type(x) # revealed: None
# invalid invocation, too many kwargs
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, y=5): # TODO diagnostic
# no args/narrowing
reveal_type(x) # revealed: Literal[1] | None
if not bool():
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many positional args
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, 5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many kwargs
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, y=5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
```

View File

@@ -9,85 +9,67 @@ Similarly, in `and` expressions, the right-hand side is evaluated only if the le
## Narrowing in `or`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x: A | None = A() if flag else None
class A: ...
x: A | None = A() if bool_instance() else None
isinstance(x, A) or reveal_type(x) # revealed: None
x is None or reveal_type(x) # revealed: A
reveal_type(x) # revealed: A | None
isinstance(x, A) or reveal_type(x) # revealed: None
x is None or reveal_type(x) # revealed: A
reveal_type(x) # revealed: A | None
```
## Narrowing in `and`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x: A | None = A() if flag else None
class A: ...
x: A | None = A() if bool_instance() else None
isinstance(x, A) and reveal_type(x) # revealed: A
x is None and reveal_type(x) # revealed: None
reveal_type(x) # revealed: A | None
isinstance(x, A) and reveal_type(x) # revealed: A
x is None and reveal_type(x) # revealed: None
reveal_type(x) # revealed: A | None
```
## Multiple `and` arms
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
class A: ...
x: A | None = A() if flag1 else None
class A: ...
x: A | None = A() if bool_instance() else None
bool_instance() and isinstance(x, A) and reveal_type(x) # revealed: A
isinstance(x, A) and bool_instance() and reveal_type(x) # revealed: A
reveal_type(x) and isinstance(x, A) and bool_instance() # revealed: A | None
flag2 and isinstance(x, A) and reveal_type(x) # revealed: A
isinstance(x, A) and flag2 and reveal_type(x) # revealed: A
reveal_type(x) and isinstance(x, A) and flag3 # revealed: A | None
```
## Multiple `or` arms
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
class A: ...
x: A | None = A() if flag1 else None
class A: ...
x: A | None = A() if bool_instance() else None
bool_instance() or isinstance(x, A) or reveal_type(x) # revealed: None
isinstance(x, A) or bool_instance() or reveal_type(x) # revealed: None
reveal_type(x) or isinstance(x, A) or bool_instance() # revealed: A | None
flag2 or isinstance(x, A) or reveal_type(x) # revealed: None
isinstance(x, A) or flag3 or reveal_type(x) # revealed: None
reveal_type(x) or isinstance(x, A) or flag4 # revealed: A | None
```
## Multiple predicates
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class A: ...
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
class A: ...
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
```
## Mix of `and` and `or`
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
class A: ...
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
class A: ...
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
```

View File

@@ -6,15 +6,11 @@
class A: ...
class B: ...
def instance() -> A | B:
return A()
x = instance()
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: B & ~A | A & ~B
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
```
## Arms might not add narrowing constraints
@@ -23,25 +19,18 @@ else:
class A: ...
class B: ...
def bool_instance() -> bool:
return True
def _(flag: bool, x: A | B):
if isinstance(x, A) and flag:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
def instance() -> A | B:
return A()
if flag and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
x = instance()
if isinstance(x, A) and bool_instance():
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if bool_instance() and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
reveal_type(x) # revealed: A | B
```
## Statically known arms
@@ -50,39 +39,35 @@ reveal_type(x) # revealed: A | B
class A: ...
class B: ...
def instance() -> A | B:
return A()
def _(x: A | B):
if isinstance(x, A) and True:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
x = instance()
if True and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if isinstance(x, A) and True:
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if False and isinstance(x, A):
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if True and isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if False or isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if True or isinstance(x, A):
reveal_type(x) # revealed: A | B
else:
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: B & ~A
if False and isinstance(x, A):
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: A | B
if False or isinstance(x, A):
reveal_type(x) # revealed: A
else:
reveal_type(x) # revealed: B & ~A
if True or isinstance(x, A):
reveal_type(x) # revealed: A | B
else:
# TODO: should emit an `unreachable code` diagnostic
reveal_type(x) # revealed: B & ~A
reveal_type(x) # revealed: A | B
```
## The type of multiple symbols can be narrowed down
@@ -91,22 +76,17 @@ reveal_type(x) # revealed: A | B
class A: ...
class B: ...
def instance() -> A | B:
return A()
def _(x: A | B, y: A | B):
if isinstance(x, A) and isinstance(y, B):
reveal_type(x) # revealed: A
reveal_type(y) # revealed: B
else:
# No narrowing: Only-one or both checks might have failed
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
x = instance()
y = instance()
if isinstance(x, A) and isinstance(y, B):
reveal_type(x) # revealed: A
reveal_type(y) # revealed: B
else:
# No narrowing: Only-one or both checks might have failed
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
```
## Narrowing in `or` conditional
@@ -116,15 +96,11 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, A) or isinstance(x, B):
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: C & ~A & ~B
def _(x: A | B | C):
if isinstance(x, A) or isinstance(x, B):
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: C & ~A & ~B
```
## In `or`, all arms should add constraint in order to narrow
@@ -134,18 +110,11 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
def bool_instance() -> bool:
return True
x = instance()
if isinstance(x, A) or isinstance(x, B) or bool_instance():
reveal_type(x) # revealed: A | B | C
else:
reveal_type(x) # revealed: C & ~A & ~B
def _(flag: bool, x: A | B | C):
if isinstance(x, A) or isinstance(x, B) or flag:
reveal_type(x) # revealed: A | B | C
else:
reveal_type(x) # revealed: C & ~A & ~B
```
## in `or`, all arms should narrow the same set of symbols
@@ -155,28 +124,23 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
def _(x: A | B | C, y: A | B | C):
if isinstance(x, A) or isinstance(y, A):
# The predicate might be satisfied by the right side, so the type of `x` cant be narrowed down here.
reveal_type(x) # revealed: 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
x = instance()
y = instance()
if isinstance(x, A) or isinstance(y, A):
# The predicate might be satisfied by the right side, so the type of `x` cant be narrowed down here.
reveal_type(x) # revealed: 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
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.
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
else:
reveal_type(x) # revealed: A | B | C
reveal_type(y) # revealed: A | B | C
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.
reveal_type(x) # revealed: A | B
reveal_type(y) # revealed: A | B
else:
reveal_type(x) # revealed: A | B | C
reveal_type(y) # revealed: A | B | C
```
## mixing `and` and `not`
@@ -186,16 +150,12 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, B) and not isinstance(x, 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
def _(x: A | B | C):
if isinstance(x, B) and not isinstance(x, 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
```
## mixing `or` and `not`
@@ -205,15 +165,11 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | A & ~C
else:
reveal_type(x) # revealed: C & ~B
def _(x: A | B | C):
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | A & ~C
else:
reveal_type(x) # revealed: C & ~B
```
## `or` with nested `and`
@@ -223,16 +179,12 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, 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
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
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
```
## `and` with nested `or`
@@ -242,41 +194,32 @@ class A: ...
class B: ...
class C: ...
def instance() -> A | B | C:
return A()
x = instance()
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
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
# (~A | ~B) & (~A | C) ->
# [(~A | ~B) & ~A] | [(~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
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
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
# (~A | ~B) & (~A | C) ->
# [(~A | ~B) & ~A] | [(~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
```
## Boolean expression internal narrowing
```py
def optional_string() -> str | None:
return None
def _(x: str | None, y: str | None):
if x is None and y is not x:
reveal_type(y) # revealed: str
x = optional_string()
y = optional_string()
# Neither of the conditions alone is sufficient for narrowing y's type:
if x is None:
reveal_type(y) # revealed: str | None
if x is None and y is not x:
reveal_type(y) # revealed: str
# Neither of the conditions alone is sufficient for narrowing y's type:
if x is None:
reveal_type(y) # revealed: str | None
if y is not x:
reveal_type(y) # revealed: str | None
if y is not x:
reveal_type(y) # revealed: str | None
```

View File

@@ -3,55 +3,47 @@
## Positive contributions become negative in elif-else blocks
```py
def int_instance() -> int:
return 42
x = int_instance()
if x == 1:
# cannot narrow; could be a subclass of `int`
reveal_type(x) # revealed: int
elif x == 2:
reveal_type(x) # revealed: int & ~Literal[1]
elif x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
def _(x: int):
if x == 1:
# cannot narrow; could be a subclass of `int`
reveal_type(x) # revealed: int
elif x == 2:
reveal_type(x) # revealed: int & ~Literal[1]
elif x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Positive contributions become negative in elif-else blocks, with simplification
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
x = 1 if bool_instance() else 2 if bool_instance() else 3
if x == 1:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
elif x == 2:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[3]
if x == 1:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 2, 3]
elif x == 2:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[3]
```
## Multiple negative contributions using elif, with simplification
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
x = 1 if bool_instance() else 2 if bool_instance() else 3
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]
elif x == 3:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
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]
elif x == 3:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2]
```

View File

@@ -3,77 +3,64 @@
## `is None`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
flag = bool_instance()
x = None if flag else 1
if x is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
if x is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```
## `is` for other types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x = A()
y = x if flag else None
flag = bool_instance()
if y is x:
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: A | None
class A: ...
x = A()
y = x if flag else None
if y is x:
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: A | None
reveal_type(y) # revealed: A | None
```
## `is` in chained comparisons
```py
def bool_instance() -> bool:
return True
def _(x_flag: bool, y_flag: bool):
x = True if x_flag else False
y = True if y_flag else False
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is x is False: # Interpreted as `(y is x) and (x is False)`
reveal_type(x) # revealed: Literal[False]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is not x) or (x is not False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is x is False: # Interpreted as `(y is x) and (x is False)`
reveal_type(x) # revealed: Literal[False]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is not x) or (x is not False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
```
## `is` in elif clause
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = None if flag1 else (1 if flag2 else True)
x = None if bool_instance() else (1 if bool_instance() else True)
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: None | Literal[1, True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -5,34 +5,28 @@
The type guard removes `None` from the union type:
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
flag = bool_instance()
x = None if flag else 1
if x is not None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
if x is not None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```
## `is not` for other singleton types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = True if flag else False
reveal_type(x) # revealed: bool
flag = bool_instance()
x = True if flag else False
reveal_type(x) # revealed: bool
if x is not False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
if x is not False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
```
## `is not` for non-singleton types
@@ -53,20 +47,17 @@ else:
## `is not` for other types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
x = A()
y = x if flag else None
class A: ...
if y is not x:
reveal_type(y) # revealed: A | None
else:
reveal_type(y) # revealed: A
x = A()
y = x if bool_instance() else None
if y is not x:
reveal_type(y) # revealed: A | None
else:
reveal_type(y) # revealed: A
reveal_type(y) # revealed: A | None
```
## `is not` in chained comparisons
@@ -74,23 +65,20 @@ reveal_type(y) # revealed: A | None
The type guard removes `False` from the union type of the tested value only.
```py
def bool_instance() -> bool:
return True
x_flag, y_flag = bool_instance(), bool_instance()
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
reveal_type(x) # revealed: Literal[True]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is x) or (x is False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
def _(x_flag: bool, y_flag: bool):
x = True if x_flag else False
y = True if y_flag else False
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
reveal_type(x) # revealed: Literal[True]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is x) or (x is False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
```

View File

@@ -3,54 +3,45 @@
## Multiple negative contributions
```py
def int_instance() -> int:
return 42
x = int_instance()
if x != 1:
if x != 2:
if x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
def _(x: int):
if x != 1:
if x != 2:
if x != 3:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
```
## Multiple negative contributions with simplification
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
flag1, flag2 = bool_instance(), bool_instance()
x = 1 if flag1 else 2 if flag2 else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x != 2:
reveal_type(x) # revealed: Literal[3]
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x != 2:
reveal_type(x) # revealed: Literal[3]
```
## elif-else blocks
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2 if flag2 else 3
x = 1 if bool_instance() else 2 if bool_instance() else 3
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
if x == 2:
# TODO should be `Literal[2]`
if x != 1:
reveal_type(x) # revealed: Literal[2, 3]
elif x == 3:
reveal_type(x) # revealed: Literal[3]
else:
reveal_type(x) # revealed: Never
if x == 2:
# TODO should be `Literal[2]`
reveal_type(x) # revealed: Literal[2, 3]
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]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
elif x != 2:
# TODO should be Literal[1]
reveal_type(x) # revealed: Literal[1, 3]
else:
# TODO should be Never
reveal_type(x) # revealed: Literal[1, 2, 3]
```

View File

@@ -5,29 +5,25 @@ The `not` operator negates a constraint.
## `not is None`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
x = None if bool_instance() else 1
if not x is None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
if not x is None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
reveal_type(x) # revealed: None | Literal[1]
reveal_type(x) # revealed: None | Literal[1]
```
## `not isinstance`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
x = 1 if bool_instance() else "a"
if not isinstance(x, (int)):
reveal_type(x) # revealed: Literal["a"]
else:
reveal_type(x) # revealed: Literal[1]
if not isinstance(x, (int)):
reveal_type(x) # revealed: Literal["a"]
else:
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -3,82 +3,66 @@
## `x != None`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = None if flag else 1
flag = bool_instance()
x = None if flag else 1
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be None
reveal_type(x) # revealed: None | Literal[1]
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
# TODO should be None
reveal_type(x) # revealed: None | Literal[1]
```
## `!=` for other singleton types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = True if flag else False
flag = bool_instance()
x = True if flag else False
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
# TODO should be Literal[False]
reveal_type(x) # revealed: bool
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
# TODO should be Literal[False]
reveal_type(x) # revealed: bool
```
## `x != y` where `y` is of literal type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else 2
flag = bool_instance()
x = 1 if flag else 2
if x != 1:
reveal_type(x) # revealed: Literal[2]
if x != 1:
reveal_type(x) # revealed: Literal[2]
```
## `x != y` where `y` is a single-valued type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
class A: ...
class B: ...
C = A if flag else B
flag = bool_instance()
class A: ...
class B: ...
C = A if flag else B
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
# TODO should be Literal[A]
reveal_type(C) # revealed: Literal[A, B]
if C != A:
reveal_type(C) # revealed: Literal[B]
else:
# TODO should be Literal[A]
reveal_type(C) # revealed: Literal[A, B]
```
## `x != y` where `y` has multiple single-valued options
```py
def bool_instance() -> bool:
return True
def _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2
y = 2 if flag2 else 3
x = 1 if bool_instance() else 2
y = 2 if bool_instance() else 3
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[1, 2]
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
# TODO should be Literal[2]
reveal_type(x) # revealed: Literal[1, 2]
```
## `!=` for non-single-valued types
@@ -86,34 +70,22 @@ else:
Only single-valued types should narrow the type:
```py
def bool_instance() -> bool:
return True
def _(flag: bool, a: int, y: int):
x = a if flag else None
def int_instance() -> int:
return 42
flag = bool_instance()
x = int_instance() if flag else None
y = int_instance()
if x != y:
reveal_type(x) # revealed: int | None
if x != y:
reveal_type(x) # revealed: int | None
```
## Mix of single-valued and non-single-valued types
```py
def int_instance() -> int:
return 42
def _(flag1: bool, flag2: bool, a: int):
x = 1 if flag1 else 2
y = 2 if flag2 else a
def bool_instance() -> bool:
return True
x = 1 if bool_instance() else 2
y = 2 if bool_instance() else int_instance()
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
```

View File

@@ -5,23 +5,19 @@ Narrowing for `isinstance(object, classinfo)` expressions.
## `classinfo` is a single type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
if isinstance(x, str):
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, int):
reveal_type(x) # revealed: Never
reveal_type(x) # revealed: Literal[1]
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
if isinstance(x, str):
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, int):
reveal_type(x) # revealed: Never
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1, "a"]
```
## `classinfo` is a tuple of types
@@ -30,56 +26,48 @@ Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tup
The former is equivalent to `isinstance(x, int | str)`:
```py
def bool_instance() -> bool:
return True
def _(flag: bool, flag1: bool, flag2: bool):
x = 1 if flag else "a"
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1, "a"]
else:
reveal_type(x) # revealed: Never
x = 1 if flag else "a"
if isinstance(x, (int, bytes)):
reveal_type(x) # revealed: Literal[1]
if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
if isinstance(x, (bytes, str)):
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, (int, bytes)):
reveal_type(x) # revealed: Literal[1]
# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1, "a"]
else:
reveal_type(x) # revealed: Never
if isinstance(x, (bytes, str)):
reveal_type(x) # revealed: Literal["a"]
y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1, "a"]
# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1, b"b"]
y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1] | Literal["a"]
if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a", b"b"]
```
## `classinfo` is a nested tuple of types
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
if isinstance(x, (bool, (bytes, int))):
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal["a"]
if isinstance(x, (bool, (bytes, int))):
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal["a"]
```
## Class types
@@ -89,9 +77,7 @@ class A: ...
class B: ...
class C: ...
def get_object() -> object: ...
x = get_object()
x = object()
if isinstance(x, A):
reveal_type(x) # revealed: A
@@ -112,50 +98,40 @@ else:
## No narrowing for instances of `builtins.type`
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
t = type("t", (), {})
flag = bool_instance()
# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t) # revealed: type
t = type("t", (), {})
x = 1 if flag else "foo"
# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t) # revealed: type
x = 1 if flag else "foo"
if isinstance(x, t):
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
if isinstance(x, t):
reveal_type(x) # revealed: Literal[1, "foo"]
```
## Do not use custom `isinstance` for narrowing
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
def isinstance(x, t):
return True
x = 1 if flag else "a"
flag = bool_instance()
def isinstance(x, t):
return True
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1, "a"]
```
## Do support narrowing if `isinstance` is aliased
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
isinstance_alias = isinstance
flag = bool_instance()
x = 1 if flag else "a"
isinstance_alias = isinstance
x = 1 if flag else "a"
if isinstance_alias(x, int):
reveal_type(x) # revealed: Literal[1]
if isinstance_alias(x, int):
reveal_type(x) # revealed: Literal[1]
```
## Do support narrowing if `isinstance` is imported
@@ -163,46 +139,85 @@ if isinstance_alias(x, int):
```py
from builtins import isinstance as imported_isinstance
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
if imported_isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
if imported_isinstance(x, int):
reveal_type(x) # revealed: Literal[1]
```
## Do not narrow if second argument is not a type
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1, "a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1, "a"]
```
## Do not narrow if there are keyword arguments
```py
def bool_instance() -> bool:
return True
def _(flag: bool):
x = 1 if flag else "a"
flag = bool_instance()
x = 1 if flag else "a"
# TODO: this should cause us to emit a diagnostic
# (`isinstance` has no `foo` parameter)
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# error: [unknown-argument]
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1, "a"]
```
## `type[]` types are narrowed as well as class-literal types
```py
def _(x: object, y: type[int]):
if isinstance(x, y):
reveal_type(x) # revealed: int
```
## Adding a disjoint element to an existing intersection
We used to incorrectly infer `Literal` booleans for some of these.
```py
from knot_extensions import Not, Intersection, AlwaysTruthy, AlwaysFalsy
class P: ...
def f(
a: Intersection[P, AlwaysTruthy],
b: Intersection[P, AlwaysFalsy],
c: Intersection[P, Not[AlwaysTruthy]],
d: Intersection[P, Not[AlwaysFalsy]],
):
if isinstance(a, bool):
reveal_type(a) # revealed: Never
else:
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(a) # revealed: P & AlwaysTruthy & ~bool
if isinstance(b, bool):
reveal_type(b) # revealed: Never
else:
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(b) # revealed: P & AlwaysFalsy & ~bool
if isinstance(c, bool):
reveal_type(c) # revealed: Never
else:
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(c) # revealed: P & ~AlwaysTruthy & ~bool
if isinstance(d, bool):
reveal_type(d) # revealed: Never
else:
# TODO: `bool` is final, so `& ~bool` is redundant here
reveal_type(d) # revealed: P & ~AlwaysFalsy & ~bool
```

View File

@@ -7,45 +7,43 @@ Narrowing for `issubclass(class, classinfo)` expressions.
### Basic example
```py
def flag() -> bool: ...
def _(flag: bool):
t = int if flag else str
t = int if flag() else str
if issubclass(t, bytes):
reveal_type(t) # revealed: Never
if issubclass(t, object):
reveal_type(t) # revealed: Literal[int, str]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str]
if issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
if issubclass(t, int):
if issubclass(t, bytes):
reveal_type(t) # revealed: Never
if issubclass(t, object):
reveal_type(t) # revealed: Literal[int, str]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str]
if issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
if issubclass(t, int):
reveal_type(t) # revealed: Never
```
### Proper narrowing in `elif` and `else` branches
```py
def flag() -> bool: ...
def _(flag1: bool, flag2: bool):
t = int if flag1 else str if flag2 else bytes
t = int if flag() else str if flag() else bytes
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str, bytes]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
else:
reveal_type(t) # revealed: Literal[str, bytes]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
elif issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
else:
reveal_type(t) # revealed: Literal[bytes]
if issubclass(t, int):
reveal_type(t) # revealed: Literal[int]
elif issubclass(t, str):
reveal_type(t) # revealed: Literal[str]
else:
reveal_type(t) # revealed: Literal[bytes]
```
### Multiple derived classes
@@ -56,29 +54,28 @@ class Derived1(Base): ...
class Derived2(Base): ...
class Unrelated: ...
def flag() -> bool: ...
def _(flag1: bool, flag2: bool, flag3: bool):
t1 = Derived1 if flag1 else Derived2
t1 = Derived1 if flag() else Derived2
if issubclass(t1, Base):
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
if issubclass(t1, Base):
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
if issubclass(t1, Derived1):
reveal_type(t1) # revealed: Literal[Derived1]
else:
reveal_type(t1) # revealed: Literal[Derived2]
if issubclass(t1, Derived1):
reveal_type(t1) # revealed: Literal[Derived1]
else:
reveal_type(t1) # revealed: Literal[Derived2]
t2 = Derived1 if flag2 else Base
t2 = Derived1 if flag() else Base
if issubclass(t2, Base):
reveal_type(t2) # revealed: Literal[Derived1, Base]
if issubclass(t2, Base):
reveal_type(t2) # revealed: Literal[Derived1, Base]
t3 = Derived1 if flag3 else Unrelated
t3 = Derived1 if flag() else Unrelated
if issubclass(t3, Base):
reveal_type(t3) # revealed: Literal[Derived1]
else:
reveal_type(t3) # revealed: Literal[Unrelated]
if issubclass(t3, Base):
reveal_type(t3) # revealed: Literal[Derived1]
else:
reveal_type(t3) # revealed: Literal[Unrelated]
```
### Narrowing for non-literals
@@ -87,36 +84,36 @@ else:
class A: ...
class B: ...
def get_class() -> type[object]: ...
t = get_class()
if issubclass(t, A):
reveal_type(t) # revealed: type[A]
if issubclass(t, B):
reveal_type(t) # revealed: type[A] & type[B]
else:
reveal_type(t) # revealed: type[object] & ~type[A]
def _(t: type[object]):
if issubclass(t, A):
reveal_type(t) # revealed: type[A]
if issubclass(t, B):
reveal_type(t) # revealed: type[A] & type[B]
else:
reveal_type(t) # revealed: type & ~type[A]
```
### Handling of `None`
`types.NoneType` is only available in Python 3.10 and later:
```toml
[environment]
python-version = "3.10"
```
```py
# TODO: this error should ideally go away once we (1) understand `sys.version_info` branches,
# and (2) set the target Python version for this test to 3.10.
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
from types import NoneType
def flag() -> bool: ...
def _(flag: bool):
t = int if flag else NoneType
t = int if flag() else NoneType
if issubclass(t, NoneType):
reveal_type(t) # revealed: Literal[NoneType]
if issubclass(t, NoneType):
reveal_type(t) # revealed: Literal[NoneType]
if issubclass(t, type(None)):
# TODO: this should be just `Literal[NoneType]`
reveal_type(t) # revealed: Literal[int, NoneType]
if issubclass(t, type(None)):
# TODO: this should be just `Literal[NoneType]`
reveal_type(t) # revealed: Literal[int, NoneType]
```
## `classinfo` contains multiple types
@@ -126,14 +123,13 @@ if issubclass(t, type(None)):
```py
class Unrelated: ...
def flag() -> bool: ...
def _(flag1: bool, flag2: bool):
t = int if flag1 else str if flag2 else bytes
t = int if flag() else str if flag() else bytes
if issubclass(t, (int, (Unrelated, (bytes,)))):
reveal_type(t) # revealed: Literal[int, bytes]
else:
reveal_type(t) # revealed: Literal[str]
if issubclass(t, (int, (Unrelated, (bytes,)))):
reveal_type(t) # revealed: Literal[int, bytes]
else:
reveal_type(t) # revealed: Literal[str]
```
## Special cases
@@ -148,11 +144,9 @@ to `issubclass`:
```py
class A: ...
def get_object() -> object: ...
t = object()
t = get_object()
# TODO: we should emit a diagnostic here
# error: [invalid-argument-type]
if issubclass(t, A):
reveal_type(t) # revealed: type[A]
```
@@ -166,7 +160,7 @@ branch:
```py
t = 1
# TODO: we should emit a diagnostic here
# error: [invalid-argument-type]
if issubclass(t, int):
reveal_type(t) # revealed: Never
```
@@ -240,8 +234,15 @@ def flag() -> bool: ...
t = int if flag() else str
# TODO: this should cause us to emit a diagnostic
# (`issubclass` has no `foo` parameter)
# error: [unknown-argument]
if issubclass(t, int, foo="bar"):
reveal_type(t) # revealed: Literal[int, str]
```
### `type[]` types are narrowed as well as class-literal types
```py
def _(x: type, y: type[int]):
if issubclass(x, y):
reveal_type(x) # revealed: type[int]
```

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