Compare commits

..

117 Commits

Author SHA1 Message Date
David Peter
a90e404c3f [red-knot] PEP 695 type aliases (#14357)
## Summary

Add support for (non-generic) type aliases. The main motivation behind
this was to get rid of panics involving expressions in (generic) type
aliases. But it turned out the best way to fix it was to implement
(partial) support for type aliases.

```py
type IntOrStr = int | str

reveal_type(IntOrStr)  # revealed: typing.TypeAliasType
reveal_type(IntOrStr.__name__)  # revealed: Literal["IntOrStr"]

x: IntOrStr = 1

reveal_type(x)  # revealed: Literal[1]

def f() -> None:
    reveal_type(x)  # revealed: int | str
```

## Test Plan

- Updated corpus test allow list to reflect that we don't panic anymore.
- Added Markdown-based test for type aliases (`type_alias.md`)
2024-11-22 08:47:14 +01:00
Micha Reiser
8358ad8d25 Ruff 0.8 release (#14486)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: David Salvisberg <dave@daverball.com>
2024-11-22 08:45:19 +01:00
Alex Waygood
2b8b1ef178 Improve docs for some pycodestyle rules (#14517) 2024-11-21 17:26:06 +00:00
Dylan
2efa3fbb62 [flake8-import-conventions] Syntax check aliases supplied in configuration for unconventional-import-alias (ICN001) (#14477)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-11-21 15:54:49 +00:00
cmp0xff
b9da4305e6 doc(B024): #14455 add annotated but unassgined class variables (#14502)
# Summary

Closes #14455, migrated from https://github.com/astral-sh/docs/pull/106.
2024-11-21 09:08:02 -06:00
Micha Reiser
87043a2415 Limit type size assertion to 64bit (#14514) 2024-11-21 12:49:55 +00:00
David Peter
f684b6fff4 [red-knot] Fix: Infer type for typing.Union[..] tuple expression (#14510)
## Summary

Fixes a panic related to sub-expressions of `typing.Union` where we fail
to store a type for the `int, str` tuple-expression in code like this:
```
x: Union[int, str] = 1
```

relates to [my
comment](https://github.com/astral-sh/ruff/pull/14499#discussion_r1851794467)
on #14499.

## Test Plan

New corpus test
2024-11-21 11:49:20 +01:00
David Peter
47f39ed1a0 [red-knot] Meta data for Type::Todo (#14500)
## Summary

Adds meta information to `Type::Todo`, allowing developers to easily
trace back the origin of a particular `@Todo` type they encounter.

Instead of `Type::Todo`, we now write either `type_todo!()` which
creates a `@Todo[path/to/source.rs:123]` type with file and line
information, or using `type_todo!("PEP 604 unions not supported")`,
which creates a variant with a custom message.

`Type::Todo` now contains a `TodoType` field. In release mode, this is
just a zero-sized struct, in order not to create any overhead. In debug
mode, this is an `enum` that contains the meta information.

`Type` implements `Copy`, which means that `TodoType` also needs to be
copyable. This limits the design space. We could intern `TodoType`, but
I discarded this option, as it would require us to have access to the
salsa DB everywhere we want to use `Type::Todo`. And it would have made
the macro invocations less ergonomic (requiring us to pass `db`).

So for now, the meta information is simply a `&'static str` / `u32` for
the file/line variant, or a `&'static str` for the custom message.
Anything involving a chain/backtrace of several `@Todo`s or similar is
therefore currently not implemented. Also because we currently don't see
any direct use cases for this, and because all of this will eventually
go away.

Note that the size of `Type` increases from 16 to 24 bytes, but only in
debug mode.

## Test Plan

- Observed the changes in Markdown tests.
- Added custom messages for all `Type::Todo`s that were revealed in the
tests
- Ran red knot in release and debug mode on the following Python file:
  ```py
  def f(x: int) -> int:
      reveal_type(x)
  ```
Prints `@Todo` in release mode and `@Todo(function parameter type)` in
debug mode.
2024-11-21 09:59:47 +01:00
Shaygan Hooshyari
aecdb8c144 [red-knot] support typing.Union in type annotations (#14499)
Fix #14498

## Summary

This PR adds `typing.Union` support

## Test Plan

I created new tests in mdtest.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-11-20 21:55:33 +00:00
Zanie Blue
3c52d2d1bd Improve the performance of the formatter instability check job (#14471)
We should probably get rid of this entirely and subsume it's
functionality in the normal ecosystem checks? I don't think we're using
the black comparison tests anymore, but maybe someone wants it?

There are a few major parts to this:

1. Making the formatter script idempotent, so it can be run repeatedly
and is robust to changing commits
2. Reducing the overhead of the git operations, minimizing the data
transfer
3. Parallelizing all the git operations by repository

This reduces the setup time from 80s to 16s (locally).

The initial motivation for idempotency was to include the repositories
in the GitHub Actions cache. I'm not sure it's worth it yet — they're
about 1GB and would consume our limited cache space. Regardless, it
improves correctness for local invocations.

The total runtime of the job is reduced from ~4m to ~3m.

I also made some cosmetic changes to the output paths and such.
2024-11-20 08:55:10 -06:00
Micha Reiser
942d6eeb9f Stabilize A004 (#14480) 2024-11-20 13:11:51 +01:00
Alex Waygood
4ccacc80f9 [ruff-0.8] [FAST] Further improve docs for fast-api-non-annotated-depencency (FAST002) (#14467) 2024-11-20 13:11:51 +01:00
Micha Reiser
b2bb119c6a Fix failing tests for Ruff 0.8 branch (#14482) 2024-11-20 13:11:51 +01:00
Alex Waygood
cef12f4925 [ruff-0.8] Spruce up docs for newly stabilised rules (#14466)
## Summary

- Expand some docs where they're unclear about the motivation, or assume
some knowledge that hasn't been introduced yet
- Add more links to external docs
- Rename PYI063 from `PrePep570PositionalArgument` to
`Pep484StylePositionalOnlyParameter`
- Rename the file `parenthesize_logical_operators.rs` to
`parenthesize_chained_operators.rs`, since the rule is called
`ParenthesizeChainedOperators`, not `ParenthesizeLogicalOperators`

## Test Plan

`cargo test`
2024-11-20 13:11:51 +01:00
Alex Waygood
aa7ac2ce0f [ruff-0.8] [ruff] Stabilise unsorted-dunder-all and unsorted-dunder-slots (#14468)
## Summary

These rules were implemented in January, have been very stable, and have
no open issues about them. They were highly requested by the community
prior to being implemented. Let's stabilise them!

## Test Plan

Ecosystem check on this PR.
2024-11-20 13:11:51 +01:00
Zanie Blue
70d9c90827 Use XDG (i.e. ~/.local/bin) instead of the Cargo home directory in the installer (#14457)
Closes https://github.com/astral-sh/ruff/issues/13927
2024-11-20 13:11:51 +01:00
Micha Reiser
adfa723464 Stabilize multiple rules (#14462) 2024-11-20 13:11:51 +01:00
Zanie Blue
844c07f1f0 Upgrade cargo-dist from 0.22.1 => 0.25.2-prerelease.3 (#14456)
Needed to prevent updater failures when doing
https://github.com/astral-sh/ruff/issues/13927

See 

- https://github.com/axodotdev/axoupdater/issues/210
- https://github.com/axodotdev/cargo-dist/pull/1538
- https://github.com/astral-sh/uv/pull/8958
2024-11-20 13:11:51 +01:00
Alex Waygood
11d20a1a51 [ruff-0.8] [ruff] Stabilise parenthesize-chained-operators (RUF021) (#14450) 2024-11-20 13:11:51 +01:00
Micha Reiser
e9079e7d95 Remove the deprecated E999 rule code (#14428) 2024-11-20 13:11:51 +01:00
Alex Waygood
c400725713 [ruff 0.8] [flake8-pytest-style] Remove deprecated rules PT004 and PT005 (#14385)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-20 13:11:51 +01:00
Alex Waygood
1081694140 [ruff 0.8] [flake8-annotations] Remove deprecated rules ANN101 and ANN102 (#14384)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-20 13:11:51 +01:00
Micha Reiser
52f526eb38 Warn instead of error when removed rules are used in ignore (#14435)
Closes https://github.com/astral-sh/ruff/issues/13505
2024-11-20 13:11:51 +01:00
David Salvisberg
dc05b38165 [ruff 0.8][flake8-type-checking] Rename TCH to TC (#14438)
Closes #9573
2024-11-20 13:11:51 +01:00
renovate[bot]
8c3c5ee5e3 Update Rust crate unicode-width to 0.2.0 (#13473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 13:11:51 +01:00
konsti
b46cc6ac0b Update pyproject-toml to support PEP 639 (#13902)
Fixes #13869
2024-11-20 13:11:51 +01:00
Dylan
8b925ea626 [pycodestyle] Stabilize behavior to ignore stub files in ambiguous-variable-name (E741) (#14405) 2024-11-20 13:11:51 +01:00
yataka
1b180c8342 Change default for Python version from 3.8 to 3.9 (#13896)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-20 13:11:51 +01:00
Dylan
afeb217452 [pyupgrade] Stabilize behavior to show diagnostic even when unfixable in printf-string-formatting (UP031) (#14406) 2024-11-20 13:11:51 +01:00
Dylan
c0b3dd3745 [ruff] Stabilize unsafe fix for zip-instead-of-pairwise (RUF007) (#14401)
This PR stabilizes the unsafe fix for [zip-instead-of-pairwise
(RUF007)](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/#zip-instead-of-pairwise-ruf007),
which replaces the use of zip with that of itertools.pairwise and has
been available under preview since version 0.5.7.

There are no open issues regarding RUF007 at the time of this writing.
2024-11-20 13:11:51 +01:00
Alex Waygood
5f6607bf54 [ruff 0.8] Remove deprecated rule UP027 (#14382) 2024-11-20 13:11:51 +01:00
Zanie Blue
a6deca44b5 Rename the parser fuzz job for consistency with the rest of CI (#14479) 2024-11-20 07:54:42 +00:00
Zanie Blue
0dbceccbc1 Only build the fuzz crate on main (#14478)
It's not actually doing anything per pull request and it's pretty slow?
xref #14469

It seems useful to build on `main` still to find build regressions? e.g.
https://github.com/astral-sh/ruff/issues/9368
2024-11-19 23:07:48 -06:00
Dhruv Manilawala
48680e10b6 Watch for changes to the generated file during documentation serve (#14476)
## Summary

Similar to https://github.com/astral-sh/uv/pull/9244, but we need to use
the `mkdocs.generated.yml` file because the `scripts/generate_mkdocs.py`
uses the `mkdocs.template.yml` to generate the final config.
2024-11-20 04:51:20 +00:00
Zanie Blue
b0c88a2a42 Only test release builds on main (#14475)
This is one of the slowest remaining jobs in the pull request CI. We
could use a larger runner for a trivial speed-up (in exchange for $$),
but I don't think this is going to break often enough to merit testing
on every pull request commit? It's not a required job, so I don't feel
strongly about it, but it feels like a bit of a waste of compute.

Originally added in https://github.com/astral-sh/ruff/pull/11182
2024-11-19 22:47:46 -06:00
InSync
b9c53a74f9 [pycodestyle] Exempt pytest.importorskip() calls (E402) (#14474)
## Summary

Resolves #13537.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2024-11-19 22:08:15 -05:00
cake-monotone
6a4d207db7 [red-knot] Refactoring the inference logic of lexicographic comparisons (#14422)
## Summary

closes #14279

### Limitations of the Current Implementation
#### Incorrect Error Propagation

In the current implementation of lexicographic comparisons, if the
result of an Eq operation is Ambiguous, the comparison stops
immediately, returning a bool instance. While this may yield correct
inferences, it fails to capture unsupported-operation errors that might
occur in subsequent comparisons.
```py
class A: ...

(int_instance(), A()) < (int_instance(), A())  # should error
```

#### Weak Inference in Specific Cases

> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
> Current result: `bool`
> Expected result: `Literal[False]`

`Eq` and `NotEq` have unique behavior in lexicographic comparisons
compared to other operators. Specifically:
- For `Eq`, if any non-equal pair exists within the tuples being
compared, we can immediately conclude that the tuples are not equal.
- For `NotEq`, if any equal pair exists, we can conclude that the tuples
are unequal.

```py
a = (str_instance(), int_instance(), "foo")

reveal_type(a == a)  # revealed: bool
reveal_type(a != a)  # revealed: bool

b = (str_instance(), int_instance(), "bar")

reveal_type(a == b)  # revealed: bool  # should be Literal[False]
reveal_type(a != b)  # revealed: bool  # should be Literal[True]
```
#### Incorrect Support for Non-Boolean Rich Comparisons

In CPython, aside from `==` and `!=`, tuple comparisons return a
non-boolean result as-is. Tuples do not convert the value into `bool`.

Note: If all pairwise `==` comparisons between elements in the tuples
return Truthy, the comparison then considers the tuples' lengths.
Regardless of the return type of the dunder methods, the final result
can still be a boolean.

```py
from __future__ import annotations

class A:
    def __eq__(self, o: object) -> str:
        return "hello"

    def __ne__(self, o: object) -> bytes:
        return b"world"

    def __lt__(self, o: A) -> float:
        return 3.14

a = (A(), A())

reveal_type(a == a)  # revealed: bool
reveal_type(a != a)  # revealed: bool
reveal_type(a < a)  # revealed: bool # should be: `float | Literal[False]`

```

### Key Changes
One of the major changes is that comparisons no longer end with a `bool`
result when a pairwise `Eq` result is `Ambiguous`. Instead, the function
attempts to infer all possible cases and unions the results. This
improvement allows for more robust type inference and better error
detection.

Additionally, as the function is now optimized for tuple comparisons,
the name has been changed from the more general
`infer_lexicographic_comparison` to `infer_tuple_rich_comparison`.

## Test Plan

mdtest included
2024-11-19 17:32:43 -08:00
Zanie Blue
42c35b6f44 Use larger GitHub runner for testing on Windows (#14461)
Reduces to 3m 50s (extra large) or 6m 5s (large) vs 9m 7s (standard)
2024-11-19 18:00:59 -06:00
Zanie Blue
9e79d64d62 Use Depot 16-core runner for testing on Linux (#14460)
Reduces Linux test CI to 1m 40s (16 core) or 2m 56s (8 core) to from 4m
25s. Times are approximate, as runner performance is pretty variable.

In uv, we use the 16 core runners.
2024-11-19 18:00:51 -06:00
Zanie Blue
582857f292 Use Depot 8-core runner for ecosystem tests (#14463)
I noticed this was exceedingly slow.

Reduces to 3m from 14m
2024-11-19 18:00:38 -06:00
Zanie Blue
9bbeb793e5 Specify the wasm-pack version explicitly (#14465)
There is an upstream bug with latest version detection
https://github.com/jetli/wasm-pack-action/issues/23

This causes random flakes of the wasm build job.
2024-11-19 18:00:27 -06:00
Micha Reiser
dbbe7a773c Mark UP043 fix unsafe when the type annotation contains any comments (#14458) 2024-11-19 15:24:02 +01:00
InSync
5f09d4a90a [ruff] re and regex calls with unraw string as first argument (RUF039) (#14446) 2024-11-19 13:44:55 +01:00
David Peter
f8c20258ae [red-knot] Do not panic on f-string format spec expressions (#14436)
## Summary

Previously, we panicked on expressions like `f"{v:{f'0.2f'}}"` because
we did not infer types for expressions nested inside format spec
elements.

## Test Plan

```
cargo nextest run -p red_knot_workspace -- --ignored linter_af linter_gz
```
2024-11-19 10:04:51 +01:00
David Peter
d8538d8c98 [red-knot] Narrowing for type(x) is C checks (#14432)
## Summary

Add type narrowing for `type(x) is C` conditions (and `else` clauses of
`type(x) is not C` conditionals):

```py
if type(x) is A:
    reveal_type(x)  # revealed: A
else:
    reveal_type(x)  # revealed: A | B
```

closes: #14431, part of: #13694

## Test Plan

New Markdown-based tests.
2024-11-18 16:21:46 +01:00
InSync
3642381489 [ruff] Add rule forbidding map(int, package.__version__.split('.')) (RUF048) (#14373)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-11-18 13:43:24 +00:00
Micha Reiser
1f07880d5c Add tests for python version compatibility (#14430) 2024-11-18 12:26:55 +00:00
David Peter
d81b6cd334 [red-knot] Types for subexpressions of annotations (#14426)
## Summary

This patches up various missing paths where sub-expressions of type
annotations previously had no type attached. Examples include:
```py
tuple[int, str]
#     ~~~~~~~~

type[MyClass]
#    ~~~~~~~

Literal["foo"]
#       ~~~~~

Literal["foo", Literal[1, 2]]
#              ~~~~~~~~~~~~~

Literal[1, "a", random.illegal(sub[expr + ession])]
#               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```

## Test Plan

```
cargo nextest run -p red_knot_workspace -- --ignored linter_af linter_gz
```
2024-11-18 13:03:27 +01:00
Micha Reiser
d99210c049 [red-knot] Default to python 3.9 (#14429) 2024-11-18 11:27:40 +00:00
Steve C
577653551c [pylint] - use sets when possible for PLR1714 autofix (repeated-equality-comparison) (#14372) 2024-11-18 08:57:43 +01:00
Dhruv Manilawala
38a385fb6f Simplify quote annotator logic for list expression (#14425)
## Summary

Follow-up to #14371, this PR simplifies the visitor logic for list
expressions to remove the state management. We just need to make sure
that we visit the nested expressions using the `QuoteAnnotator` and not
the `Generator`. This is similar to what's being done for binary
expressions.

As per the
[grammar](https://typing.readthedocs.io/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression),
list expressions can be present which can contain other type expressions
(`Callable`):
```
       | <Callable> '[' <Concatenate> '[' (type_expression ',')+
                    (name | '...') ']' ',' type_expression ']'
             (where name must be a valid in-scope ParamSpec)
       | <Callable> '[' '[' maybe_unpacked (',' maybe_unpacked)*
                    ']' ',' type_expression ']'
```

## Test Plan

`cargo insta test`
2024-11-18 12:33:19 +05:30
renovate[bot]
cd2ae5aa2d Update NPM Development dependencies (#14416)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@cloudflare/workers-types](https://redirect.github.com/cloudflare/workerd)
| [`4.20241106.0` ->
`4.20241112.0`](https://renovatebot.com/diffs/npm/@cloudflare%2fworkers-types/4.20241106.0/4.20241112.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@cloudflare%2fworkers-types/4.20241112.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@cloudflare%2fworkers-types/4.20241112.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@cloudflare%2fworkers-types/4.20241106.0/4.20241112.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@cloudflare%2fworkers-types/4.20241106.0/4.20241112.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin))
| [`8.13.0` ->
`8.14.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/8.13.0/8.14.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2feslint-plugin/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@typescript-eslint%2feslint-plugin/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@typescript-eslint%2feslint-plugin/8.13.0/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2feslint-plugin/8.13.0/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@typescript-eslint/parser](https://typescript-eslint.io/packages/parser)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser))
| [`8.13.0` ->
`8.14.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2fparser/8.13.0/8.14.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2fparser/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@typescript-eslint%2fparser/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@typescript-eslint%2fparser/8.13.0/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2fparser/8.13.0/8.14.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [postcss](https://postcss.org/)
([source](https://redirect.github.com/postcss/postcss)) | [`8.4.48` ->
`8.4.49`](https://renovatebot.com/diffs/npm/postcss/8.4.48/8.4.49) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/postcss/8.4.49?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/postcss/8.4.49?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/postcss/8.4.48/8.4.49?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/postcss/8.4.48/8.4.49?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [tailwindcss](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss)) |
[`3.4.14` ->
`3.4.15`](https://renovatebot.com/diffs/npm/tailwindcss/3.4.14/3.4.15) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tailwindcss/3.4.15?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/tailwindcss/3.4.15?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/tailwindcss/3.4.14/3.4.15?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tailwindcss/3.4.14/3.4.15?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [vite](https://vite.dev)
([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite))
| [`5.4.10` ->
`5.4.11`](https://renovatebot.com/diffs/npm/vite/5.4.10/5.4.11) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vite/5.4.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite/5.4.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite/5.4.10/5.4.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/5.4.10/5.4.11?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [wrangler](https://redirect.github.com/cloudflare/workers-sdk)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler))
| [`3.86.0` ->
`3.87.0`](https://renovatebot.com/diffs/npm/wrangler/3.86.0/3.87.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/3.87.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/wrangler/3.87.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/wrangler/3.86.0/3.87.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/3.86.0/3.87.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>cloudflare/workerd (@&#8203;cloudflare/workers-types)</summary>

###
[`v4.20241112.0`](8bf3af4699...7b28eb5032)

[Compare
Source](8bf3af4699...7b28eb5032)

</details>

<details>
<summary>typescript-eslint/typescript-eslint
(@&#8203;typescript-eslint/eslint-plugin)</summary>

###
[`v8.14.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8140-2024-11-11)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.13.0...v8.14.0)

##### 🚀 Features

- **eslint-plugin:** \[await-thenable] report unnecessary `await using`
statements
([#&#8203;10209](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10209))
- **eslint-plugin:** \[no-confusing-void-expression] add an option to
ignore void<->void
([#&#8203;10067](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10067))

##### 🩹 Fixes

- **scope-manager:** fix asserted increments not being marked as write
references
([#&#8203;10271](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10271))
- **eslint-plugin:** \[no-misused-promises] improve report loc for
methods
([#&#8203;10216](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10216))
- **eslint-plugin:** \[no-unnecessary-condition] improve error message
for literal comparisons
([#&#8203;10194](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10194))

##### ❤️  Thank You

-   Gyumong [@&#8203;Gyumong](https://redirect.github.com/Gyumong)
-   Jan Ochwat [@&#8203;janek515](https://redirect.github.com/janek515)
- Kirk Waiblinger
[@&#8203;kirkwaiblinger](https://redirect.github.com/kirkwaiblinger)
-   Ronen Amiel

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

</details>

<details>
<summary>typescript-eslint/typescript-eslint
(@&#8203;typescript-eslint/parser)</summary>

###
[`v8.14.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/parser/CHANGELOG.md#8140-2024-11-11)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.13.0...v8.14.0)

This was a version bump only for parser to align it with other projects,
there were no code changes.

You can read about our [versioning
strategy](https://main--typescript-eslint.netlify.app/users/versioning)
and
[releases](https://main--typescript-eslint.netlify.app/users/releases)
on our website.

</details>

<details>
<summary>postcss/postcss (postcss)</summary>

###
[`v8.4.49`](https://redirect.github.com/postcss/postcss/blob/HEAD/CHANGELOG.md#8449)

[Compare
Source](https://redirect.github.com/postcss/postcss/compare/8.4.48...8.4.49)

- Fixed custom syntax without `source.offset` (by
[@&#8203;romainmenke](https://redirect.github.com/romainmenke)).

</details>

<details>
<summary>tailwindlabs/tailwindcss (tailwindcss)</summary>

###
[`v3.4.15`](https://redirect.github.com/tailwindlabs/tailwindcss/releases/tag/v3.4.15)

[Compare
Source](https://redirect.github.com/tailwindlabs/tailwindcss/compare/v3.4.14...v3.4.15)

- Bump versions for security vulnerabilities
([#&#8203;14697](https://redirect.github.com/tailwindlabs/tailwindcss/pull/14697))
- Ensure the TypeScript types for the `boxShadow` theme configuration
allows arrays
([#&#8203;14856](https://redirect.github.com/tailwindlabs/tailwindcss/pull/14856))
- Set fallback for opacity variables to ensure setting colors with the
`selection:*` variant works in Chrome 131
([#&#8203;15003](https://redirect.github.com/tailwindlabs/tailwindcss/pull/15003))

</details>

<details>
<summary>vitejs/vite (vite)</summary>

###
[`v5.4.11`](https://redirect.github.com/vitejs/vite/releases/tag/v5.4.11)

[Compare
Source](https://redirect.github.com/vitejs/vite/compare/v5.4.10...v5.4.11)

Please refer to
[CHANGELOG.md](https://redirect.github.com/vitejs/vite/blob/v5.4.11/packages/vite/CHANGELOG.md)
for details.

</details>

<details>
<summary>cloudflare/workers-sdk (wrangler)</summary>

###
[`v3.87.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3870)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.86.1...wrangler@3.87.0)

##### Minor Changes

-
[#&#8203;7201](https://redirect.github.com/cloudflare/workers-sdk/pull/7201)
[`beed72e`](beed72e7f3)
Thanks [@&#8203;GregBrimble](https://redirect.github.com/GregBrimble)! -
feat: Tail Consumers are now supported for Workers with assets.

You can now configure `tail_consumers` in conjunction with `assets` in
your `wrangler.toml` file. Read more about [Static
Assets](https://developers.cloudflare.com/workers/static-assets/) and
[Tail
Consumers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/)
in the documentation.

-
[#&#8203;7212](https://redirect.github.com/cloudflare/workers-sdk/pull/7212)
[`837f2f5`](837f2f569b)
Thanks [@&#8203;jonesphillip](https://redirect.github.com/jonesphillip)!
- Added r2 bucket info command to Wrangler. Improved formatting of r2
bucket list output

##### Patch Changes

-
[#&#8203;7210](https://redirect.github.com/cloudflare/workers-sdk/pull/7210)
[`c12c0fe`](c12c0fed88)
Thanks [@&#8203;taylorlee](https://redirect.github.com/taylorlee)! -
Avoid an unnecessary GET request during `wrangler deploy`.

-
[#&#8203;7197](https://redirect.github.com/cloudflare/workers-sdk/pull/7197)
[`4814455`](4814455717)
Thanks
[@&#8203;michelheusschen](https://redirect.github.com/michelheusschen)!
- fix console output for `wrangler d1 migrations create`

-
[#&#8203;6795](https://redirect.github.com/cloudflare/workers-sdk/pull/6795)
[`94f07ee`](94f07eec15)
Thanks [@&#8203;benmccann](https://redirect.github.com/benmccann)! -
chore: upgrade chokidar to v4

-
[#&#8203;7133](https://redirect.github.com/cloudflare/workers-sdk/pull/7133)
[`c46e02d`](c46e02dfd7)
Thanks [@&#8203;gpanders](https://redirect.github.com/gpanders)! - Do
not emit escape sequences when stdout is not a TTY

###
[`v3.86.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/wrangler/CHANGELOG.md#3861)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/wrangler@3.86.0...wrangler@3.86.1)

##### Patch Changes

-
[#&#8203;7069](https://redirect.github.com/cloudflare/workers-sdk/pull/7069)
[`b499b74`](b499b743e2)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Internal refactor to remove the non `--x-dev-env` flow from `wrangler
dev`

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOS4wIiwidXBkYXRlZEluVmVyIjoiMzkuMTkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 12:25:29 +05:30
renovate[bot]
41694f21c6 Update pre-commit dependencies (#14419)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[abravalheri/validate-pyproject](https://redirect.github.com/abravalheri/validate-pyproject)
| repository | minor | `v0.22` -> `v0.23` |
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.7.3` -> `v0.7.4` |

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>abravalheri/validate-pyproject
(abravalheri/validate-pyproject)</summary>

###
[`v0.23`](https://redirect.github.com/abravalheri/validate-pyproject/releases/tag/v0.23)

[Compare
Source](https://redirect.github.com/abravalheri/validate-pyproject/compare/v0.22...v0.23)

#### What's Changed

- Validate SPDX license expressions by
[@&#8203;cdce8p](https://redirect.github.com/cdce8p) in
[https://github.com/abravalheri/validate-pyproject/pull/217](https://redirect.github.com/abravalheri/validate-pyproject/pull/217)

#### New Contributors

- [@&#8203;cdce8p](https://redirect.github.com/cdce8p) made their first
contribution in
[https://github.com/abravalheri/validate-pyproject/pull/217](https://redirect.github.com/abravalheri/validate-pyproject/pull/217)

**Full Changelog**:
https://github.com/abravalheri/validate-pyproject/compare/v0.22...v0.23

</details>

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

###
[`v0.7.4`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.7.4)

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

See: https://github.com/astral-sh/ruff/releases/tag/0.7.4

</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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOS4wIiwidXBkYXRlZEluVmVyIjoiMzkuMTkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW50ZXJuYWwiXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 11:27:14 +05:30
Charlie Marsh
fccbe56d23 Reverse order of __contains__ arguments (#14424)
## Summary

Closes https://github.com/astral-sh/ruff/issues/14423.
2024-11-18 03:58:12 +00:00
Shantanu
c46555da41 Drive by typo fix (#14420)
Introduced in
https://github.com/astral-sh/ruff/pull/14397/files#diff-42314c006689490bbdfbeeb973de64046b3e069e3d88f67520aeba375f20e655
2024-11-18 03:03:36 +00:00
InSync
0a27c9dabd [flake8-pie] Mark fix as unsafe if the following statement is a string literal (PIE790) (#14393)
## Summary

Resolves #12616.

## Test Plan

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

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-11-18 02:30:06 +00:00
InSync
3c9e76eb66 [flake8-datetimez] Also exempt .time() (DTZ901) (#14394)
## Summary

Resolves #14378.

## Test Plan

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

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-11-18 02:24:35 +00:00
renovate[bot]
80f5cdcf66 Update dependency tomli to v2.1.0 (#14418) 2024-11-18 01:56:05 +00:00
renovate[bot]
35fe0e90da Update Rust crate bstr to v1.11.0 (#14417) 2024-11-17 20:41:49 -05:00
renovate[bot]
157b49a8ee Update dependency ruff to v0.7.4 (#14415) 2024-11-17 20:41:40 -05:00
renovate[bot]
8a6e223df5 Update dependency react-resizable-panels to v2.1.7 (#14414) 2024-11-17 20:41:34 -05:00
renovate[bot]
5a48da53da Update Rust crate serde_json to v1.0.133 (#14413) 2024-11-17 20:41:29 -05:00
renovate[bot]
58005b590c Update Rust crate serde to v1.0.215 (#14412) 2024-11-17 20:41:23 -05:00
renovate[bot]
884835e386 Update Rust crate libc to v0.2.164 (#14411) 2024-11-17 20:41:17 -05:00
renovate[bot]
efd4407f7f Update Rust crate indicatif to v0.17.9 (#14410) 2024-11-17 20:41:13 -05:00
renovate[bot]
761588a60e Update Rust crate clap to v4.5.21 (#14409) 2024-11-17 20:41:06 -05:00
Charlie Marsh
e1eb188049 Avoid panic in unfixable redundant-numeric-union (#14402)
## Summary

Closes https://github.com/astral-sh/ruff/issues/14396.
2024-11-17 12:15:44 -05:00
Shaygan Hooshyari
ff19629b11 Understand typing.Optional in annotations (#14397) 2024-11-17 17:04:58 +00:00
Micha Reiser
cd80c9d907 Fix Red Knot benchmarks on Windows (#14400) 2024-11-17 16:21:09 +00:00
Matt Norton
abb34828bd Improve rule & options documentation (#14329)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-11-17 10:16:47 +01:00
InSync
cab7caf80b [flake8-logging] Suggest .getLogger(__name__) instead of .getLogger(__file__) (LOG015) (#14392) 2024-11-17 09:22:52 +01:00
David Peter
d470f29093 [red-knot] Disable linter-corpus tests (#14391)
## Summary

Disable the no-panic tests for the linter corpus, as there are too many
problems right now, requiring linter-contributors to add their test
files to the allow-list.

We can still run the tests using `cargo test -p red_knot_workspace --
--ignored linter_af linter_gz`. This is also why I left the
`crates/ruff_linter/` entries in the allow list for now, even if they
will get out of sync. But let me know if I should rather remove them.
2024-11-16 23:33:19 +01:00
Simon Brugman
1fbed6c325 [ruff] Implement redundant-bool-literal (RUF038) (#14319)
## Summary

Implements `redundant-bool-literal`

## Test Plan

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

`cargo test`

The ecosystem results are all correct, but for `Airflow` the rule is not
relevant due to the use of overloading (and is marked as unsafe
correctly).

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-11-16 21:52:51 +00:00
David Peter
4dcb7ddafe [red-knot] Remove duplicates from KNOWN_FAILURES (#14386)
## Summary

- Sort the list of `KNOWN_FAILURES`
- Remove accidental duplicates
2024-11-16 20:54:21 +01:00
Micha Reiser
5be90c3a67 Split the corpus tests into smaller tests (#14367)
## Summary

This PR splits the corpus tests into smaller chunks because running all
of them takes 8s on my windows machine and it's by far the longest test
in `red_knot_workspace`.

Splitting the tests has the advantage that they run in parallel. This PR
brings down the wall time from 8s to 4s.

This PR also limits the glob for the linter tests because it's common to
clone cpython into the `ruff_linter/resources/test` folder for
benchmarks (because that's what's written in the contributing guides)

## Test Plan

`cargo test`
2024-11-16 20:29:21 +01:00
Tom Kuson
d0dca7bfcf [pydoclint] Update diagnostics to target the docstring (#14381)
## Summary

Updates the `pydoclint` diagnostics to target the docstring instead of a
related statement.

Closes #13184

## Test Plan

`cargo nextest run`
2024-11-16 13:32:20 -05:00
Simon Brugman
78210b198b [flake8-pyi] Implement redundant-none-literal (PYI061) (#14316)
## Summary

`Literal[None]` can be simplified into `None` in type annotations.

Surprising to see that this is not that rare:
-
https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/chat_models/base.py#L54
-
https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/sql/annotation.py#L69
- https://github.com/jax-ml/jax/blob/main/jax/numpy/__init__.pyi#L961
-
https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/inference/_common.py#L179

## Test Plan

`cargo test`

Reviewed all ecosystem results, and they are true positives.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-11-16 18:22:51 +00:00
Simon Brugman
4a2310b595 [flake8-pyi] Implement autofix for redundant-numeric-union (PYI041) (#14273)
## Summary

This PR adds autofix for `redundant-numeric-union` (`PYI041`)

There are some comments below to explain the reasoning behind some
choices that might help review.

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

Resolves part of https://github.com/astral-sh/ruff/issues/14185.

## Test Plan

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

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-11-16 18:13:23 +00:00
Dylan
fc392c663a [flake8-type-checking] Fix helper function which surrounds annotations in quotes (#14371)
This PR adds corrected handling of list expressions to the `Visitor`
implementation of `QuotedAnnotator` in `flake8_type_checking::helpers`.

Closes #14368
2024-11-16 12:58:02 -05:00
Alex Waygood
81d3c419e9 [red-knot] Simplify some traits in ast_ids.rs (#14379) 2024-11-16 17:22:23 +00:00
Micha Reiser
a6a3d3f656 Fix file watcher panic when event has no paths (#14364) 2024-11-16 08:36:57 +01:00
Micha Reiser
c847cad389 Update insta snapshots (#14366) 2024-11-15 19:31:15 +01:00
Micha Reiser
81e5830585 Workspace discovery (#14308) 2024-11-15 19:20:15 +01:00
Micha Reiser
2b58705cc1 Remove the optional salsa dependency from the AST crate (#14363) 2024-11-15 16:46:04 +00:00
David Peter
9f3235a37f [red-knot] Expand test corpus (#14360)
## Summary

- Add 383 files from `crates/ruff_python_parser/resources` to the test
corpus
- Add 1296 files from `crates/ruff_linter/resources` to the test corpus
- Use in-memory file system for tests
- Improve test isolation by cleaning the test environment between checks
- Add a mechanism for "known failures". Mark ~80 files as known
failures.
- The corpus test is now a lot slower (6 seconds).

Note:
While `red_knot` as a command line tool can run over all of these
files without panicking, we still have a lot of test failures caused by
explicitly "pulling" all types.

## Test Plan

Run `cargo test -p red_knot_workspace` while making sure that
- Introducing code that is known to lead to a panic fails the test
- Removing code that is known to lead to a panic from
`KNOWN_FAILURES`-files also fails the test
2024-11-15 17:09:15 +01:00
Alex Waygood
62d650226b [red-knot] Derive more Default methods (#14361) 2024-11-15 13:15:41 +00:00
David Peter
5d8a391a3e [red-knot] Mark LoggingGuard as must_use (#14356) 2024-11-15 12:47:25 +01:00
Dhruv Manilawala
ed7b98cf9b Bump version to 0.7.4 (#14358) 2024-11-15 11:17:32 +00:00
Shaygan Hooshyari
6591775cd9 [flake8-type-checking] Skip quoting annotation if it becomes invalid syntax (TCH001) (#14285)
Fix: #13934 

## Summary

Current implementation has a bug when the current annotation contains a
string with single and double quotes.

TL;DR: I think these cases happen less than other use cases of Literal.
So instead of fixing them we skip the fix in those cases.

One of the problematic cases:

```
from typing import Literal
from third_party import Type

def error(self, type1: Type[Literal["'"]]):
    pass
```

The outcome is:

```
- def error(self, type1: Type[Literal["'"]]):
+ def error(self, type1: "Type[Literal[''']]"):
```

While it should be:

```
"Type[Literal['\'']"
```

The solution in this case is that we check if there’s any quotes same as
the quote style we want to use for this Literal parameter then escape
that same quote used in the string.

Also this case is not uncommon to have:
<https://grep.app/search?current=2&q=Literal["'>

But this can get more complicated for example in case of:

```
- def error(self, type1: Type[Literal["\'"]]):
+ def error(self, type1: "Type[Literal[''']]"):
```

Here we escaped the inner quote but in the generated annotation it gets
removed. Then we flip the quote style of the Literal paramter and the
formatting is wrong.

In this case the solution is more complicated.
1. When generating the string of the source code preserve the backslash.
2. After we have the annotation check if there isn’t any escaped quote
of the same type we want to use for the Literal parameter. In this case
check if we have any `’` without `\` before them. This can get more
complicated since there can be multiple backslashes so checking for only
`\’` won’t be enough.

Another problem is when the string contains `\n`. In case of
`Type[Literal["\n"]]` we generate `'Type[Literal["\n"]]'` and both
pyright and mypy reject this annotation.

https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAySMApiAIYA2AUAMaXkDOjUAKoiQNqsC6AXFAB0w6tQAmJYLBKMYAfQCOAVzCk5tMChjlUjOQCNytANaMGjABYAKRiUrAANLA4BGAQHJ2CLkVIVKnABEADoogTw87gCUfNRQ8VAITIyiElKksooqahpaOih6hiZmTNa29k7w3m5sHJy%2BZFRBoeE8MXEJScxAA

## Test Plan

I added test cases for the original code in the reported issue and two
more cases for backslash and new line.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-11-15 11:11:46 +00:00
Dhruv Manilawala
1f82731856 Use CWD to resolve settings from ruff.configuration (#14352)
## Summary

This PR fixes a bug in the Ruff language server where the
editor-specified configuration was resolved relative to the
configuration directory and not the current working directory.

The existing behavior is confusing given that this config file is
specified by the user and is not _discovered_ by Ruff itself. The
behavior of resolving this configuration file should be similar to that
of the `--config` flag on the command-line which uses the current
working directory:
3210f1a23b/crates/ruff/src/resolve.rs (L34-L48)

This creates problems where certain configuration options doesn't work
because the paths resolved in that case are relative to the
configuration directory and not the current working directory in which
the editor is expected to be in. For example, the
`lint.per-file-ignores` doesn't work as mentioned in the linked issue
along with `exclude`, `extend-exclude`, etc.

fixes: #14282 

## Test Plan

Using the following directory tree structure:
```
.
├── .config
│   └── ruff.toml
└── src
    └── migrations
        └── versions
            └── a.py
```

where, the `ruff.toml` is:
```toml
# 1. Comment this out to test `per-file-ignores`
extend-exclude = ["**/versions/*.py"]

[lint]
select = ["D"]

# 2. Comment this out to test `extend-exclude`
[lint.per-file-ignores]
"**/versions/*.py" = ["D"]

# 3. Comment both `per-file-ignores` and `extend-exclude` to test selection works
```

And, the content of `a.py`:
```py
"""Test"""
```

And, the VS Code settings:
```jsonc
{
  "ruff.nativeServer": "on",
  "ruff.path": ["/Users/dhruv/work/astral/ruff/target/debug/ruff"],
  // For single-file mode where current working directory is `/`
  // "ruff.configuration": "/tmp/ruff-repro/.config/ruff.toml",
  // When a workspace is opened containing this path
  "ruff.configuration": "./.config/ruff.toml",
  "ruff.trace.server": "messages",
  "ruff.logLevel": "trace"
}
```

I also tested out just opening the file in single-file mode where the
current working directory is `/` in VS Code. Here, the
`ruff.configuration` needs to be updated to use absolute path as shown
in the above VS Code settings.
2024-11-15 13:45:00 +05:30
Dhruv Manilawala
874da9c400 [red-knot] Display raw characters for string literal (#14351)
## Summary

Closes: #14330 

| `main` | PR |
|--------|--------|
| <img width="693" alt="Screenshot 2024-11-15 at 9 41 09 AM"
src="https://github.com/user-attachments/assets/0d10f2be-2155-4387-8d39-eb1b5027cfd4">
| <img width="800" alt="Screenshot 2024-11-15 at 9 40 27 AM"
src="https://github.com/user-attachments/assets/ba68911c-f4bf-405a-a597-44207b4bde7a">
|


## Test Plan

Add test cases for escape and quote characters.
2024-11-15 13:44:04 +05:30
github-actions[bot]
375cead202 Sync vendored typeshed stubs (#14350) 2024-11-14 22:29:29 -08:00
Dhruv Manilawala
9ec690b8f8 [red-knot] Add support for string annotations (#14151)
## Summary

This PR adds support for parsing and inferring types within string
annotations.

### Implementation (attempt 1)

This is preserved in
6217f48924.

The implementation here would separate the inference of string
annotations in the deferred query. This requires the following:
* Two ways of evaluating the deferred definitions - lazily and eagerly. 
* An eager evaluation occurs right outside the definition query which in
this case would be in `binding_ty` and `declaration_ty`.
* A lazy evaluation occurs on demand like using the
`definition_expression_ty` to determine the function return type and
class bases.
* The above point means that when trying to get the binding type for a
variable in an annotated assignment, the definition query won't include
the type. So, it'll require going through the deferred query to get the
type.

This has the following limitations:
* Nested string annotations, although not necessarily a useful feature,
is difficult to implement unless we convert the implementation in an
infinite loop
* Partial string annotations require complex layout because inferring
the types for stringified and non-stringified parts of the annotation
are done in separate queries. This means we need to maintain additional
information

### Implementation (attempt 2)

This is the final diff in this PR.

The implementation here does the complete inference of string annotation
in the same definition query by maintaining certain state while trying
to infer different parts of an expression and take decisions
accordingly. These are:
* Allow names that are part of a string annotation to not exists in the
symbol table. For example, in `x: "Foo"`, if the "Foo" symbol is not
defined then it won't exists in the symbol table even though it's being
used. This is an invariant which is being allowed only for symbols in a
string annotation.
* Similarly, lookup name is updated to do the same and if the symbol
doesn't exists, then it's not bounded.
* Store the final type of a string annotation on the string expression
itself and not for any of the sub-expressions that are created after
parsing. This is because those sub-expressions won't exists in the
semantic index.

Design document:
https://www.notion.so/astral-sh/String-Annotations-12148797e1ca801197a9f146641e5b71?pvs=4

Closes: #13796 

## Test Plan

* Add various test cases in our markdown framework
* Run `red_knot` on LibCST (contains a lot of string annotations,
specifically
https://github.com/Instagram/LibCST/blob/main/libcst/matchers/_matcher_base.py),
FastAPI (good amount of annotated code including `typing.Literal`) and
compare against the `main` branch output
2024-11-15 04:10:18 +00:00
Carl Meyer
a48d779c4e [red-knot] function signature representation (#14304)
## Summary

Add a typed representation of function signatures (parameters and return
type) and infer it correctly from a function.

Convert existing usage of function return types to use the signature
representation.

This does not yet add inferred types for parameters within function body
scopes based on the annotations, but it should be easy to add as a next
step.

Part of #14161 and #13693.

## Test Plan

Added tests.
2024-11-14 23:34:24 +00:00
Dylan
ba6c7f6897 [pylint] Remove check for dot in alias name in useless-import-alias (PLC0414) (#14345)
Follow-up to #14287 : when checking that `name` is the same as `as_name`
in `import name as as_name`, we do not need to first do an early return
if `'.'` is found in `name`.
2024-11-14 16:26:50 -06:00
Dylan
8095ff0e55 enforce required imports even with useless alias (#14287)
This PR handles a panic that occurs when applying unsafe fixes if a user
inserts a required import (I002) that has a "useless alias" in it, like
`import numpy as numpy`, and also selects PLC0414 (useless-import-alias)

In this case, the fixes alternate between adding the required import
statement, then removing the alias, until the recursion limit is
reached. See linked issue for an example.

Closes #14283

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-11-14 15:39:38 -06:00
Micha Reiser
24cd592a1d Avoid module lookup for known classes when possible (#14343) 2024-11-14 20:24:12 +00:00
Simon Brugman
a40bc6a460 [ruff] Implement none-not-at-end-of-union (RUF036) (#14314) 2024-11-14 19:37:13 +01:00
Alex Waygood
577de6c599 [red-knot] Clarify a TODO comment in a sys.version_info test (#14340) 2024-11-14 17:22:43 +00:00
InSync
d8b1afbc6e [ruff] Also report problems for attrs dataclasses in preview mode (RUF008, RUF009) (#14327)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-11-14 15:13:49 +00:00
David Peter
9a3001b571 [red-knot] Do not attach diagnostics to wrong file (#14337)
## Summary

Avoid attaching diagnostics to the wrong file. See related issue for
details.

Closes #14334

## Test Plan

New regression test.
2024-11-14 15:39:51 +01:00
Pierre GIRAUD
ec2c7cad0e Improve docs for ALE plugin for vim (#14335)
2 different fixers are available in ALE :
- ruff which runs `ruff check --fix` command (useful for example when
isort is enabled in lint config),
 - ruff_format which runs `run format` command.

The documentation was missing `ruff` as a possible fixer in ALE.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-11-14 13:01:34 +00:00
Shaygan Hooshyari
924741cb11 [red-knot] Infer unary not operation for instances (#13827)
Handle unary `not` on instances by calling the `__bool__` dunder.

## Test Plan

Added a new test case with some examples from these resources:

- https://docs.python.org/3/library/stdtypes.html#truth-value-testing
- <https://docs.python.org/3/reference/datamodel.html#object.__len__>
- <https://docs.python.org/3/reference/datamodel.html#object.__bool__>

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-11-13 23:31:36 +00:00
David Peter
77e8da7497 [red-knot] Avoid panics for ipython magic commands (#14326)
## Summary

Avoids panics when encountering Jupyter notebooks with [IPython magic
commands](https://ipython.readthedocs.io/en/stable/interactive/magics.html).

## Test Plan

Added Jupyter notebook to corpus.
2024-11-13 20:58:08 +01:00
David Peter
5e64863895 [red-knot] Handle invalid assignment targets (#14325)
## Summary

This fixes several panics related to invalid assignment targets. All of
these led to some a crash, previously:
```py
(x.y := 1)  # only name-expressions are valid targets of named expressions
([x, y] := [1, 2])  # same
(x, y): tuple[int, int] = (2, 3)  # tuples are not valid targets for annotated assignments
(x, y) += 2  # tuples are not valid targets for augmented assignments
```

closes #14321
closes #14322

## Test Plan

I symlinked four files from `crates/ruff_python_parser/resources` into
the red knot corpus, as they seemed like ideal test files for this exact
scenario. I think eventually, it might be a good idea to simply include *all*
invalid-syntax examples from the parser tests into red knots corpus (I believe
we're actually not too far from that goal). Or expand the scope of the corpus
test to this directory. Then we can get rid of these symlinks again.
2024-11-13 20:50:39 +01:00
Alex Waygood
78e4753d74 Remove unused flags and functions from the semantic model (#14318) 2024-11-13 17:35:48 +00:00
Simon Brugman
eb55b9b5a0 [flake8-pyi] Always autofix duplicate-union-members (PYI016) (#14270) 2024-11-13 16:42:06 +00:00
David Peter
0eb36e4345 [red-knot] Avoid panic for generic type aliases (#14312)
## Summary

This avoids a panic inside `TypeInferenceBuilder::infer_type_parameters`
when encountering generic type aliases:
```py
type ListOrSet[T] = list[T] | set[T]
```

To fix this properly, we would have to treat type aliases as being their own
annotation scope [1]. The left hand side is a definition for the type parameter
`T` which is being used in the special annotation scope on the right hand side.
Similar to how it works for generic functions and classes.

[1] https://docs.python.org/3/reference/compound_stmts.html#generic-type-aliases


closes #14307

## Test Plan

Added new example to the corpus.
2024-11-13 16:01:15 +01:00
Carl Meyer
5fcf0afff4 [red-knot] simplify type lookup in function/class definitions (#14303)
When we look up the types of class bases or keywords (`metaclass`), we
currently do this little dance: if there are type params, then look up
the type using `SemanticModel` in the type-params scope, if not, look up
the type directly in the definition's own scope, with support for
deferred types.

With inference of function parameter types, I'm now adding another case
of this same dance, so I'm motivated to make it a bit more ergonomic.

Add support to `definition_expression_ty` to handle any sub-expression
of a definition, whether it is in the definition's own scope or in a
type-params sub-scope.

Related to both #13693 and #14161.
2024-11-13 13:53:56 +00:00
David Peter
b946cfd1f7 [red-knot] Use memory address as AST node key (#14317)
## Summary

Use the memory address to uniquely identify AST nodes, instead of
relying on source range and kind. The latter fails for ASTs resulting
from invalid syntax examples. See #14313 for details.

Also results in a 1-2% speedup
(https://codspeed.io/astral-sh/ruff/runs/67349cf55f36b36baa211360)

closes #14313 

## Review

Here are the places where we use `NodeKey` directly or indirectly (via
`ExpressionNodeKey` or `DefinitionNodeKey`):

```rs
// semantic_index.rs
pub(crate) struct SemanticIndex<'db> { 
    // [...]
    /// Map expressions to their corresponding scope.
    scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,

    /// Map from a node creating a definition to its definition.
    definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,

    /// Map from a standalone expression to its [`Expression`] ingredient.
    expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
    // [...]
}

// semantic_index/builder.rs
pub(super) struct SemanticIndexBuilder<'db> {
    // [...]
    scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
    definitions_by_node: FxHashMap<ExpressionNodeKey, Definition<'db>>,
    expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
}

// semantic_index/ast_ids.rs
pub(crate) struct AstIds {
    /// Maps expressions to their expression id.
    expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
    /// Maps expressions which "use" a symbol (that is, [`ast::ExprName`]) to a use id.
    uses_map: FxHashMap<ExpressionNodeKey, ScopedUseId>,
}

pub(super) struct AstIdsBuilder {
    expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
    uses_map: FxHashMap<ExpressionNodeKey, ScopedUseId>,
}
```

## Test Plan

Added two failing examples to the corpus.
2024-11-13 14:35:54 +01:00
Charlie Marsh
95c8f5fd0f Document comment policy around fix safety (#14300)
## Summary

Closes https://github.com/astral-sh/ruff/issues/9790.
2024-11-13 08:03:58 -05:00
David Salvisberg
89aa804b2d [flake8-type-checking] Fix false positives for typing.Annotated (#14311) 2024-11-13 12:17:52 +00:00
InSync
f789b12705 [flake8-logging] Root logger calls (LOG015) (#14302) 2024-11-13 09:11:55 +00:00
David Peter
3e36a7ab81 [red-knot] Fix assertion for invalid match pattern (#14306)
## Summary

Fixes a failing debug assertion that triggers for the following code:
```py
match some_int:
    case x:=2:
        pass
```

closes #14305

## Test Plan

Added problematic code example to corpus.
2024-11-13 10:07:29 +01:00
InSync
5c548dcc04 [flake8-datetimez] Usages of datetime.max/datetime.min (DTZ901) (#14288)
## Summary

Resolves #13217.

## Test Plan

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

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-11-12 20:36:07 +00:00
Simon Brugman
bd30701980 [flake8-pyi] Improve autofix for nested and mixed type unions unnecessary-type-union (PYI055) (#14272)
## Summary

This PR improves the fix for `PYI055` to be able to handle nested and
mixed type unions.

It also marks the fix as unsafe when comments are present. 
 
<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->
2024-11-12 15:33:51 -05:00
Harutaka Kawamura
2b6d66b793 Fix pytest-raises-too-broad (PT011) to flag pytest.raises call with keyword expected_exception (#14298)
## Summary

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

`pytest-raises-too-broad (PT011)` should be raised when
`expected_exception` is provided as a keyword argument.

```python
def test_foo():
    with pytest.raises(ValueError):  # raises PT011
        raise ValueError("Can't divide 1 by 0")

    # This is minor but a valid pytest.raises call
    with pytest.raises(expected_exception=ValueError):  # doesn't raise PT011 but should
        raise ValueError("Can't divide 1 by 0")
```

`pytest.raises` doc:
https://docs.pytest.org/en/8.3.x/reference/reference.html#pytest.raises

## Test Plan

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

Unit tests

Signed-off-by: harupy <hkawamura0130@gmail.com>
2024-11-12 14:28:42 -05:00
3008 changed files with 17510 additions and 14200 deletions

View File

@@ -17,4 +17,7 @@ indent_size = 4
trim_trailing_whitespace = false
[*.md]
max_line_length = 100
max_line_length = 100
[*.toml]
indent_size = 4

View File

@@ -115,7 +115,7 @@ jobs:
cargo-test-linux:
name: "cargo test (linux)"
runs-on: ubuntu-latest
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
@@ -159,7 +159,7 @@ jobs:
cargo-test-windows:
name: "cargo test (windows)"
runs-on: windows-latest
runs-on: windows-latest-xlarge
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
@@ -197,6 +197,8 @@ jobs:
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: Swatinem/rust-cache@v2
- name: "Test ruff_wasm"
run: |
@@ -211,7 +213,7 @@ jobs:
name: "cargo build (release)"
runs-on: macos-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -255,11 +257,11 @@ jobs:
NEXTEST_PROFILE: "ci"
run: cargo +${{ steps.msrv.outputs.value }} insta test --all-features --unreferenced reject --test-runner nextest
cargo-fuzz:
name: "cargo fuzz"
cargo-fuzz-build:
name: "cargo fuzz build"
runs-on: ubuntu-latest
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
@@ -278,7 +280,7 @@ jobs:
- run: cargo fuzz build -s none
fuzz-parser:
name: "Fuzz the parser"
name: "fuzz parser"
runs-on: ubuntu-latest
needs:
- cargo-test-linux
@@ -331,7 +333,7 @@ jobs:
ecosystem:
name: "ecosystem"
runs-on: ubuntu-latest
runs-on: depot-ubuntu-latest-8
needs:
- cargo-test-linux
- determine_changes
@@ -561,12 +563,12 @@ jobs:
run: rustup show
- name: "Cache rust"
uses: Swatinem/rust-cache@v2
- name: "Formatter progress"
- name: "Run checks"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
run: cat target/formatter-ecosystem/stats.txt > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects
run: rm -r target/formatter-ecosystem
check-ruff-lsp:
name: "test ruff-lsp"

View File

@@ -1,4 +1,4 @@
# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
@@ -6,7 +6,7 @@
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with cargo-dist (archives, installers, hashes)
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
@@ -24,10 +24,10 @@ permissions:
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (cargo-dist-able) packages in the workspace with that version (this mode is
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
@@ -48,7 +48,7 @@ on:
type: string
jobs:
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-20.04"
outputs:
@@ -62,16 +62,16 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh"
- name: Cache cargo-dist
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/cargo-dist
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
@@ -79,8 +79,8 @@ jobs:
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
cargo dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "cargo dist ran successfully"
dist ${{ (inputs.tag && inputs.tag != 'dry-run' && format('host --steps=create --tag={0}', inputs.tag)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
@@ -124,12 +124,12 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
@@ -140,8 +140,8 @@ jobs:
- id: cargo-dist
shell: bash
run: |
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "cargo dist ran successfully"
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
@@ -174,12 +174,12 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
- name: Install cached dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
@@ -191,7 +191,7 @@ jobs:
- id: host
shell: bash
run: |
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"

View File

@@ -17,7 +17,7 @@ exclude: |
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.22
rev: v0.23
hooks:
- id: validate-pyproject
@@ -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.7.3
rev: v0.7.4
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,5 +1,30 @@
# Breaking Changes
## 0.8.0
- **Default to Python 3.9**
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
- **Changed location of `pydoclint` diagnostics**
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
If you've opted into these preview rules but have them suppressed using
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
some places, this change may mean that you need to move the `noqa` suppression
comments. Most users should be unaffected by this change.
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
- **Changes to the line width calculation**
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
## 0.7.0
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments

View File

@@ -1,5 +1,153 @@
# Changelog
## 0.8.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.8.0) for a migration guide and overview of the changes!
### Breaking changes
See also, the "Remapped rules" section which may result in disabled rules.
- **Default to Python 3.9**
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
- **Changed location of `pydoclint` diagnostics**
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
If you've opted into these preview rules but have them suppressed using
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
some places, this change may mean that you need to move the `noqa` suppression
comments. Most users should be unaffected by this change.
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
- **Changes to the line width calculation**
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
### Removed Rules
The following deprecated rules have been removed:
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`)
- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`)
- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`)
- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`)
### Remapped rules
The following rules have been remapped to new rule codes:
- [`flake8-type-checking`](https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc): `TCH` to `TC`
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`builtin-import-shadowing`](https://docs.astral.sh/ruff/rules/builtin-import-shadowing/) (`A004`)
- [`mutable-contextvar-default`](https://docs.astral.sh/ruff/rules/mutable-contextvar-default/) (`B039`)
- [`fast-api-redundant-response-model`](https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/) (`FAST001`)
- [`fast-api-non-annotated-dependency`](https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/) (`FAST002`)
- [`dict-index-missing-items`](https://docs.astral.sh/ruff/rules/dict-index-missing-items/) (`PLC0206`)
- [`pep484-style-positional-only-argument`](https://docs.astral.sh/ruff/rules/pep484-style-positional-only-argument/) (`PYI063`)
- [`redundant-final-literal`](https://docs.astral.sh/ruff/rules/redundant-final-literal/) (`PYI064`)
- [`bad-version-info-order`](https://docs.astral.sh/ruff/rules/bad-version-info-order/) (`PYI066`)
- [`parenthesize-chained-operators`](https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/) (`RUF021`)
- [`unsorted-dunder-all`](https://docs.astral.sh/ruff/rules/unsorted-dunder-all/) (`RUF022`)
- [`unsorted-dunder-slots`](https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/) (`RUF023`)
- [`assert-with-print-message`](https://docs.astral.sh/ruff/rules/assert-with-print-message/) (`RUF030`)
- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`)
The following behaviors have been stabilized:
- [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name/) (`E741`): Violations in stub files are now ignored. Stub authors typically don't control variable names.
- [`printf-string-formatting`](https://docs.astral.sh/ruff/rules/printf-string-formatting/) (`UP031`): Report all `printf`-like usages even if no autofix is available
The following fixes have been stabilized:
- [`zip-instead-of-pairwise`](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/) (`RUF007`)
### Preview features
- \[`flake8-datetimez`\] Exempt `min.time()` and `max.time()` (`DTZ901`) ([#14394](https://github.com/astral-sh/ruff/pull/14394))
- \[`flake8-pie`\] Mark fix as unsafe if the following statement is a string literal (`PIE790`) ([#14393](https://github.com/astral-sh/ruff/pull/14393))
- \[`flake8-pyi`\] New rule `redundant-none-literal` (`PYI061`) ([#14316](https://github.com/astral-sh/ruff/pull/14316))
- \[`flake8-pyi`\] Add autofix for `redundant-numeric-union` (`PYI041`) ([#14273](https://github.com/astral-sh/ruff/pull/14273))
- \[`ruff`\] New rule `map-int-version-parsing` (`RUF048`) ([#14373](https://github.com/astral-sh/ruff/pull/14373))
- \[`ruff`\] New rule `redundant-bool-literal` (`RUF038`) ([#14319](https://github.com/astral-sh/ruff/pull/14319))
- \[`ruff`\] New rule `unraw-re-pattern` (`RUF039`) ([#14446](https://github.com/astral-sh/ruff/pull/14446))
- \[`pycodestyle`\] Exempt `pytest.importorskip()` calls (`E402`) ([#14474](https://github.com/astral-sh/ruff/pull/14474))
- \[`pylint`\] Autofix suggests using sets when possible (`PLR1714`) ([#14372](https://github.com/astral-sh/ruff/pull/14372))
### Rule changes
- [`invalid-pyproject-toml`](https://docs.astral.sh/ruff/rules/invalid-pyproject-toml/) (`RUF200`): Updated to reflect the provisionally accepted [PEP 639](https://peps.python.org/pep-0639/).
- \[`flake8-pyi`\] Avoid panic in unfixable case (`PYI041`) ([#14402](https://github.com/astral-sh/ruff/pull/14402))
- \[`flake8-type-checking`\] Correctly handle quotes in subscript expression when generating an autofix ([#14371](https://github.com/astral-sh/ruff/pull/14371))
- \[`pylint`\] Suggest correct autofix for `__contains__` (`PLC2801`) ([#14424](https://github.com/astral-sh/ruff/pull/14424))
### Configuration
- Ruff now emits a warning instead of an error when a configuration [`ignore`](https://docs.astral.sh/ruff/settings/#lint_ignore)s a rule that has been removed ([#14435](https://github.com/astral-sh/ruff/pull/14435))
- Ruff now validates that `lint.flake8-import-conventions.aliases` only uses valid module names and aliases ([#14477](https://github.com/astral-sh/ruff/pull/14477))
## 0.7.4
### Preview features
- \[`flake8-datetimez`\] Detect usages of `datetime.max`/`datetime.min` (`DTZ901`) ([#14288](https://github.com/astral-sh/ruff/pull/14288))
- \[`flake8-logging`\] Implement `root-logger-calls` (`LOG015`) ([#14302](https://github.com/astral-sh/ruff/pull/14302))
- \[`flake8-no-pep420`\] Detect empty implicit namespace packages (`INP001`) ([#14236](https://github.com/astral-sh/ruff/pull/14236))
- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI019`) ([#14238](https://github.com/astral-sh/ruff/pull/14238))
- \[`perflint`\] Implement quick-fix for `manual-list-comprehension` (`PERF401`) ([#13919](https://github.com/astral-sh/ruff/pull/13919))
- \[`pylint`\] Implement `shallow-copy-environ` (`W1507`) ([#14241](https://github.com/astral-sh/ruff/pull/14241))
- \[`ruff`\] Implement `none-not-at-end-of-union` (`RUF036`) ([#14314](https://github.com/astral-sh/ruff/pull/14314))
- \[`ruff`\] Implementation `unsafe-markup-call` from `flake8-markupsafe` plugin (`RUF035`) ([#14224](https://github.com/astral-sh/ruff/pull/14224))
- \[`ruff`\] Report problems for `attrs` dataclasses (`RUF008`, `RUF009`) ([#14327](https://github.com/astral-sh/ruff/pull/14327))
### Rule changes
- \[`flake8-boolean-trap`\] Exclude dunder methods that define operators (`FBT001`) ([#14203](https://github.com/astral-sh/ruff/pull/14203))
- \[`flake8-pyi`\] Add "replace with `Self`" fix (`PYI034`) ([#14217](https://github.com/astral-sh/ruff/pull/14217))
- \[`flake8-pyi`\] Always autofix `duplicate-union-members` (`PYI016`) ([#14270](https://github.com/astral-sh/ruff/pull/14270))
- \[`flake8-pyi`\] Improve autofix for nested and mixed type unions for `unnecessary-type-union` (`PYI055`) ([#14272](https://github.com/astral-sh/ruff/pull/14272))
- \[`flake8-pyi`\] Mark fix as unsafe when type annotation contains comments for `duplicate-literal-member` (`PYI062`) ([#14268](https://github.com/astral-sh/ruff/pull/14268))
### Server
- Use the current working directory to resolve settings from `ruff.configuration` ([#14352](https://github.com/astral-sh/ruff/pull/14352))
### Bug fixes
- Avoid conflicts between `PLC014` (`useless-import-alias`) and `I002` (`missing-required-import`) by considering `lint.isort.required-imports` for `PLC014` ([#14287](https://github.com/astral-sh/ruff/pull/14287))
- \[`flake8-type-checking`\] Skip quoting annotation if it becomes invalid syntax (`TCH001`)
- \[`flake8-pyi`\] Avoid using `typing.Self` in stub files pre-Python 3.11 (`PYI034`) ([#14230](https://github.com/astral-sh/ruff/pull/14230))
- \[`flake8-pytest-style`\] Flag `pytest.raises` call with keyword argument `expected_exception` (`PT011`) ([#14298](https://github.com/astral-sh/ruff/pull/14298))
- \[`flake8-simplify`\] Infer "unknown" truthiness for literal iterables whose items are all unpacks (`SIM222`) ([#14263](https://github.com/astral-sh/ruff/pull/14263))
- \[`flake8-type-checking`\] Fix false positives for `typing.Annotated` (`TCH001`) ([#14311](https://github.com/astral-sh/ruff/pull/14311))
- \[`pylint`\] Allow `await` at the top-level scope of a notebook (`PLE1142`) ([#14225](https://github.com/astral-sh/ruff/pull/14225))
- \[`pylint`\] Fix miscellaneous issues in `await-outside-async` detection (`PLE1142`) ([#14218](https://github.com/astral-sh/ruff/pull/14218))
- \[`pyupgrade`\] Avoid applying PEP 646 rewrites in invalid contexts (`UP044`) ([#14234](https://github.com/astral-sh/ruff/pull/14234))
- \[`pyupgrade`\] Detect permutations in redundant open modes (`UP015`) ([#14255](https://github.com/astral-sh/ruff/pull/14255))
- \[`refurb`\] Avoid triggering `hardcoded-string-charset` for reordered sets (`FURB156`) ([#14233](https://github.com/astral-sh/ruff/pull/14233))
- \[`refurb`\] Further special cases added to `verbose-decimal-constructor` (`FURB157`) ([#14216](https://github.com/astral-sh/ruff/pull/14216))
- \[`refurb`\] Use `UserString` instead of non-existent `UserStr` (`FURB189`) ([#14209](https://github.com/astral-sh/ruff/pull/14209))
- \[`ruff`\] Avoid treating lowercase letters as `# noqa` codes (`RUF100`) ([#14229](https://github.com/astral-sh/ruff/pull/14229))
- \[`ruff`\] Do not report when `Optional` has no type arguments (`RUF013`) ([#14181](https://github.com/astral-sh/ruff/pull/14181))
### Documentation
- Add "Notebook behavior" section for `F704`, `PLE1142` ([#14266](https://github.com/astral-sh/ruff/pull/14266))
- Document comment policy around fix safety ([#14300](https://github.com/astral-sh/ruff/pull/14300))
## 0.7.3
### Preview features

147
Cargo.lock generated
View File

@@ -170,6 +170,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.22.0"
@@ -207,10 +213,16 @@ dependencies = [
]
[[package]]
name = "bstr"
version = "1.10.0"
name = "boxcar"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
[[package]]
name = "bstr"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
dependencies = [
"memchr",
"regex-automata 0.4.8",
@@ -341,9 +353,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.20"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
dependencies = [
"clap_builder",
"clap_derive",
@@ -351,9 +363,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.20"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
dependencies = [
"anstream",
"anstyle",
@@ -829,6 +841,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.17"
@@ -1307,16 +1325,16 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.8"
version = "0.17.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
dependencies = [
"console",
"instant",
"number_prefix",
"portable-atomic",
"unicode-width 0.1.13",
"unicode-width 0.2.0",
"vt100",
"web-time",
]
[[package]]
@@ -1358,6 +1376,7 @@ dependencies = [
"pest",
"pest_derive",
"regex",
"ron",
"serde",
"similar",
"walkdir",
@@ -1501,9 +1520,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.162"
version = "0.2.164"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
[[package]]
name = "libcst"
@@ -1922,18 +1941,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
[[package]]
name = "pep440_rs"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c29f9c43de378b4e4e0cd7dbcce0e5cfb80443de8c05620368b2948bc936a1"
dependencies = [
"once_cell",
"regex",
"serde",
"unicode-width 0.1.13",
]
[[package]]
name = "pep440_rs"
version = "0.7.2"
@@ -1943,22 +1950,29 @@ dependencies = [
"serde",
"unicode-width 0.2.0",
"unscanny",
"version-ranges",
]
[[package]]
name = "pep508_rs"
version = "0.3.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "910c513bea0f4f833122321c0f20e8c704e01de98692f6989c2ec21f43d88b1e"
checksum = "8c2feee999fa547bacab06a4881bacc74688858b92fa8ef1e206c748b0a76048"
dependencies = [
"boxcar",
"indexmap",
"itertools 0.13.0",
"once_cell",
"pep440_rs 0.4.0",
"pep440_rs",
"regex",
"rustc-hash 2.0.0",
"serde",
"smallvec",
"thiserror 1.0.67",
"tracing",
"unicode-width 0.1.13",
"unicode-width 0.2.0",
"url",
"urlencoding",
"version-ranges",
]
[[package]]
@@ -2122,14 +2136,15 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.9.0"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95c3dd745f99aa3c554b7bb00859f7d18c2f1d6afd749ccc86d60b61e702abd9"
checksum = "643af57c3f36ba90a8b53e972727d8092f7408a9ebfbaf4c3d2c17b07c58d835"
dependencies = [
"indexmap",
"pep440_rs 0.4.0",
"pep440_rs",
"pep508_rs",
"serde",
"thiserror 1.0.67",
"toml",
]
@@ -2269,6 +2284,7 @@ dependencies = [
"ruff_text_size",
"rustc-hash 2.0.0",
"salsa",
"serde",
"smallvec",
"static_assertions",
"tempfile",
@@ -2353,7 +2369,10 @@ version = "0.0.0"
dependencies = [
"anyhow",
"crossbeam",
"glob",
"insta",
"notify",
"pep440_rs",
"rayon",
"red_knot_python_semantic",
"red_knot_vendored",
@@ -2363,7 +2382,9 @@ dependencies = [
"ruff_text_size",
"rustc-hash 2.0.0",
"salsa",
"tempfile",
"serde",
"thiserror 2.0.3",
"toml",
"tracing",
]
@@ -2455,9 +2476,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "ron"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
"serde",
]
[[package]]
name = "ruff"
version = "0.7.3"
version = "0.8.0"
dependencies = [
"anyhow",
"argfile",
@@ -2556,7 +2588,9 @@ dependencies = [
"camino",
"countme",
"dashmap 6.1.0",
"dunce",
"filetime",
"glob",
"ignore",
"insta",
"matchit",
@@ -2642,7 +2676,7 @@ dependencies = [
"serde",
"static_assertions",
"tracing",
"unicode-width 0.1.13",
"unicode-width 0.2.0",
]
[[package]]
@@ -2674,7 +2708,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.7.3"
version = "0.8.0"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2697,7 +2731,7 @@ dependencies = [
"natord",
"path-absolutize",
"pathdiff",
"pep440_rs 0.7.2",
"pep440_rs",
"pyproject-toml",
"quick-junit",
"regex",
@@ -2728,7 +2762,7 @@ dependencies = [
"toml",
"typed-arena",
"unicode-normalization",
"unicode-width 0.1.13",
"unicode-width 0.2.0",
"unicode_names2",
"url",
]
@@ -2778,7 +2812,6 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.0.0",
"salsa",
"schemars",
"serde",
]
@@ -2990,7 +3023,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.7.3"
version = "0.8.0"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3029,7 +3062,7 @@ dependencies = [
"matchit",
"path-absolutize",
"path-slash",
"pep440_rs 0.7.2",
"pep440_rs",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -3039,6 +3072,7 @@ dependencies = [
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_source_file",
"rustc-hash 2.0.0",
"schemars",
@@ -3218,9 +3252,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.214"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
@@ -3238,9 +3272,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.214"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
@@ -3260,9 +3294,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.132"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
@@ -3907,7 +3941,7 @@ version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a"
dependencies = [
"base64",
"base64 0.22.0",
"flate2",
"log",
"once_cell",
@@ -3929,6 +3963,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf16_iter"
version = "1.0.5"
@@ -3976,6 +4016,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version-ranges"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8d079415ceb2be83fc355adbadafe401307d5c309c7e6ade6638e6f9f42f42d"
dependencies = [
"smallvec",
]
[[package]]
name = "version_check"
version = "0.9.4"

View File

@@ -66,6 +66,7 @@ criterion = { version = "0.5.1", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.3.0" }
dunce = { version = "1.0.5" }
drop_bomb = { version = "0.1.5" }
env_logger = { version = "0.11.0" }
etcetera = { version = "0.8.0" }
@@ -81,7 +82,7 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indexmap = {version = "2.6.0" }
indexmap = { version = "2.6.0" }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1" }
@@ -110,7 +111,7 @@ pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.7.1" }
pretty_assertions = "1.3.0"
proc-macro2 = { version = "1.0.79" }
pyproject-toml = { version = "0.9.0" }
pyproject-toml = { version = "0.13.4" }
quick-junit = { version = "0.5.0" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
@@ -150,7 +151,7 @@ tracing-tree = { version = "0.4.0" }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.1.11" }
unicode-width = { version = "0.2.0" }
unicode_names2 = { version = "1.2.2" }
unicode-normalization = { version = "0.1.23" }
ureq = { version = "2.9.6" }
@@ -247,10 +248,10 @@ debug = 1
[profile.dist]
inherits = "release"
# Config for 'cargo dist'
# Config for 'dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.22.1"
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.25.2-prerelease.3"
# CI backends to support
ci = "github"
# The installers to generate for each app
@@ -281,13 +282,13 @@ targets = [
]
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
auto-includes = false
# Whether cargo-dist should create a GitHub Release or use an existing draft
# Whether dist should create a Github Release or use an existing draft
create-release = true
# Which actions to run on pull requests
pr-run-mode = "skip"
# Whether CI should trigger releases with dispatches instead of tag pushes
dispatch-releases = true
# Which phase cargo-dist should use to create the GitHub release
# Which phase dist should use to create the GitHub release
github-release = "announce"
# Whether CI should include auto-generated code to build local artifacts
build-local-artifacts = false
@@ -296,14 +297,10 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"]
# Publish jobs to run in CI
publish-jobs = ["./publish-pypi", "./publish-wasm"]
# Post-announce jobs to run in CI
post-announce-jobs = [
"./notify-dependents",
"./publish-docs",
"./publish-playground",
]
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
# Custom permissions for GitHub Jobs
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
# Whether to install an updater program
install-updater = false
# Path that installers should place binaries in
install-path = "CARGO_HOME"
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]

View File

@@ -136,8 +136,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.7.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.7.3/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.8.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -170,7 +170,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.7.3
rev: v0.8.0
hooks:
# Run the linter.
- id: ruff
@@ -238,8 +238,8 @@ exclude = [
line-length = 88
indent-width = 4
# Assume Python 3.8
target-version = "py38"
# Assume Python 3.9
target-version = "py39"
[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.

View File

@@ -1,6 +1,11 @@
[files]
# https://github.com/crate-ci/typos/issues/868
extend-exclude = ["crates/red_knot_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*"]
extend-exclude = [
"crates/red_knot_vendored/vendor/**/*",
"**/resources/**/*",
"**/snapshots/**/*",
"crates/red_knot_workspace/src/workspace/pyproject/package_name.rs"
]
[default.extend-words]
"arange" = "arange" # e.g. `numpy.arange`

View File

@@ -34,6 +34,7 @@ tracing-tree = { workspace = true }
[dev-dependencies]
filetime = { workspace = true }
tempfile = { workspace = true }
ruff_db = { workspace = true, features = ["testing"] }
[lints]
workspace = true

View File

@@ -183,10 +183,10 @@ fn run() -> anyhow::Result<ExitStatus> {
let system = OsSystem::new(cwd.clone());
let cli_configuration = args.to_configuration(&cwd);
let workspace_metadata = WorkspaceMetadata::from_path(
let workspace_metadata = WorkspaceMetadata::discover(
system.current_directory(),
&system,
Some(cli_configuration.clone()),
Some(&cli_configuration),
)?;
// TODO: Use the `program_settings` to compute the key for the database's persistent

View File

@@ -4,8 +4,8 @@
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
pub enum TargetVersion {
Py37,
#[default]
Py38,
#[default]
Py39,
Py310,
Py311,
@@ -46,3 +46,17 @@ impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
}
}
}
#[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

@@ -6,7 +6,7 @@ use std::time::Duration;
use anyhow::{anyhow, Context};
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
use red_knot_workspace::db::RootDatabase;
use red_knot_workspace::db::{Db, RootDatabase};
use red_knot_workspace::watch;
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
@@ -14,6 +14,7 @@ use red_knot_workspace::workspace::WorkspaceMetadata;
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::testing::setup_logging;
use ruff_db::Upcast;
struct TestCase {
@@ -69,7 +70,6 @@ impl TestCase {
Some(all_events)
}
#[cfg(unix)]
fn take_watch_changes(&self) -> Vec<watch::ChangeEvent> {
self.try_take_watch_changes(Duration::from_secs(10))
.expect("Expected watch changes but observed none")
@@ -110,8 +110,8 @@ impl TestCase {
) -> anyhow::Result<()> {
let program = Program::get(self.db());
self.configuration.search_paths = configuration.clone();
let new_settings = configuration.into_settings(self.db.workspace().root(&self.db));
let new_settings = configuration.to_settings(self.db.workspace().root(&self.db));
self.configuration.search_paths = configuration;
program.update_search_paths(&mut self.db, &new_settings)?;
@@ -204,7 +204,9 @@ where
.as_utf8_path()
.canonicalize_utf8()
.with_context(|| "Failed to canonicalize root path.")?,
);
)
.simplified()
.to_path_buf();
let workspace_path = root_path.join("workspace");
@@ -241,8 +243,7 @@ where
search_paths,
};
let workspace =
WorkspaceMetadata::from_path(&workspace_path, &system, Some(configuration.clone()))?;
let workspace = WorkspaceMetadata::discover(&workspace_path, &system, Some(&configuration))?;
let db = RootDatabase::new(workspace, system)?;
@@ -1311,3 +1312,138 @@ mod unix {
Ok(())
}
}
#[test]
fn nested_packages_delete_root() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, workspace_root: &SystemPath| {
std::fs::write(
workspace_root.join("pyproject.toml").as_std_path(),
r#"
[project]
name = "inner"
"#,
)?;
std::fs::write(
root.join("pyproject.toml").as_std_path(),
r#"
[project]
name = "outer"
"#,
)?;
Ok(())
})?;
assert_eq!(
case.db().workspace().root(case.db()),
&*case.workspace_path("")
);
std::fs::remove_file(case.workspace_path("pyproject.toml").as_std_path())?;
let changes = case.stop_watch();
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 _ = setup_logging();
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();
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();
case.apply_changes(changes);
assert_eq!(case.db().workspace().packages(case.db()).len(), 2);
Ok(())
}

View File

@@ -13,7 +13,8 @@ license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_index = { workspace = true }
ruff_python_ast = { workspace = true, features = ["salsa"] }
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 }
@@ -32,6 +33,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
hashbrown = { workspace = true }
serde = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
test-case = { workspace = true }

View File

@@ -0,0 +1,47 @@
# Optional
## Annotation
`typing.Optional` is equivalent to using the type with a None in a Union.
```py
from typing import Optional
a: Optional[int]
a1: Optional[bool]
a2: Optional[Optional[bool]]
a3: Optional[None]
def f():
# revealed: int | None
reveal_type(a)
# revealed: bool | None
reveal_type(a1)
# revealed: bool | None
reveal_type(a2)
# revealed: None
reveal_type(a3)
```
## Assignment
```py
from typing import Optional
a: Optional[int] = 1
a = None
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int | None`"
a = ""
```
## Typing Extensions
```py
from typing_extensions import Optional
a: Optional[int]
def f():
# revealed: int | None
reveal_type(a)
```

View File

@@ -9,10 +9,10 @@ Ts = TypeVarTuple("Ts")
def append_int(*args: *Ts) -> tuple[*Ts, int]:
# TODO: should show some representation of the variadic generic type
reveal_type(args) # revealed: @Todo
reveal_type(args) # revealed: @Todo(function parameter type)
return (*args, 1)
# TODO should be tuple[Literal[True], Literal["a"], int]
reveal_type(append_int(True, "a")) # revealed: @Todo
reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support)
```

View File

@@ -1,9 +1,191 @@
# String annotations
## Simple
```py
def f() -> "int":
return 1
# TODO: We do not support string annotations, but we should not panic if we encounter them
reveal_type(f()) # revealed: @Todo
reveal_type(f()) # revealed: int
```
## Nested
```py
def f() -> "'int'":
return 1
reveal_type(f()) # 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]
```
## Partial
```py
def f() -> tuple[int, "str"]:
return 1
reveal_type(f()) # revealed: tuple[int, str]
```
## Deferred
```py
def f() -> "Foo":
return Foo()
class Foo:
pass
reveal_type(f()) # revealed: Foo
```
## Deferred (undefined)
```py
# error: [unresolved-reference]
def f() -> "Foo":
pass
reveal_type(f()) # revealed: Unknown
```
## Partial deferred
```py
def f() -> int | "Foo":
return 1
class Foo:
pass
reveal_type(f()) # revealed: int | Foo
```
## `typing.Literal`
```py
from typing import Literal
def f1() -> Literal["Foo", "Bar"]:
return "Foo"
def f2() -> 'Literal["Foo", "Bar"]':
return "Foo"
class Foo:
pass
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
```
## 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
```
## Various string kinds in `typing.Literal`
```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"]
```
## Class variables
```py
MyType = int
class Aliases:
MyType = str
forward: "MyType"
not_forward: MyType
reveal_type(Aliases.forward) # revealed: str
reveal_type(Aliases.not_forward) # revealed: str
```
## Annotated assignment
```py
a: "int" = 1
b: "'int'" = 1
c: "Foo"
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
d: "Foo" = 1
class Foo:
pass
c = Foo()
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[1]
reveal_type(c) # revealed: Foo
reveal_type(d) # revealed: Foo
```
## Parameter
TODO: Add tests once parameter inference is supported

View File

@@ -0,0 +1,61 @@
# Union
## Annotation
`typing.Union` can be used to construct union types same as `|` operator.
```py
from typing import Union
a: Union[int, str]
a1: Union[int, bool]
a2: Union[int, Union[float, str]]
a3: Union[int, None]
a4: Union[Union[float, str]]
a5: Union[int]
a6: Union[()]
def f():
# revealed: int | str
reveal_type(a)
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
# revealed: int
reveal_type(a1)
# revealed: int | float | str
reveal_type(a2)
# revealed: int | None
reveal_type(a3)
# revealed: float | str
reveal_type(a4)
# revealed: int
reveal_type(a5)
# revealed: Never
reveal_type(a6)
```
## Assignment
```py
from typing import Union
a: Union[int, str]
a = 1
a = ""
a1: Union[int, bool]
a1 = 1
a1 = True
# error: [invalid-assignment] "Object of type `Literal[b""]` is not assignable to `int | str`"
a = b""
```
## Typing Extensions
```py
from typing_extensions import Union
a: Union[int, str]
def f():
# revealed: int | str
reveal_type(a)
```

View File

@@ -51,12 +51,12 @@ reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
# TODO: homogenous tuples, PEP-646 tuples
reveal_type(e) # revealed: @Todo
reveal_type(f) # revealed: @Todo
reveal_type(g) # revealed: @Todo
reveal_type(e) # revealed: @Todo(full tuple[...] support)
reveal_type(f) # revealed: @Todo(full tuple[...] support)
reveal_type(g) # revealed: @Todo(full tuple[...] support)
# TODO: support more kinds of type expressions in annotations
reveal_type(h) # revealed: @Todo
reveal_type(h) # revealed: @Todo(full tuple[...] support)
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]
@@ -110,3 +110,29 @@ c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = ((
# error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `tuple[tuple[int, int], int]`"
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = "foo"
```
## Future annotations are deferred
```py
from __future__ import annotations
x: Foo
class Foo:
pass
x = Foo()
reveal_type(x) # revealed: Foo
```
## Annotations in stub files are deferred
```pyi path=main.pyi
x: Foo
class Foo:
pass
x = Foo()
reveal_type(x) # revealed: Foo
```

View File

@@ -317,7 +317,7 @@ reveal_type(1 + A()) # revealed: int
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type("foo" + A()) # revealed: @Todo
reveal_type("foo" + A()) # revealed: @Todo(return type)
reveal_type(A() + b"foo") # revealed: A
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
@@ -325,7 +325,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
reveal_type(A() + ()) # revealed: A
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
reveal_type(() + A()) # revealed: @Todo
reveal_type(() + A()) # revealed: @Todo(return type)
literal_string_instance = "foo" * 1_000_000_000
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
@@ -334,7 +334,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
reveal_type(A() + literal_string_instance) # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances
# TODO overloads
reveal_type(literal_string_instance + A()) # revealed: @Todo
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
```
## Operations involving instances of classes inheriting from `Any`

View File

@@ -16,7 +16,16 @@ async def get_int_async() -> int:
return 42
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
reveal_type(get_int_async()) # revealed: @Todo
reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
```
## Generic
```py
def get_int[T]() -> int:
return 42
reveal_type(get_int()) # revealed: int
```
## Decorated
@@ -35,7 +44,7 @@ def bar() -> str:
return "bar"
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
reveal_type(bar()) # revealed: @Todo
reveal_type(bar()) # revealed: @Todo(return type)
```
## Invalid callable

View File

@@ -58,7 +58,9 @@ reveal_type(c >= d) # revealed: Literal[True]
#### Results with Ambiguity
```py
def bool_instance() -> bool: ...
def bool_instance() -> bool:
return True
def int_instance() -> int:
return 42
@@ -134,23 +136,158 @@ reveal_type(c >= c) # revealed: Literal[True]
#### Non Boolean Rich Comparisons
Rich comparison methods defined in a class affect tuple comparisons as well. Proper type inference
should be possible even in cases where these methods return non-boolean types.
Note: Tuples use lexicographic comparisons. If the `==` result for all paired elements in the tuple
is True, the comparison then considers the tuples length. Regardless of the return type of the
dunder methods, the final result can still be a boolean value.
(+cpython: For tuples, `==` and `!=` always produce boolean results, regardless of the return type
of the dunder methods.)
```py
from __future__ import annotations
class A:
def __eq__(self, o) -> str: ...
def __ne__(self, o) -> int: ...
def __lt__(self, o) -> float: ...
def __le__(self, o) -> object: ...
def __gt__(self, o) -> tuple: ...
def __ge__(self, o) -> list: ...
def __eq__(self, o: object) -> str:
return "hello"
def __ne__(self, o: object) -> bytes:
return b"world"
def __lt__(self, o: A) -> float:
return 3.14
def __le__(self, o: A) -> complex:
return complex(0.5, -0.5)
def __gt__(self, o: A) -> tuple:
return (1, 2, 3)
def __ge__(self, o: A) -> list:
return [1, 2, 3]
a = (A(), A())
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: float | Literal[False]
reveal_type(a <= a) # revealed: complex | Literal[True]
reveal_type(a > a) # revealed: tuple | Literal[False]
reveal_type(a >= a) # revealed: list | Literal[True]
# If lexicographic comparison is finished before comparing A()
b = ("1_foo", A())
c = ("2_bar", A())
reveal_type(b == c) # revealed: Literal[False]
reveal_type(b != c) # revealed: Literal[True]
reveal_type(b < c) # revealed: Literal[True]
reveal_type(b <= c) # revealed: Literal[True]
reveal_type(b > c) # revealed: Literal[False]
reveal_type(b >= c) # revealed: Literal[False]
class B:
def __lt__(self, o: B) -> set:
return set()
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")`
`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
immediately conclude that the tuples are not equal. Conversely, for `NotEq`, if any non-equal pair
exists, we can determine that the tuples are unequal.
In contrast, with operators like `<` and `>`, the comparison must consider each pair of elements
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
```py
def str_instance() -> str:
return "hello"
def int_instance() -> int:
return 42
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]
a = (str_instance(), int_instance(), "foo")
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
b = (str_instance(), int_instance(), "bar")
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
```
#### Error Propagation
Errors occurring within a tuple comparison should propagate outward. However, if the tuple
comparison can clearly conclude before encountering an error, the error should not be raised.
```py
def int_instance() -> int:
return 42
def str_instance() -> str:
return "hello"
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()
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]
```
### Membership Test Comparisons

View File

@@ -4,6 +4,8 @@
def bool_instance() -> bool:
return True
class A: ...
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
reveal_type(a) # revealed: bool
@@ -33,4 +35,8 @@ reveal_type(e) # revealed: bool
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
```

View File

@@ -41,21 +41,20 @@ except EXCEPTIONS as f:
## Dynamic exception types
```py
# TODO: we should not emit these `call-possibly-unbound-method` errors for `tuple.__class_getitem__`
def foo(
x: type[AttributeError],
y: tuple[type[OSError], type[RuntimeError]], # error: [call-possibly-unbound-method]
z: tuple[type[BaseException], ...], # error: [call-possibly-unbound-method]
y: tuple[type[OSError], type[RuntimeError]],
z: tuple[type[BaseException], ...],
):
try:
help()
except x as e:
# TODO: should be `AttributeError`
reveal_type(e) # revealed: @Todo
reveal_type(e) # revealed: @Todo(exception type)
except y as f:
# TODO: should be `OSError | RuntimeError`
reveal_type(f) # revealed: @Todo
reveal_type(f) # revealed: @Todo(exception type)
except z as g:
# TODO: should be `BaseException`
reveal_type(g) # revealed: @Todo
reveal_type(g) # revealed: @Todo(exception type)
```

View File

@@ -18,7 +18,7 @@ box: MyBox[int] = MyBox(5)
wrong_innards: MyBox[int] = MyBox("five")
# TODO reveal int
reveal_type(box.data) # revealed: @Todo
reveal_type(box.data) # revealed: @Todo(instance attributes)
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
```
@@ -39,7 +39,7 @@ class MySecureBox[T](MyBox[T]): ...
secure_box: MySecureBox[int] = MySecureBox(5)
reveal_type(secure_box) # revealed: MySecureBox
# TODO reveal int
reveal_type(secure_box.data) # revealed: @Todo
reveal_type(secure_box.data) # revealed: @Todo(instance attributes)
```
## Cyclical class definition
@@ -65,31 +65,31 @@ A PEP695 type variable defines a value of type `typing.TypeVar` with attributes
```py
def f[T, U: A, V: (A, B), W = A, X: A = A1]():
reveal_type(T) # revealed: TypeVar
reveal_type(T) # revealed: T
reveal_type(T.__name__) # revealed: Literal["T"]
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
reveal_type(T.__default__) # revealed: NoDefault
reveal_type(U) # revealed: TypeVar
reveal_type(U) # revealed: U
reveal_type(U.__name__) # revealed: Literal["U"]
reveal_type(U.__bound__) # revealed: type[A]
reveal_type(U.__constraints__) # revealed: tuple[()]
reveal_type(U.__default__) # revealed: NoDefault
reveal_type(V) # revealed: TypeVar
reveal_type(V) # revealed: V
reveal_type(V.__name__) # revealed: Literal["V"]
reveal_type(V.__bound__) # revealed: None
reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]]
reveal_type(V.__default__) # revealed: NoDefault
reveal_type(W) # revealed: TypeVar
reveal_type(W) # revealed: W
reveal_type(W.__name__) # revealed: Literal["W"]
reveal_type(W.__bound__) # revealed: None
reveal_type(W.__constraints__) # revealed: tuple[()]
reveal_type(W.__default__) # revealed: type[A]
reveal_type(X) # revealed: TypeVar
reveal_type(X) # revealed: X
reveal_type(X.__name__) # revealed: Literal["X"]
reveal_type(X.__bound__) # revealed: type[A]
reveal_type(X.__constraints__) # revealed: tuple[()]

View File

@@ -51,6 +51,8 @@ invalid1: Literal[3 + 4]
invalid2: Literal[4 + 3j]
# error: [invalid-literal-parameter]
invalid3: Literal[(3, 4)]
hello = "hello"
invalid4: Literal[
1 + 2, # error: [invalid-literal-parameter]
"foo",
@@ -76,7 +78,7 @@ from other import Literal
a1: Literal[26]
def f():
reveal_type(a1) # revealed: @Todo
reveal_type(a1) # revealed: @Todo(generics)
```
## Detecting typing_extensions.Literal

View File

@@ -18,7 +18,7 @@ async def foo():
pass
# TODO: should reveal `Unknown` because `__aiter__` is not defined
# revealed: @Todo
# revealed: @Todo(async iterables/iterators)
# error: [possibly-unresolved-reference]
reveal_type(x)
```
@@ -40,6 +40,6 @@ async def foo():
pass
# error: [possibly-unresolved-reference]
# revealed: @Todo
# revealed: @Todo(async iterables/iterators)
reveal_type(x)
```

View File

@@ -171,7 +171,7 @@ def f(*args, **kwargs) -> int: ...
class A(metaclass=f): ...
# TODO should be `type[int]`
reveal_type(A.__class__) # revealed: @Todo
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
```
## Cyclic

View File

@@ -0,0 +1,152 @@
# Narrowing for checks involving `type(x)`
## `type(x) is C`
```py
class A: ...
class B: ...
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if type(x) is A:
reveal_type(x) # revealed: A
else:
# It would be wrong to infer `B` here. The type
# of `x` could be a subclass of `A`, so we need
# to infer the full union type:
reveal_type(x) # revealed: A | B
```
## `type(x) is not C`
```py
class A: ...
class B: ...
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if type(x) is not A:
# Same reasoning as above: no narrowing should occur here.
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A
```
## `type(x) == C`, `type(x) != C`
No narrowing can occur for equality comparisons, since there might be a custom `__eq__`
implementation on the metaclass.
TODO: Narrowing might be possible in some cases where the classes themselves are `@final` or their
metaclass is `@final`.
```py
class IsEqualToEverything(type):
def __eq__(cls, other):
return True
class A(metaclass=IsEqualToEverything): ...
class B(metaclass=IsEqualToEverything): ...
def get_a_or_b() -> A | B:
return B()
x = get_a_or_b()
if type(x) == A:
reveal_type(x) # revealed: A | B
if type(x) != A:
reveal_type(x) # revealed: A | B
```
## No narrowing for custom `type` callable
```py
class A: ...
class B: ...
def type(x):
return int
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if type(x) is A:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A | B
```
## No narrowing for multiple arguments
No narrowing should occur if `type` is used to dynamically create a class:
```py
def get_str_or_int() -> str | int:
return "test"
x = get_str_or_int()
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
else:
reveal_type(x) # revealed: str | int
```
## No narrowing for keyword arguments
`type` can't be used with a keyword argument:
```py
def get_str_or_int() -> str | int:
return "test"
x = get_str_or_int()
# TODO: we could issue a diagnostic here
if type(object=x) is str:
reveal_type(x) # revealed: str | int
```
## Narrowing if `type` is aliased
```py
class A: ...
class B: ...
alias_for_type = type
def get_a_or_b() -> A | B:
return A()
x = get_a_or_b()
if alias_for_type(x) is A:
reveal_type(x) # revealed: A
```
## Limitations
```py
class Base: ...
class Derived(Base): ...
def get_base() -> Base:
return Base()
x = get_base()
if type(x) is Base:
# Ideally, this could be narrower, but there is now way to
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
reveal_type(x) # revealed: Base
```

View File

@@ -0,0 +1,13 @@
# Regression test for #14334
Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334).
```py path=base.py
# error: [invalid-base]
class Base(2): ...
```
```py path=a.py
# No error here
from base import Base
```

View File

@@ -17,8 +17,7 @@ reveal_type(__doc__) # revealed: str | None
# (needs support for `*` imports)
reveal_type(__spec__) # revealed: Unknown | None
# TODO: generics
reveal_type(__path__) # revealed: @Todo
reveal_type(__path__) # revealed: @Todo(generics)
class X:
reveal_type(__name__) # revealed: str
@@ -64,7 +63,7 @@ reveal_type(typing.__class__) # revealed: Literal[type]
# TODO: needs support for attribute access on instances, properties and generics;
# should be `dict[str, Any]`
reveal_type(typing.__dict__) # revealed: @Todo
reveal_type(typing.__dict__) # revealed: @Todo(instance attributes)
```
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
@@ -96,8 +95,8 @@ from foo import __dict__ as foo_dict
# TODO: needs support for attribute access on instances, properties, and generics;
# should be `dict[str, Any]` for both of these:
reveal_type(foo.__dict__) # revealed: @Todo
reveal_type(foo_dict) # revealed: @Todo
reveal_type(foo.__dict__) # revealed: @Todo(instance attributes)
reveal_type(foo_dict) # revealed: @Todo(instance attributes)
```
## Conditionally global or `ModuleType` attribute

View File

@@ -27,7 +27,7 @@ def int_instance() -> int:
a = b"abcde"[int_instance()]
# TODO: Support overloads... Should be `bytes`
reveal_type(a) # revealed: @Todo
reveal_type(a) # revealed: @Todo(return type)
```
## Slices
@@ -47,11 +47,11 @@ def int_instance() -> int: ...
byte_slice1 = b[int_instance() : int_instance()]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice1) # revealed: @Todo
reveal_type(byte_slice1) # revealed: @Todo(return type)
def bytes_instance() -> bytes: ...
byte_slice2 = bytes_instance()[0:5]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice2) # revealed: @Todo
reveal_type(byte_slice2) # revealed: @Todo(return type)
```

View File

@@ -12,13 +12,13 @@ x = [1, 2, 3]
reveal_type(x) # revealed: list
# TODO reveal int
reveal_type(x[0]) # revealed: @Todo
reveal_type(x[0]) # revealed: @Todo(return type)
# TODO reveal list
reveal_type(x[0:1]) # revealed: @Todo
reveal_type(x[0:1]) # revealed: @Todo(return type)
# TODO error
reveal_type(x["a"]) # revealed: @Todo
reveal_type(x["a"]) # revealed: @Todo(return type)
```
## Assignments within list assignment

View File

@@ -23,7 +23,7 @@ def int_instance() -> int: ...
a = "abcde"[int_instance()]
# TODO: Support overloads... Should be `str`
reveal_type(a) # revealed: @Todo
reveal_type(a) # revealed: @Todo(return type)
```
## Slices
@@ -78,13 +78,13 @@ def int_instance() -> int: ...
substring1 = s[int_instance() : int_instance()]
# TODO: Support overloads... Should be `LiteralString`
reveal_type(substring1) # revealed: @Todo
reveal_type(substring1) # revealed: @Todo(return type)
def str_instance() -> str: ...
substring2 = str_instance()[0:5]
# TODO: Support overloads... Should be `str`
reveal_type(substring2) # revealed: @Todo
reveal_type(substring2) # revealed: @Todo(return type)
```
## Unsupported slice types

View File

@@ -71,5 +71,5 @@ def int_instance() -> int: ...
tuple_slice = t[int_instance() : int_instance()]
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: @Todo
reveal_type(tuple_slice) # revealed: @Todo(return type)
```

View File

@@ -22,23 +22,23 @@ type:
```py
import sys
reveal_type(sys.version_info >= (3, 8)) # revealed: Literal[True]
reveal_type((3, 8) <= sys.version_info) # revealed: Literal[True]
reveal_type(sys.version_info >= (3, 9)) # revealed: Literal[True]
reveal_type((3, 9) <= sys.version_info) # revealed: Literal[True]
reveal_type(sys.version_info > (3, 8)) # revealed: Literal[True]
reveal_type((3, 8) < sys.version_info) # revealed: Literal[True]
reveal_type(sys.version_info > (3, 9)) # revealed: Literal[True]
reveal_type((3, 9) < sys.version_info) # revealed: Literal[True]
reveal_type(sys.version_info < (3, 8)) # revealed: Literal[False]
reveal_type((3, 8) > sys.version_info) # revealed: Literal[False]
reveal_type(sys.version_info < (3, 9)) # revealed: Literal[False]
reveal_type((3, 9) > sys.version_info) # revealed: Literal[False]
reveal_type(sys.version_info <= (3, 8)) # revealed: Literal[False]
reveal_type((3, 8) >= sys.version_info) # revealed: Literal[False]
reveal_type(sys.version_info <= (3, 9)) # revealed: Literal[False]
reveal_type((3, 9) >= sys.version_info) # revealed: Literal[False]
reveal_type(sys.version_info == (3, 8)) # revealed: Literal[False]
reveal_type((3, 8) == sys.version_info) # revealed: Literal[False]
reveal_type(sys.version_info == (3, 9)) # revealed: Literal[False]
reveal_type((3, 9) == sys.version_info) # revealed: Literal[False]
reveal_type(sys.version_info != (3, 8)) # revealed: Literal[True]
reveal_type((3, 8) != sys.version_info) # revealed: Literal[True]
reveal_type(sys.version_info != (3, 9)) # revealed: Literal[True]
reveal_type((3, 9) != sys.version_info) # revealed: Literal[True]
```
## Non-literal types from comparisons
@@ -49,15 +49,16 @@ sometimes not:
```py
import sys
reveal_type(sys.version_info >= (3, 8, 1)) # revealed: bool
reveal_type(sys.version_info >= (3, 8, 1, "final", 0)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1)) # revealed: bool
reveal_type(sys.version_info >= (3, 9, 1, "final", 0)) # revealed: bool
# TODO: this is an invalid comparison (`sys.version_info` is a tuple of length 5)
# Should we issue a diagnostic here?
reveal_type(sys.version_info >= (3, 8, 1, "final", 0, 5)) # revealed: bool
# TODO: While this won't fail at runtime, the user has probably made a mistake
# if they're comparing a tuple of length >5 with `sys.version_info`
# (`sys.version_info` is a tuple of length 5). It might be worth
# emitting a lint diagnostic of some kind warning them about the probable error?
reveal_type(sys.version_info >= (3, 9, 1, "final", 0, 5)) # revealed: bool
# TODO: this should be `Literal[False]`; see #14279
reveal_type(sys.version_info == (3, 8, 1, "finallllll", 0)) # revealed: bool
reveal_type(sys.version_info == (3, 8, 1, "finallllll", 0)) # revealed: Literal[False]
```
## Imports and aliases
@@ -69,11 +70,11 @@ another name:
from sys import version_info
from sys import version_info as foo
reveal_type(version_info >= (3, 8)) # revealed: Literal[True]
reveal_type(foo >= (3, 8)) # revealed: Literal[True]
reveal_type(version_info >= (3, 9)) # revealed: Literal[True]
reveal_type(foo >= (3, 9)) # revealed: Literal[True]
bar = version_info
reveal_type(bar >= (3, 8)) # revealed: Literal[True]
reveal_type(bar >= (3, 9)) # revealed: Literal[True]
```
## Non-stdlib modules named `sys`
@@ -90,7 +91,7 @@ version_info: tuple[int, int] = (4, 2)
```py path=package/script.py
from .sys import version_info
reveal_type(version_info >= (3, 8)) # revealed: bool
reveal_type(version_info >= (3, 9)) # revealed: bool
```
## Accessing fields by name
@@ -101,8 +102,8 @@ The fields of `sys.version_info` can be accessed by name:
import sys
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 8) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 9) # revealed: Literal[False]
reveal_type(sys.version_info.minor >= 9) # revealed: Literal[True]
reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
```
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
@@ -111,9 +112,9 @@ properties on instance types:
```py path=b.py
import sys
reveal_type(sys.version_info.micro) # revealed: @Todo
reveal_type(sys.version_info.releaselevel) # revealed: @Todo
reveal_type(sys.version_info.serial) # revealed: @Todo
reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes)
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes)
reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes)
```
## Accessing fields by index/slice
@@ -124,14 +125,14 @@ The fields of `sys.version_info` can be accessed by index or by slice:
import sys
reveal_type(sys.version_info[0] < 3) # revealed: Literal[False]
reveal_type(sys.version_info[1] > 8) # revealed: Literal[False]
reveal_type(sys.version_info[1] > 9) # revealed: Literal[False]
# revealed: tuple[Literal[3], Literal[8], int, Literal["alpha", "beta", "candidate", "final"], int]
# revealed: tuple[Literal[3], Literal[9], int, Literal["alpha", "beta", "candidate", "final"], int]
reveal_type(sys.version_info[:5])
reveal_type(sys.version_info[:2] >= (3, 8)) # revealed: Literal[True]
reveal_type(sys.version_info[0:2] >= (3, 9)) # revealed: Literal[False]
reveal_type(sys.version_info[:3] >= (3, 9, 1)) # revealed: Literal[False]
reveal_type(sys.version_info[:2] >= (3, 9)) # revealed: Literal[True]
reveal_type(sys.version_info[0:2] >= (3, 10)) # revealed: Literal[False]
reveal_type(sys.version_info[:3] >= (3, 10, 1)) # revealed: Literal[False]
reveal_type(sys.version_info[3] == "final") # revealed: bool
reveal_type(sys.version_info[3] == "finalllllll") # revealed: Literal[False]
```

View File

@@ -0,0 +1,71 @@
# Type aliases
## Basic
```py
type IntOrStr = int | str
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
x: IntOrStr = 1
reveal_type(x) # revealed: Literal[1]
def f() -> None:
reveal_type(x) # revealed: int | str
```
## `__value__` attribute
```py
type IntOrStr = int | str
# TODO: This should either fall back to the specified type from typeshed,
# which is `Any`, or be the actual type of the runtime value expression
# `int | str`, i.e. `types.UnionType`.
reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes)
```
## Invalid assignment
```py
type OptionalInt = int | None
# error: [invalid-assignment]
x: OptionalInt = "1"
```
## Type aliases in type aliases
```py
type IntOrStr = int | str
type IntOrStrOrBytes = IntOrStr | bytes
x: IntOrStrOrBytes = 1
def f() -> None:
reveal_type(x) # revealed: int | str | bytes
```
## Aliased type aliases
```py
type IntOrStr = int | str
MyIntOrStr = IntOrStr
x: MyIntOrStr = 1
# error: [invalid-assignment]
y: MyIntOrStr = None
```
## Generic type aliases
```py
type ListOrSet[T] = list[T] | set[T]
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
# as specified in the `typeshed` stubs.
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes)
```

View File

@@ -1,4 +1,6 @@
# Unary Operations
# Invert, UAdd, USub
## Instance
```py
from typing import Literal

View File

@@ -113,3 +113,101 @@ reveal_type(not ()) # revealed: Literal[True]
reveal_type(not ("hello",)) # revealed: Literal[False]
reveal_type(not (1, "hello")) # revealed: Literal[False]
```
## Instance
Not operator is inferred based on
<https://docs.python.org/3/library/stdtypes.html#truth-value-testing>. An instance is True or False
if the `__bool__` method says so.
At runtime, the `__len__` method is a fallback for `__bool__`, but we can't make use of that. If we
have a class that defines `__len__` but not `__bool__`, it is possible that any subclass could add a
`__bool__` method that would invalidate whatever conclusion we drew from `__len__`. So instances of
classes without a `__bool__` method, with or without `__len__`, must be inferred as unknown
truthiness.
```py
class AlwaysTrue:
def __bool__(self) -> Literal[True]:
return True
# revealed: Literal[False]
reveal_type(not AlwaysTrue())
class AlwaysFalse:
def __bool__(self) -> Literal[False]:
return False
# revealed: Literal[True]
reveal_type(not AlwaysFalse())
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
class BoolIsBool:
__bool__ = bool
# revealed: bool
reveal_type(not BoolIsBool())
# At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because
# a subclass could add a `__bool__` method.
class NoBoolMethod: ...
# revealed: bool
reveal_type(not NoBoolMethod())
# And we can't rely on `__len__` for the same reason: a subclass could add `__bool__`.
class LenZero:
def __len__(self) -> Literal[0]:
return 0
# revealed: bool
reveal_type(not LenZero())
class LenNonZero:
def __len__(self) -> Literal[1]:
return 1
# revealed: bool
reveal_type(not LenNonZero())
class WithBothLenAndBool1:
def __bool__(self) -> Literal[False]:
return False
def __len__(self) -> Literal[2]:
return 2
# revealed: Literal[True]
reveal_type(not WithBothLenAndBool1())
class WithBothLenAndBool2:
def __bool__(self) -> Literal[True]:
return True
def __len__(self) -> Literal[0]:
return 0
# revealed: Literal[False]
reveal_type(not WithBothLenAndBool2())
# TODO: raise diagnostic when __bool__ method is not valid: [unsupported-operator] "Method __bool__ for type `MethodBoolInvalid` should return `bool`, returned type `int`"
# https://docs.python.org/3/reference/datamodel.html#object.__bool__
class MethodBoolInvalid:
def __bool__(self) -> int:
return 0
# revealed: bool
reveal_type(not MethodBoolInvalid())
# Don't trust a possibly-unbound `__bool__` method:
def get_flag() -> bool:
return True
class PossiblyUnboundBool:
if get_flag():
def __bool__(self) -> Literal[False]:
return False
# revealed: bool
reveal_type(not PossiblyUnboundBool())
```

View File

@@ -84,7 +84,7 @@ reveal_type(b) # revealed: Literal[2]
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[2]
reveal_type(d) # revealed: Unknown
```
@@ -95,7 +95,7 @@ reveal_type(d) # revealed: Unknown
[a, *b, c] = (1, 2)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[2]
```
@@ -105,7 +105,7 @@ reveal_type(c) # revealed: Literal[2]
[a, *b, c] = (1, 2, 3)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[3]
```
@@ -115,7 +115,7 @@ reveal_type(c) # revealed: Literal[3]
[a, *b, c, d] = (1, 2, 3, 4, 5, 6)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: Literal[5]
reveal_type(d) # revealed: Literal[6]
```
@@ -127,7 +127,7 @@ reveal_type(d) # revealed: Literal[6]
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(c) # revealed: @Todo
reveal_type(c) # revealed: @Todo(starred unpacking)
```
### Starred expression (6)
@@ -138,7 +138,7 @@ reveal_type(c) # revealed: @Todo
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: @Todo
reveal_type(d) # revealed: @Todo(starred unpacking)
reveal_type(e) # revealed: Unknown
reveal_type(f) # revealed: Unknown
```
@@ -222,7 +222,7 @@ reveal_type(b) # revealed: LiteralString
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
reveal_type(d) # revealed: Unknown
```
@@ -233,7 +233,7 @@ reveal_type(d) # revealed: Unknown
(a, *b, c) = "ab"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[Any] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
```
@@ -243,7 +243,7 @@ reveal_type(c) # revealed: LiteralString
(a, *b, c) = "abc"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
```
@@ -253,7 +253,7 @@ reveal_type(c) # revealed: LiteralString
(a, *b, c, d) = "abcdef"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
reveal_type(b) # revealed: @Todo
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: LiteralString
reveal_type(d) # revealed: LiteralString
```
@@ -265,5 +265,5 @@ reveal_type(d) # revealed: LiteralString
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
# TODO: Should be list[int] once support for assigning to starred expression is added
reveal_type(c) # revealed: @Todo
reveal_type(c) # revealed: @Todo(starred unpacking)
```

View File

@@ -17,5 +17,5 @@ class Manager:
async def test():
async with Manager() as f:
reveal_type(f) # revealed: @Todo
reveal_type(f) # revealed: @Todo(async with statement)
```

View File

@@ -459,11 +459,11 @@ foo: 3.8- # trailing comment
";
let parsed_versions = TypeshedVersions::from_str(VERSIONS).unwrap();
assert_eq!(parsed_versions.len(), 3);
assert_snapshot!(parsed_versions.to_string(), @r###"
assert_snapshot!(parsed_versions.to_string(), @r"
bar: 2.7-3.10
bar.baz: 3.1-3.9
foo: 3.8-
"###
"
);
}

View File

@@ -1,14 +1,14 @@
use ruff_python_ast::{AnyNodeRef, NodeKind};
use ruff_text_size::{Ranged, TextRange};
use ruff_python_ast::AnyNodeRef;
/// Compact key for a node for use in a hash map.
///
/// Compares two nodes by their kind and text range.
/// Stores the memory address of the node, because using the range and the kind
/// of the node is not enough to uniquely identify them in ASTs resulting from
/// invalid syntax. For example, parsing the input `for` results in a `StmtFor`
/// AST node where both the `target` and the `iter` field are `ExprName` nodes
/// with the same (empty) range `3..3`.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub(super) struct NodeKey {
kind: NodeKind,
range: TextRange,
}
pub(super) struct NodeKey(usize);
impl NodeKey {
pub(super) fn from_node<'a, N>(node: N) -> Self
@@ -16,9 +16,6 @@ impl NodeKey {
N: Into<AnyNodeRef<'a>>,
{
let node = node.into();
NodeKey {
kind: node.kind(),
range: node.range(),
}
NodeKey(node.as_ptr().as_ptr() as usize)
}
}

View File

@@ -54,6 +54,7 @@ impl Program {
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ProgramSettings {
pub target_version: PythonVersion,
pub search_paths: SearchPathSettings,
@@ -61,6 +62,7 @@ pub struct ProgramSettings {
/// Configures the search paths for module resolution.
#[derive(Eq, PartialEq, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SearchPathSettings {
/// List of user-provided paths that should take first priority in the module resolution.
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
@@ -91,6 +93,7 @@ impl SearchPathSettings {
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum SitePackages {
Derived {
venv_path: SystemPathBuf,

View File

@@ -5,6 +5,7 @@ use std::fmt;
/// Unlike the `TargetVersion` enums in the CLI crates,
/// this does not necessarily represent a Python version that we actually support.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct PythonVersion {
pub major: u8,
pub minor: u8,
@@ -38,7 +39,7 @@ impl PythonVersion {
impl Default for PythonVersion {
fn default() -> Self {
Self::PY38
Self::PY39
}
}

View File

@@ -49,64 +49,50 @@ fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds {
semantic_index(db, scope.file(db)).ast_ids(scope.file_scope_id(db))
}
pub trait HasScopedUseId {
/// The type of the ID uniquely identifying the use.
type Id: Copy;
/// Returns the ID that uniquely identifies the use in `scope`.
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id;
}
/// Uniquely identifies a use of a name in a [`crate::semantic_index::symbol::FileScopeId`].
#[newtype_index]
pub struct ScopedUseId;
impl HasScopedUseId for ast::ExprName {
type Id = ScopedUseId;
pub trait HasScopedUseId {
/// Returns the ID that uniquely identifies the use in `scope`.
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId;
}
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
impl HasScopedUseId for ast::ExprName {
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
let expression_ref = ExpressionRef::from(self);
expression_ref.scoped_use_id(db, scope)
}
}
impl HasScopedUseId for ast::ExpressionRef<'_> {
type Id = ScopedUseId;
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId {
let ast_ids = ast_ids(db, scope);
ast_ids.use_id(*self)
}
}
pub trait HasScopedAstId {
/// The type of the ID uniquely identifying the node.
type Id: Copy;
/// Returns the ID that uniquely identifies the node in `scope`.
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id;
}
impl<T: HasScopedAstId> HasScopedAstId for Box<T> {
type Id = <T as HasScopedAstId>::Id;
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
self.as_ref().scoped_ast_id(db, scope)
}
}
/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`].
#[newtype_index]
pub struct ScopedExpressionId;
pub trait HasScopedExpressionId {
/// Returns the ID that uniquely identifies the node in `scope`.
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId;
}
impl<T: HasScopedExpressionId> HasScopedExpressionId for Box<T> {
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
self.as_ref().scoped_expression_id(db, scope)
}
}
macro_rules! impl_has_scoped_expression_id {
($ty: ty) => {
impl HasScopedAstId for $ty {
type Id = ScopedExpressionId;
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
impl HasScopedExpressionId for $ty {
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
let expression_ref = ExpressionRef::from(self);
expression_ref.scoped_ast_id(db, scope)
expression_ref.scoped_expression_id(db, scope)
}
}
};
@@ -146,29 +132,20 @@ impl_has_scoped_expression_id!(ast::ExprSlice);
impl_has_scoped_expression_id!(ast::ExprIpyEscapeCommand);
impl_has_scoped_expression_id!(ast::Expr);
impl HasScopedAstId for ast::ExpressionRef<'_> {
type Id = ScopedExpressionId;
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
impl HasScopedExpressionId for ast::ExpressionRef<'_> {
fn scoped_expression_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedExpressionId {
let ast_ids = ast_ids(db, scope);
ast_ids.expression_id(*self)
}
}
#[derive(Debug)]
#[derive(Debug, Default)]
pub(super) struct AstIdsBuilder {
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
uses_map: FxHashMap<ExpressionNodeKey, ScopedUseId>,
}
impl AstIdsBuilder {
pub(super) fn new() -> Self {
Self {
expressions_map: FxHashMap::default(),
uses_map: FxHashMap::default(),
}
}
/// Adds `expr` to the expression ids map and returns its id.
pub(super) fn record_expression(&mut self, expr: &ast::Expr) -> ScopedExpressionId {
let expression_id = self.expressions_map.len().into();

View File

@@ -124,14 +124,15 @@ impl<'db> SemanticIndexBuilder<'db> {
self.try_node_context_stack_manager.enter_nested_scope();
let file_scope_id = self.scopes.push(scope);
self.symbol_tables.push(SymbolTableBuilder::new());
self.use_def_maps.push(UseDefMapBuilder::new());
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::new());
self.symbol_tables.push(SymbolTableBuilder::default());
self.use_def_maps.push(UseDefMapBuilder::default());
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default());
let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default());
self.scope_ids_by_scope.push(scope_id);
self.scopes_by_node.insert(node.node_key(), file_scope_id);
let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id);
debug_assert_eq!(previous, None);
debug_assert_eq!(ast_id_scope, file_scope_id);
@@ -594,17 +595,17 @@ where
type_alias
.name
.as_name_expr()
.expect("type alias name is a name expr")
.id
.clone(),
.map(|name| name.id.clone())
.unwrap_or("<unknown>".into()),
);
self.add_definition(symbol, type_alias);
self.visit_expr(&type_alias.name);
self.with_type_params(
NodeWithScopeRef::TypeAliasTypeParameters(type_alias),
type_alias.type_params.as_ref(),
|builder| {
builder.push_scope(NodeWithScopeRef::TypeAliasTypeParameters(type_alias));
builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias));
builder.visit_expr(&type_alias.value);
builder.pop_scope()
},
@@ -697,9 +698,18 @@ where
if let Some(value) = &node.value {
self.visit_expr(value);
}
self.push_assignment(node.into());
self.visit_expr(&node.target);
self.pop_assignment();
// See https://docs.python.org/3/library/ast.html#ast.AnnAssign
if matches!(
*node.target,
ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_)
) {
self.push_assignment(node.into());
self.visit_expr(&node.target);
self.pop_assignment();
} else {
self.visit_expr(&node.target);
}
}
ast::Stmt::AugAssign(
aug_assign @ ast::StmtAugAssign {
@@ -711,9 +721,18 @@ where
) => {
debug_assert_eq!(&self.current_assignments, &[]);
self.visit_expr(value);
self.push_assignment(aug_assign.into());
self.visit_expr(target);
self.pop_assignment();
// See https://docs.python.org/3/library/ast.html#ast.AugAssign
if matches!(
**target,
ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_)
) {
self.push_assignment(aug_assign.into());
self.visit_expr(target);
self.pop_assignment();
} else {
self.visit_expr(target);
}
}
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
@@ -1093,9 +1112,15 @@ where
ast::Expr::Named(node) => {
// TODO walrus in comprehensions is implicitly nonlocal
self.visit_expr(&node.value);
self.push_assignment(node.into());
self.visit_expr(&node.target);
self.pop_assignment();
// See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements
if node.target.is_name_expr() {
self.push_assignment(node.into());
self.visit_expr(&node.target);
self.pop_assignment();
} else {
self.visit_expr(&node.target);
}
}
ast::Expr::Lambda(lambda) => {
if let Some(parameters) = &lambda.parameters {

View File

@@ -116,14 +116,11 @@ impl<'db> ScopeId<'db> {
// Type parameter scopes behave like function scopes in terms of name resolution; CPython
// symbol table also uses the term "function-like" for these scopes.
matches!(
self.node(db),
NodeWithScopeKind::ClassTypeParameters(_)
| NodeWithScopeKind::FunctionTypeParameters(_)
| NodeWithScopeKind::Function(_)
| NodeWithScopeKind::ListComprehension(_)
| NodeWithScopeKind::SetComprehension(_)
| NodeWithScopeKind::DictComprehension(_)
| NodeWithScopeKind::GeneratorExpression(_)
self.node(db).scope_kind(),
ScopeKind::Annotation
| ScopeKind::Function
| ScopeKind::TypeAlias
| ScopeKind::Comprehension
)
}
@@ -142,13 +139,14 @@ impl<'db> ScopeId<'db> {
NodeWithScopeKind::Class(class) | NodeWithScopeKind::ClassTypeParameters(class) => {
class.name.as_str()
}
NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias
NodeWithScopeKind::Function(function)
| NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(),
NodeWithScopeKind::TypeAlias(type_alias)
| NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias
.name
.as_name_expr()
.map(|name| name.id.as_str())
.unwrap_or("<type alias>"),
NodeWithScopeKind::Function(function)
| NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(),
NodeWithScopeKind::Lambda(_) => "<lambda>",
NodeWithScopeKind::ListComprehension(_) => "<listcomp>",
NodeWithScopeKind::SetComprehension(_) => "<setcomp>",
@@ -206,6 +204,7 @@ pub enum ScopeKind {
Class,
Function,
Comprehension,
TypeAlias,
}
impl ScopeKind {
@@ -215,7 +214,7 @@ impl ScopeKind {
}
/// Symbol table for a specific [`Scope`].
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct SymbolTable {
/// The symbols in this scope.
symbols: IndexVec<ScopedSymbolId, Symbol>,
@@ -225,13 +224,6 @@ pub struct SymbolTable {
}
impl SymbolTable {
fn new() -> Self {
Self {
symbols: IndexVec::new(),
symbols_by_name: SymbolMap::default(),
}
}
fn shrink_to_fit(&mut self) {
self.symbols.shrink_to_fit();
}
@@ -283,18 +275,12 @@ impl PartialEq for SymbolTable {
impl Eq for SymbolTable {}
#[derive(Debug)]
#[derive(Debug, Default)]
pub(super) struct SymbolTableBuilder {
table: SymbolTable,
}
impl SymbolTableBuilder {
pub(super) fn new() -> Self {
Self {
table: SymbolTable::new(),
}
}
pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
let hash = SymbolTable::hash_name(&name);
let entry = self
@@ -344,6 +330,7 @@ pub(crate) enum NodeWithScopeRef<'a> {
Lambda(&'a ast::ExprLambda),
FunctionTypeParameters(&'a ast::StmtFunctionDef),
ClassTypeParameters(&'a ast::StmtClassDef),
TypeAlias(&'a ast::StmtTypeAlias),
TypeAliasTypeParameters(&'a ast::StmtTypeAlias),
ListComprehension(&'a ast::ExprListComp),
SetComprehension(&'a ast::ExprSetComp),
@@ -366,6 +353,9 @@ impl NodeWithScopeRef<'_> {
NodeWithScopeRef::Function(function) => {
NodeWithScopeKind::Function(AstNodeRef::new(module, function))
}
NodeWithScopeRef::TypeAlias(type_alias) => {
NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias))
}
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias))
}
@@ -409,6 +399,9 @@ impl NodeWithScopeRef<'_> {
NodeWithScopeRef::ClassTypeParameters(class) => {
NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class))
}
NodeWithScopeRef::TypeAlias(type_alias) => {
NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias))
}
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias))
}
@@ -437,6 +430,7 @@ pub enum NodeWithScopeKind {
Function(AstNodeRef<ast::StmtFunctionDef>),
FunctionTypeParameters(AstNodeRef<ast::StmtFunctionDef>),
TypeAliasTypeParameters(AstNodeRef<ast::StmtTypeAlias>),
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
Lambda(AstNodeRef<ast::ExprLambda>),
ListComprehension(AstNodeRef<ast::ExprListComp>),
SetComprehension(AstNodeRef<ast::ExprSetComp>),
@@ -449,11 +443,11 @@ impl NodeWithScopeKind {
match self {
Self::Module => ScopeKind::Module,
Self::Class(_) => ScopeKind::Class,
Self::Function(_) => ScopeKind::Function,
Self::Lambda(_) => ScopeKind::Function,
Self::Function(_) | Self::Lambda(_) => ScopeKind::Function,
Self::FunctionTypeParameters(_)
| Self::ClassTypeParameters(_)
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
Self::TypeAlias(_) => ScopeKind::TypeAlias,
Self::ListComprehension(_)
| Self::SetComprehension(_)
| Self::DictComprehension(_)
@@ -474,6 +468,13 @@ impl NodeWithScopeKind {
_ => panic!("expected function"),
}
}
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
match self {
Self::TypeAlias(type_alias) => type_alias.node(),
_ => panic!("expected type alias"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
@@ -483,6 +484,7 @@ pub(crate) enum NodeWithScopeKey {
ClassTypeParameters(NodeKey),
Function(NodeKey),
FunctionTypeParameters(NodeKey),
TypeAlias(NodeKey),
TypeAliasTypeParameters(NodeKey),
Lambda(NodeKey),
ListComprehension(NodeKey),

View File

@@ -459,10 +459,6 @@ pub(super) struct UseDefMapBuilder<'db> {
}
impl<'db> UseDefMapBuilder<'db> {
pub(super) fn new() -> Self {
Self::default()
}
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
let new_symbol = self.symbol_states.push(SymbolState::undefined());
debug_assert_eq!(symbol, new_symbol);

View File

@@ -6,7 +6,7 @@ use ruff_source_file::LineIndex;
use crate::module_name::ModuleName;
use crate::module_resolver::{resolve_module, Module};
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::semantic_index;
use crate::types::{binding_ty, infer_scope_types, Type};
use crate::Db;
@@ -54,7 +54,7 @@ impl HasTy for ast::ExpressionRef<'_> {
let file_scope = index.expression_scope_id(*self);
let scope = file_scope.to_scope_id(model.db, model.file);
let expression_id = self.scoped_ast_id(model.db, scope);
let expression_id = self.scoped_expression_id(model.db, scope);
infer_scope_types(model.db, scope).expression_ty(expression_id)
}
}

View File

@@ -732,7 +732,20 @@ mod tests {
let system = TestSystem::default();
assert!(matches!(
VirtualEnvironment::new("/.venv", &system),
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(_))
Err(SitePackagesDiscoveryError::VenvDirCanonicalizationError(..))
));
}
#[test]
fn reject_venv_that_is_not_a_directory() {
let system = TestSystem::default();
system
.memory_file_system()
.write_file("/.venv", "")
.unwrap();
assert!(matches!(
VirtualEnvironment::new("/.venv", &system),
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(..))
));
}

View File

@@ -6,7 +6,15 @@ use itertools::Itertools;
use ruff_db::files::File;
use ruff_python_ast as ast;
use crate::semantic_index::ast_ids::HasScopedAstId;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
};
pub(crate) use self::signatures::Signature;
use crate::module_resolver::file_to_module;
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
use crate::semantic_index::{
@@ -20,14 +28,7 @@ use crate::symbol::{Boundness, Symbol};
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder;
use crate::types::mro::{ClassBase, Mro, MroError, MroIterator};
use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, HasTy, Module, Program, SemanticModel};
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
pub(crate) use self::display::TypeArrayDisplay;
pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
};
use crate::{Db, FxOrderSet, Module, Program};
mod builder;
mod diagnostic;
@@ -35,6 +36,8 @@ mod display;
mod infer;
mod mro;
mod narrow;
mod signatures;
mod string_annotation;
mod unpacker;
#[salsa::tracked(return_ref)]
@@ -44,7 +47,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
tracing::debug!("Checking file '{path}'", path = file.path(db));
let index = semantic_index(db, file);
let mut diagnostics = TypeCheckDiagnostics::new();
let mut diagnostics = TypeCheckDiagnostics::default();
for scope_id in index.scope_ids() {
let result = infer_scope_types(db, scope_id);
@@ -56,7 +59,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
/// Infer the public type of a symbol (its type as seen from outside its scope).
fn symbol_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymbolId) -> Symbol<'db> {
let _span = tracing::trace_span!("symbol_ty_by_id", ?symbol).entered();
let _span = tracing::trace_span!("symbol_by_id", ?symbol).entered();
let use_def = use_def_map(db, scope);
@@ -191,6 +194,8 @@ fn declaration_ty<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db
/// Infer the type of a (possibly deferred) sub-expression of a [`Definition`].
///
/// Supports expressions that are evaluated within a type-params sub-scope.
///
/// ## Panics
/// If the given expression is not a sub-expression of the given [`Definition`].
fn definition_expression_ty<'db>(
@@ -198,12 +203,22 @@ fn definition_expression_ty<'db>(
definition: Definition<'db>,
expression: &ast::Expr,
) -> Type<'db> {
let expr_id = expression.scoped_ast_id(db, definition.scope(db));
let inference = infer_definition_types(db, definition);
if let Some(ty) = inference.try_expression_ty(expr_id) {
ty
let file = definition.file(db);
let index = semantic_index(db, file);
let file_scope = index.expression_scope_id(expression);
let scope = file_scope.to_scope_id(db, file);
let expr_id = expression.scoped_expression_id(db, scope);
if scope == definition.scope(db) {
// expression is in the definition scope
let inference = infer_definition_types(db, definition);
if let Some(ty) = inference.try_expression_ty(expr_id) {
ty
} else {
infer_deferred_types(db, definition).expression_ty(expr_id)
}
} else {
infer_deferred_types(db, definition).expression_ty(expr_id)
// expression is in a type-params sub-scope
infer_scope_types(db, scope).expression_ty(expr_id)
}
}
@@ -309,6 +324,61 @@ fn declarations_ty<'db>(
}
}
/// Meta data for `Type::Todo`, which represents a known limitation in red-knot.
#[cfg(debug_assertions)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum TodoType {
FileAndLine(&'static str, u32),
Message(&'static str),
}
#[cfg(debug_assertions)]
impl std::fmt::Display for TodoType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TodoType::FileAndLine(file, line) => write!(f, "[{file}:{line}]"),
TodoType::Message(msg) => write!(f, "({msg})"),
}
}
}
#[cfg(not(debug_assertions))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TodoType;
#[cfg(not(debug_assertions))]
impl std::fmt::Display for TodoType {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
/// Create a `Type::Todo` variant to represent a known limitation in the type system.
///
/// It can be used with a custom message (preferred): `todo_type!("PEP 604 not supported")`,
/// or simply using `todo_type!()`, which will include information about the file and line.
#[cfg(debug_assertions)]
macro_rules! todo_type {
() => {
Type::Todo(crate::types::TodoType::FileAndLine(file!(), line!()))
};
($message:literal) => {
Type::Todo(crate::types::TodoType::Message($message))
};
}
#[cfg(not(debug_assertions))]
macro_rules! todo_type {
() => {
Type::Todo(crate::types::TodoType)
};
($message:literal) => {
Type::Todo(crate::types::TodoType)
};
}
pub(crate) use todo_type;
/// Representation of a type: a set of possible values at runtime.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub enum Type<'db> {
@@ -325,7 +395,9 @@ pub enum Type<'db> {
/// General rule: `Todo` should only propagate when the presence of the input `Todo` caused the
/// output to be unknown. An output should only be `Todo` if fixing all `Todo` inputs to be not
/// `Todo` would change the output type.
Todo,
///
/// This variant should be created with the `todo_type!` macro.
Todo(TodoType),
/// The empty set of values
Never,
/// A specific function object
@@ -369,7 +441,7 @@ impl<'db> Type<'db> {
}
pub const fn is_todo(&self) -> bool {
matches!(self, Type::Todo)
matches!(self, Type::Todo(_))
}
pub const fn class_literal(class: Class<'db>) -> Self {
@@ -515,8 +587,8 @@ impl<'db> Type<'db> {
return true;
}
match (self, target) {
(Type::Unknown | Type::Any | Type::Todo, _) => false,
(_, Type::Unknown | Type::Any | Type::Todo) => false,
(Type::Unknown | Type::Any | Type::Todo(_), _) => false,
(_, Type::Unknown | Type::Any | Type::Todo(_)) => false,
(Type::Never, _) => true,
(_, Type::Never) => false,
(_, Type::Instance(InstanceType { class }))
@@ -651,8 +723,8 @@ impl<'db> Type<'db> {
return true;
}
match (self, target) {
(Type::Unknown | Type::Any | Type::Todo, _) => true,
(_, Type::Unknown | Type::Any | Type::Todo) => true,
(Type::Unknown | Type::Any | Type::Todo(_), _) => true,
(_, Type::Unknown | Type::Any | Type::Todo(_)) => true,
(Type::Union(union), ty) => union
.elements(db)
.iter()
@@ -688,6 +760,7 @@ impl<'db> Type<'db> {
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
// we understand `sys.version_info` branches.
self == other
|| matches!((self, other), (Type::Todo(_), Type::Todo(_)))
|| matches!((self, other),
(
Type::Instance(InstanceType { class: self_class }),
@@ -711,7 +784,7 @@ impl<'db> Type<'db> {
(Type::Any, _) | (_, Type::Any) => false,
(Type::Unknown, _) | (_, Type::Unknown) => false,
(Type::Todo, _) | (_, Type::Todo) => false,
(Type::Todo(_), _) | (_, Type::Todo(_)) => false,
(Type::Union(union), other) | (other, Type::Union(union)) => union
.elements(db)
@@ -916,7 +989,7 @@ impl<'db> Type<'db> {
Type::Any
| Type::Never
| Type::Unknown
| Type::Todo
| Type::Todo(_)
| Type::IntLiteral(..)
| Type::StringLiteral(..)
| Type::BytesLiteral(..)
@@ -992,7 +1065,10 @@ impl<'db> Type<'db> {
Type::Instance(InstanceType { class }) => match class.known(db) {
Some(
KnownClass::NoneType | KnownClass::NoDefaultType | KnownClass::VersionInfo,
KnownClass::NoneType
| KnownClass::NoDefaultType
| KnownClass::VersionInfo
| KnownClass::TypeAliasType,
) => true,
Some(
KnownClass::Bool
@@ -1019,7 +1095,7 @@ impl<'db> Type<'db> {
Type::Any
| Type::Never
| Type::Unknown
| Type::Todo
| Type::Todo(_)
| Type::Union(..)
| Type::Intersection(..)
| Type::LiteralString => false,
@@ -1037,12 +1113,12 @@ impl<'db> Type<'db> {
Type::Any => Type::Any.into(),
Type::Never => {
// TODO: attribute lookup on Never type
Type::Todo.into()
todo_type!().into()
}
Type::Unknown => Type::Unknown.into(),
Type::FunctionLiteral(_) => {
// TODO: attribute lookup on function type
Type::Todo.into()
todo_type!().into()
}
Type::ModuleLiteral(file) => {
// `__dict__` is a very special member that is never overridden by module globals;
@@ -1092,7 +1168,7 @@ impl<'db> Type<'db> {
Type::IntLiteral(Program::get(db).target_version(db).minor.into())
}
// TODO MRO? get_own_instance_member, get_instance_member
_ => Type::Todo,
_ => todo_type!("instance attributes"),
};
ty.into()
}
@@ -1134,36 +1210,36 @@ impl<'db> Type<'db> {
Type::Intersection(_) => {
// TODO perform the get_member on each type in the intersection
// TODO return the intersection of those results
Type::Todo.into()
todo_type!().into()
}
Type::IntLiteral(_) => {
// TODO raise error
Type::Todo.into()
todo_type!().into()
}
Type::BooleanLiteral(_) => Type::Todo.into(),
Type::BooleanLiteral(_) => todo_type!().into(),
Type::StringLiteral(_) => {
// TODO defer to `typing.LiteralString`/`builtins.str` methods
// from typeshed's stubs
Type::Todo.into()
todo_type!().into()
}
Type::LiteralString => {
// TODO defer to `typing.LiteralString`/`builtins.str` methods
// from typeshed's stubs
Type::Todo.into()
todo_type!().into()
}
Type::BytesLiteral(_) => {
// TODO defer to Type::Instance(<bytes from typeshed>).member
Type::Todo.into()
todo_type!().into()
}
Type::SliceLiteral(_) => {
// TODO defer to `builtins.slice` methods
Type::Todo.into()
todo_type!().into()
}
Type::Tuple(_) => {
// TODO: implement tuple methods
Type::Todo.into()
todo_type!().into()
}
Type::Todo => Type::Todo.into(),
&todo @ Type::Todo(_) => todo.into(),
}
}
@@ -1173,7 +1249,7 @@ impl<'db> Type<'db> {
/// when `bool(x)` is called on an object `x`.
fn bool(&self, db: &'db dyn Db) -> Truthiness {
match self {
Type::Any | Type::Todo | Type::Never | Type::Unknown => Truthiness::Ambiguous,
Type::Any | Type::Todo(_) | Type::Never | Type::Unknown => Truthiness::Ambiguous,
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
Type::ClassLiteral(_) => {
@@ -1185,14 +1261,40 @@ impl<'db> Type<'db> {
// TODO: see above
Truthiness::Ambiguous
}
Type::Instance(InstanceType { class }) => {
// TODO: lookup `__bool__` and `__len__` methods on the instance's class
// More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing
// For now, we only special-case some builtin classes
instance_ty @ Type::Instance(InstanceType { class }) => {
if class.is_known(db, KnownClass::NoneType) {
Truthiness::AlwaysFalse
} else {
Truthiness::Ambiguous
// We only check the `__bool__` method for truth testing, even though at
// runtime there is a fallback to `__len__`, since `__bool__` takes precedence
// and a subclass could add a `__bool__` method. We don't use
// `Type::call_dunder` here because of the need to check for `__bool__ = bool`.
// Don't trust a maybe-unbound `__bool__` method.
let Symbol::Type(bool_method, Boundness::Bound) =
instance_ty.to_meta_type(db).member(db, "__bool__")
else {
return Truthiness::Ambiguous;
};
// Check if the class has `__bool__ = bool` and avoid infinite recursion, since
// `Type::call` on `bool` will call `Type::bool` on the argument.
if bool_method
.into_class_literal()
.is_some_and(|ClassLiteralType { class }| {
class.is_known(db, KnownClass::Bool)
})
{
return Truthiness::Ambiguous;
}
if let Some(Type::BooleanLiteral(bool_val)) =
bool_method.call(db, &[*instance_ty]).return_ty(db)
{
bool_val.into()
} else {
Truthiness::Ambiguous
}
}
}
Type::KnownInstance(known_instance) => known_instance.bool(),
@@ -1233,11 +1335,11 @@ impl<'db> Type<'db> {
Type::FunctionLiteral(function_type) => {
if function_type.is_known(db, KnownFunction::RevealType) {
CallOutcome::revealed(
function_type.return_ty(db),
function_type.signature(db).return_ty,
*arg_types.first().unwrap_or(&Type::Unknown),
)
} else {
CallOutcome::callable(function_type.return_ty(db))
CallOutcome::callable(function_type.signature(db).return_ty)
}
}
@@ -1288,7 +1390,7 @@ impl<'db> Type<'db> {
// `Any` is callable, and its return type is also `Any`.
Type::Any => CallOutcome::callable(Type::Any),
Type::Todo => CallOutcome::callable(Type::Todo),
Type::Todo(_) => CallOutcome::callable(todo_type!()),
Type::Unknown => CallOutcome::callable(Type::Unknown),
@@ -1301,7 +1403,7 @@ impl<'db> Type<'db> {
),
// TODO: intersection types
Type::Intersection(_) => CallOutcome::callable(Type::Todo),
Type::Intersection(_) => CallOutcome::callable(todo_type!()),
_ => CallOutcome::not_callable(self),
}
@@ -1340,7 +1442,7 @@ impl<'db> Type<'db> {
};
}
if matches!(self, Type::Unknown | Type::Any | Type::Todo) {
if matches!(self, Type::Unknown | Type::Any | Type::Todo(_)) {
// Explicit handling of `Unknown` and `Any` necessary until `type[Unknown]` and
// `type[Any]` are not defined as `Todo` anymore.
return IterationOutcome::Iterable { element_ty: self };
@@ -1399,14 +1501,14 @@ impl<'db> Type<'db> {
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Any => Type::Any,
Type::Todo => Type::Todo,
todo @ Type::Todo(_) => *todo,
Type::Unknown => Type::Unknown,
Type::Never => Type::Never,
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class),
Type::SubclassOf(SubclassOfType { class }) => Type::instance(*class),
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
// TODO: we can probably do better here: --Alex
Type::Intersection(_) => Type::Todo,
Type::Intersection(_) => todo_type!(),
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
// since they already indicate that the object is an instance of some kind:
Type::BooleanLiteral(_)
@@ -1423,6 +1525,25 @@ impl<'db> Type<'db> {
}
}
/// If we see a value of this type used as a type expression, what type does it name?
///
/// For example, the builtin `int` as a value expression is of type
/// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type
/// expression, it names the type `Type::Instance(builtins.int)`, that is, all objects whose
/// `__class__` is `int`.
#[must_use]
pub fn in_type_expression(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::ClassLiteral(_) | Type::SubclassOf(_) => self.to_instance(db),
Type::Union(union) => union.map(db, |element| element.in_type_expression(db)),
Type::Unknown => Type::Unknown,
// TODO map this to a new `Type::TypeVar` variant
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self,
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db),
_ => todo_type!(),
}
}
/// The type `NoneType` / `None`
pub fn none(db: &'db dyn Db) -> Type<'db> {
KnownClass::NoneType.to_instance(db)
@@ -1494,8 +1615,8 @@ impl<'db> Type<'db> {
// TODO: `type[Unknown]`?
Type::Unknown => Type::Unknown,
// TODO intersections
Type::Intersection(_) => Type::Todo,
Type::Todo => Type::Todo,
Type::Intersection(_) => todo_type!(),
todo @ Type::Todo(_) => *todo,
}
}
@@ -1583,6 +1704,7 @@ pub enum KnownClass {
// Typing
SpecialForm,
TypeVar,
TypeAliasType,
NoDefaultType,
// sys
VersionInfo,
@@ -1609,6 +1731,7 @@ impl<'db> KnownClass {
Self::NoneType => "NoneType",
Self::SpecialForm => "_SpecialForm",
Self::TypeVar => "TypeVar",
Self::TypeAliasType => "TypeAliasType",
Self::NoDefaultType => "_NoDefaultType",
// This is the name the type of `sys.version_info` has in typeshed,
// which is different to what `type(sys.version_info).__name__` is at runtime.
@@ -1647,7 +1770,7 @@ impl<'db> KnownClass {
Self::VersionInfo => CoreStdlibModule::Sys,
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
Self::NoneType => CoreStdlibModule::Typeshed,
Self::SpecialForm | Self::TypeVar => CoreStdlibModule::Typing,
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing,
// TODO when we understand sys.version_info, we will need an explicit fallback here,
// because typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
// singleton, but not for `typing._NoDefaultType`
@@ -1661,7 +1784,7 @@ impl<'db> KnownClass {
const fn is_singleton(self) -> bool {
// TODO there are other singleton types (EllipsisType, NotImplementedType)
match self {
Self::NoneType | Self::NoDefaultType | Self::VersionInfo => true,
Self::NoneType | Self::NoDefaultType | Self::VersionInfo | Self::TypeAliasType => true,
Self::Bool
| Self::Object
| Self::Bytes
@@ -1682,7 +1805,7 @@ impl<'db> KnownClass {
}
}
pub fn try_from_module(module: &Module, class_name: &str) -> Option<Self> {
pub fn try_from_file(db: &dyn Db, file: File, class_name: &str) -> Option<Self> {
// Note: if this becomes hard to maintain (as rust can't ensure at compile time that all
// variants of `Self` are covered), we might use a macro (in-house or dependency)
// See: https://stackoverflow.com/q/39070244
@@ -1703,13 +1826,15 @@ impl<'db> KnownClass {
"NoneType" => Self::NoneType,
"ModuleType" => Self::ModuleType,
"FunctionType" => Self::FunctionType,
"TypeAliasType" => Self::TypeAliasType,
"_SpecialForm" => Self::SpecialForm,
"_NoDefaultType" => Self::NoDefaultType,
"_version_info" => Self::VersionInfo,
_ => return None,
};
candidate.check_module(module).then_some(candidate)
let module = file_to_module(db, file)?;
candidate.check_module(&module).then_some(candidate)
}
/// Return `true` if the module of `self` matches `module_name`
@@ -1735,7 +1860,7 @@ impl<'db> KnownClass {
| Self::VersionInfo
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
Self::SpecialForm | Self::TypeVar | Self::NoDefaultType => {
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
matches!(module.name().as_str(), "typing" | "typing_extensions")
}
}
@@ -1747,8 +1872,14 @@ impl<'db> KnownClass {
pub enum KnownInstanceType<'db> {
/// The symbol `typing.Literal` (which can also be found as `typing_extensions.Literal`)
Literal,
/// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`)
Optional,
/// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`)
Union,
/// A single instance of `typing.TypeVar`
TypeVar(TypeVarInstance<'db>),
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
TypeAliasType(TypeAliasType<'db>),
// TODO: fill this enum out with more special forms, etc.
}
@@ -1756,15 +1887,21 @@ impl<'db> KnownInstanceType<'db> {
pub const fn as_str(self) -> &'static str {
match self {
KnownInstanceType::Literal => "Literal",
KnownInstanceType::Optional => "Optional",
KnownInstanceType::Union => "Union",
KnownInstanceType::TypeVar(_) => "TypeVar",
KnownInstanceType::TypeAliasType(_) => "TypeAliasType",
}
}
/// Evaluate the known instance in boolean context
pub const fn bool(self) -> Truthiness {
match self {
Self::Literal => Truthiness::AlwaysTrue,
Self::TypeVar(_) => Truthiness::AlwaysTrue,
Self::Literal
| Self::Optional
| Self::TypeVar(_)
| Self::Union
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
}
}
@@ -1772,7 +1909,10 @@ impl<'db> KnownInstanceType<'db> {
pub fn repr(self, db: &'db dyn Db) -> &'db str {
match self {
Self::Literal => "typing.Literal",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::TypeVar(typevar) => typevar.name(db),
Self::TypeAliasType(_) => "typing.TypeAliasType",
}
}
@@ -1780,7 +1920,10 @@ impl<'db> KnownInstanceType<'db> {
pub const fn class(self) -> KnownClass {
match self {
Self::Literal => KnownClass::SpecialForm,
Self::Optional => KnownClass::SpecialForm,
Self::Union => KnownClass::SpecialForm,
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
}
}
@@ -1799,6 +1942,8 @@ impl<'db> KnownInstanceType<'db> {
}
match (module.name().as_str(), instance_name) {
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
("typing" | "typing_extensions", "Union") => Some(Self::Union),
_ => None,
}
}
@@ -1823,6 +1968,7 @@ impl<'db> KnownInstanceType<'db> {
.default_ty(db)
.map(|ty| ty.to_meta_type(db))
.unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)),
(Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)),
_ => return self.instance_fallback(db).member(db, name),
};
ty.into()
@@ -2283,7 +2429,10 @@ impl<'db> FunctionType<'db> {
self.decorators(db).contains(&decorator)
}
/// inferred return type for this function
/// Typed externally-visible signature for this function.
///
/// This is the signature as seen by external callers, possibly modified by decorators and/or
/// overloaded.
///
/// ## Why is this a salsa query?
///
@@ -2292,34 +2441,32 @@ impl<'db> FunctionType<'db> {
///
/// Were this not a salsa query, then the calling query
/// would depend on the function's AST and rerun for every change in that file.
#[salsa::tracked]
pub fn return_ty(self, db: &'db dyn Db) -> Type<'db> {
#[salsa::tracked(return_ref)]
pub fn signature(self, db: &'db dyn Db) -> Signature<'db> {
let function_stmt_node = self.body_scope(db).node(db).expect_function();
let internal_signature = self.internal_signature(db);
if function_stmt_node.decorator_list.is_empty() {
return internal_signature;
}
// TODO process the effect of decorators on the signature
Signature::todo()
}
/// Typed internally-visible signature for this function.
///
/// This represents the annotations on the function itself, unmodified by decorators and
/// overloads.
///
/// These are the parameter and return types that should be used for type checking the body of
/// the function.
///
/// Don't call this when checking any other file; only when type-checking the function body
/// scope.
fn internal_signature(self, db: &'db dyn Db) -> Signature<'db> {
let scope = self.body_scope(db);
let function_stmt_node = scope.node(db).expect_function();
// TODO if a function `bar` is decorated by `foo`,
// where `foo` is annotated as returning a type `X` that is a subtype of `Callable`,
// we need to infer the return type from `X`'s return annotation
// rather than from `bar`'s return annotation
// in order to determine the type that `bar` returns
if !function_stmt_node.decorator_list.is_empty() {
return Type::Todo;
}
function_stmt_node
.returns
.as_ref()
.map(|returns| {
if function_stmt_node.is_async {
// TODO: generic `types.CoroutineType`!
Type::Todo
} else {
let definition =
semantic_index(db, scope.file(db)).definition(function_stmt_node);
definition_expression_ty(db, definition, returns.as_ref())
}
})
.unwrap_or(Type::Unknown)
let definition = semantic_index(db, scope.file(db)).definition(function_stmt_node);
Signature::from_function(db, definition, function_stmt_node)
}
pub fn is_known(self, db: &'db dyn Db, known_function: KnownFunction) -> bool {
@@ -2422,26 +2569,13 @@ impl<'db> Class<'db> {
fn explicit_bases_query(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
let class_stmt = self.node(db);
if class_stmt.type_params.is_some() {
// when we have a specialized scope, we'll look up the inference
// within that scope
let model = SemanticModel::new(db, self.file(db));
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
class_stmt
.bases()
.iter()
.map(|base| base.ty(&model))
.collect()
} else {
// Otherwise, we can do the lookup based on the definition scope
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
class_stmt
.bases()
.iter()
.map(|base_node| definition_expression_ty(db, class_definition, base_node))
.collect()
}
class_stmt
.bases()
.iter()
.map(|base_node| definition_expression_ty(db, class_definition, base_node))
.collect()
}
fn file(self, db: &dyn Db) -> File {
@@ -2502,16 +2636,9 @@ impl<'db> Class<'db> {
.as_ref()?
.find_keyword("metaclass")?
.value;
Some(if class_stmt.type_params.is_some() {
// when we have a specialized scope, we'll look up the inference
// within that scope
let model = SemanticModel::new(db, self.file(db));
metaclass_node.ty(&model)
} else {
// Otherwise, we can do the lookup based on the definition scope
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
definition_expression_ty(db, class_definition, metaclass_node)
})
let class_definition = semantic_index(db, self.file(db)).definition(class_stmt);
let metaclass_ty = definition_expression_ty(db, class_definition, metaclass_node);
Some(metaclass_ty)
}
/// Return the metaclass of this class, or `Unknown` if the metaclass cannot be inferred.
@@ -2553,7 +2680,7 @@ impl<'db> Class<'db> {
// TODO: If the metaclass is not a class, we should verify that it's a callable
// which accepts the same arguments as `type.__new__` (otherwise error), and return
// the meta-type of its return type. (And validate that is a class type?)
return Ok(Type::Todo);
return Ok(todo_type!("metaclass not a class"));
};
// Reconcile all base classes' metaclasses with the candidate metaclass.
@@ -2667,6 +2794,27 @@ impl<'db> Class<'db> {
}
}
#[salsa::interned]
pub struct TypeAliasType<'db> {
#[return_ref]
pub name: ast::name::Name,
rhs_scope: ScopeId<'db>,
}
#[salsa::tracked]
impl<'db> TypeAliasType<'db> {
#[salsa::tracked]
pub fn value_ty(self, db: &'db dyn Db) -> Type<'db> {
let scope = self.rhs_scope(db);
let type_alias_stmt_node = scope.node(db).expect_type_alias();
let definition = semantic_index(db, scope.file(db)).definition(type_alias_stmt_node);
definition_expression_ty(db, definition, &type_alias_stmt_node.value)
}
}
/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct MetaclassCandidate<'db> {
@@ -2853,6 +3001,11 @@ impl<'db> TupleType<'db> {
}
}
// Make sure that the `Type` enum does not grow unexpectedly.
#[cfg(not(debug_assertions))]
#[cfg(target_pointer_width = "64")]
static_assertions::assert_eq_size!(Type, [u8; 16]);
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -2868,13 +3021,6 @@ pub(crate) mod tests {
use ruff_python_ast as ast;
use test_case::test_case;
#[cfg(target_pointer_width = "64")]
#[test]
fn no_bloat_enum_sizes() {
use std::mem::size_of;
assert_eq!(size_of::<Type>(), 16);
}
pub(crate) fn setup_db() -> TestDb {
let db = TestDb::new();
@@ -2928,7 +3074,7 @@ pub(crate) mod tests {
Ty::Unknown => Type::Unknown,
Ty::None => Type::none(db),
Ty::Any => Type::Any,
Ty::Todo => Type::Todo,
Ty::Todo => todo_type!("Ty::Todo"),
Ty::IntLiteral(n) => Type::IntLiteral(n),
Ty::StringLiteral(s) => Type::string_literal(db, s),
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
@@ -3519,4 +3665,95 @@ pub(crate) mod tests {
Ok(())
}
#[test]
fn type_alias_types() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/mod.py",
r#"
type Alias1 = int
type Alias2 = int
"#,
)?;
let mod_py = system_path_to_file(&db, "src/mod.py")?;
let ty_alias1 = global_symbol(&db, mod_py, "Alias1").expect_type();
let ty_alias2 = global_symbol(&db, mod_py, "Alias2").expect_type();
let Type::KnownInstance(KnownInstanceType::TypeAliasType(alias1)) = ty_alias1 else {
panic!("Expected TypeAliasType, got {ty_alias1:?}");
};
assert_eq!(alias1.name(&db), "Alias1");
assert_eq!(alias1.value_ty(&db), KnownClass::Int.to_instance(&db));
// Two type aliases are distinct and disjoint, even if they refer to the same type
assert!(!ty_alias1.is_equivalent_to(&db, ty_alias2));
assert!(ty_alias1.is_disjoint_from(&db, ty_alias2));
Ok(())
}
/// All other tests also make sure that `Type::Todo` works as expected. This particular
/// test makes sure that we handle `Todo` types correctly, even if they originate from
/// different sources.
#[test]
fn todo_types() {
let db = setup_db();
let todo1 = todo_type!("1");
let todo2 = todo_type!("2");
let todo3 = todo_type!();
let todo4 = todo_type!();
assert!(todo1.is_equivalent_to(&db, todo2));
assert!(todo3.is_equivalent_to(&db, todo4));
assert!(todo1.is_equivalent_to(&db, todo3));
assert!(todo1.is_subtype_of(&db, todo2));
assert!(todo2.is_subtype_of(&db, todo1));
assert!(todo3.is_subtype_of(&db, todo4));
assert!(todo4.is_subtype_of(&db, todo3));
assert!(todo1.is_subtype_of(&db, todo3));
assert!(todo3.is_subtype_of(&db, todo1));
let int = KnownClass::Int.to_instance(&db);
assert!(int.is_assignable_to(&db, todo1));
assert!(int.is_assignable_to(&db, todo3));
assert!(todo1.is_assignable_to(&db, int));
assert!(todo3.is_assignable_to(&db, int));
// We lose information when combining several `Todo` types. This is an
// acknowledged limitation of the current implementation. We can not
// easily store the meta information of several `Todo`s in a single
// variant, as `TodoType` needs to implement `Copy`, meaning it can't
// contain `Vec`/`Box`/etc., and can't be boxed itself.
//
// Lifting this restriction would require us to intern `TodoType` in
// salsa, but that would mean we would have to pass in `db` everywhere.
// A union of several `Todo` types collapses to a single `Todo` type:
assert!(UnionType::from_elements(&db, vec![todo1, todo2, todo3, todo4]).is_todo());
// And similar for intersection types:
assert!(IntersectionBuilder::new(&db)
.add_positive(todo1)
.add_positive(todo2)
.add_positive(todo3)
.add_positive(todo4)
.build()
.is_todo());
assert!(IntersectionBuilder::new(&db)
.add_positive(todo1)
.add_negative(todo2)
.add_positive(todo3)
.add_negative(todo4)
.build()
.is_todo());
}
}

View File

@@ -128,7 +128,7 @@ impl<'db> IntersectionBuilder<'db> {
pub(crate) fn new(db: &'db dyn Db) -> Self {
Self {
db,
intersections: vec![InnerIntersectionBuilder::new()],
intersections: vec![InnerIntersectionBuilder::default()],
}
}
@@ -231,10 +231,6 @@ struct InnerIntersectionBuilder<'db> {
}
impl<'db> InnerIntersectionBuilder<'db> {
fn new() -> Self {
Self::default()
}
/// Adds a positive type to this intersection.
fn add_positive(&mut self, db: &'db dyn Db, new_positive: Type<'db>) {
if let Type::Intersection(other) = new_positive {
@@ -253,7 +249,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
.iter()
.find(|element| element.is_boolean_literal())
{
*self = Self::new();
*self = Self::default();
self.positive.insert(Type::BooleanLiteral(!value));
return;
}
@@ -272,7 +268,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
}
// A & B = Never if A and B are disjoint
if new_positive.is_disjoint_from(db, *existing_positive) {
*self = Self::new();
*self = Self::default();
self.positive.insert(Type::Never);
return;
}
@@ -285,7 +281,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
for (index, existing_negative) in self.negative.iter().enumerate() {
// S & ~T = Never if S <: T
if new_positive.is_subtype_of(db, *existing_negative) {
*self = Self::new();
*self = Self::default();
self.positive.insert(Type::Never);
return;
}
@@ -313,7 +309,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.add_positive(db, *neg);
}
}
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => {
// Adding any of these types to the negative side of an intersection
// is equivalent to adding it to the positive side. We do this to
// simplify the representation.
@@ -326,7 +322,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
.iter()
.any(|pos| *pos == KnownClass::Bool.to_instance(db)) =>
{
*self = Self::new();
*self = Self::default();
self.positive.insert(Type::BooleanLiteral(!bool));
}
_ => {
@@ -348,7 +344,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
for existing_positive in &self.positive {
// S & ~T = Never if S <: T
if existing_positive.is_subtype_of(db, new_negative) {
*self = Self::new();
*self = Self::default();
self.positive.insert(Type::Never);
return;
}
@@ -383,7 +379,7 @@ mod tests {
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::stdlib::typing_symbol;
use crate::types::{global_symbol, KnownClass, UnionBuilder};
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
use crate::ProgramSettings;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@@ -991,7 +987,7 @@ mod tests {
#[test_case(Type::Any)]
#[test_case(Type::Unknown)]
#[test_case(Type::Todo)]
#[test_case(todo_type!())]
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
let db = setup_db();

View File

@@ -73,10 +73,6 @@ pub struct TypeCheckDiagnostics {
}
impl TypeCheckDiagnostics {
pub fn new() -> Self {
Self { inner: Vec::new() }
}
pub(super) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
self.inner.push(Arc::new(diagnostic));
}
@@ -148,7 +144,7 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
Self {
db,
file,
diagnostics: TypeCheckDiagnostics::new(),
diagnostics: TypeCheckDiagnostics::default(),
}
}

View File

@@ -1,13 +1,14 @@
//! Display implementations for types.
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Display, Formatter, Write};
use ruff_db::display::FormatterJoinExtension;
use ruff_python_ast::str::Quote;
use ruff_python_literal::escape::AsciiEscape;
use crate::types::{
ClassLiteralType, InstanceType, IntersectionType, KnownClass, SubclassOfType, Type, UnionType,
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
SubclassOfType, Type, UnionType,
};
use crate::Db;
use rustc_hash::FxHashMap;
@@ -76,7 +77,7 @@ impl Display for DisplayRepresentation<'_> {
}
// `[Type::Todo]`'s display should be explicit that is not a valid display of
// any other type
Type::Todo => f.write_str("@Todo"),
Type::Todo(todo) => write!(f, "@Todo{todo}"),
Type::ModuleLiteral(file) => {
write!(f, "<module '{:?}'>", file.path(self.db))
}
@@ -85,15 +86,13 @@ impl Display for DisplayRepresentation<'_> {
Type::SubclassOf(SubclassOfType { class }) => {
write!(f, "type[{}]", class.name(self.db))
}
Type::KnownInstance(known_instance) => f.write_str(known_instance.as_str()),
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
Type::IntLiteral(n) => n.fmt(f),
Type::BooleanLiteral(boolean) => f.write_str(if boolean { "True" } else { "False" }),
Type::StringLiteral(string) => {
write!(f, r#""{}""#, string.value(self.db).replace('"', r#"\""#))
}
Type::StringLiteral(string) => string.display(self.db).fmt(f),
Type::LiteralString => f.write_str("LiteralString"),
Type::BytesLiteral(bytes) => {
let escape =
@@ -328,13 +327,40 @@ impl<'db> Display for DisplayTypeArray<'_, 'db> {
}
}
impl<'db> StringLiteralType<'db> {
fn display(&'db self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> {
DisplayStringLiteralType { db, ty: self }
}
}
struct DisplayStringLiteralType<'db> {
ty: &'db StringLiteralType<'db>,
db: &'db dyn Db,
}
impl Display for DisplayStringLiteralType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let value = self.ty.value(self.db);
f.write_char('"')?;
for ch in value.chars() {
match ch {
// `escape_debug` will escape even single quotes, which is not necessary for our
// use case as we are already using double quotes to wrap the string.
'\'' => f.write_char('\'')?,
_ => write!(f, "{}", ch.escape_debug())?,
}
}
f.write_char('"')
}
}
#[cfg(test)]
mod tests {
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use crate::db::tests::TestDb;
use crate::types::{global_symbol, SliceLiteralType, Type, UnionType};
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
fn setup_db() -> TestDb {
@@ -451,4 +477,28 @@ mod tests {
"slice[None, None, Literal[2]]"
);
}
#[test]
fn string_literal_display() {
let db = setup_db();
assert_eq!(
Type::StringLiteral(StringLiteralType::new(&db, r"\n"))
.display(&db)
.to_string(),
r#"Literal["\\n"]"#
);
assert_eq!(
Type::StringLiteral(StringLiteralType::new(&db, "'"))
.display(&db)
.to_string(),
r#"Literal["'"]"#
);
assert_eq!(
Type::StringLiteral(StringLiteralType::new(&db, r#"""#))
.display(&db)
.to_string(),
r#"Literal["\""]"#
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ use itertools::Either;
use rustc_hash::FxHashSet;
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type};
use crate::Db;
use crate::{types::todo_type, Db};
/// The inferred method resolution order of a given class.
///
@@ -354,7 +354,7 @@ impl<'db> ClassBase<'db> {
match ty {
Type::Any => Some(Self::Any),
Type::Unknown => Some(Self::Unknown),
Type::Todo => Some(Self::Todo),
Type::Todo(_) => Some(Self::Todo),
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
@@ -371,8 +371,11 @@ impl<'db> ClassBase<'db> {
| Type::ModuleLiteral(_)
| Type::SubclassOf(_) => None,
Type::KnownInstance(known_instance) => match known_instance {
KnownInstanceType::Literal => None,
KnownInstanceType::TypeVar(_) => None,
KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::Literal
| KnownInstanceType::Union
| KnownInstanceType::Optional => None,
},
}
}
@@ -404,7 +407,7 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
fn from(value: ClassBase<'db>) -> Self {
match value {
ClassBase::Any => Type::Any,
ClassBase::Todo => Type::Todo,
ClassBase::Todo => todo_type!(),
ClassBase::Unknown => Type::Unknown,
ClassBase::Class(class) => Type::class_literal(class),
}

View File

@@ -1,4 +1,4 @@
use crate::semantic_index::ast_ids::HasScopedAstId;
use crate::semantic_index::ast_ids::HasScopedExpressionId;
use crate::semantic_index::constraint::{Constraint, ConstraintNode, PatternConstraint};
use crate::semantic_index::definition::Definition;
use crate::semantic_index::expression::Expression;
@@ -257,17 +257,26 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
expression: Expression<'db>,
is_positive: bool,
) -> Option<NarrowingConstraints<'db>> {
fn is_narrowing_target_candidate(expr: &ast::Expr) -> bool {
matches!(expr, ast::Expr::Name(_) | ast::Expr::Call(_))
}
let ast::ExprCompare {
range: _,
left,
ops,
comparators,
} = expr_compare;
if !left.is_name_expr() && comparators.iter().all(|c| !c.is_name_expr()) {
// If none of the comparators are name expressions,
// we have no symbol to narrow down the type of.
// Performance optimization: early return if there are no potential narrowing targets.
if !is_narrowing_target_candidate(left)
&& comparators
.iter()
.all(|c| !is_narrowing_target_candidate(c))
{
return None;
}
if !is_positive && comparators.len() > 1 {
// We can't negate a constraint made by a multi-comparator expression, since we can't
// know which comparison part is the one being negated.
@@ -283,42 +292,85 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
.tuple_windows::<(&ruff_python_ast::Expr, &ruff_python_ast::Expr)>();
let mut constraints = NarrowingConstraints::default();
for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) {
if let ast::Expr::Name(ast::ExprName {
range: _,
id,
ctx: _,
}) = left
{
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let rhs_ty = inference.expression_ty(right.scoped_ast_id(self.db, scope));
let rhs_ty = inference.expression_ty(right.scoped_expression_id(self.db, scope));
match if is_positive { *op } else { op.negate() } {
ast::CmpOp::IsNot => {
if rhs_ty.is_singleton(self.db) {
let ty = IntersectionBuilder::new(self.db)
.add_negative(rhs_ty)
.build();
constraints.insert(symbol, ty);
} else {
// Non-singletons cannot be safely narrowed using `is not`
match left {
ast::Expr::Name(ast::ExprName {
range: _,
id,
ctx: _,
}) => {
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("Should always have a symbol for every Name node");
match if is_positive { *op } else { op.negate() } {
ast::CmpOp::IsNot => {
if rhs_ty.is_singleton(self.db) {
let ty = IntersectionBuilder::new(self.db)
.add_negative(rhs_ty)
.build();
constraints.insert(symbol, ty);
} else {
// Non-singletons cannot be safely narrowed using `is not`
}
}
}
ast::CmpOp::Is => {
constraints.insert(symbol, rhs_ty);
}
ast::CmpOp::NotEq => {
if rhs_ty.is_single_valued(self.db) {
let ty = IntersectionBuilder::new(self.db)
.add_negative(rhs_ty)
.build();
constraints.insert(symbol, ty);
ast::CmpOp::Is => {
constraints.insert(symbol, rhs_ty);
}
ast::CmpOp::NotEq => {
if rhs_ty.is_single_valued(self.db) {
let ty = IntersectionBuilder::new(self.db)
.add_negative(rhs_ty)
.build();
constraints.insert(symbol, ty);
}
}
_ => {
// TODO other comparison types
}
}
_ => {
// TODO other comparison types
}
}
ast::Expr::Call(ast::ExprCall {
range: _,
func: callable,
arguments:
ast::Arguments {
args,
keywords,
range: _,
},
}) if rhs_ty.is_class_literal() && keywords.is_empty() => {
let [ast::Expr::Name(ast::ExprName { id, .. })] = &**args else {
continue;
};
let is_valid_constraint = if is_positive {
op == &ast::CmpOp::Is
} else {
op == &ast::CmpOp::IsNot
};
if !is_valid_constraint {
continue;
}
let callable_ty =
inference.expression_ty(callable.scoped_expression_id(self.db, scope));
if callable_ty
.into_class_literal()
.is_some_and(|c| c.class.is_known(self.db, KnownClass::Type))
{
let symbol = self
.symbols()
.symbol_id_by_name(id)
.expect("Should always have a symbol for every Name node");
constraints.insert(symbol, rhs_ty.to_instance(self.db));
}
}
_ => {}
}
}
Some(constraints)
@@ -336,7 +388,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
// TODO: add support for PEP 604 union types on the right hand side of `isinstance`
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
match inference
.expression_ty(expr_call.func.scoped_ast_id(self.db, scope))
.expression_ty(expr_call.func.scoped_expression_id(self.db, scope))
.into_function_literal()
.and_then(|f| f.known(self.db))
.and_then(KnownFunction::constraint_function)
@@ -348,7 +400,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let class_info_ty =
inference.expression_ty(class_info.scoped_ast_id(self.db, scope));
inference.expression_ty(class_info.scoped_expression_id(self.db, scope));
let to_constraint = match function {
KnownConstraintFunction::IsInstance => {
@@ -414,7 +466,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
// filter our arms with statically known truthiness
.filter(|expr| {
inference
.expression_ty(expr.scoped_ast_id(self.db, scope))
.expression_ty(expr.scoped_expression_id(self.db, scope))
.bool(self.db)
!= match expr_bool_op.op {
BoolOp::And => Truthiness::AlwaysTrue,

View File

@@ -0,0 +1,479 @@
#![allow(dead_code)]
use super::{definition_expression_ty, Type};
use crate::Db;
use crate::{semantic_index::definition::Definition, types::todo_type};
use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Signature<'db> {
parameters: Parameters<'db>,
/// Annotated return type (Unknown if no annotation.)
pub(crate) return_ty: Type<'db>,
}
impl<'db> Signature<'db> {
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
pub(crate) fn todo() -> Self {
Self {
parameters: Parameters::todo(),
return_ty: todo_type!("return type"),
}
}
/// Return a typed signature from a function definition.
pub(super) fn from_function(
db: &'db dyn Db,
definition: Definition<'db>,
function_node: &'db ast::StmtFunctionDef,
) -> Self {
let return_ty = function_node
.returns
.as_ref()
.map(|returns| {
if function_node.is_async {
todo_type!("generic types.CoroutineType")
} else {
definition_expression_ty(db, definition, returns.as_ref())
}
})
.unwrap_or(Type::Unknown);
Self {
parameters: Parameters::from_parameters(
db,
definition,
function_node.parameters.as_ref(),
),
return_ty,
}
}
}
/// The parameters portion of a typed signature.
///
/// The ordering of parameters is always as given in this struct: first positional-only parameters,
/// then positional-or-keyword, then optionally the variadic parameter, then keyword-only
/// parameters, and last, optionally the variadic keywords parameter.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct Parameters<'db> {
/// Parameters which may only be filled by positional arguments.
positional_only: Box<[ParameterWithDefault<'db>]>,
/// Parameters which may be filled by positional or keyword arguments.
positional_or_keyword: Box<[ParameterWithDefault<'db>]>,
/// The `*args` variadic parameter, if any.
variadic: Option<Parameter<'db>>,
/// Parameters which may only be filled by keyword arguments.
keyword_only: Box<[ParameterWithDefault<'db>]>,
/// The `**kwargs` variadic keywords parameter, if any.
keywords: Option<Parameter<'db>>,
}
impl<'db> Parameters<'db> {
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
fn todo() -> Self {
Self {
variadic: Some(Parameter {
name: Some(Name::new_static("args")),
annotated_ty: todo_type!(),
}),
keywords: Some(Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: todo_type!(),
}),
..Default::default()
}
}
fn from_parameters(
db: &'db dyn Db,
definition: Definition<'db>,
parameters: &'db ast::Parameters,
) -> Self {
let ast::Parameters {
posonlyargs,
args,
vararg,
kwonlyargs,
kwarg,
range: _,
} = parameters;
let positional_only = posonlyargs
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let positional_or_keyword = args
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let variadic = vararg
.as_ref()
.map(|arg| Parameter::from_node(db, definition, arg));
let keyword_only = kwonlyargs
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let keywords = kwarg
.as_ref()
.map(|arg| Parameter::from_node(db, definition, arg));
Self {
positional_only,
positional_or_keyword,
variadic,
keyword_only,
keywords,
}
}
}
/// A single parameter of a typed signature, with optional default value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct ParameterWithDefault<'db> {
parameter: Parameter<'db>,
/// Type of the default value, if any.
default_ty: Option<Type<'db>>,
}
impl<'db> ParameterWithDefault<'db> {
fn from_node(
db: &'db dyn Db,
definition: Definition<'db>,
parameter_with_default: &'db ast::ParameterWithDefault,
) -> Self {
Self {
default_ty: parameter_with_default
.default
.as_deref()
.map(|default| definition_expression_ty(db, definition, default)),
parameter: Parameter::from_node(db, definition, &parameter_with_default.parameter),
}
}
}
/// A single parameter of a typed signature.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Parameter<'db> {
/// Parameter name.
///
/// It is possible for signatures to be defined in ways that leave positional-only parameters
/// nameless (e.g. via `Callable` annotations).
name: Option<Name>,
/// Annotated type of the parameter (Unknown if no annotation.)
annotated_ty: Type<'db>,
}
impl<'db> Parameter<'db> {
fn from_node(
db: &'db dyn Db,
definition: Definition<'db>,
parameter: &'db ast::Parameter,
) -> Self {
Parameter {
name: Some(parameter.name.id.clone()),
annotated_ty: parameter
.annotation
.as_deref()
.map(|annotation| definition_expression_ty(db, definition, annotation))
.unwrap_or(Type::Unknown),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::types::{global_symbol, FunctionType};
use crate::ProgramSettings;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
pub(crate) fn setup_db() -> TestDb {
let db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db,
&ProgramSettings {
target_version: PythonVersion::default(),
search_paths: SearchPathSettings::new(src_root),
},
)
.expect("Valid search path settings");
db
}
#[track_caller]
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> {
let module = ruff_db::files::system_path_to_file(db, file).unwrap();
global_symbol(db, module, "f")
.expect_type()
.expect_function_literal()
}
#[track_caller]
fn assert_param_with_default<'db>(
db: &'db TestDb,
param_with_default: &ParameterWithDefault<'db>,
expected_name: &'static str,
expected_annotation_ty_display: &'static str,
expected_default_ty_display: Option<&'static str>,
) {
assert_eq!(
param_with_default
.default_ty
.map(|ty| ty.display(db).to_string()),
expected_default_ty_display.map(ToString::to_string)
);
assert_param(
db,
&param_with_default.parameter,
expected_name,
expected_annotation_ty_display,
);
}
#[track_caller]
fn assert_param<'db>(
db: &'db TestDb,
param: &Parameter<'db>,
expected_name: &'static str,
expected_annotation_ty_display: &'static str,
) {
assert_eq!(param.name.as_ref().unwrap(), expected_name);
assert_eq!(
param.annotated_ty.display(db).to_string(),
expected_annotation_ty_display
);
}
#[test]
fn empty() {
let mut db = setup_db();
db.write_dedented("/src/a.py", "def f(): ...").unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
assert_eq!(sig.return_ty.display(&db).to_string(), "Unknown");
let params = sig.parameters;
assert!(params.positional_only.is_empty());
assert!(params.positional_or_keyword.is_empty());
assert!(params.variadic.is_none());
assert!(params.keyword_only.is_empty());
assert!(params.keywords.is_none());
}
#[test]
#[allow(clippy::many_single_char_names)]
fn full() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
"
def f(a, b: int, c = 1, d: int = 2, /,
e = 3, f: Literal[4] = 4, *args: object,
g = 5, h: Literal[6] = 6, **kwargs: str) -> bytes: ...
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
assert_eq!(sig.return_ty.display(&db).to_string(), "bytes");
let params = sig.parameters;
let [a, b, c, d] = &params.positional_only[..] else {
panic!("expected four positional-only parameters");
};
let [e, f] = &params.positional_or_keyword[..] else {
panic!("expected two positional-or-keyword parameters");
};
let Some(args) = params.variadic else {
panic!("expected a variadic parameter");
};
let [g, h] = &params.keyword_only[..] else {
panic!("expected two keyword-only parameters");
};
let Some(kwargs) = params.keywords else {
panic!("expected a kwargs parameter");
};
assert_param_with_default(&db, a, "a", "Unknown", None);
assert_param_with_default(&db, b, "b", "int", None);
assert_param_with_default(&db, c, "c", "Unknown", Some("Literal[1]"));
assert_param_with_default(&db, d, "d", "int", Some("Literal[2]"));
assert_param_with_default(&db, e, "e", "Unknown", Some("Literal[3]"));
assert_param_with_default(&db, f, "f", "Literal[4]", Some("Literal[4]"));
assert_param_with_default(&db, g, "g", "Unknown", Some("Literal[5]"));
assert_param_with_default(&db, h, "h", "Literal[6]", Some("Literal[6]"));
assert_param(&db, &args, "args", "object");
assert_param(&db, &kwargs, "kwargs", "str");
}
#[test]
fn not_deferred() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
"
class A: ...
class B: ...
alias = A
def f(a: alias): ...
alias = B
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
let [a] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected one positional-or-keyword parameter");
};
// Parameter resolution not deferred; we should see A not B
assert_param_with_default(&db, a, "a", "A", None);
}
#[test]
fn deferred_in_stub() {
let mut db = setup_db();
db.write_dedented(
"/src/a.pyi",
"
class A: ...
class B: ...
alias = A
def f(a: alias): ...
alias = B
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.pyi");
let sig = func.internal_signature(&db);
let [a] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected one positional-or-keyword parameter");
};
// Parameter resolution deferred; we should see B
assert_param_with_default(&db, a, "a", "B", None);
}
#[test]
fn generic_not_deferred() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
"
class A: ...
class B: ...
alias = A
def f[T](a: alias, b: T) -> T: ...
alias = B
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let sig = func.internal_signature(&db);
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected two positional-or-keyword parameters");
};
// TODO resolution should not be deferred; we should see A not B
assert_param_with_default(&db, a, "a", "B", None);
assert_param_with_default(&db, b, "b", "T", None);
}
#[test]
fn generic_deferred_in_stub() {
let mut db = setup_db();
db.write_dedented(
"/src/a.pyi",
"
class A: ...
class B: ...
alias = A
def f[T](a: alias, b: T) -> T: ...
alias = B
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.pyi");
let sig = func.internal_signature(&db);
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected two positional-or-keyword parameters");
};
// Parameter resolution deferred; we should see B
assert_param_with_default(&db, a, "a", "B", None);
assert_param_with_default(&db, b, "b", "T", None);
}
#[test]
fn external_signature_no_decorator() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
"
def f(a: int) -> int: ...
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let expected_sig = func.internal_signature(&db);
// With no decorators, internal and external signature are the same
assert_eq!(func.signature(&db), &expected_sig);
}
#[test]
fn external_signature_decorated() {
let mut db = setup_db();
db.write_dedented(
"/src/a.py",
"
def deco(func): ...
@deco
def f(a: int) -> int: ...
",
)
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let expected_sig = Signature::todo();
// With no decorators, internal and external signature are the same
assert_eq!(func.signature(&db), &expected_sig);
}
}

View File

@@ -0,0 +1,77 @@
use ruff_db::files::File;
use ruff_db::source::source_text;
use ruff_python_ast::str::raw_contents;
use ruff_python_ast::{self as ast, ModExpression, StringFlags};
use ruff_python_parser::{parse_expression_range, Parsed};
use ruff_text_size::Ranged;
use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::Db;
type AnnotationParseResult = Result<Parsed<ModExpression>, TypeCheckDiagnostics>;
/// Parses the given expression as a string annotation.
pub(crate) fn parse_string_annotation(
db: &dyn Db,
file: File,
string_expr: &ast::ExprStringLiteral,
) -> AnnotationParseResult {
let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), file=%file.path(db)).entered();
let source = source_text(db.upcast(), file);
let node_text = &source[string_expr.range()];
let mut diagnostics = TypeCheckDiagnosticsBuilder::new(db, file);
if let [string_literal] = string_expr.value.as_slice() {
let prefix = string_literal.flags.prefix();
if prefix.is_raw() {
diagnostics.add(
string_literal.into(),
"annotation-raw-string",
format_args!("Type expressions cannot use raw string literal"),
);
// Compare the raw contents (without quotes) of the expression with the parsed contents
// contained in the string literal.
} else if raw_contents(node_text)
.is_some_and(|raw_contents| raw_contents == string_literal.as_str())
{
let range_excluding_quotes = string_literal
.range()
.add_start(string_literal.flags.opener_len())
.sub_end(string_literal.flags.closer_len());
// TODO: Support multiline strings like:
// ```py
// x: """
// int
// | float
// """ = 1
// ```
match parse_expression_range(source.as_str(), range_excluding_quotes) {
Ok(parsed) => return Ok(parsed),
Err(parse_error) => diagnostics.add(
string_literal.into(),
"forward-annotation-syntax-error",
format_args!("Syntax error in forward annotation: {}", parse_error.error),
),
}
} else {
// The raw contents of the string doesn't match the parsed content. This could be the
// case for annotations that contain escape sequences.
diagnostics.add(
string_expr.into(),
"annotation-escape-character",
format_args!("Type expressions cannot contain escape characters"),
);
}
} else {
// String is implicitly concatenated.
diagnostics.add(
string_expr.into(),
"annotation-implicit-concat",
format_args!("Type expressions cannot span multiple string literals"),
);
}
Err(diagnostics.finish())
}

View File

@@ -4,9 +4,9 @@ use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use rustc_hash::FxHashMap;
use crate::semantic_index::ast_ids::{HasScopedAstId, ScopedExpressionId};
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::symbol::ScopeId;
use crate::types::{Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::types::{todo_type, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
use crate::Db;
/// Unpacks the value expression type to their respective targets.
@@ -29,7 +29,7 @@ impl<'db> Unpacker<'db> {
match target {
ast::Expr::Name(target_name) => {
self.targets
.insert(target_name.scoped_ast_id(self.db, scope), value_ty);
.insert(target_name.scoped_expression_id(self.db, scope), value_ty);
}
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
self.unpack(value, value_ty, scope);
@@ -59,7 +59,7 @@ impl<'db> Unpacker<'db> {
// TODO: Combine the types into a list type. If the
// starred_element_types is empty, then it should be `List[Any]`.
// combine_types(starred_element_types);
element_types.push(Type::Todo);
element_types.push(todo_type!("starred unpacking"));
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
@@ -72,7 +72,7 @@ impl<'db> Unpacker<'db> {
// index.
element_types.resize(elts.len() - 1, Type::Unknown);
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, Type::Todo);
element_types.insert(starred_index, todo_type!("starred unpacking"));
Cow::Owned(element_types)
}
} else {

View File

@@ -68,7 +68,7 @@ impl Session {
let system = LSPSystem::new(index.clone());
// TODO(dhruvmanila): Get the values from the client settings
let metadata = WorkspaceMetadata::from_path(system_path, &system, None)?;
let metadata = WorkspaceMetadata::discover(system_path, &system, None)?;
// TODO(micha): Handle the case where the program settings are incorrect more gracefully.
workspaces.insert(path, RootDatabase::new(metadata, system)?);
}

View File

@@ -7,8 +7,8 @@ use lsp_types::Url;
use ruff_db::file_revision::FileRevision;
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
use ruff_db::system::{
DirectoryEntry, FileType, Metadata, OsSystem, Result, System, SystemPath, SystemPathBuf,
SystemVirtualPath, SystemVirtualPathBuf,
DirectoryEntry, FileType, GlobError, Metadata, OsSystem, PatternError, Result, System,
SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf,
};
use ruff_notebook::{Notebook, NotebookError};
@@ -198,6 +198,16 @@ impl System for LSPSystem {
self.os_system.walk_directory(path)
}
fn glob(
&self,
pattern: &str,
) -> std::result::Result<
Box<dyn Iterator<Item = std::result::Result<SystemPathBuf, GlobError>>>,
PatternError,
> {
self.os_system.glob(pattern)
}
fn as_any(&self) -> &dyn Any {
self
}

View File

@@ -1 +1 @@
d262beb07502cda412db2179fb406d45d1a9486f
5052fa2f18db4493892e0f2775030683c9d06531

View File

@@ -24,18 +24,22 @@ _asyncio: 3.0-
_bisect: 3.0-
_blake2: 3.6-
_bootlocale: 3.4-3.9
_bz2: 3.3-
_codecs: 3.0-
_collections_abc: 3.3-
_compat_pickle: 3.1-
_compression: 3.5-
_contextvars: 3.7-
_csv: 3.0-
_ctypes: 3.0-
_curses: 3.0-
_dbm: 3.0-
_decimal: 3.3-
_dummy_thread: 3.0-3.8
_dummy_threading: 3.0-3.8
_frozen_importlib: 3.0-
_frozen_importlib_external: 3.5-
_gdbm: 3.0-
_heapq: 3.0-
_imp: 3.0-
_interpchannels: 3.13-
@@ -45,6 +49,7 @@ _io: 3.0-
_json: 3.0-
_locale: 3.0-
_lsprof: 3.0-
_lzma: 3.3-
_markupbase: 3.0-
_msi: 3.0-3.12
_operator: 3.4-
@@ -52,12 +57,14 @@ _osx_support: 3.0-
_posixsubprocess: 3.2-
_py_abc: 3.7-
_pydecimal: 3.5-
_queue: 3.7-
_random: 3.0-
_sitebuiltins: 3.4-
_socket: 3.0- # present in 3.0 at runtime, but not in typeshed
_sqlite3: 3.0-
_ssl: 3.0-
_stat: 3.4-
_struct: 3.0-
_thread: 3.0-
_threading_local: 3.0-
_tkinter: 3.0-

View File

@@ -0,0 +1,18 @@
from _typeshed import ReadableBuffer
from typing import final
@final
class BZ2Compressor:
def __init__(self, compresslevel: int = 9) -> None: ...
def compress(self, data: ReadableBuffer, /) -> bytes: ...
def flush(self) -> bytes: ...
@final
class BZ2Decompressor:
def decompress(self, data: ReadableBuffer, max_length: int = -1) -> bytes: ...
@property
def eof(self) -> bool: ...
@property
def needs_input(self) -> bool: ...
@property
def unused_data(self) -> bytes: ...

View File

@@ -0,0 +1,61 @@
import sys
from collections.abc import Callable, Iterator, Mapping
from typing import Any, ClassVar, Generic, TypeVar, final, overload
from typing_extensions import ParamSpec
if sys.version_info >= (3, 9):
from types import GenericAlias
_T = TypeVar("_T")
_D = TypeVar("_D")
_P = ParamSpec("_P")
@final
class ContextVar(Generic[_T]):
@overload
def __init__(self, name: str) -> None: ...
@overload
def __init__(self, name: str, *, default: _T) -> None: ...
def __hash__(self) -> int: ...
@property
def name(self) -> str: ...
@overload
def get(self) -> _T: ...
@overload
def get(self, default: _T, /) -> _T: ...
@overload
def get(self, default: _D, /) -> _D | _T: ...
def set(self, value: _T, /) -> Token[_T]: ...
def reset(self, token: Token[_T], /) -> None: ...
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
@final
class Token(Generic[_T]):
@property
def var(self) -> ContextVar[_T]: ...
@property
def old_value(self) -> Any: ... # returns either _T or MISSING, but that's hard to express
MISSING: ClassVar[object]
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
def copy_context() -> Context: ...
# It doesn't make sense to make this generic, because for most Contexts each ContextVar will have
# a different value.
@final
class Context(Mapping[ContextVar[Any], Any]):
def __init__(self) -> None: ...
@overload
def get(self, key: ContextVar[_T], default: None = None, /) -> _T | None: ...
@overload
def get(self, key: ContextVar[_T], default: _T, /) -> _T: ...
@overload
def get(self, key: ContextVar[_T], default: _D, /) -> _T | _D: ...
def run(self, callable: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> _T: ...
def copy(self) -> Context: ...
def __getitem__(self, key: ContextVar[_T], /) -> _T: ...
def __iter__(self) -> Iterator[ContextVar[Any]]: ...
def __len__(self) -> int: ...
def __eq__(self, value: object, /) -> bool: ...

View File

@@ -0,0 +1,43 @@
import sys
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
if sys.platform != "win32":
_T = TypeVar("_T")
_KeyType: TypeAlias = str | ReadOnlyBuffer
_ValueType: TypeAlias = str | ReadOnlyBuffer
class error(OSError): ...
library: str
# Actual typename dbm, not exposed by the implementation
class _dbm:
def close(self) -> None: ...
if sys.version_info >= (3, 13):
def clear(self) -> None: ...
def __getitem__(self, item: _KeyType) -> bytes: ...
def __setitem__(self, key: _KeyType, value: _ValueType) -> None: ...
def __delitem__(self, key: _KeyType) -> None: ...
def __len__(self) -> int: ...
def __del__(self) -> None: ...
def __enter__(self) -> Self: ...
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None: ...
@overload
def get(self, k: _KeyType) -> bytes | None: ...
@overload
def get(self, k: _KeyType, default: _T) -> bytes | _T: ...
def keys(self) -> list[bytes]: ...
def setdefault(self, k: _KeyType, default: _ValueType = ...) -> bytes: ...
# Don't exist at runtime
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...

View File

@@ -107,9 +107,9 @@ class FileLoader:
def get_filename(self, name: str | None = None) -> str: ...
def load_module(self, name: str | None = None) -> types.ModuleType: ...
if sys.version_info >= (3, 10):
def get_resource_reader(self, module: types.ModuleType) -> importlib.readers.FileReader: ...
def get_resource_reader(self, name: str | None = None) -> importlib.readers.FileReader: ...
else:
def get_resource_reader(self, module: types.ModuleType) -> Self | None: ...
def get_resource_reader(self, name: str | None = None) -> Self | None: ...
def open_resource(self, resource: str) -> _io.FileIO: ...
def resource_path(self, resource: str) -> str: ...
def is_resource(self, name: str) -> bool: ...

View File

@@ -0,0 +1,47 @@
import sys
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
if sys.platform != "win32":
_T = TypeVar("_T")
_KeyType: TypeAlias = str | ReadOnlyBuffer
_ValueType: TypeAlias = str | ReadOnlyBuffer
open_flags: str
class error(OSError): ...
# Actual typename gdbm, not exposed by the implementation
class _gdbm:
def firstkey(self) -> bytes | None: ...
def nextkey(self, key: _KeyType) -> bytes | None: ...
def reorganize(self) -> None: ...
def sync(self) -> None: ...
def close(self) -> None: ...
if sys.version_info >= (3, 13):
def clear(self) -> None: ...
def __getitem__(self, item: _KeyType) -> bytes: ...
def __setitem__(self, key: _KeyType, value: _ValueType) -> None: ...
def __delitem__(self, key: _KeyType) -> None: ...
def __contains__(self, key: _KeyType) -> bool: ...
def __len__(self) -> int: ...
def __enter__(self) -> Self: ...
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None: ...
@overload
def get(self, k: _KeyType) -> bytes | None: ...
@overload
def get(self, k: _KeyType, default: _T) -> bytes | _T: ...
def keys(self) -> list[bytes]: ...
def setdefault(self, k: _KeyType, default: _ValueType = ...) -> bytes: ...
# Don't exist at runtime
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...

View File

@@ -33,10 +33,10 @@ class _IOBase:
def readable(self) -> bool: ...
read: Callable[..., Any]
def readlines(self, hint: int = -1, /) -> list[bytes]: ...
def seek(self, offset: int, whence: int = ..., /) -> int: ...
def seek(self, offset: int, whence: int = 0, /) -> int: ...
def seekable(self) -> bool: ...
def tell(self) -> int: ...
def truncate(self, size: int | None = ..., /) -> int: ...
def truncate(self, size: int | None = None, /) -> int: ...
def writable(self) -> bool: ...
write: Callable[..., Any]
def writelines(self, lines: Iterable[ReadableBuffer], /) -> None: ...
@@ -59,8 +59,8 @@ class _BufferedIOBase(_IOBase):
def readinto(self, buffer: WriteableBuffer, /) -> int: ...
def write(self, buffer: ReadableBuffer, /) -> int: ...
def readinto1(self, buffer: WriteableBuffer, /) -> int: ...
def read(self, size: int | None = ..., /) -> bytes: ...
def read1(self, size: int = ..., /) -> bytes: ...
def read(self, size: int | None = -1, /) -> bytes: ...
def read1(self, size: int = -1, /) -> bytes: ...
class FileIO(RawIOBase, _RawIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of writelines in the base classes
mode: str
@@ -69,13 +69,15 @@ class FileIO(RawIOBase, _RawIOBase, BinaryIO): # type: ignore[misc] # incompat
# "name" is a str. In the future, making FileIO generic might help.
name: Any
def __init__(
self, file: FileDescriptorOrPath, mode: str = ..., closefd: bool = ..., opener: _Opener | None = ...
self, file: FileDescriptorOrPath, mode: str = "r", closefd: bool = True, opener: _Opener | None = None
) -> None: ...
@property
def closefd(self) -> bool: ...
def seek(self, pos: int, whence: int = 0, /) -> int: ...
def read(self, size: int | None = -1, /) -> bytes | MaybeNone: ...
class BytesIO(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of methods in the base classes
def __init__(self, initial_bytes: ReadableBuffer = ...) -> None: ...
def __init__(self, initial_bytes: ReadableBuffer = b"") -> None: ...
# BytesIO does not contain a "name" field. This workaround is necessary
# to allow BytesIO sub-classes to add this field, as it is defined
# as a read-only property on IO[].
@@ -83,16 +85,22 @@ class BytesIO(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc]
def getvalue(self) -> bytes: ...
def getbuffer(self) -> memoryview: ...
def read1(self, size: int | None = -1, /) -> bytes: ...
def readlines(self, size: int | None = None, /) -> list[bytes]: ...
def seek(self, pos: int, whence: int = 0, /) -> int: ...
class BufferedReader(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of methods in the base classes
raw: RawIOBase
def __init__(self, raw: RawIOBase, buffer_size: int = 8192) -> None: ...
def peek(self, size: int = 0, /) -> bytes: ...
def seek(self, target: int, whence: int = 0, /) -> int: ...
def truncate(self, pos: int | None = None, /) -> int: ...
class BufferedWriter(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of writelines in the base classes
raw: RawIOBase
def __init__(self, raw: RawIOBase, buffer_size: int = 8192) -> None: ...
def write(self, buffer: ReadableBuffer, /) -> int: ...
def seek(self, target: int, whence: int = 0, /) -> int: ...
def truncate(self, pos: int | None = None, /) -> int: ...
class BufferedRandom(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore[misc] # incompatible definitions of methods in the base classes
mode: str
@@ -101,10 +109,11 @@ class BufferedRandom(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore
def __init__(self, raw: RawIOBase, buffer_size: int = 8192) -> None: ...
def seek(self, target: int, whence: int = 0, /) -> int: ... # stubtest needs this
def peek(self, size: int = 0, /) -> bytes: ...
def truncate(self, pos: int | None = None, /) -> int: ...
class BufferedRWPair(BufferedIOBase, _BufferedIOBase):
def __init__(self, reader: RawIOBase, writer: RawIOBase, buffer_size: int = 8192) -> None: ...
def peek(self, size: int = ..., /) -> bytes: ...
def peek(self, size: int = 0, /) -> bytes: ...
class _TextIOBase(_IOBase):
encoding: str
@@ -115,9 +124,9 @@ class _TextIOBase(_IOBase):
def detach(self) -> BinaryIO: ...
def write(self, s: str, /) -> int: ...
def writelines(self, lines: Iterable[str], /) -> None: ... # type: ignore[override]
def readline(self, size: int = ..., /) -> str: ... # type: ignore[override]
def readline(self, size: int = -1, /) -> str: ... # type: ignore[override]
def readlines(self, hint: int = -1, /) -> list[str]: ... # type: ignore[override]
def read(self, size: int | None = ..., /) -> str: ...
def read(self, size: int | None = -1, /) -> str: ...
@type_check_only
class _WrappedBuffer(Protocol):
@@ -177,9 +186,10 @@ class TextIOWrapper(TextIOBase, _TextIOBase, TextIO, Generic[_BufferT_co]): # t
# TextIOWrapper's version of seek only supports a limited subset of
# operations.
def seek(self, cookie: int, whence: int = 0, /) -> int: ...
def truncate(self, pos: int | None = None, /) -> int: ...
class StringIO(TextIOBase, _TextIOBase, TextIO): # type: ignore[misc] # incompatible definitions of write in the base classes
def __init__(self, initial_value: str | None = ..., newline: str | None = ...) -> None: ...
def __init__(self, initial_value: str | None = "", newline: str | None = "\n") -> None: ...
# StringIO does not contain a "name" field. This workaround is necessary
# to allow StringIO sub-classes to add this field, as it is defined
# as a read-only property on IO[].
@@ -187,9 +197,11 @@ class StringIO(TextIOBase, _TextIOBase, TextIO): # type: ignore[misc] # incomp
def getvalue(self) -> str: ...
@property
def line_buffering(self) -> bool: ...
def seek(self, pos: int, whence: int = 0, /) -> int: ...
def truncate(self, pos: int | None = None, /) -> int: ...
class IncrementalNewlineDecoder:
def __init__(self, decoder: codecs.IncrementalDecoder | None, translate: bool, errors: str = ...) -> None: ...
def __init__(self, decoder: codecs.IncrementalDecoder | None, translate: bool, errors: str = "strict") -> None: ...
def decode(self, input: ReadableBuffer | str, final: bool = False) -> str: ...
@property
def newlines(self) -> str | tuple[str, ...] | None: ...

View File

@@ -0,0 +1,60 @@
from _typeshed import ReadableBuffer
from collections.abc import Mapping, Sequence
from typing import Any, Final, final
from typing_extensions import TypeAlias
_FilterChain: TypeAlias = Sequence[Mapping[str, Any]]
FORMAT_AUTO: Final = 0
FORMAT_XZ: Final = 1
FORMAT_ALONE: Final = 2
FORMAT_RAW: Final = 3
CHECK_NONE: Final = 0
CHECK_CRC32: Final = 1
CHECK_CRC64: Final = 4
CHECK_SHA256: Final = 10
CHECK_ID_MAX: Final = 15
CHECK_UNKNOWN: Final = 16
FILTER_LZMA1: int # v big number
FILTER_LZMA2: Final = 33
FILTER_DELTA: Final = 3
FILTER_X86: Final = 4
FILTER_IA64: Final = 6
FILTER_ARM: Final = 7
FILTER_ARMTHUMB: Final = 8
FILTER_SPARC: Final = 9
FILTER_POWERPC: Final = 5
MF_HC3: Final = 3
MF_HC4: Final = 4
MF_BT2: Final = 18
MF_BT3: Final = 19
MF_BT4: Final = 20
MODE_FAST: Final = 1
MODE_NORMAL: Final = 2
PRESET_DEFAULT: Final = 6
PRESET_EXTREME: int # v big number
@final
class LZMADecompressor:
def __init__(self, format: int | None = ..., memlimit: int | None = ..., filters: _FilterChain | None = ...) -> None: ...
def decompress(self, data: ReadableBuffer, max_length: int = -1) -> bytes: ...
@property
def check(self) -> int: ...
@property
def eof(self) -> bool: ...
@property
def unused_data(self) -> bytes: ...
@property
def needs_input(self) -> bool: ...
@final
class LZMACompressor:
def __init__(
self, format: int | None = ..., check: int = ..., preset: int | None = ..., filters: _FilterChain | None = ...
) -> None: ...
def compress(self, data: ReadableBuffer, /) -> bytes: ...
def flush(self) -> bytes: ...
class LZMAError(Exception): ...
def is_check_supported(check_id: int, /) -> bool: ...

View File

@@ -0,0 +1,20 @@
import sys
from typing import Any, Generic, TypeVar
if sys.version_info >= (3, 9):
from types import GenericAlias
_T = TypeVar("_T")
class Empty(Exception): ...
class SimpleQueue(Generic[_T]):
def __init__(self) -> None: ...
def empty(self) -> bool: ...
def get(self, block: bool = True, timeout: float | None = None) -> _T: ...
def get_nowait(self) -> _T: ...
def put(self, item: _T, block: bool = True, timeout: float | None = None) -> None: ...
def put_nowait(self, item: _T) -> None: ...
def qsize(self) -> int: ...
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...

View File

@@ -0,0 +1,22 @@
from _typeshed import ReadableBuffer, WriteableBuffer
from collections.abc import Iterator
from typing import Any
def pack(fmt: str | bytes, /, *v: Any) -> bytes: ...
def pack_into(fmt: str | bytes, buffer: WriteableBuffer, offset: int, /, *v: Any) -> None: ...
def unpack(format: str | bytes, buffer: ReadableBuffer, /) -> tuple[Any, ...]: ...
def unpack_from(format: str | bytes, /, buffer: ReadableBuffer, offset: int = 0) -> tuple[Any, ...]: ...
def iter_unpack(format: str | bytes, buffer: ReadableBuffer, /) -> Iterator[tuple[Any, ...]]: ...
def calcsize(format: str | bytes, /) -> int: ...
class Struct:
@property
def format(self) -> str: ...
@property
def size(self) -> int: ...
def __init__(self, format: str | bytes) -> None: ...
def pack(self, *v: Any) -> bytes: ...
def pack_into(self, buffer: WriteableBuffer, offset: int, *v: Any) -> None: ...
def unpack(self, buffer: ReadableBuffer, /) -> tuple[Any, ...]: ...
def unpack_from(self, buffer: ReadableBuffer, offset: int = 0) -> tuple[Any, ...]: ...
def iter_unpack(self, buffer: ReadableBuffer, /) -> Iterator[tuple[Any, ...]]: ...

View File

@@ -1,5 +1,5 @@
from typing import Any
from typing_extensions import TypeAlias
from typing_extensions import Self, TypeAlias
from weakref import ReferenceType
__all__ = ["local"]
@@ -12,6 +12,7 @@ class _localimpl:
def create_dict(self) -> _LocalDict: ...
class local:
def __new__(cls, /, *args: Any, **kw: Any) -> Self: ...
def __getattribute__(self, name: str) -> Any: ...
def __setattr__(self, name: str, value: Any) -> None: ...
def __delattr__(self, name: str) -> None: ...

View File

@@ -1284,9 +1284,7 @@ class property:
@final
class _NotImplementedType(Any):
# A little weird, but typing the __call__ as NotImplemented makes the error message
# for NotImplemented() much better
__call__: NotImplemented # type: ignore[valid-type] # pyright: ignore[reportInvalidTypeForm]
__call__: None
NotImplemented: _NotImplementedType
@@ -1917,7 +1915,7 @@ class StopIteration(Exception):
class OSError(Exception):
errno: int | None
strerror: str
strerror: str | None
# filename, filename2 are actually str | bytes | None
filename: Any
filename2: Any

View File

@@ -1,9 +1,10 @@
import _compression
import sys
from _bz2 import BZ2Compressor as BZ2Compressor, BZ2Decompressor as BZ2Decompressor
from _compression import BaseStream
from _typeshed import ReadableBuffer, StrOrBytesPath, WriteableBuffer
from collections.abc import Iterable
from typing import IO, Any, Literal, Protocol, SupportsIndex, TextIO, final, overload
from typing import IO, Any, Literal, Protocol, SupportsIndex, TextIO, overload
from typing_extensions import Self, TypeAlias
__all__ = ["BZ2File", "BZ2Compressor", "BZ2Decompressor", "open", "compress", "decompress"]
@@ -128,19 +129,3 @@ class BZ2File(BaseStream, IO[bytes]):
def seek(self, offset: int, whence: int = 0) -> int: ...
def write(self, data: ReadableBuffer) -> int: ...
def writelines(self, seq: Iterable[ReadableBuffer]) -> None: ...
@final
class BZ2Compressor:
def __init__(self, compresslevel: int = 9) -> None: ...
def compress(self, data: ReadableBuffer, /) -> bytes: ...
def flush(self) -> bytes: ...
@final
class BZ2Decompressor:
def decompress(self, data: ReadableBuffer, max_length: int = -1) -> bytes: ...
@property
def eof(self) -> bool: ...
@property
def needs_input(self) -> bool: ...
@property
def unused_data(self) -> bytes: ...

View File

@@ -1,63 +1,3 @@
import sys
from collections.abc import Callable, Iterator, Mapping
from typing import Any, ClassVar, Generic, TypeVar, final, overload
from typing_extensions import ParamSpec
if sys.version_info >= (3, 9):
from types import GenericAlias
from _contextvars import Context as Context, ContextVar as ContextVar, Token as Token, copy_context as copy_context
__all__ = ("Context", "ContextVar", "Token", "copy_context")
_T = TypeVar("_T")
_D = TypeVar("_D")
_P = ParamSpec("_P")
@final
class ContextVar(Generic[_T]):
@overload
def __init__(self, name: str) -> None: ...
@overload
def __init__(self, name: str, *, default: _T) -> None: ...
def __hash__(self) -> int: ...
@property
def name(self) -> str: ...
@overload
def get(self) -> _T: ...
@overload
def get(self, default: _T, /) -> _T: ...
@overload
def get(self, default: _D, /) -> _D | _T: ...
def set(self, value: _T, /) -> Token[_T]: ...
def reset(self, token: Token[_T], /) -> None: ...
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
@final
class Token(Generic[_T]):
@property
def var(self) -> ContextVar[_T]: ...
@property
def old_value(self) -> Any: ... # returns either _T or MISSING, but that's hard to express
MISSING: ClassVar[object]
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
def copy_context() -> Context: ...
# It doesn't make sense to make this generic, because for most Contexts each ContextVar will have
# a different value.
@final
class Context(Mapping[ContextVar[Any], Any]):
def __init__(self) -> None: ...
@overload
def get(self, key: ContextVar[_T], default: None = None, /) -> _T | None: ...
@overload
def get(self, key: ContextVar[_T], default: _T, /) -> _T: ...
@overload
def get(self, key: ContextVar[_T], default: _D, /) -> _T | _D: ...
def run(self, callable: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> _T: ...
def copy(self) -> Context: ...
def __getitem__(self, key: ContextVar[_T], /) -> _T: ...
def __iter__(self) -> Iterator[ContextVar[Any]]: ...
def __len__(self) -> int: ...
def __eq__(self, value: object, /) -> bool: ...

View File

@@ -1,47 +1 @@
import sys
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
if sys.platform != "win32":
_T = TypeVar("_T")
_KeyType: TypeAlias = str | ReadOnlyBuffer
_ValueType: TypeAlias = str | ReadOnlyBuffer
open_flags: str
class error(OSError): ...
# Actual typename gdbm, not exposed by the implementation
class _gdbm:
def firstkey(self) -> bytes | None: ...
def nextkey(self, key: _KeyType) -> bytes | None: ...
def reorganize(self) -> None: ...
def sync(self) -> None: ...
def close(self) -> None: ...
if sys.version_info >= (3, 13):
def clear(self) -> None: ...
def __getitem__(self, item: _KeyType) -> bytes: ...
def __setitem__(self, key: _KeyType, value: _ValueType) -> None: ...
def __delitem__(self, key: _KeyType) -> None: ...
def __contains__(self, key: _KeyType) -> bool: ...
def __len__(self) -> int: ...
def __enter__(self) -> Self: ...
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None: ...
@overload
def get(self, k: _KeyType) -> bytes | None: ...
@overload
def get(self, k: _KeyType, default: _T) -> bytes | _T: ...
def keys(self) -> list[bytes]: ...
def setdefault(self, k: _KeyType, default: _ValueType = ...) -> bytes: ...
# Don't exist at runtime
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _gdbm: ...
from _gdbm import *

View File

@@ -1,43 +1 @@
import sys
from _typeshed import ReadOnlyBuffer, StrOrBytesPath
from types import TracebackType
from typing import TypeVar, overload
from typing_extensions import Self, TypeAlias
if sys.platform != "win32":
_T = TypeVar("_T")
_KeyType: TypeAlias = str | ReadOnlyBuffer
_ValueType: TypeAlias = str | ReadOnlyBuffer
class error(OSError): ...
library: str
# Actual typename dbm, not exposed by the implementation
class _dbm:
def close(self) -> None: ...
if sys.version_info >= (3, 13):
def clear(self) -> None: ...
def __getitem__(self, item: _KeyType) -> bytes: ...
def __setitem__(self, key: _KeyType, value: _ValueType) -> None: ...
def __delitem__(self, key: _KeyType) -> None: ...
def __len__(self) -> int: ...
def __del__(self) -> None: ...
def __enter__(self) -> Self: ...
def __exit__(
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
) -> None: ...
@overload
def get(self, k: _KeyType) -> bytes | None: ...
@overload
def get(self, k: _KeyType, default: _T) -> bytes | _T: ...
def keys(self) -> list[bytes]: ...
def setdefault(self, k: _KeyType, default: _ValueType = ...) -> bytes: ...
# Don't exist at runtime
__new__: None # type: ignore[assignment]
__init__: None # type: ignore[assignment]
if sys.version_info >= (3, 11):
def open(filename: StrOrBytesPath, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...
else:
def open(filename: str, flags: str = "r", mode: int = 0o666, /) -> _dbm: ...
from _dbm import *

View File

@@ -1,5 +1,5 @@
from _typeshed import BytesPath, StrPath, Unused
from collections.abc import Callable, Iterable
from collections.abc import Callable, Iterable, Sequence
from distutils.file_util import _BytesPathT, _StrPathT
from typing import Literal, overload
from typing_extensions import TypeAlias, TypeVarTuple, Unpack
@@ -63,7 +63,7 @@ class CCompiler:
def set_executables(self, **args: str) -> None: ...
def compile(
self,
sources: list[str],
sources: Sequence[StrPath],
output_dir: str | None = None,
macros: list[_Macro] | None = None,
include_dirs: list[str] | None = None,

View File

@@ -2,7 +2,7 @@ import sys
from collections.abc import Callable
from decimal import Decimal
from numbers import Integral, Rational, Real
from typing import Any, Literal, SupportsIndex, overload
from typing import Any, Literal, Protocol, SupportsIndex, overload
from typing_extensions import Self, TypeAlias
_ComparableNum: TypeAlias = int | float | Decimal | Real
@@ -20,11 +20,19 @@ else:
@overload
def gcd(a: Integral, b: Integral) -> Integral: ...
class _ConvertibleToIntegerRatio(Protocol):
def as_integer_ratio(self) -> tuple[int | Rational, int | Rational]: ...
class Fraction(Rational):
@overload
def __new__(cls, numerator: int | Rational = 0, denominator: int | Rational | None = None) -> Self: ...
@overload
def __new__(cls, value: float | Decimal | str, /) -> Self: ...
if sys.version_info >= (3, 14):
@overload
def __new__(cls, value: _ConvertibleToIntegerRatio) -> Self: ...
@classmethod
def from_float(cls, f: float) -> Self: ...
@classmethod

View File

@@ -1,7 +1,11 @@
from collections.abc import Iterable
__all__ = ["GetoptError", "error", "getopt", "gnu_getopt"]
def getopt(args: list[str], shortopts: str, longopts: list[str] = []) -> tuple[list[tuple[str, str]], list[str]]: ...
def gnu_getopt(args: list[str], shortopts: str, longopts: list[str] = []) -> tuple[list[tuple[str, str]], list[str]]: ...
def getopt(args: list[str], shortopts: str, longopts: Iterable[str] | str = []) -> tuple[list[tuple[str, str]], list[str]]: ...
def gnu_getopt(
args: list[str], shortopts: str, longopts: Iterable[str] | str = []
) -> tuple[list[tuple[str, str]], list[str]]: ...
class GetoptError(Exception):
msg: str

View File

@@ -128,19 +128,6 @@ class _BaseNetwork(_IPAddressBase, Generic[_A]):
@property
def hostmask(self) -> _A: ...
class _BaseInterface(_BaseAddress, Generic[_A, _N]):
hostmask: _A
netmask: _A
network: _N
@property
def ip(self) -> _A: ...
@property
def with_hostmask(self) -> str: ...
@property
def with_netmask(self) -> str: ...
@property
def with_prefixlen(self) -> str: ...
class _BaseV4:
@property
def version(self) -> Literal[4]: ...
@@ -154,9 +141,21 @@ class IPv4Address(_BaseV4, _BaseAddress):
class IPv4Network(_BaseV4, _BaseNetwork[IPv4Address]): ...
class IPv4Interface(IPv4Address, _BaseInterface[IPv4Address, IPv4Network]):
class IPv4Interface(IPv4Address):
netmask: IPv4Address
network: IPv4Network
def __eq__(self, other: object) -> bool: ...
def __hash__(self) -> int: ...
@property
def hostmask(self) -> IPv4Address: ...
@property
def ip(self) -> IPv4Address: ...
@property
def with_hostmask(self) -> str: ...
@property
def with_netmask(self) -> str: ...
@property
def with_prefixlen(self) -> str: ...
class _BaseV6:
@property
@@ -184,9 +183,21 @@ class IPv6Network(_BaseV6, _BaseNetwork[IPv6Address]):
@property
def is_site_local(self) -> bool: ...
class IPv6Interface(IPv6Address, _BaseInterface[IPv6Address, IPv6Network]):
class IPv6Interface(IPv6Address):
netmask: IPv6Address
network: IPv6Network
def __eq__(self, other: object) -> bool: ...
def __hash__(self) -> int: ...
@property
def hostmask(self) -> IPv6Address: ...
@property
def ip(self) -> IPv6Address: ...
@property
def with_hostmask(self) -> str: ...
@property
def with_netmask(self) -> str: ...
@property
def with_prefixlen(self) -> str: ...
def v4_int_to_packed(address: int) -> bytes: ...
def v6_int_to_packed(address: int) -> bytes: ...

View File

@@ -1,6 +1,6 @@
import sys
from _typeshed import StrOrBytesPath
from collections.abc import Callable, Hashable, Iterable, Sequence
from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
from configparser import RawConfigParser
from re import Pattern
from threading import Thread
@@ -63,7 +63,7 @@ def dictConfig(config: _DictConfigArgs | dict[str, Any]) -> None: ...
if sys.version_info >= (3, 10):
def fileConfig(
fname: StrOrBytesPath | IO[str] | RawConfigParser,
defaults: dict[str, str] | None = None,
defaults: Mapping[str, str] | None = None,
disable_existing_loggers: bool = True,
encoding: str | None = None,
) -> None: ...
@@ -71,7 +71,7 @@ if sys.version_info >= (3, 10):
else:
def fileConfig(
fname: StrOrBytesPath | IO[str] | RawConfigParser,
defaults: dict[str, str] | None = None,
defaults: Mapping[str, str] | None = None,
disable_existing_loggers: bool = True,
) -> None: ...

View File

@@ -260,6 +260,8 @@ class QueueHandler(Handler):
def __init__(self, queue: _QueueLike[Any]) -> None: ...
def prepare(self, record: LogRecord) -> Any: ...
def enqueue(self, record: LogRecord) -> None: ...
if sys.version_info >= (3, 12):
listener: QueueListener | None
class QueueListener:
handlers: tuple[Handler, ...] # undocumented

View File

@@ -1,7 +1,41 @@
from _compression import BaseStream
from _lzma import (
CHECK_CRC32 as CHECK_CRC32,
CHECK_CRC64 as CHECK_CRC64,
CHECK_ID_MAX as CHECK_ID_MAX,
CHECK_NONE as CHECK_NONE,
CHECK_SHA256 as CHECK_SHA256,
CHECK_UNKNOWN as CHECK_UNKNOWN,
FILTER_ARM as FILTER_ARM,
FILTER_ARMTHUMB as FILTER_ARMTHUMB,
FILTER_DELTA as FILTER_DELTA,
FILTER_IA64 as FILTER_IA64,
FILTER_LZMA1 as FILTER_LZMA1,
FILTER_LZMA2 as FILTER_LZMA2,
FILTER_POWERPC as FILTER_POWERPC,
FILTER_SPARC as FILTER_SPARC,
FILTER_X86 as FILTER_X86,
FORMAT_ALONE as FORMAT_ALONE,
FORMAT_AUTO as FORMAT_AUTO,
FORMAT_RAW as FORMAT_RAW,
FORMAT_XZ as FORMAT_XZ,
MF_BT2 as MF_BT2,
MF_BT3 as MF_BT3,
MF_BT4 as MF_BT4,
MF_HC3 as MF_HC3,
MF_HC4 as MF_HC4,
MODE_FAST as MODE_FAST,
MODE_NORMAL as MODE_NORMAL,
PRESET_DEFAULT as PRESET_DEFAULT,
PRESET_EXTREME as PRESET_EXTREME,
LZMACompressor as LZMACompressor,
LZMADecompressor as LZMADecompressor,
LZMAError as LZMAError,
_FilterChain,
is_check_supported as is_check_supported,
)
from _typeshed import ReadableBuffer, StrOrBytesPath
from collections.abc import Mapping, Sequence
from typing import IO, Any, Final, Literal, TextIO, final, overload
from typing import IO, Literal, TextIO, overload
from typing_extensions import Self, TypeAlias
__all__ = [
@@ -48,62 +82,6 @@ _OpenTextWritingMode: TypeAlias = Literal["wt", "xt", "at"]
_PathOrFile: TypeAlias = StrOrBytesPath | IO[bytes]
_FilterChain: TypeAlias = Sequence[Mapping[str, Any]]
FORMAT_AUTO: Final = 0
FORMAT_XZ: Final = 1
FORMAT_ALONE: Final = 2
FORMAT_RAW: Final = 3
CHECK_NONE: Final = 0
CHECK_CRC32: Final = 1
CHECK_CRC64: Final = 4
CHECK_SHA256: Final = 10
CHECK_ID_MAX: Final = 15
CHECK_UNKNOWN: Final = 16
FILTER_LZMA1: int # v big number
FILTER_LZMA2: Final = 33
FILTER_DELTA: Final = 3
FILTER_X86: Final = 4
FILTER_IA64: Final = 6
FILTER_ARM: Final = 7
FILTER_ARMTHUMB: Final = 8
FILTER_SPARC: Final = 9
FILTER_POWERPC: Final = 5
MF_HC3: Final = 3
MF_HC4: Final = 4
MF_BT2: Final = 18
MF_BT3: Final = 19
MF_BT4: Final = 20
MODE_FAST: Final = 1
MODE_NORMAL: Final = 2
PRESET_DEFAULT: Final = 6
PRESET_EXTREME: int # v big number
# from _lzma.c
@final
class LZMADecompressor:
def __init__(self, format: int | None = ..., memlimit: int | None = ..., filters: _FilterChain | None = ...) -> None: ...
def decompress(self, data: ReadableBuffer, max_length: int = -1) -> bytes: ...
@property
def check(self) -> int: ...
@property
def eof(self) -> bool: ...
@property
def unused_data(self) -> bytes: ...
@property
def needs_input(self) -> bool: ...
# from _lzma.c
@final
class LZMACompressor:
def __init__(
self, format: int | None = ..., check: int = ..., preset: int | None = ..., filters: _FilterChain | None = ...
) -> None: ...
def compress(self, data: ReadableBuffer, /) -> bytes: ...
def flush(self) -> bytes: ...
class LZMAError(Exception): ...
class LZMAFile(BaseStream, IO[bytes]): # type: ignore[misc] # incompatible definitions of writelines in the base classes
def __init__(
self,
@@ -194,4 +172,3 @@ def compress(
def decompress(
data: ReadableBuffer, format: int = 0, memlimit: int | None = None, filters: _FilterChain | None = None
) -> bytes: ...
def is_check_supported(check_id: int, /) -> bool: ...

View File

@@ -34,9 +34,22 @@ class mmap:
if sys.platform == "win32":
def __init__(self, fileno: int, length: int, tagname: str | None = ..., access: int = ..., offset: int = ...) -> None: ...
else:
def __init__(
self, fileno: int, length: int, flags: int = ..., prot: int = ..., access: int = ..., offset: int = ...
) -> None: ...
if sys.version_info >= (3, 13):
def __init__(
self,
fileno: int,
length: int,
flags: int = ...,
prot: int = ...,
access: int = ...,
offset: int = ...,
*,
trackfd: bool = True,
) -> None: ...
else:
def __init__(
self, fileno: int, length: int, flags: int = ..., prot: int = ..., access: int = ..., offset: int = ...
) -> None: ...
def close(self) -> None: ...
def flush(self, offset: int = ..., size: int = ...) -> None: ...

View File

@@ -10,6 +10,7 @@ from typing_extensions import Self, TypeAlias
from .connection import Connection
from .context import BaseContext
from .shared_memory import _SLT, ShareableList as _ShareableList, SharedMemory as _SharedMemory
from .util import Finalize as _Finalize
__all__ = ["BaseManager", "SyncManager", "BaseProxy", "Token", "SharedMemoryManager"]
@@ -60,31 +61,58 @@ class ValueProxy(BaseProxy, Generic[_T]):
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
class DictProxy(BaseProxy, MutableMapping[_KT, _VT]):
__builtins__: ClassVar[dict[str, Any]]
def __len__(self) -> int: ...
def __getitem__(self, key: _KT, /) -> _VT: ...
def __setitem__(self, key: _KT, value: _VT, /) -> None: ...
def __delitem__(self, key: _KT, /) -> None: ...
def __iter__(self) -> Iterator[_KT]: ...
def copy(self) -> dict[_KT, _VT]: ...
@overload # type: ignore[override]
def get(self, key: _KT, /) -> _VT | None: ...
@overload
def get(self, key: _KT, default: _VT, /) -> _VT: ...
@overload
def get(self, key: _KT, default: _T, /) -> _VT | _T: ...
@overload
def pop(self, key: _KT, /) -> _VT: ...
@overload
def pop(self, key: _KT, default: _VT, /) -> _VT: ...
@overload
def pop(self, key: _KT, default: _T, /) -> _VT | _T: ...
def keys(self) -> list[_KT]: ... # type: ignore[override]
def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override]
def values(self) -> list[_VT]: ... # type: ignore[override]
if sys.version_info >= (3, 13):
def __class_getitem__(cls, args: Any, /) -> Any: ...
if sys.version_info >= (3, 13):
class _BaseDictProxy(BaseProxy, MutableMapping[_KT, _VT]):
__builtins__: ClassVar[dict[str, Any]]
def __len__(self) -> int: ...
def __getitem__(self, key: _KT, /) -> _VT: ...
def __setitem__(self, key: _KT, value: _VT, /) -> None: ...
def __delitem__(self, key: _KT, /) -> None: ...
def __iter__(self) -> Iterator[_KT]: ...
def copy(self) -> dict[_KT, _VT]: ...
@overload # type: ignore[override]
def get(self, key: _KT, /) -> _VT | None: ...
@overload
def get(self, key: _KT, default: _VT, /) -> _VT: ...
@overload
def get(self, key: _KT, default: _T, /) -> _VT | _T: ...
@overload
def pop(self, key: _KT, /) -> _VT: ...
@overload
def pop(self, key: _KT, default: _VT, /) -> _VT: ...
@overload
def pop(self, key: _KT, default: _T, /) -> _VT | _T: ...
def keys(self) -> list[_KT]: ... # type: ignore[override]
def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override]
def values(self) -> list[_VT]: ... # type: ignore[override]
class DictProxy(_BaseDictProxy[_KT, _VT]):
def __class_getitem__(cls, args: Any, /) -> GenericAlias: ...
else:
class DictProxy(BaseProxy, MutableMapping[_KT, _VT]):
__builtins__: ClassVar[dict[str, Any]]
def __len__(self) -> int: ...
def __getitem__(self, key: _KT, /) -> _VT: ...
def __setitem__(self, key: _KT, value: _VT, /) -> None: ...
def __delitem__(self, key: _KT, /) -> None: ...
def __iter__(self) -> Iterator[_KT]: ...
def copy(self) -> dict[_KT, _VT]: ...
@overload # type: ignore[override]
def get(self, key: _KT, /) -> _VT | None: ...
@overload
def get(self, key: _KT, default: _VT, /) -> _VT: ...
@overload
def get(self, key: _KT, default: _T, /) -> _VT | _T: ...
@overload
def pop(self, key: _KT, /) -> _VT: ...
@overload
def pop(self, key: _KT, default: _VT, /) -> _VT: ...
@overload
def pop(self, key: _KT, default: _T, /) -> _VT | _T: ...
def keys(self) -> list[_KT]: ... # type: ignore[override]
def items(self) -> list[tuple[_KT, _VT]]: ... # type: ignore[override]
def values(self) -> list[_VT]: ... # type: ignore[override]
class BaseListProxy(BaseProxy, MutableSequence[_T]):
__builtins__: ClassVar[dict[str, Any]]
@@ -156,7 +184,7 @@ class BaseManager:
def get_server(self) -> Server: ...
def connect(self) -> None: ...
def start(self, initializer: Callable[..., object] | None = None, initargs: Iterable[Any] = ()) -> None: ...
def shutdown(self) -> None: ... # only available after start() was called
shutdown: _Finalize # only available after start() was called
def join(self, timeout: float | None = None) -> None: ... # undocumented
@property
def address(self) -> Any: ...

View File

@@ -23,7 +23,7 @@ def get_command_line(**kwds: Any) -> list[str]: ...
def spawn_main(pipe_handle: int, parent_pid: int | None = None, tracker_fd: int | None = None) -> None: ...
# undocumented
def _main(fd: int) -> Any: ...
def _main(fd: int, parent_sentinel: int) -> int: ...
def get_preparation_data(name: str) -> dict[str, Any]: ...
old_main_modules: list[ModuleType]

View File

@@ -1,4 +1,5 @@
import sys
from _queue import Empty as Empty, SimpleQueue as SimpleQueue
from threading import Condition, Lock
from typing import Any, Generic, TypeVar
@@ -11,7 +12,6 @@ if sys.version_info >= (3, 13):
_T = TypeVar("_T")
class Empty(Exception): ...
class Full(Exception): ...
if sys.version_info >= (3, 13):
@@ -55,14 +55,3 @@ class PriorityQueue(Queue[_T]):
class LifoQueue(Queue[_T]):
queue: list[_T]
class SimpleQueue(Generic[_T]):
def __init__(self) -> None: ...
def empty(self) -> bool: ...
def get(self, block: bool = True, timeout: float | None = None) -> _T: ...
def get_nowait(self) -> _T: ...
def put(self, item: _T, block: bool = True, timeout: float | None = None) -> None: ...
def put_nowait(self, item: _T) -> None: ...
def qsize(self) -> int: ...
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...

View File

@@ -1,26 +1,5 @@
from _typeshed import ReadableBuffer, WriteableBuffer
from collections.abc import Iterator
from typing import Any
from _struct import *
__all__ = ["calcsize", "pack", "pack_into", "unpack", "unpack_from", "iter_unpack", "Struct", "error"]
class error(Exception): ...
def pack(fmt: str | bytes, /, *v: Any) -> bytes: ...
def pack_into(fmt: str | bytes, buffer: WriteableBuffer, offset: int, /, *v: Any) -> None: ...
def unpack(format: str | bytes, buffer: ReadableBuffer, /) -> tuple[Any, ...]: ...
def unpack_from(format: str | bytes, /, buffer: ReadableBuffer, offset: int = 0) -> tuple[Any, ...]: ...
def iter_unpack(format: str | bytes, buffer: ReadableBuffer, /) -> Iterator[tuple[Any, ...]]: ...
def calcsize(format: str | bytes, /) -> int: ...
class Struct:
@property
def format(self) -> str: ...
@property
def size(self) -> int: ...
def __init__(self, format: str | bytes) -> None: ...
def pack(self, *v: Any) -> bytes: ...
def pack_into(self, buffer: WriteableBuffer, offset: int, *v: Any) -> None: ...
def unpack(self, buffer: ReadableBuffer, /) -> tuple[Any, ...]: ...
def unpack_from(self, buffer: ReadableBuffer, offset: int = 0) -> tuple[Any, ...]: ...
def iter_unpack(self, buffer: ReadableBuffer, /) -> Iterator[tuple[Any, ...]]: ...

View File

@@ -1,7 +1,7 @@
import bz2
import io
import sys
from _typeshed import StrOrBytesPath, StrPath
from _typeshed import StrOrBytesPath, StrPath, SupportsRead
from builtins import list as _list # aliases to avoid name clashes with fields named "type" or "list"
from collections.abc import Callable, Iterable, Iterator, Mapping
from gzip import _ReadableFileobj as _GzipReadableFileobj, _WritableFileobj as _GzipWritableFileobj
@@ -481,7 +481,7 @@ class TarFile:
*,
filter: Callable[[TarInfo], TarInfo | None] | None = None,
) -> None: ...
def addfile(self, tarinfo: TarInfo, fileobj: IO[bytes] | None = None) -> None: ...
def addfile(self, tarinfo: TarInfo, fileobj: SupportsRead[bytes] | None = None) -> None: ...
def gettarinfo(
self, name: StrOrBytesPath | None = None, arcname: str | None = None, fileobj: IO[bytes] | None = None
) -> TarInfo: ...

View File

@@ -3,15 +3,15 @@ use std::any::Any;
use js_sys::Error;
use wasm_bindgen::prelude::*;
use red_knot_workspace::db::RootDatabase;
use red_knot_workspace::db::{Db, RootDatabase};
use red_knot_workspace::workspace::settings::Configuration;
use red_knot_workspace::workspace::WorkspaceMetadata;
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
use ruff_db::system::{
DirectoryEntry, MemoryFileSystem, Metadata, System, SystemPath, SystemPathBuf,
SystemVirtualPath,
DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System, SystemPath,
SystemPathBuf, SystemVirtualPath,
};
use ruff_notebook::Notebook;
@@ -42,10 +42,10 @@ impl Workspace {
#[wasm_bindgen(constructor)]
pub fn new(root: &str, settings: &Settings) -> Result<Workspace, Error> {
let system = WasmSystem::new(SystemPath::new(root));
let workspace = WorkspaceMetadata::from_path(
let workspace = WorkspaceMetadata::discover(
SystemPath::new(root),
&system,
Some(Configuration {
Some(&Configuration {
target_version: Some(settings.target_version.into()),
..Configuration::default()
}),
@@ -184,8 +184,8 @@ impl Settings {
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum TargetVersion {
Py37,
#[default]
Py38,
#[default]
Py39,
Py310,
Py311,
@@ -226,7 +226,7 @@ impl System for WasmSystem {
}
fn canonicalize_path(&self, path: &SystemPath) -> ruff_db::system::Result<SystemPathBuf> {
Ok(self.fs.canonicalize(path))
self.fs.canonicalize(path)
}
fn read_to_string(&self, path: &SystemPath) -> ruff_db::system::Result<String> {
@@ -272,6 +272,13 @@ impl System for WasmSystem {
self.fs.walk_directory(path)
}
fn glob(
&self,
pattern: &str,
) -> Result<Box<dyn Iterator<Item = Result<SystemPathBuf, GlobError>>>, PatternError> {
Ok(Box::new(self.fs.glob(pattern)?))
}
fn as_any(&self) -> &dyn Any {
self
}
@@ -284,3 +291,17 @@ impl System for WasmSystem {
fn not_found() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory")
}
#[cfg(test)]
mod tests {
use crate::TargetVersion;
use red_knot_python_semantic::PythonVersion;
#[test]
fn same_default_as_python_version() {
assert_eq!(
PythonVersion::from(TargetVersion::default()),
PythonVersion::default()
);
}
}

View File

@@ -15,22 +15,29 @@ license.workspace = true
red_knot_python_semantic = { workspace = true }
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["os", "cache"] }
ruff_python_ast = { workspace = true }
ruff_db = { workspace = true, features = ["os", "cache", "serde"] }
ruff_python_ast = { workspace = true, features = ["serde"] }
ruff_text_size = { workspace = true }
red_knot_vendored = { workspace = true }
anyhow = { workspace = true }
crossbeam = { workspace = true }
glob = { workspace = true }
notify = { workspace = true }
pep440_rs = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
red_knot_python_semantic = { workspace = true, features = ["serde"] }
ruff_db = { workspace = true, features = ["testing"] }
tempfile = { workspace = true }
glob = { workspace = true }
insta = { workspace = true, features = ["redactions", "ron"] }
[features]
default = ["zstd"]

View File

@@ -0,0 +1 @@
../../../../ruff_python_parser/resources/invalid/statements/invalid_assignment_targets.py

View File

@@ -0,0 +1 @@
../../../../ruff_python_parser/resources/invalid/expressions/named/invalid_target.py

View File

@@ -0,0 +1 @@
../../../../ruff_python_parser/resources/invalid/statements/invalid_augmented_assignment_target.py

View File

@@ -0,0 +1 @@
x if $z

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