Compare commits

...

123 Commits

Author SHA1 Message Date
David Peter
7e90171fc8 Make try_call_dunder_get a query 2025-02-20 17:57:51 +01:00
David Peter
fe9f634e87 Use or_fall_back_to 2025-02-20 17:26:02 +01:00
David Peter
8829b65fde Use __kwdefaults__ instead of __module__ 2025-02-20 17:26:02 +01:00
David Peter
f0cd11867b Attempt to model getattr_static on gradual types 2025-02-20 17:26:02 +01:00
David Peter
c796daa515 Model fallback MethodType => FunctionType 2025-02-20 17:26:02 +01:00
David Peter
3ccd03004c Remove incorrect __class__ lookup branch in static_member 2025-02-20 17:26:02 +01:00
David Peter
f47d866958 Add doc comment for try_call_dunder_get 2025-02-20 17:26:02 +01:00
David Peter
eacb866c9c Add TODO for builtins.tuple attribute lookups 2025-02-20 17:26:02 +01:00
David Peter
6a00cd30b7 Add FunctionLiteral and BoundMethod to property tests 2025-02-20 17:26:02 +01:00
David Peter
c70f82abb3 Wording 2025-02-20 17:26:02 +01:00
David Peter
42e92a47e9 Add TODO: Type::member should return Result 2025-02-20 17:26:02 +01:00
David Peter
6667fc2c69 Return a Result from try_call_dunder_get 2025-02-20 17:26:02 +01:00
David Peter
96ae84822d Add reference to 'Functions and methods' section in the descriptor guide 2025-02-20 17:26:02 +01:00
David Peter
bc607d7d9a Properly catch errors to known function calls 2025-02-20 17:26:02 +01:00
David Peter
f2be4d4385 Wording and typos 2025-02-20 17:26:02 +01:00
David Peter
b6f9af15e5 Introduce CallArguments::none() 2025-02-20 17:26:02 +01:00
David Peter
9775eecc81 Fix two typos 2025-02-20 17:26:02 +01:00
David Peter
27ea06d140 Remove memoryview as a KnownClass 2025-02-20 17:26:02 +01:00
David Peter
5edd2ecd89 Fix clippy suggestion 2025-02-20 17:26:02 +01:00
David Peter
c01b1c8f5c Handle errors in __get__ calls 2025-02-20 17:26:02 +01:00
David Peter
31987b173f Use write!(…) 2025-02-20 17:26:02 +01:00
David Peter
bed80764b1 Patch is_assignable_to to add partial support for SupportsIndex 2025-02-20 17:26:02 +01:00
David Peter
fb44f254ab [red-knot] Method calls and descriptor protocol 2025-02-20 17:26:02 +01:00
Brent Westbrook
b385c7d22a Specify the wasm-pack version for release workflows (#16278)
This PR uses the same version specified in
https://github.com/astral-sh/ruff/pull/14465 for the CI workflow to
prevent random versions from being pulled like in the 0.9.7
[release](https://github.com/astral-sh/ruff/actions/runs/13436100909/job/37539387595).
2025-02-20 10:17:58 -05:00
Douglas Creager
529950fba1 [red-knot] Separate definitions_by_definition into separate fields (#16277)
A minor cleanup that breaks up a `HashMap` of an enum into separate
`HashMap`s for each variant. (These separate fields were already how
this cache was being described in the big comment at the top of the
file!)
2025-02-20 09:47:01 -05:00
Andrew Gallant
205222ca6b red_knot_python_semantic: avoid adding callable_ty to CallBindingError
This is a small tweak to avoid adding the callable `Type` on the error
value itself. Namely, it's always available regardless of the error, and
it's easy to pass it down explicitly to the diagnostic generating code.

It's likely that the other `CallBindingError` variants will also want
the callable `Type` to improve diagnostics too. This way, we don't have
to duplicate the `Type` on each variant. It's just available to all of
them.

Ref https://github.com/astral-sh/ruff/pull/16239#discussion_r1962352646
2025-02-20 08:18:59 -05:00
Brent Westbrook
54fccb3ee2 Bump version to 0.9.7 (#16271) 2025-02-20 08:12:11 -05:00
David Peter
8198668fc3 [red-knot] MDTest: Use custom class names instead of builtins (#16269)
## Summary

Follow up on the discussion
[here](https://github.com/astral-sh/ruff/pull/16121#discussion_r1962973298).
Replace builtin classes with custom placeholder names, which should
hopefully make the tests a bit easier to understand.

I carefully renamed things one after the other, to make sure that there
is no functional change in the tests.
2025-02-20 12:25:55 +00:00
Dhruv Manilawala
fc6b03c8da Handle requests received after shutdown message (#16262)
## Summary

This PR should help in
https://github.com/astral-sh/ruff-vscode/issues/676.

There are two issues that this is trying to fix all related to the way
shutdown should happen as per the protocol:
1. After the server handled the [shutdown
request](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#shutdown)
and while waiting for the exit notification:
	
> If a server receives requests after a shutdown request those requests
should error with `InvalidRequest`.
    
But, we raised an error and exited. This PR fixes it by entering a loop
which responds to any request during this period with `InvalidRequest`

2. If the server received an [exit
notification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#exit)
but the shutdown request was never received, the server handled that by
logging and exiting with success but as per the spec:

> The server should exit with success code 0 if the shutdown request has
been received before; otherwise with error code 1.

    So, this PR fixes that as well by raising an error in this case.

## Test Plan

I'm not sure how to go about testing this without using a mock server.
2025-02-20 11:10:42 +00:00
Micha Reiser
fb09d63e55 [red-knot] Prefix Type::call and dunder_call with try (#16261) 2025-02-20 09:05:04 +00:00
Alex Waygood
16d0625dfb Improve internal docs for various string-node APIs (#16256) 2025-02-19 16:13:45 +00:00
Alex Waygood
25920fe489 Rename ExprStringLiteral::as_unconcatenated_string() to ExprStringLiteral::as_single_part_string() (#16253) 2025-02-19 16:06:57 +00:00
Brent Westbrook
97d0659ce3 Pass ParserOptions to the parser (#16220)
## Summary

This is part of the preparation for detecting syntax errors in the
parser from https://github.com/astral-sh/ruff/pull/16090/. As suggested
in [this
comment](https://github.com/astral-sh/ruff/pull/16090/#discussion_r1953084509),
I started working on a `ParseOptions` struct that could be stored in the
parser. For this initial refactor, I only made it hold the existing
`Mode` option, but for syntax errors, we will also need it to have a
`PythonVersion`. For that use case, I'm picturing something like a
`ParseOptions::with_python_version` method, so you can extend the
current calls to something like

```rust
ParseOptions::from(mode).with_python_version(settings.target_version)
```

But I thought it was worth adding `ParseOptions` alone without changing
any other behavior first.

Most of the diff is just updating call sites taking `Mode` to take
`ParseOptions::from(Mode)` or those taking `PySourceType`s to take
`ParseOptions::from(PySourceType)`. The interesting changes are in the
new `parser/options.rs` file and smaller parts of `parser/mod.rs` and
`ruff_python_parser/src/lib.rs`.

## Test Plan

Existing tests, this should not change any behavior.
2025-02-19 10:50:50 -05:00
Douglas Creager
cfc6941d5c [red-knot] Resolve references in eager nested scopes eagerly (#16079)
We now resolve references in "eager" scopes correctly — using the
bindings and declarations that are visible at the point where the eager
scope is created, not the "public" type of the symbol (typically the
bindings visible at the end of the scope).

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2025-02-19 10:22:30 -05:00
Alex Waygood
f50849aeef Add text_len() methods to more *Prefix enums in ruff_python_ast (#16254) 2025-02-19 14:47:07 +00:00
Micha Reiser
55ea09401a [red-knot] Allow any Ranged argument for report_lint and report_diagnostic (#16252) 2025-02-19 14:34:56 +01:00
Vasco Schiavo
3032867603 [pycodestyle] Exempt site.addsitedir(...) calls (E402) (#16251) 2025-02-19 14:31:47 +01:00
Andrew Gallant
3ea32e2cdd red_knot_python_semantic: improve diagnostic message for "invalid argument type"
This uses the refactoring and support for secondary diagnostic messages
to improve the diagnostic for "invalid argument type." The main
improvement here is that we show where the function being called is
defined, and annotate the span corresponding to the invalid parameter.
2025-02-19 08:24:19 -05:00
Andrew Gallant
87668e24b1 ruff_db: add "secondary" messages to Diagnostic trait
This is a small little hack to make the `Diagnostic` trait
capable of supporting attaching multiple spans.

This design should be considered transient. This was just the
quickest way that I could see to pass multiple spans through from
the type checker to the diagnostic renderer.
2025-02-19 08:24:19 -05:00
Andrew Gallant
18a9eddf60 ruff_db: refactor snippet rendering
This commit has no behavioral changes.

This refactor moves the logic for turning a `D: Diagnostic` into
an `annotate_snippets::Message` into its own types. This would
ideally just be a function or something, but the `annotate-snippets`
types want borrowed data, and sometimes we need to produce owned
data. So we gather everything we need into our own types and then
spit it back out in the format that `annotate-snippets` wants.

This factor was motivated by wanting to render multiple snippets.
The logic for generating a code frame is complicated enough that
it's worth splitting out so that we can reuse it for other spans.

(Note that one should consider this prototype-level code. It is
unlikely to survive for long.)
2025-02-19 08:24:19 -05:00
Andrew Gallant
222660170c red_knot_python_semantic: remove Ranged impl for TypeCheckDiagnostic
It seems nothing is using it, and I'm not sure if it makes semantic
sense. Particularly if we want to support multiple ranges. One could
make an argument that this ought to correspond to the "primary"
range (which we should have), but I think such a concept is better
expressed as an explicit routine if possible.
2025-02-19 08:24:19 -05:00
Micha Reiser
e84985e9b3 [red-knot] Refactor infer_chained_boolean_types to have access to TypeInferenceBuilder (#16222) 2025-02-19 11:13:35 +01:00
InSync
01c3e6b94f Add red_knot/README.md (#16230)
## Summary

Resolves #15979.

The file explains what Red Knot is (a type checker), what state it is in
(not yet ready for user testing), what its goals ("extremely fast") and
non-goals (not a drop-in replacement for other type checkers) are as
well as what the crates contain.

## Test Plan

None.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-18 23:31:02 -08:00
Wei Lee
e92d43dfcd [airflow] move class attributed related cases to AIR302_class_attribute (AIR302) (#16226)
## Summary


Move class attribute (property, methods, variables) related cases in
AIR302_names to AIR302_class_attribute

## Test Plan


No functionality change. Test fixture is reogranized
2025-02-19 11:13:17 +05:30
David Peter
877c1066d3 [red-knot] Update tests for attributes inferred from parameters (#16208)
## Summary

Update description and remove TODOs from out `attributes.md` test suite
to reflect our current intentions.

closes https://github.com/astral-sh/ruff/issues/15960
2025-02-18 22:43:11 +01:00
Carl Meyer
00b022d472 [red-knot] update TODO comment in mdtest (#16242)
This comment gave wrong/misleading info about the reason for the wrong
output, just updating it to be correct to avoid confusing our future
selves.
2025-02-18 20:52:17 +00:00
Dylan
a23e489c79 [refurb] Correctly handle lengths of literal strings in slice-to-remove-prefix-or-suffix (FURB188) (#16237)
Fixes false negative when slice bound uses length of string literal.

We were meant to check the following, for example. Given:

```python
  text[:bound] if text.endswith(suffix) else text
```
We want to know whether:
   - `suffix` is a string literal and `bound` is a number literal
   - `suffix` is an expression and `bound` is
       exactly `-len(suffix)` (as AST nodes, prior to evaluation.)
       
The issue is that negative number literals like `-10` are stored as
unary operators applied to a number literal in the AST. So when `suffix`
was a string literal but `bound` was `-len(suffix)` we were getting
caught in the match arm where `bound` needed to be a number. This is now
fixed with a guard.


Closes #16231
2025-02-18 12:52:26 -06:00
Brent Westbrook
1907e60fab Pass ast::PythonVersion to type_hint_resolves_to_any (#16236)
This is a follow-up to #16170 to use `ast::PythonVersion` in the
`type_hint_resolves_to_any` call chain, as suggested (and implemented!)
by Alex
[here](https://github.com/astral-sh/ruff/pull/16170#discussion_r1960015181).

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-18 13:22:50 -05:00
Brent Westbrook
a9efdea113 Use ast::PythonVersion internally in the formatter and linter (#16170)
## Summary

This PR updates the formatter and linter to use the `PythonVersion`
struct from the `ruff_python_ast` crate internally. While this doesn't
remove the need for the `linter::PythonVersion` enum, it does remove the
`formatter::PythonVersion` enum and limits the use in the linter to
deserializing from CLI arguments and config files and moves most of the
remaining methods to the `ast::PythonVersion` struct.

## Test Plan

Existing tests, with some inputs and outputs updated to reflect the new
(de)serialization format. I think these are test-specific and shouldn't
affect any external (de)serialization.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-18 12:03:13 -05:00
InSync
0868e73d2c Add SECURITY.md (#16224)
## Summary

Resolves #16206.

The file was copied almost verbatim from
[uv's](929e7c3ad9/SECURITY.md),
with the first section removed.

## Test Plan

None.
2025-02-18 08:42:55 -06:00
InSync
711af0d929 [refurb] Manual timezone monkeypatching (FURB162) (#16113)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-18 14:35:33 +01:00
sobolevn
d8e3fcca97 [pyupgrade] Do not upgrade functional TypedDicts with private field names to the class-based syntax (UP013) (#16219) 2025-02-18 13:03:27 +00:00
Alex Waygood
66a0467305 Improve docs for PYI019 (#16229) 2025-02-18 12:52:46 +00:00
Micha Reiser
4ed5db0d42 Refactor CallOutcome to Result (#16161) 2025-02-18 13:34:39 +01:00
Alex Waygood
5cd0de3e4c Fix minor punctuation errors (#16228)
Co-authored-by: eqsdxr <rxdsqe@gmail.com>
2025-02-18 12:24:57 +00:00
Dhruv Manilawala
ed9c18d9b4 Include document specific debug info (#16215)
## Summary

Related https://github.com/astral-sh/ruff-vscode/pull/692.

## Test Plan

**When there's no active text document:**

```
[Info  - 10:57:03 PM] Global:
executable = /Users/dhruv/work/astral/ruff/target/debug/ruff
version = 0.9.6
position_encoding = UTF16
workspace_root_folders = [
    "/Users/dhruv/playground/ruff",
]
indexed_configuration_files = [
    "/Users/dhruv/playground/ruff/pyproject.toml",
    "/Users/dhruv/playground/ruff/formatter/ruff.toml",
]
open_documents = 0
client_capabilities = ResolvedClientCapabilities {
    code_action_deferred_edit_resolution: true,
    apply_edit: true,
    document_changes: true,
    workspace_refresh: true,
    pull_diagnostics: true,
}

global_client_settings = ResolvedClientSettings {
    fix_all: true,
    organize_imports: true,
    lint_enable: true,
    disable_rule_comment_enable: true,
    fix_violation_enable: true,
    show_syntax_errors: true,
    editor_settings: ResolvedEditorSettings {
        configuration: None,
        lint_preview: None,
        format_preview: None,
        select: None,
        extend_select: None,
        ignore: None,
        exclude: None,
        line_length: None,
        configuration_preference: EditorFirst,
    },
}
```

**When there's an active text document that's been passed as param:**

```
[Info  - 10:53:33 PM] Global:
executable = /Users/dhruv/work/astral/ruff/target/debug/ruff
version = 0.9.6
position_encoding = UTF16
workspace_root_folders = [
    "/Users/dhruv/playground/ruff",
]
indexed_configuration_files = [
    "/Users/dhruv/playground/ruff/pyproject.toml",
    "/Users/dhruv/playground/ruff/formatter/ruff.toml",
]
open_documents = 1
client_capabilities = ResolvedClientCapabilities {
    code_action_deferred_edit_resolution: true,
    apply_edit: true,
    document_changes: true,
    workspace_refresh: true,
    pull_diagnostics: true,
}

Document:
uri = file:///Users/dhruv/playground/ruff/lsp/play.py
kind = Text
version = 1
client_settings = ResolvedClientSettings {
    fix_all: true,
    organize_imports: true,
    lint_enable: true,
    disable_rule_comment_enable: true,
    fix_violation_enable: true,
    show_syntax_errors: true,
    editor_settings: ResolvedEditorSettings {
        configuration: None,
        lint_preview: None,
        format_preview: None,
        select: None,
        extend_select: None,
        ignore: None,
        exclude: None,
        line_length: None,
        configuration_preference: EditorFirst,
    },
}
config_path = Some("/Users/dhruv/playground/ruff/pyproject.toml")

...
```

Replace `...` at the end with the output of `ruff check --show-settings
path.py`
2025-02-18 15:38:30 +05:30
Dhruv Manilawala
bb2a712f6a Update server to return the debug info as string (#16214)
## Summary

This PR updates the `ruff.printDebugInformation` command to return the
info as string in the response. Currently, we send a `window/logMessage`
request with the info but that has the disadvantage that it's not
visible to the user directly.

What `rust-analyzer` does with it's `rust-analyzer/status` request which
returns it as a string which then the client can just display it in a
separate window. This is what I'm thinking of doing as well.

Other editors can also benefit from it by directly opening a temporary
file with this information that the user can see directly.

There are couple of options here:
1. Keep using the command, keep the log request and return the string
2. Keep using the command, remove the log request and return the string
3. Create a new request similar to `rust-analyzer/status` which returns
a string

This PR implements (1) but I'd want to move towards (2) and remove the
log request completely. We haven't advertised it as such so this would
only require updating the VS Code extension to handle it by opening a
new document with the debug content.

## Test plan

For VS Code, refer to https://github.com/astral-sh/ruff-vscode/pull/694.

For Neovim, one could do:
```lua
local function execute_ruff_command(command)
  local client = vim.lsp.get_clients({ 
    bufnr = vim.api.nvim_get_current_buf(), 
    name = name,
    method = 'workspace/executeCommand',
  })[1]
  if not client then
    return
  end
  client.request('workspace/executeCommand', {
    command = command,
    arguments = {
      { uri = vim.uri_from_bufnr(0) }
    },
    function(err, result)
      if err then
        -- log error
        return
      end
      vim.print(result)
      -- Or, open a new window with the `result` content
    end
  }
```
2025-02-18 15:16:41 +05:30
Wei Lee
2d8ccfe6f2 [airflow] Group ImportPathMoved and ProviderName to avoid misusing (AIR303) (#16157)
## Summary

Separate ImportPathMoved and ProviderName to avoid misusing (AIR303)

## Test Plan

only code arrangement is updated. existing test fixture should be not be
changed
2025-02-18 15:11:58 +05:30
Micha Reiser
31180a84e4 Fix unstable formatting of trailing end-of-line comments of parenthesized attribute values (#16187) 2025-02-18 08:43:51 +01:00
Dhruv Manilawala
82eae511ca Ignore source code actions for a notebook cell (#16154)
## Summary

Related to https://github.com/astral-sh/ruff-vscode/pull/686, this PR
ignores handling source code actions for notebooks which are not
prefixed with `notebook`.

The main motivation is that the native server does not actually handle
it well which results in gibberish code. There's some context about this
in
https://github.com/astral-sh/ruff-vscode/issues/680#issuecomment-2647490812
and the following comments.

closes: https://github.com/astral-sh/ruff-vscode/issues/680

## Test Plan

Running a notebook with the following does nothing except log the
message:
```json
  "notebook.codeActionsOnSave": {
    "source.organizeImports.ruff": "explicit",
  },
```

while, including the `notebook` code actions does make the edit (as
usual):
```json
  "notebook.codeActionsOnSave": {
    "notebook.source.organizeImports.ruff": "explicit"
  },
```
2025-02-18 10:28:03 +05:30
Dhruv Manilawala
b5cd4f2f70 Add FAQ entry for source.* code actions in Notebook (#16212)
## Summary

This PR adds a FAQ entry to provide a brief explanation on why Ruff does
not support `source.*` code actions for Notebook.
2025-02-17 20:04:33 +05:30
Dhruv Manilawala
9f111eaebf red-knot: move symbol lookups in symbol.rs (#16152)
## Summary

This PR does the following:
* Moves the following from `types.rs` in `symbol.rs`:
	* `symbol`
	* `global_symbol`
	* `imported_symbol`
	* `symbol_from_bindings`
	* `symbol_from_declarations`
	* `SymbolAndQualifiers`
	* `SymbolFromDeclarationsResult`
* Moves the following from `stdlib.rs` in `symbol.rs` and removes
`stdlib.rs`:
	* `known_module_symbol`
	* `builtins_symbol`
	* `typing_symbol` (only for tests)
	* `typing_extensions_symbol`
	* `builtins_module_scope`
	* `core_module_scope`
* Add `symbol_from_bindings_impl` and `symbol_from_declarations_impl` to
keep `RequiresExplicitReExport` an implementation detail
* Make `declaration_type` a `pub(crate)` as it's required in
`symbol_from_declarations` (`binding_type` is already `pub(crate)`

The main motivation is to keep the implementation details private and
only expose an ergonomic API which uses sane defaults for various
scenario to avoid any mistakes from the caller. Refer to
https://github.com/astral-sh/ruff/pull/16133#discussion_r1955262772,
https://github.com/astral-sh/ruff/pull/16133#issue-2850146612 for
details.
2025-02-17 17:45:38 +05:30
purajit
9304fdf4ec better error messages while loading configuration extends (#15658)
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-17 10:35:30 +01:00
Micha Reiser
0babbca43f Format index.css (#16207)
## Summary

I did ran the NPM dev commands before merging
https://github.com/astral-sh/ruff/pull/16199 but I didn't notice that
one file got reformatted.

This PR formats the `index.css` with the now used Prettier version.
2025-02-17 08:38:26 +00:00
Alex Waygood
b6b1947010 Improve API exposed on ExprStringLiteral nodes (#16192)
## Summary

This PR makes the following changes:
- It adjusts various callsites to use the new
`ast::StringLiteral::contents_range()` method that was introduced in
https://github.com/astral-sh/ruff/pull/16183. This is less verbose and
more type-safe than using the `ast::str::raw_contents()` helper
function.
- It adds a new `ast::ExprStringLiteral::as_unconcatenated_literal()`
helper method, and adjusts various callsites to use it. This addresses
@MichaReiser's review comment at
https://github.com/astral-sh/ruff/pull/16183#discussion_r1957334365.
There is no functional change here, but it helps readability to make it
clearer that we're differentiating between implicitly concatenated
strings and unconcatenated strings at various points.
- It renames the `StringLiteralValue::flags()` method to
`StringLiteralFlags::first_literal_flags()`. If you're dealing with an
implicitly concatenated string `string_node`,
`string_node.value.flags().closer_len()` could give an incorrect result;
this renaming makes it clearer that the `StringLiteralFlags` instance
returned by the method is only guaranteed to give accurate information
for the first `StringLiteral` contained in the `ExprStringLiteral` node.
- It deletes the unused `BytesLiteralValue::flags()` method. This seems
prone to misuse in the same way as `StringLiteralValue::flags()`: if
it's an implicitly concatenated bytestring, the `BytesLiteralFlags`
instance returned by the method would only give accurate information for
the first `BytesLiteral` in the bytestring.

## Test Plan

`cargo test`
2025-02-17 07:58:54 +00:00
renovate[bot]
21999b3be7 Update Rust crate tempfile to v3.17.0 (#16202)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tempfile](https://stebalien.com/projects/tempfile-rs/)
([source](https://redirect.github.com/Stebalien/tempfile)) |
workspace.dependencies | minor | `3.16.0` -> `3.17.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>Stebalien/tempfile (tempfile)</summary>

###
[`v3.17.0`](https://redirect.github.com/Stebalien/tempfile/blob/HEAD/CHANGELOG.md#3170)

[Compare
Source](https://redirect.github.com/Stebalien/tempfile/compare/v3.16.0...v3.17.0)

- Make sure to use absolute paths in when creating unnamed temporary
files (avoids a small race in the "immediate unlink" logic) and in
`Builder::make_in` (when creating temporary files of arbitrary types).
- Prevent a theoretical crash that could (maybe) happen when a temporary
file is created from a drop function run in a TLS destructor. Nobody has
actually reported a case of this happening in practice and I have been
unable to create this scenario in a test.
- When reseeding with `getrandom`, use platform (e.g., CPU) specific
randomness sources where possible.
-   Clarify some documentation.
- Unlink unnamed temporary files on windows *immediately* when possible
instead of waiting for the handle to be closed. We open files with
"Unix" semantics, so this is generally possible.

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-17 08:37:33 +01:00
renovate[bot]
1ecc6a0d19 Update cloudflare/wrangler-action action to v3.14.0 (#16203)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cloudflare/wrangler-action](https://redirect.github.com/cloudflare/wrangler-action)
| action | minor | `v3.13.1` -> `v3.14.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>cloudflare/wrangler-action
(cloudflare/wrangler-action)</summary>

###
[`v3.14.0`](https://redirect.github.com/cloudflare/wrangler-action/releases/tag/v3.14.0)

[Compare
Source](https://redirect.github.com/cloudflare/wrangler-action/compare/v3.13.1...v3.14.0)

##### Minor Changes

-
[#&#8203;351](https://redirect.github.com/cloudflare/wrangler-action/pull/351)
[`4ff07f4`](4ff07f4310)
Thanks [@&#8203;Maximo-Guk](https://redirect.github.com/Maximo-Guk)! -
Use wrangler outputs for version upload and wrangler deploy

##### Patch Changes

-
[#&#8203;350](https://redirect.github.com/cloudflare/wrangler-action/pull/350)
[`e209094`](e209094e62)
Thanks [@&#8203;Maximo-Guk](https://redirect.github.com/Maximo-Guk)! -
Handle failures in createGitHubDeployment and createGitHubJobSummary

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:32:33 +01:00
renovate[bot]
79f43c9cab Update NPM Development dependencies (#16199)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@cloudflare/workers-types](https://redirect.github.com/cloudflare/workerd)
| [`4.20250204.0` ->
`4.20250214.0`](https://renovatebot.com/diffs/npm/@cloudflare%2fworkers-types/4.20250204.0/4.20250214.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@cloudflare%2fworkers-types/4.20250214.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@cloudflare%2fworkers-types/4.20250214.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@cloudflare%2fworkers-types/4.20250204.0/4.20250214.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@cloudflare%2fworkers-types/4.20250204.0/4.20250214.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@types/react](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react))
| [`19.0.8` ->
`19.0.9`](https://renovatebot.com/diffs/npm/@types%2freact/19.0.8/19.0.9)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2freact/19.0.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@types%2freact/19.0.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@types%2freact/19.0.8/19.0.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2freact/19.0.8/19.0.9?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.23.0` ->
`8.24.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/8.23.0/8.24.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2feslint-plugin/8.24.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@typescript-eslint%2feslint-plugin/8.24.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@typescript-eslint%2feslint-plugin/8.23.0/8.24.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2feslint-plugin/8.23.0/8.24.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.23.0` ->
`8.24.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2fparser/8.23.0/8.24.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2fparser/8.24.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@typescript-eslint%2fparser/8.24.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@typescript-eslint%2fparser/8.23.0/8.24.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2fparser/8.23.0/8.24.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[miniflare](https://redirect.github.com/cloudflare/workers-sdk/tree/main/packages/miniflare#readme)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/miniflare))
| [`3.20250129.0` ->
`3.20250204.1`](https://renovatebot.com/diffs/npm/miniflare/3.20250129.0/3.20250204.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/miniflare/3.20250204.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/miniflare/3.20250204.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/miniflare/3.20250129.0/3.20250204.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/miniflare/3.20250129.0/3.20250204.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [postcss](https://postcss.org/)
([source](https://redirect.github.com/postcss/postcss)) | [`8.5.1` ->
`8.5.2`](https://renovatebot.com/diffs/npm/postcss/8.5.1/8.5.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/postcss/8.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/postcss/8.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/postcss/8.5.1/8.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/postcss/8.5.1/8.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [prettier](https://prettier.io)
([source](https://redirect.github.com/prettier/prettier)) | [`3.5.0` ->
`3.5.1`](https://renovatebot.com/diffs/npm/prettier/3.5.0/3.5.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/prettier/3.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/prettier/3.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/prettier/3.5.0/3.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/prettier/3.5.0/3.5.1?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.107.3` ->
`3.109.1`](https://renovatebot.com/diffs/npm/wrangler/3.107.3/3.109.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/3.109.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/wrangler/3.109.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/wrangler/3.107.3/3.109.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/3.107.3/3.109.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

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

###
[`v4.20250214.0`](2c2b2d00d5...28b2bb16d9)

[Compare
Source](2c2b2d00d5...28b2bb16d9)

</details>

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

###
[`v8.24.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8240-2025-02-10)

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

##### 🚀 Features

- **eslint-plugin:** \[no-unnecessary-condition] make
`allowConstantLoopConditions` more granular
([#&#8203;10639](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10639))

##### 🩹 Fixes

- **eslint-plugin:** \[no-misused-spread] correct and elaborate string
spread report message
([#&#8203;10751](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10751))
- **eslint-plugin:** \[restrict-plus-operands] report adding bigints to
strings when `allowNumberAndString` is `false`
([#&#8203;10737](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/10737))

##### ❤️ Thank You

-   Josh Goldberg 
-   noah
-   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.24.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/parser/CHANGELOG.md#8240-2025-02-10)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.23.0...v8.24.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>cloudflare/workers-sdk (miniflare)</summary>

###
[`v3.20250204.1`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/miniflare/CHANGELOG.md#3202502041)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/miniflare@3.20250204.0...miniflare@3.20250204.1)

##### Patch Changes

-
[#&#8203;7950](https://redirect.github.com/cloudflare/workers-sdk/pull/7950)
[`4db1fb5`](4db1fb5696)
Thanks [@&#8203;cmackenzie1](https://redirect.github.com/cmackenzie1)! -
Add local binding support for Worker Pipelines

###
[`v3.20250204.0`](https://redirect.github.com/cloudflare/workers-sdk/blob/HEAD/packages/miniflare/CHANGELOG.md#3202502040)

[Compare
Source](https://redirect.github.com/cloudflare/workers-sdk/compare/miniflare@3.20250129.0...miniflare@3.20250204.0)

##### Patch Changes

-
[#&#8203;8032](https://redirect.github.com/cloudflare/workers-sdk/pull/8032)
[`c80dbd8`](c80dbd8d5e)
Thanks
[@&#8203;dependabot](https://redirect.github.com/apps/dependabot)! -
chore: update dependencies of "miniflare" package

    The following dependency versions have been updated:

    | Dependency                | From          | To            |
    | ------------------------- | ------------- | ------------- |
    | workerd                   | 1.20250129.0  | 1.20250204.0  |
|
[@&#8203;cloudflare/workers-types](https://redirect.github.com/cloudflare/workers-types)
| ^4.20250129.0 | ^4.20250204.0 |

-
[#&#8203;7290](https://redirect.github.com/cloudflare/workers-sdk/pull/7290)
[`0c0374c`](0c0374cce3)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: add support for workers with assets when running multiple workers
in one `wrangler dev` instance


[https://github.com/cloudflare/workers-sdk/pull/7251](https://redirect.github.com/cloudflare/workers-sdk/pull/7251)
added support for running multiple Workers in one `wrangler
dev`/miniflare session. e.g. `wrangler dev -c wrangler.toml -c
../worker2/wrangler.toml`, which among other things, allowed
cross-service RPC to Durable Objects.

However this did not work in the same way as production when there was a
Worker with assets - this PR should fix that.

</details>

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

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

[Compare
Source](https://redirect.github.com/postcss/postcss/compare/8.5.1...8.5.2)

- Fixed end position of rules with semicolon (by
[@&#8203;romainmenke](https://redirect.github.com/romainmenke)).

</details>

<details>
<summary>prettier/prettier (prettier)</summary>

###
[`v3.5.1`](https://redirect.github.com/prettier/prettier/blob/HEAD/CHANGELOG.md#351)

[Compare
Source](https://redirect.github.com/prettier/prettier/compare/3.5.0...3.5.1)


[diff](https://redirect.github.com/prettier/prettier/compare/3.5.0...3.5.1)

##### Fix CLI crash when cache for old version exists
([#&#8203;17100](https://redirect.github.com/prettier/prettier/pull/17100)
by [@&#8203;sosukesuzuki](https://redirect.github.com/sosukesuzuki))

Prettier 3.5 uses a different cache format than previous versions,
Prettier 3.5.0 crashes when reading existing cache file, Prettier 3.5.1
fixed the problem.

##### Support dockercompose and github-actions-workflow in VSCode
([#&#8203;17101](https://redirect.github.com/prettier/prettier/pull/17101)
by [@&#8203;remcohaszing](https://redirect.github.com/remcohaszing))

Prettier now supports the `dockercompose` and `github-actions-workflow`
languages in Visual Studio Code.

</details>

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

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

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

##### Patch Changes

-
[#&#8203;8021](https://redirect.github.com/cloudflare/workers-sdk/pull/8021)
[`28b1dc7`](28b1dc7c6f)
Thanks [@&#8203;0xD34DC0DE](https://redirect.github.com/0xD34DC0DE)! -
fix: prevent \__cf_cjs name collision in the hybrid Nodejs compat plugin

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

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

##### Minor Changes

-
[#&#8203;8120](https://redirect.github.com/cloudflare/workers-sdk/pull/8120)
[`3fb801f`](3fb801f734)
Thanks [@&#8203;sdnts](https://redirect.github.com/sdnts)! - Add a new
`update` subcommand for Queues to allow updating Queue settings

-
[#&#8203;8120](https://redirect.github.com/cloudflare/workers-sdk/pull/8120)
[`3fb801f`](3fb801f734)
Thanks [@&#8203;sdnts](https://redirect.github.com/sdnts)! - Allow
overriding message retention duration when creating Queues

-
[#&#8203;8026](https://redirect.github.com/cloudflare/workers-sdk/pull/8026)
[`542c6ea`](542c6ead5d)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Add
`--outfile` to `wrangler deploy` for generating a worker bundle.

This is an advanced feature that most users won't need to use. When set,
Wrangler will output your built Worker bundle in a Cloudflare specific
format that captures all information needed to deploy a Worker using the
[Worker Upload
API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/)

-
[#&#8203;8026](https://redirect.github.com/cloudflare/workers-sdk/pull/8026)
[`542c6ea`](542c6ead5d)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - Add a
`wrangler check startup` command to generate a CPU profile of your
Worker's startup phase.

This can be imported into Chrome DevTools or opened directly in VSCode
to view a flamegraph of your Worker's startup phase. Additionally, when
a Worker deployment fails with a startup time error Wrangler will
automatically generate a CPU profile for easy investigation.

    Advanced usage:

- `--args`: to customise the way `wrangler check startup` builds your
Worker for analysis, provide the exact arguments you use when deploying
your Worker with `wrangler deploy`. For instance, if you deploy your
Worker with `wrangler deploy --no-bundle`, you should use `wrangler
check startup --args="--no-bundle"` to profile the startup phase.
- `--worker-bundle`: if you don't use Wrangler to deploy your Worker,
you can use this argument to provide a Worker bundle to analyse. This
should be a file path to a serialised multipart upload, with the exact
same format as the API expects:
https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/

##### Patch Changes

-
[#&#8203;8112](https://redirect.github.com/cloudflare/workers-sdk/pull/8112)
[`fff677e`](fff677e35f)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! - When
reporting errors to Sentry, Wrangler will now include the console output
as additional metadata

-
[#&#8203;8120](https://redirect.github.com/cloudflare/workers-sdk/pull/8120)
[`3fb801f`](3fb801f734)
Thanks [@&#8203;sdnts](https://redirect.github.com/sdnts)! - Check
bounds when overriding delivery delay when creating Queues

-
[#&#8203;7950](https://redirect.github.com/cloudflare/workers-sdk/pull/7950)
[`4db1fb5`](4db1fb5696)
Thanks [@&#8203;cmackenzie1](https://redirect.github.com/cmackenzie1)! -
Add local binding support for Worker Pipelines

-
[#&#8203;8119](https://redirect.github.com/cloudflare/workers-sdk/pull/8119)
[`1bc60d7`](1bc60d761e)
Thanks [@&#8203;penalosa](https://redirect.github.com/penalosa)! -
Output correct config format from `wrangler d1 create`. Previously, this
command would always output TOML, regardless of the config file format

-
[#&#8203;8130](https://redirect.github.com/cloudflare/workers-sdk/pull/8130)
[`1aa2a91`](1aa2a91985)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
Include default values for wrangler types --path and --x-include-runtime
in telemetry

    User provided strings are still left redacted as always.

-
[#&#8203;8061](https://redirect.github.com/cloudflare/workers-sdk/pull/8061)
[`35710e5`](35710e590f)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: respect `WRANGLER_LOG` in `wrangler dev`

Previously, `--log-level=debug` was the only way to see debug logs in
`wrangler dev`, which was unlike all other commands.

- Updated dependencies
\[[`4db1fb5`](4db1fb5696)]:
    -   miniflare@3.20250204.1

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

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

##### Patch Changes

-
[#&#8203;8103](https://redirect.github.com/cloudflare/workers-sdk/pull/8103)
[`a025ad2`](a025ad2ecb)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: fix bug where `wrangler secret list --format=json` was printing the
wrangler banner.

-   Updated dependencies \[]:
    -   miniflare@3.20250204.0

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

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

##### Minor Changes

-
[#&#8203;7990](https://redirect.github.com/cloudflare/workers-sdk/pull/7990)
[`b1966df`](b1966dfe57)
Thanks [@&#8203;cmsparks](https://redirect.github.com/cmsparks)! - Add
WRANGLER_CI_OVERRIDE_NAME for Workers CI

-
[#&#8203;8028](https://redirect.github.com/cloudflare/workers-sdk/pull/8028)
[`b2dca9a`](b2dca9a2fb)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
feat: Also log when *no* bindings are found.

We currently print a worker's bindings during dev, versions upload and
deploy. This just also prints something when there's no bindings found,
in case you *were* expecting bindings.

-
[#&#8203;8037](https://redirect.github.com/cloudflare/workers-sdk/pull/8037)
[`71fd250`](71fd250f67)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
Provides unsafe.metadata configurations when using wrangler versions
secret put.

##### Patch Changes

-
[#&#8203;8058](https://redirect.github.com/cloudflare/workers-sdk/pull/8058)
[`1f80d69`](1f80d69f56)
Thanks
[@&#8203;WillTaylorDev](https://redirect.github.com/WillTaylorDev)! -
Bugfix: Modified versions secret put to inherit all known bindings,
which circumvents a limitation in the API which does not return all
fields for all bindings.

-
[#&#8203;7986](https://redirect.github.com/cloudflare/workers-sdk/pull/7986)
[`88514c8`](88514c82d4)
Thanks [@&#8203;andyjessop](https://redirect.github.com/andyjessop)! -
docs: clarifies that local resources are "simulated locally" or
"connected to remote resource", and adds console messages to help
explain local dev

-
[#&#8203;8008](https://redirect.github.com/cloudflare/workers-sdk/pull/8008)
[`9d08af8`](9d08af8189)
Thanks [@&#8203;ns476](https://redirect.github.com/ns476)! - Add support
for Images bindings (in private beta for now), with optional local
support for platforms where Sharp is available.

-
[#&#8203;7769](https://redirect.github.com/cloudflare/workers-sdk/pull/7769)
[`6abe69c`](6abe69c3fe)
Thanks [@&#8203;cmackenzie1](https://redirect.github.com/cmackenzie1)! -
Adds the following new option for `wrangler pipelines create` and
`wrangler pipelines update` commands:

--cors-origins CORS origin allowlist for HTTP endpoint (use * for any
origin) [array]

-
[#&#8203;7290](https://redirect.github.com/cloudflare/workers-sdk/pull/7290)
[`0c0374c`](0c0374cce3)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
fix: add support for workers with assets when running multiple workers
in one `wrangler dev` instance


[https://github.com/cloudflare/workers-sdk/pull/7251](https://redirect.github.com/cloudflare/workers-sdk/pull/7251)
added support for running multiple Workers in one `wrangler
dev`/miniflare session. e.g. `wrangler dev -c wrangler.toml -c
../worker2/wrangler.toml`, which among other things, allowed
cross-service RPC to Durable Objects.

However this did not work in the same way as production when there was a
Worker with assets - this PR should fix that.

-
[#&#8203;7769](https://redirect.github.com/cloudflare/workers-sdk/pull/7769)
[`6abe69c`](6abe69c3fe)
Thanks [@&#8203;cmackenzie1](https://redirect.github.com/cmackenzie1)! -
Rename wrangler pipelines \<create|update> flags

    The following parameters have been renamed:

    | Previous Name     | New Name              |
    | ----------------- | --------------------- |
    | access-key-id     | r2-access-key-id      |
    | secret-access-key | r2-secret-access-key  |
    | transform         | transform-worker      |
    | r2                | r2-bucket             |
    | prefix            | r2-prefix             |
    | binding           | enable-worker-binding |
    | http              | enable-http           |
    | authentication    | require-http-auth     |
    | filename          | file-template         |
    | filepath          | partition-template    |

-
[#&#8203;8012](https://redirect.github.com/cloudflare/workers-sdk/pull/8012)
[`c412a31`](c412a31985)
Thanks [@&#8203;mtlemilio](https://redirect.github.com/mtlemilio)! - Use
fetchPagedListResult when listing Hyperdrive configs from the API

    This fixes an issue where only 20 configs were being listed.

-
[#&#8203;8077](https://redirect.github.com/cloudflare/workers-sdk/pull/8077)
[`60310cd`](60310cd796)
Thanks [@&#8203;emily-shen](https://redirect.github.com/emily-shen)! -
feat: add telemetry to experimental auto-provisioning

- Updated dependencies
\[[`c80dbd8`](c80dbd8d5e),
[`0c0374c`](0c0374cce3)]:
    -   miniflare@3.20250204.0

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:32:14 +01:00
renovate[bot]
4ea397adb0 Update Rust crate smallvec to v1.14.0 (#16201)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [smallvec](https://redirect.github.com/servo/rust-smallvec) |
workspace.dependencies | minor | `1.13.2` -> `1.14.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>servo/rust-smallvec (smallvec)</summary>

###
[`v1.14.0`](https://redirect.github.com/servo/rust-smallvec/releases/tag/v1.14.0)

[Compare
Source](https://redirect.github.com/servo/rust-smallvec/compare/v1.13.2...v1.14.0)

#### What's Changed

- Implement `MallocSizeOf` for SmallVec (v1) by
[@&#8203;nicoburns](https://redirect.github.com/nicoburns) in
[https://github.com/servo/rust-smallvec/pull/370](https://redirect.github.com/servo/rust-smallvec/pull/370)

#### New Contributors

- [@&#8203;nicoburns](https://redirect.github.com/nicoburns) made their
first contribution in
[https://github.com/servo/rust-smallvec/pull/370](https://redirect.github.com/servo/rust-smallvec/pull/370)

**Full Changelog**:
https://github.com/servo/rust-smallvec/compare/v1.13.2...v1.14.0

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:24:24 +01:00
renovate[bot]
033f16233d Update Rust crate codspeed-criterion-compat to v2.8.0 (#16200)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [codspeed-criterion-compat](https://codspeed.io)
([source](https://redirect.github.com/CodSpeedHQ/codspeed-rust)) |
workspace.dependencies | minor | `2.7.2` -> `2.8.0` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>CodSpeedHQ/codspeed-rust (codspeed-criterion-compat)</summary>

###
[`v2.8.0`](https://redirect.github.com/CodSpeedHQ/codspeed-rust/releases/tag/v2.8.0)

[Compare
Source](https://redirect.github.com/CodSpeedHQ/codspeed-rust/compare/v2.7.2...v2.8.0)

#### What's Changed

This introduces Divan compatibility layer and also Wall Time support.
Check out the documentation to try it out
[here](https://docs.codspeed.io/benchmarks/rust/divan).

##### Details

- ci: bump actions/checkout to v4 by
[@&#8203;fargito](https://redirect.github.com/fargito) in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/56](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/56)
- docs: simplify rust benchmarks definition by
[@&#8203;adriencaccia](https://redirect.github.com/adriencaccia) in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/44](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/44)
- Support walltime runs with divan by
[@&#8203;art049](https://redirect.github.com/art049) in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/66](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/66)
- Make `cargo-codspeed` build targets to different directories between
walltime and instrumented by
[@&#8203;GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange)
in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/68](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/68)
- feat: make codspeed raw results in the walltime directory as well by
[@&#8203;GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange)
in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/70](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/70)
- chore: add an internal divan fork by
[@&#8203;art049](https://redirect.github.com/art049) in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/69](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/69)
- Add codspeed<>divan compat layer by
[@&#8203;GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange)
in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/65](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/65)
- fix: only show walltime collection warning when appropriate by
[@&#8203;art049](https://redirect.github.com/art049) in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/71](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/71)
- feat(divan_compat): support types and manage types and args in
codspeed uri by
[@&#8203;GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange)
in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/72](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/72)
- feat: add some TheAlgorithm benches by
[@&#8203;art049](https://redirect.github.com/art049) in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/73](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/73)
- Add divan_compat msrv check in CI by
[@&#8203;GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange)
in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/74](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/74)
- feat: add readme to divan_compat by
[@&#8203;GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange)
in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/75](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/75)

#### New Contributors

- [@&#8203;fargito](https://redirect.github.com/fargito) made their
first contribution in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/56](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/56)

#### New Contributors

- [@&#8203;fargito](https://redirect.github.com/fargito) made their
first contribution in
[https://github.com/CodSpeedHQ/codspeed-rust/pull/56](https://redirect.github.com/CodSpeedHQ/codspeed-rust/pull/56)

**Full Changelog**:
https://github.com/CodSpeedHQ/codspeed-rust/compare/v2.7.2...v2.8.0

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:24:03 +01:00
renovate[bot]
b10be97eae Update Rust crate strum to v0.27.1 (#16195)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [strum](https://redirect.github.com/Peternator7/strum) |
workspace.dependencies | patch | `0.27.0` -> `0.27.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>Peternator7/strum (strum)</summary>

###
[`v0.27.1`](https://redirect.github.com/Peternator7/strum/blob/HEAD/CHANGELOG.md#0271)

[Compare
Source](https://redirect.github.com/Peternator7/strum/compare/v0.27.0...v0.27.1)

- [#&#8203;414](https://redirect.github.com/Peternator7/strum/pull/414):
Fix docrs build error.
- [#&#8203;417](https://redirect.github.com/Peternator7/strum/pull/417):
Mention `parse_error_ty` and `parse_error_fn` that had been
    left out of the docs accidentally.
-
[#&#8203;421](https://redirect.github.com/Peternator7/strum/pull/421)[#&#8203;331](https://redirect.github.com/Peternator7/strum/pull/331):
Implement
`#[strum(transparent)]` attribute on `IntoStaticStr`, `Display` and
`AsRefStr` that forwards the implmenentation to
the inner value. Note that for static strings, the inner value must be
convertible to an `&'static str`.

    ```rust
    #[derive(strum::Display)]
    enum SurveyResponse {
      Yes,
      No,
      #[strum(transparent)]
      Other(String)
    }

    fn main() {
      let response = SurveyResponse::Other("It was good".into());
      println!("Question: Did you have fun?");
      println!("Answer: {}", response);
      // prints: Answer: It was good
    }
    ```

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:22:44 +01:00
renovate[bot]
f3743e30d0 Update Rust crate clap to v4.5.29 (#16194)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.28` -> `4.5.29` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.29`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4529---2025-02-11)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.28...v4.5.29)

##### Fixes

- Change `ArgMatches::args_present` so not-present flags are considered
not-present (matching the documentation)

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:22:31 +01:00
renovate[bot]
4d083e579d Update Rust crate strum_macros to v0.27.1 (#16196)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [strum_macros](https://redirect.github.com/Peternator7/strum) |
workspace.dependencies | patch | `0.27.0` -> `0.27.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>Peternator7/strum (strum_macros)</summary>

###
[`v0.27.1`](https://redirect.github.com/Peternator7/strum/blob/HEAD/CHANGELOG.md#0271)

[Compare
Source](https://redirect.github.com/Peternator7/strum/compare/v0.27.0...v0.27.1)

- [#&#8203;414](https://redirect.github.com/Peternator7/strum/pull/414):
Fix docrs build error.
- [#&#8203;417](https://redirect.github.com/Peternator7/strum/pull/417):
Mention `parse_error_ty` and `parse_error_fn` that had been
    left out of the docs accidentally.
-
[#&#8203;421](https://redirect.github.com/Peternator7/strum/pull/421)[#&#8203;331](https://redirect.github.com/Peternator7/strum/pull/331):
Implement
`#[strum(transparent)]` attribute on `IntoStaticStr`, `Display` and
`AsRefStr` that forwards the implmenentation to
the inner value. Note that for static strings, the inner value must be
convertible to an `&'static str`.

    ```rust
    #[derive(strum::Display)]
    enum SurveyResponse {
      Yes,
      No,
      #[strum(transparent)]
      Other(String)
    }

    fn main() {
      let response = SurveyResponse::Other("It was good".into());
      println!("Question: Did you have fun?");
      println!("Answer: {}", response);
      // prints: Answer: It was good
    }
    ```

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:22:15 +01:00
renovate[bot]
7899e8756e Update pre-commit dependencies (#16198)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[astral-sh/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit)
| repository | patch | `v0.9.5` -> `v0.9.6` |
| [crate-ci/typos](https://redirect.github.com/crate-ci/typos) |
repository | patch | `v1.29.5` -> `v1.29.7` |
|
[rbubley/mirrors-prettier](https://redirect.github.com/rbubley/mirrors-prettier)
| repository | minor | `v3.4.2` -> `v3.5.1` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

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

---

### Release Notes

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

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

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

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

</details>

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

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

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

#### \[1.29.7] - 2025-02-13

##### Fixes

-   Don't correct `implementors`

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

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

#### \[1.29.6] - 2025-02-13

##### Features

- Updated the dictionary with the [January
2025](https://redirect.github.com/crate-ci/typos/issues/1200) changes

</details>

<details>
<summary>rbubley/mirrors-prettier (rbubley/mirrors-prettier)</summary>

###
[`v3.5.1`](https://redirect.github.com/rbubley/mirrors-prettier/compare/v3.5.0...v3.5.1)

[Compare
Source](https://redirect.github.com/rbubley/mirrors-prettier/compare/v3.5.0...v3.5.1)

###
[`v3.5.0`](https://redirect.github.com/rbubley/mirrors-prettier/compare/v3.4.2...v3.5.0)

[Compare
Source](https://redirect.github.com/rbubley/mirrors-prettier/compare/v3.4.2...v3.5.0)

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:21:44 +01:00
renovate[bot]
f3d1bf845e Update dependency ruff to v0.9.6 (#16197)
This PR contains the following updates:

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

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

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

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

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

##### Preview features

- \[`airflow`] Add `external_task.{ExternalTaskMarker,
ExternalTaskSensor}` for `AIR302`
([#&#8203;16014](https://redirect.github.com/astral-sh/ruff/pull/16014))
- \[`flake8-builtins`] Make strict module name comparison optional
(`A005`)
([#&#8203;15951](https://redirect.github.com/astral-sh/ruff/pull/15951))
- \[`flake8-pyi`] Extend fix to Python <= 3.9 for
`redundant-none-literal` (`PYI061`)
([#&#8203;16044](https://redirect.github.com/astral-sh/ruff/pull/16044))
- \[`pylint`] Also report when the object isn't a literal (`PLE1310`)
([#&#8203;15985](https://redirect.github.com/astral-sh/ruff/pull/15985))
- \[`ruff`] Implement `indented-form-feed` (`RUF054`)
([#&#8203;16049](https://redirect.github.com/astral-sh/ruff/pull/16049))
- \[`ruff`] Skip type definitions for `missing-f-string-syntax`
(`RUF027`)
([#&#8203;16054](https://redirect.github.com/astral-sh/ruff/pull/16054))

##### Rule changes

- \[`flake8-annotations`] Correct syntax for `typing.Union` in suggested
return type fixes for `ANN20x` rules
([#&#8203;16025](https://redirect.github.com/astral-sh/ruff/pull/16025))
- \[`flake8-builtins`] Match upstream module name comparison (`A005`)
([#&#8203;16006](https://redirect.github.com/astral-sh/ruff/pull/16006))
- \[`flake8-comprehensions`] Detect overshadowed `list`/`set`/`dict`,
ignore variadics and named expressions (`C417`)
([#&#8203;15955](https://redirect.github.com/astral-sh/ruff/pull/15955))
- \[`flake8-pie`] Remove following comma correctly when the unpacked
dictionary is empty (`PIE800`)
([#&#8203;16008](https://redirect.github.com/astral-sh/ruff/pull/16008))
- \[`flake8-simplify`] Only trigger `SIM401` on known dictionaries
([#&#8203;15995](https://redirect.github.com/astral-sh/ruff/pull/15995))
- \[`pylint`] Do not report calls when object type and argument type
mismatch, remove custom escape handling logic (`PLE1310`)
([#&#8203;15984](https://redirect.github.com/astral-sh/ruff/pull/15984))
- \[`pyupgrade`] Comments within parenthesized value ranges should not
affect applicability (`UP040`)
([#&#8203;16027](https://redirect.github.com/astral-sh/ruff/pull/16027))
- \[`pyupgrade`] Don't introduce invalid syntax when upgrading old-style
type aliases with parenthesized multiline values (`UP040`)
([#&#8203;16026](https://redirect.github.com/astral-sh/ruff/pull/16026))
- \[`pyupgrade`] Ensure we do not rename two type parameters to the same
name (`UP049`)
([#&#8203;16038](https://redirect.github.com/astral-sh/ruff/pull/16038))
- \[`pyupgrade`] \[`ruff`] Don't apply renamings if the new name is
shadowed in a scope of one of the references to the binding (`UP049`,
`RUF052`)
([#&#8203;16032](https://redirect.github.com/astral-sh/ruff/pull/16032))
- \[`ruff`] Update `RUF009` to behave similar to `B008` and ignore
attributes with immutable types
([#&#8203;16048](https://redirect.github.com/astral-sh/ruff/pull/16048))

##### Server

- Root exclusions in the server to project root
([#&#8203;16043](https://redirect.github.com/astral-sh/ruff/pull/16043))

##### Bug fixes

- \[`flake8-datetime`] Ignore `.replace()` calls while looking for
`.astimezone`
([#&#8203;16050](https://redirect.github.com/astral-sh/ruff/pull/16050))
- \[`flake8-type-checking`] Avoid `TC004` false positive where the
runtime definition is provided by `__getattr__`
([#&#8203;16052](https://redirect.github.com/astral-sh/ruff/pull/16052))

##### Documentation

- Improve `ruff-lsp` migration document
([#&#8203;16072](https://redirect.github.com/astral-sh/ruff/pull/16072))
- Undeprecate `ruff.nativeServer`
([#&#8203;16039](https://redirect.github.com/astral-sh/ruff/pull/16039))

</details>

---

### Configuration

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

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

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

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

---

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

---

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

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 08:21:29 +01:00
Alex Waygood
4941975e74 [red-knot] Recognize ... as a singleton (#16184) 2025-02-16 22:01:02 +00:00
Dylan
d4b4f65e20 [pep8-naming] Clarify preview behavior in invalid-first-argument-name-for-class-method (N804) (#16193)
Adds clarification in the documentation for
[invalid-first-argument-name-for-class-method
(N804)](https://docs.astral.sh/ruff/rules/invalid-first-argument-name-for-class-method/#invalid-first-argument-name-for-class-method-n804)

(Also fixes an unrelated typo).
2025-02-16 15:02:50 -06:00
cake-monotone
96dd1b1587 Consider __new__ methods as special function type for enforcing class method or static method rules (#13305)
## Summary

`__new__` methods are technically static methods, with `cls` as their
first argument. However, Ruff currently classifies them as classmethod,
which causes two issues:

- It conveys incorrect information, leading to confusion. For example,
in cases like ARG003, `__new__` is explicitly treated as a classmethod.
- Future rules that should apply to staticmethod may not be applied
correctly due to this misclassification.

Motivated by this, the current PR makes the following adjustments:

1. Introduces `FunctionType::NewMethod` as an enum variant, since, for
the purposes of lint rules, `__new__` sometimes behaves like a static
method and other times like a class method. This is an internal change.

2. The following rule behaviors and messages are totally unchanged:
- [too-many-arguments
(PLR0913)](https://docs.astral.sh/ruff/rules/too-many-arguments/#too-many-arguments-plr0913)
- [too-many-positional-arguments
(PLR0917)](https://docs.astral.sh/ruff/rules/too-many-positional-arguments/#too-many-positional-arguments-plr0917)
3. The following rule behaviors are unchanged, but the messages have
been changed for correctness to use "`__new__` method" instead of "class
method":
- [self-or-cls-assignment
(PLW0642)](https://docs.astral.sh/ruff/rules/self-or-cls-assignment/#self-or-cls-assignment-plw0642)
4. The following rules are changed _unconditionally_ (not gated behind
preview) because their current behavior is an honest bug: it just isn't
true that `__new__` is a class method, and it _is_ true that `__new__`
is a static method:
- [unused-class-method-argument
(ARG003)](https://docs.astral.sh/ruff/rules/unused-class-method-argument/#unused-class-method-argument-arg003)
no longer applies to `__new__`
- [unused-static-method-argument
(ARG004)](https://docs.astral.sh/ruff/rules/unused-static-method-argument/#unused-static-method-argument-arg004)
now applies to `__new__`
5. The only changes which differ based on `preview` are the following:
- [invalid-first-argument-name-for-class-method
(N804)](https://docs.astral.sh/ruff/rules/invalid-first-argument-name-for-class-method/#invalid-first-argument-name-for-class-method-n804):
This is _skipped_ when `preview` is _enabled_. When `preview` is
_disabled_, the rule is the same but the _message_ has been modified to
say "`__new__` method" instead of "class method".
- [bad-staticmethod-argument
(PLW0211)](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/#bad-staticmethod-argument-plw0211):
When `preview` is enabled, this now applies to `__new__`.

Closes #13154

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-16 14:12:25 -06:00
Dylan
f29c7b03ec Warn on invalid noqa even when there are no diagnostics (#16178)
On `main` we warn the user if there is an invalid noqa comment[^1] and
at least one of the following holds:

- There is at least one diagnostic
- A lint rule related to `noqa`s is enabled (e.g. `RUF100`)

This is probably strange behavior from the point of view of the user, so
we now show invalid `noqa`s even when there are no diagnostics.

Closes #12831

[^1]: For the current definition of "invalid noqa comment", which may be
expanded in #12811 . This PR is independent of loc. cit. in the sense
that the CLI warnings should be consistent, regardless of which `noqa`
comments are considered invalid.
2025-02-16 13:58:18 -06:00
Brent Westbrook
3a0d45c85b [flake8-debugger] Also flag sys.breakpointhook and sys.__breakpointhook__ (T100) (#16191)
## Summary

Fixes #16189.

Only `sys.breakpointhook` is flagged by the upstream linter:

007a745c86/pylint/checkers/stdlib.py (L38)

but I think it makes sense to flag
[`__breakpointhook__`](https://docs.python.org/3/library/sys.html#sys.__breakpointhook__)
too, as suggested in the issue because it
> contain[s] the original value of breakpointhook [...] in case [it
happens] to get replaced with broken or alternative objects.

## Test Plan

New T100 test cases
2025-02-16 14:50:16 -05:00
Aaron Gokaslan
1f17916224 Add doc about usedforsecurity flag for S324 (#16190)
## Summary

Provides documentation about the FIPS compliant flag for Python hashlib
`usedforsecurity`
Fixes #16188 

## Test Plan

* pre-commit hooks

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-02-16 13:06:55 -05:00
Alex Waygood
61fef0a64a Reduce memory usage of Docstring struct (#16183) 2025-02-16 15:23:52 +00:00
Alex Waygood
93aff36147 [red-knot] Improve handling of inherited class attributes (#16160) 2025-02-15 18:22:35 +00:00
Ayush Baweja
df45a9db64 [flake8-comprehensions]: Handle trailing comma in C403 fix (#16110)
## Summary

Resolves [#16099 ](https://github.com/astral-sh/ruff/issues/16099) based
on [#15929 ](https://github.com/astral-sh/ruff/pull/15929)

## Test Plan

Added test case `s = set([x for x in range(3)],)` and updated snapshot.

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-02-15 11:45:41 -06:00
InSync
3c69b685ee [ruff] Implicit class variable in dataclass (RUF045) (#14349)
## Summary

Implement lint rule to flag un-annotated variable assignments in dataclass definitions.

Resolves #12877.

---------

Co-authored-by: dylwil3 <dylwil3@gmail.com>
2025-02-15 09:08:13 -06:00
github-actions[bot]
171facd960 Sync vendored typeshed stubs (#16173)
Close and reopen this PR to trigger CI

Co-authored-by: typeshedbot <>
2025-02-15 10:01:34 +00:00
InSync
977447f9b8 Sort linters alphabetically (#16168)
## Summary

Resolves #16164.

Linters are now sorted by their names case-insensitively.

## Test Plan


![](https://github.com/user-attachments/assets/87ffd4d8-1ba5-4a4b-8fed-dd21a020bd27)

Also unit tests.
2025-02-14 22:05:08 +01:00
Micha Reiser
b3e99b25bf Fix missing serde feature for red_knot_python_semantic (#16169)
## Summary

Running `cargo test -p red_knot_python_semantic` failed because of a
missing serde feature. This PR enables the `ruff_python_ast`'`s `serde`
if the crate's `serde` feature is enabled

## Test Plan

`cargo test -p red_knot_python_semantic` compiles again
2025-02-14 20:31:55 +00:00
Carl Meyer
dcabb948f3 [red-knot] add special case for float/complex (#16166)
When adjusting the existing tests, I aimed to avoid dealing with the
special case in other tests if it's not necessary to do so (that is,
avoid using `float` and `complex` as examples where we just need "some
type"), and keep the tests for the special case mostly collected in the
mdtest dedicated to that purpose.

Fixes https://github.com/astral-sh/ruff/issues/14932
2025-02-14 12:24:10 -08:00
Vlad Nedelcu
219712860c [refurb] Check for subclasses includes subscript expressions (FURB189) (#16155)
## Summary

Added checks for subscript expressions on builtin classes as in FURB189.
The object is changed to use the collections objects and the types from
the subscript are kept.

Resolves #16130 

> Note: Added some comments in the code explaining why
## Test Plan


- Added a subscript dict and list class to the test file.
- Tested locally to check that the symbols are changed and the types are
kept.
- No modifications changed on optional `str` values.
2025-02-14 20:21:26 +01:00
Brent Westbrook
f58a54f043 Move red_knot_python_semantic::PythonVersion to the ruff_python_ast crate (#16147)
## Summary

This PR moves the `PythonVersion` struct from the
`red_knot_python_semantic` crate to the `ruff_python_ast` crate so that
it can be used more easily in the syntax error detection work. Compared
to that [prototype](https://github.com/astral-sh/ruff/pull/16090/) these
changes reduce us from 2 `PythonVersion` structs to 1.

This does not unify any of the `PythonVersion` *enums*, but I hope to
make some progress on that in a follow-up.

## Test Plan

Existing tests, this should not change any external behavior.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-14 12:48:08 -05:00
Junhson Jean-Baptiste
fa28dc5ccf [internal] Move Linter OperatorPrecedence into ruff_python_ast crate (#16162)
## Summary

This change begins to resolve #16071 by moving the `OperatorPrecedence`
structs from the `ruff_python_linter` crate into `ruff_python_ast`. This
PR also implements `precedence()` methods on the `Expr` and `ExprRef`
enums.

## Test Plan

Since this change mainly shifts existing logic, I didn't add any
additional tests. Existing tests do pass.
2025-02-14 15:55:07 +01:00
Dhruv Manilawala
63dd68e0ed Refactor symbol lookup APIs to hide re-export implementation details (#16133)
## Summary

This PR refactors the symbol lookup APIs to better facilitate the
re-export implementation. Specifically,
* Add `module_type_symbol` which returns the `Symbol` that's a member of
`types.ModuleType`
* Rename `symbol` -> `symbol_impl`; add `symbol` which delegates to
`symbol_impl` with `RequireExplicitReExport::No`
* Update `global_symbol` to do `symbol_impl` -> fall back to
`module_type_symbol` and default to `RequireExplicitReExport::No`
* Add `imported_symbol` to do `symbol_impl` with
`RequireExplicitReExport` as `Yes` if the module is in a stub file else
`No`
* Update `known_module_symbol` to use `imported_symbol` with a fallback
to `module_type_symbol`
* Update `ModuleLiteralType::member` to use `imported_symbol` with a
custom fallback

We could potentially also update `symbol_from_declarations` and
`symbol_from_bindings` to avoid passing in the `RequireExplicitReExport`
as it would be always `No` if called directly. We could add
`symbol_from_declarations_impl` and `symbol_from_bindings_impl`.

Looking at the `_impl` functions, I think we should move all of these
symbol related logic into `symbol.rs` where `Symbol` is defined and the
`_impl` could be private while we expose the public APIs at the crate
level. This would also make the `RequireExplicitReExport` an
implementation detail and the caller doesn't need to worry about it.
2025-02-14 15:25:48 +05:30
Dhruv Manilawala
60b3ef2c98 [red-knot] Support re-export conventions for stub files (#16073)
This is an alternative implementation to #15848.

## Summary

This PR adds support for re-export conventions for imports for stub
files.

**How does this work?**
* Add a new flag on the `Import` and `ImportFrom` definitions to
indicate whether they're being exported or not
* Add a new enum to indicate whether the symbol lookup is happening
within the same file or is being queried from another file (e.g., an
import statement)
* When a `Symbol` is being queried, we'll skip the definitions that are
(a) coming from a stub file (b) external lookup and (c) check the
re-export flag on the definition

This implementation does not yet support `__all__` and `*` imports as
both are features that needs to be implemented independently.

closes: #14099
closes: #15476 

## Test Plan

Add test cases, update existing ones if required.
2025-02-14 15:17:51 +05:30
InSync
3d0a58eb60 [pyupgrade] Unwrap unary expressions correctly (UP018) (#15919)
## Summary

Resolves #15859.

The rule now adds parentheses if the original call wraps an unary
expression and is:

* The left-hand side of a binary expression where the operator is `**`.
* The caller of a call expression.
* The subscripted of a subscript expression.
* The object of an attribute access.

The fix will also be marked as unsafe if there are any comments in its
range.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-02-14 08:42:00 +01:00
InSync
1db8392a5a Check for backtick-quoted shortcut links in CI (#16114)
## Summary

Follow-up to #16035.

`check_docs_formatted.py` will now report backtick-quoted shortcut links
in rule documentation. It uses a regular expression to find them. Such a
link:

* Starts with `[`, followed by <code>\`</code>, then a "name" sequence
of at least one non-backtick non-newline character, followed by another
<code>\`</code>, then ends with `]`.
* Is not followed by either a `[` or a `(`.
* Is not placed within a code block.

If the name is a known Ruff option name, that link is not considered a
violation.

## Test Plan

Manual.
2025-02-14 08:37:46 +01:00
Micha Reiser
81e202ed52 Make CallBinding::callable_ty required (#16135)
## Summary

The `callable_ty` is always known except in some TODO code where we can
use a `TODO` type instead.

## Test Plan

`cargo test`
2025-02-14 08:15:24 +01:00
Vlad Nedelcu
63c67750b1 Replace dead link for rome tools playground (#16153)
## Summary

Rome Tools Playground was renamed to Biome Playground. The link was
replaced to the new website.

Resolves #16143


## Test Plan

- Checked the linked is accessible from the README
2025-02-14 12:27:14 +05:30
Shaygan Hooshyari
0a75a1d56b Replace is-macro with implementation in enums (#16144) 2025-02-13 22:49:00 +00:00
Shaygan Hooshyari
bb15c7653a Use ubuntu-24 to run benchmarks (#16145) 2025-02-13 22:05:51 +00:00
Vlad Nedelcu
cb8b23d609 [flake8-pyi] Avoid flagging custom-typevar-for-self on metaclass methods (PYI019) (#16141) 2025-02-13 18:44:11 +00:00
Alex Waygood
be49151a3d [red-knot] Remove a parameter from the symbol_by_id() query (#16138) 2025-02-13 13:33:40 +00:00
InSync
7d2e40be2d [pylint] Do not offer fix for raw strings (PLE251) (#16132)
## Summary

Resolves #13294, follow-up to #13882.

At #13882, it was concluded that a fix should not be offered for raw
strings. This change implements that. The five rules in question are now
no longer always fixable.

## Test Plan

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

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-13 08:36:11 +00:00
Brent Westbrook
f8093b65ea [flake8-builtins] Update documentation (A005) (#16097)
Follow-up to https://github.com/astral-sh/ruff/pull/15951 to update
* the options links in A005 to reference
`lint.flake8-builtins.builtins-strict-checking`
* the description of the rule to explain strict vs non-strict checking
* the option documentation to point back to the rule
2025-02-12 12:50:13 -05:00
Alex Waygood
c31352f52b [ruff] Skip RUF001 diagnostics when visiting string type definitions (#16122) 2025-02-12 16:27:38 +00:00
Andrew Gallant
a9671e7008 ruff_db: add a vector for configuring diagnostic output (#16118)
For now, the only thing one can configure is whether color is enabled or
not. This avoids needing to ask the `colored` crate whether colors have
been globally enabled or disabled. And, more crucially, avoids the need
to _set_ this global flag for testing diagnostic output. Doing so can
have unintended consequences, as outlined in #16115.

Fixes #16115
2025-02-12 14:38:05 +00:00
Micha Reiser
03f08283ad [red-knot] Fallback to requires-python if no python-version is specified (#16028)
## Summary

Add support for the `project.requires-python` field in `pyproject.toml`
files.

Fall back to the resolved lower bound of `project.requires-python` if
the `environment.python-version` field is `None` (or more accurately,
initialize `environment.python-version with `requires-python`'s lower
bound if left unspecified).

## UX design

There are two options on how we can handle the fallback to
`requires-python`'s lower bound:

1. Store the resolved lower bound in `environment.python-version` if
that field is `None` (Implemented in this PR)
2. Store the `requires-python` constraint separately. 

There's no observed difference unless a user-level configuration (or any
other inherited configuration is used). Let's discuss it on the given
example


**User configuration**

```toml
[environment]
python-version = "3.10"
```

**Project configuration (`pyproject.toml`)**

```toml
[project]
name = "test"
requires-python = ">= 3.12"

[tool.knot]
# No environment table
```

The resolved version for 1. is 3.12 because the `requires-python`
constraint precedence takes precedence over the `python-version` in the
user configuration. 2. resolves to 3.10 because all `python-version`
constraints take precedence before falling back to `requires-python`.

Ruff implements 1. It's also the easier to implement and it does seem
intuitive to me that the more local `requires-python` constraint takes
precedence.


## Test plan

Added CLI and unit tests.
2025-02-12 11:47:59 +00:00
Vasco Schiavo
ae1b381c06 [pylint] Correct ordering of arguments in fix for if-stmt-min-max (PLR1730) (#16080)
The PR addresses the issue #16040 .

---

The logic used into the rule is the following:

Suppose to have an expression of the form 

```python
if a cmp b:
    c = d
```
where `a`,` b`, `c` and `d` are Python obj and `cmp` one of `<`, `>`,
`<=`, `>=`.

Then:

- `if a=c and b=d`
    
    - if `<=` fix with `a = max(b, a)`
    - if `>=`  fix with `a = min(b, a)`
    - if `>` fix with `a = min(a, b)`
    - if `<` fix with `a = max(a, b)`

- `if a=d and b=c`

    - if `<=` fix with `b = min(a, b)`
    - if `>=`  fix with `b = max(a, b)`
    - if `>` fix with `b = max(b, a)`
    - if `<` fix with `b = min(b, a)`
 
- do nothing, i.e., we cannot fix this case.

---

In total we have 8 different and possible cases.

```

| Case  | Expression       | Fix           |
|-------|------------------|---------------|
| 1     | if a >= b: a = b | a = min(b, a) |
| 2     | if a <= b: a = b | a = max(b, a) |
| 3     | if a <= b: b = a | b = min(a, b) |
| 4     | if a >= b: b = a | b = max(a, b) |
| 5     | if a > b: a = b  | a = min(a, b) |
| 6     | if a < b: a = b  | a = max(a, b) |
| 7     | if a < b: b = a  | b = min(b, a) |
| 8     | if a > b: b = a  | b = max(b, a) |
```

I added them in the tests. 

Please double-check that I didn't make any mistakes. It's quite easy to
mix up > and <.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-12 10:27:46 +01:00
David Peter
366ae1feaa [red-knot] Document 'public type of undeclared symbols' behavior (#16096)
## Summary

After I was asked twice within the same day, I thought it would be a
good idea to write some *user facing* documentation that explains our
reasoning behind inferring `Unknown | T_inferred` for public uses of
undeclared symbols. This is a major deviation from the behavior of other
type checkers and it seems like a good practice to defend our choice
like this.
2025-02-12 08:52:11 +01:00
Wei Lee
86c5cba472 [airflow] Fix ImportPathMoved / ProviderName misuse (AIR303) (#16013)
## Summary


* fix ImportPathMoved / ProviderName misuse
* oncrete names, such as `["airflow", "config_templates",
"default_celery", "DEFAULT_CELERY_CONFIG"]`, should use `ProviderName`.
In contrast, module paths like `"airflow", "operators", "weekday", ...`
should use `ImportPathMoved`. Misuse may lead to incorrect detection.

## Test Plan

update test fixture
2025-02-12 12:34:16 +05:30
Andrew Gallant
6e34f74c16 add diagnostic Span (couples File and TextRange) (#16101)
This essentially makes it impossible to construct a `Diagnostic`
that has a `TextRange` but no `File`.

This is meant to be a precursor to multi-span support.

(Note that I consider this more of a prototyping-change and not
necessarily what this is going to look like longer term.)

Reviewers can probably review this PR as one big diff instead of
commit-by-commit.
2025-02-11 14:55:12 -05:00
Micha Reiser
9c179314ed Remove Hash and Eq from AstNodeRef for types not implementing Eq or Hash (#16100)
## Summary

This is a follow up to
https://github.com/astral-sh/ruff/pull/15763#discussion_r1949681336

It reverts the change to using ptr equality for `AstNodeRef`s, which in
turn removes the `Eq`, `PartialEq`, and `Hash` implementations for
`AstNodeRef`s parametrized with AST nodes.
Cheap comparisons shouldn't be needed because the node field is
generally marked as `[#tracked]` and `#[no_eq]` and removing the
implementations even enforces that those
attributes are set on all `AstNodeRef` fields (which is good).

The only downside this has is that we technically wouldn't have to mark
the `Unpack::target` as `#[tracked]` because
the `target` field is accessed in every query accepting `Unpack` as an
argument.

Overall, enforcing the use of `#[tracked]` seems like a good trade off,
espacially considering that it's very likely that
we'd probably forget to mark the `Unpack::target` field as tracked if we
add a new `Unpack` query that doesn't access the target.

## Test Plan

`cargo test`
2025-02-11 19:55:50 +01:00
Micha Reiser
ce31c2693b Fix release build warning about unused todo type message (#16102) 2025-02-11 18:38:41 +00:00
Brent Westbrook
7b487d853a [pydocstyle] Handle arguments with the same names as sections (D417) (#16011)
## Summary

Fixes #16007. The logic from the last fix for this (#9427) was
sufficient, it just wasn't being applied because `Attributes` sections
aren't expected to have nested sections. I just deleted the outer
conditional, which should hopefully fix this for all section types.

## Test Plan

New regression test, plus the existing D417 tests.
2025-02-11 12:05:29 -05:00
Alex Waygood
df1d430294 [red-knot] Reduce usage of From<Type> implementations when working with Symbols (#16076) 2025-02-11 11:09:37 +00:00
Ibraheem Ahmed
69d86d1d69 Transition to salsa coarse-grained tracked structs (#15763)
## Summary

Transition to using coarse-grained tracked structs (depends on
https://github.com/salsa-rs/salsa/pull/657). For now, this PR doesn't
add any `#[tracked]` fields, meaning that any changes cause the entire
struct to be invalidated. It also changes `AstNodeRef` to be
compared/hashed by pointer address, instead of performing a deep AST
comparison.

## Test Plan

This yields a 10-15% improvement on my machine (though weirdly some runs
were 5-10% without being flagged as inconsistent by criterion, is there
some non-determinism involved?). It's possible that some of this is
unrelated, I'll try applying the patch to the current salsa version to
make sure.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-11 11:38:50 +01:00
InSync
7fbd89cb39 [pyupgrade] Handle micro version numbers correctly (UP036) (#16091)
## Summary

Resolves #16082.

`UP036` will now also take into consideration whether or not a micro
version number is set:

* If a third element doesn't exist, the existing logic is preserved.
* If it exists but is not an integer literal, the check will not be
reported.
* If it is an integer literal but doesn't fit into a `u8`, the check
will be reported as invalid.
* Otherwise, the compared version is determined to always be less than
the target version when:
	* The target's minor version is smaller than that of the comparator, or
* The operator is `<`, the micro version is 0, and the two minor
versions compare equal.

As this is considered a bugfix, it is not preview-gated.

## Test Plan

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

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-11 07:40:56 +00:00
David Peter
0019d39f6e [red-knot] T | object == object (#16088)
## Summary

- Simplify unions with `object` to `object`.
- Add a new `Type::object(db)` constructor to abbreviate
`KnownClass::Object.to_instance(db)` in some places.
- Add a `Type::is_object` and `Class::is_object` function to make some
tests for a bit easier to read.

closes #16084

## Test Plan

New Markdown tests.
2025-02-10 23:07:06 +01:00
Dylan
f30fac6326 [ruff] Skip singleton starred expressions for incorrectly-parenthesized-tuple-in-subscript (RUF031) (#16083)
The index in subscript access like `d[*y]` will not be linted or
autofixed with parentheses, even when
`lint.ruff.parenthesize-tuple-in-subscript = true`.

Closes #16077
2025-02-10 11:30:07 -06:00
Micha Reiser
a4c8c49ac2 Delete left-over `verbosity.rs (#16081) 2025-02-10 16:06:05 +00:00
Micha Reiser
af832560fc [red-knot] User-level configuration (#16021)
## Summary

This PR adds support for user-level configurations
(`~/.config/knot/knot.toml`) to Red Knot.

Red Knot will watch the user-level configuration file for changes but
only if it exists
when the process start. It doesn't watch for new configurations, 
mainly to simplify things for now (it would require watching the entire
`.config` directory because the `knot` subfolder might not exist
either).

The new `ConfigurationFile` struct seems a bit overkill for now but I
plan to use it for
hierarchical configurations as well. 


Red Knot uses the same strategy as uv and Ruff by using the etcetera
crate.

## Test Plan

Added CLI and file watching test
2025-02-10 16:44:23 +01:00
Micha Reiser
f7819e553f Add user_configuration_directory to System (#16020)
## Summary

This PR adds a new `user_configuration_directory` method to `System`. We
need it to resolve where to lookup a user-level `knot.toml`
configuration file.
The method belongs to `System` because not all platforms have a
convention of where to store such configuration files (e.g. wasm).


I refactored `TestSystem` to be a simple wrapper around an `Arc<dyn
System...>` and use the `System.as_any` method instead to cast it down
to an `InMemory` system. I also removed some `System` specific methods
from `InMemoryFileSystem`, they don't belong there.

This PR removes the `os` feature as a default feature from `ruff_db`.
Most crates depending on `ruff_db` don't need it because they only
depend on `System` or only depend on `os` for testing. This was
necessary to fix a compile error with `red_knot_wasm`

## Test Plan

I'll make use of the method in my next PR. So I guess we won't know if
it works before then but I copied the code from Ruff/uv, so I have high
confidence that it is correct.

`cargo test`
2025-02-10 15:50:55 +01:00
Micha Reiser
678b0c2d39 [red-knot] Resolve Options to Settings (#16000)
## Summary

This PR generalize the idea that we may want to emit diagnostics for 
invalid or incompatible configuration values similar to how we already 
do it for `rules`. 

This PR introduces a new `Settings` struct that is similar to `Options`
but, unlike
`Options`, are fields have their default values filled in and they use a
representation optimized for reads.

The diagnostics created during loading the `Settings` are stored on the
`Project` so that we can emit them when calling `check`.

The motivation for this work is that it simplifies adding new settings.
That's also why I went ahead and added the `terminal.error-on-warning`
setting to demonstrate how new settings are added.

## Test Plan

Existing tests, new CLI test.
2025-02-10 15:28:45 +01:00
491 changed files with 18727 additions and 7206 deletions

View File

@@ -712,7 +712,7 @@ jobs:
just test
benchmarks:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20

View File

@@ -35,6 +35,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: jetli/wasm-bindgen-action@v0.2.0
- name: "Run wasm-pack"
run: wasm-pack build --target web --out-dir ../../playground/src/pkg crates/ruff_wasm
@@ -49,7 +51,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.13.1
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -35,6 +35,8 @@ jobs:
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.13.1
- uses: jetli/wasm-bindgen-action@v0.2.0
- name: "Run wasm-pack build"
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm

View File

@@ -60,7 +60,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.29.5
rev: v1.29.7
hooks:
- id: typos
@@ -74,7 +74,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.5
rev: v0.9.6
hooks:
- id: ruff-format
- id: ruff
@@ -84,7 +84,7 @@ repos:
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.4.2
rev: v3.5.1
hooks:
- id: prettier
types: [yaml]

View File

@@ -209,8 +209,8 @@ This change only affects those using Ruff under its default rule set. Users that
### Remove support for emoji identifiers ([#7212](https://github.com/astral-sh/ruff/pull/7212))
Previously, Ruff supported the non-standard compliant emoji identifiers e.g. `📦 = 1`.
We decided to remove this non-standard language extension, and Ruff now reports syntax errors for emoji identifiers in your code, the same as CPython.
Previously, Ruff supported non-standards-compliant emoji identifiers such as `📦 = 1`.
We decided to remove this non-standard language extension. Ruff now reports syntax errors for invalid emoji identifiers in your code, the same as CPython.
### Improved GitLab fingerprints ([#7203](https://github.com/astral-sh/ruff/pull/7203))

View File

@@ -1,5 +1,55 @@
# Changelog
## 0.9.7
### Preview features
- Consider `__new__` methods as special function type for enforcing class method or static method rules ([#13305](https://github.com/astral-sh/ruff/pull/13305))
- \[`airflow`\] Improve the internal logic to differentiate deprecated symbols (`AIR303`) ([#16013](https://github.com/astral-sh/ruff/pull/16013))
- \[`refurb`\] Manual timezone monkeypatching (`FURB162`) ([#16113](https://github.com/astral-sh/ruff/pull/16113))
- \[`ruff`\] Implicit class variable in dataclass (`RUF045`) ([#14349](https://github.com/astral-sh/ruff/pull/14349))
- \[`ruff`\] Skip singleton starred expressions for `incorrectly-parenthesized-tuple-in-subscript` (`RUF031`) ([#16083](https://github.com/astral-sh/ruff/pull/16083))
- \[`refurb`\] Check for subclasses includes subscript expressions (`FURB189`) ([#16155](https://github.com/astral-sh/ruff/pull/16155))
### Rule changes
- \[`flake8-comprehensions`\]: Handle trailing comma in `C403` fix ([#16110](https://github.com/astral-sh/ruff/pull/16110))
- \[`flake8-debugger`\] Also flag `sys.breakpointhook` and `sys.__breakpointhook__` (`T100`) ([#16191](https://github.com/astral-sh/ruff/pull/16191))
- \[`pydocstyle`\] Handle arguments with the same names as sections (`D417`) ([#16011](https://github.com/astral-sh/ruff/pull/16011))
- \[`pylint`\] Correct ordering of arguments in fix for `if-stmt-min-max` (`PLR1730`) ([#16080](https://github.com/astral-sh/ruff/pull/16080))
- \[`pylint`\] Do not offer fix for raw strings (`PLE251`) ([#16132](https://github.com/astral-sh/ruff/pull/16132))
- \[`pyupgrade`\] Do not upgrade functional `TypedDicts` with private field names to the class-based syntax (`UP013`) ([#16219](https://github.com/astral-sh/ruff/pull/16219))
- \[`pyupgrade`\] Handle micro version numbers correctly (`UP036`) ([#16091](https://github.com/astral-sh/ruff/pull/16091))
- \[`pyupgrade`\] Unwrap unary expressions correctly (`UP018`) ([#15919](https://github.com/astral-sh/ruff/pull/15919))
- \[`ruff`\] Skip `RUF001` diagnostics when visiting string type definitions ([#16122](https://github.com/astral-sh/ruff/pull/16122))
- \[`flake8-pyi`\] Avoid flagging `custom-typevar-for-self` on metaclass methods (`PYI019`) ([#16141](https://github.com/astral-sh/ruff/pull/16141))
- \[`pycodestyle`\] Exempt `site.addsitedir(...)` calls (`E402`) ([#16251](https://github.com/astral-sh/ruff/pull/16251))
### Formatter
- Fix unstable formatting of trailing end-of-line comments of parenthesized attribute values ([#16187](https://github.com/astral-sh/ruff/pull/16187))
### Server
- Fix handling of requests received after shutdown message ([#16262](https://github.com/astral-sh/ruff/pull/16262))
- Ignore `source.organizeImports.ruff` and `source.fixAll.ruff` code actions for a notebook cell ([#16154](https://github.com/astral-sh/ruff/pull/16154))
- Include document specific debug info for `ruff.printDebugInformation` ([#16215](https://github.com/astral-sh/ruff/pull/16215))
- Update server to return the debug info as string with `ruff.printDebugInformation` ([#16214](https://github.com/astral-sh/ruff/pull/16214))
### CLI
- Warn on invalid `noqa` even when there are no diagnostics ([#16178](https://github.com/astral-sh/ruff/pull/16178))
- Better error messages while loading configuration `extend`s ([#15658](https://github.com/astral-sh/ruff/pull/15658))
### Bug fixes
- \[`refurb`\] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#16237](https://github.com/astral-sh/ruff/pull/16237))
### Documentation
- Add FAQ entry for `source.*` code actions in Notebook ([#16212](https://github.com/astral-sh/ruff/pull/16212))
- Add `SECURITY.md` ([#16224](https://github.com/astral-sh/ruff/pull/16224))
## 0.9.6
### Preview features

View File

@@ -526,7 +526,7 @@ cargo benchmark
#### Benchmark-driven Development
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
`--save-baseline=<name>` to store an initial baseline benchmark (e.g. on `main`) and then use
`--save-baseline=<name>` to store an initial baseline benchmark (e.g., on `main`) and then use
`--benchmark=<name>` to compare against that benchmark. Criterion will print a message telling you
if the benchmark improved/regressed compared to that baseline.
@@ -678,9 +678,9 @@ utils with it:
23 Newline 24
```
- `cargo dev print-cst <file>`: Print the CST of a python file using
- `cargo dev print-cst <file>`: Print the CST of a Python file using
[LibCST](https://github.com/Instagram/LibCST), which is used in addition to the RustPython parser
in Ruff. E.g. for `if True: pass # comment` everything including the whitespace is represented:
in Ruff. For example, for `if True: pass # comment`, everything, including the whitespace, is represented:
```text
Module {

70
Cargo.lock generated
View File

@@ -29,6 +29,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -354,9 +360,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.28"
version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
dependencies = [
"clap_builder",
"clap_derive",
@@ -364,9 +370,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.27"
version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
dependencies = [
"anstream",
"anstyle",
@@ -438,20 +444,22 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.7.2"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "450a0e9df9df1c154156f4344f99d8f6f6e69d0fc4de96ef6e2e68b2ec3bce97"
checksum = "25d2f5a6570db487f5258e0bded6352fa2034c2aeb46bb5cc3ff060a0fcfba2f"
dependencies = [
"colored 2.2.0",
"libc",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "codspeed-criterion-compat"
version = "2.7.2"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eb1a6cb9c20e177fde58cdef97c1c7c9264eb1424fe45c4fccedc2fb078a569"
checksum = "f53a55558dedec742b14aae3c5fec389361b8b5ca28c1aadf09dd91faf710074"
dependencies = [
"codspeed",
"colored 2.2.0",
@@ -471,7 +479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -480,7 +488,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1096,6 +1104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
@@ -2407,6 +2416,7 @@ dependencies = [
"red_knot_server",
"regex",
"ruff_db",
"ruff_python_ast",
"ruff_python_trivia",
"salsa",
"tempfile",
@@ -2556,9 +2566,9 @@ dependencies = [
"js-sys",
"log",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_notebook",
"ruff_python_ast",
"wasm-bindgen",
"wasm-bindgen-test",
]
@@ -2640,7 +2650,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.6"
version = "0.9.7"
dependencies = [
"anyhow",
"argfile",
@@ -2719,7 +2729,6 @@ dependencies = [
"mimalloc",
"rayon",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_linter",
"ruff_python_ast",
@@ -2748,10 +2757,10 @@ name = "ruff_db"
version = "0.0.0"
dependencies = [
"camino",
"colored 3.0.0",
"countme",
"dashmap 6.1.0",
"dunce",
"etcetera",
"filetime",
"glob",
"ignore",
@@ -2869,12 +2878,13 @@ name = "ruff_index"
version = "0.0.0"
dependencies = [
"ruff_macros",
"salsa",
"static_assertions",
]
[[package]]
name = "ruff_linter"
version = "0.9.6"
version = "0.9.7"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2979,6 +2989,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"salsa",
"schemars",
"serde",
]
@@ -3192,7 +3203,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.6"
version = "0.9.7"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3226,6 +3237,7 @@ dependencies = [
"glob",
"globset",
"ignore",
"indexmap",
"is-macro",
"itertools 0.14.0",
"log",
@@ -3303,12 +3315,14 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
dependencies = [
"append-only-vec",
"arc-swap",
"compact_str",
"crossbeam",
"dashmap 6.1.0",
"hashbrown 0.14.5",
"hashlink",
"indexmap",
"parking_lot",
@@ -3323,12 +3337,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
dependencies = [
"heck",
"proc-macro2",
@@ -3537,9 +3551,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "smallvec"
version = "1.13.2"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "snapbox"
@@ -3599,18 +3613,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.0"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.0"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck",
"proc-macro2",
@@ -3654,9 +3668,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.16.0"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
checksum = "a40f762a77d2afa88c2d919489e390a12bdd261ed568e60cfa7e48d4e20f0d33"
dependencies = [
"cfg-if",
"fastrand",
@@ -4428,7 +4442,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

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

15
SECURITY.md Normal file
View File

@@ -0,0 +1,15 @@
# Security policy
## Reporting a vulnerability
If you have found a possible vulnerability, please email `security at astral dot sh`.
## Bug bounties
While we sincerely appreciate and encourage reports of suspected security problems, please note that
Astral does not currently run any bug bounty programs.
## Vulnerability disclosures
Critical vulnerabilities will be disclosed via GitHub's
[security advisory](https://github.com/astral-sh/ruff/security) system.

View File

@@ -16,6 +16,7 @@ red_knot_python_semantic = { workspace = true }
red_knot_project = { workspace = true, features = ["zstd"] }
red_knot_server = { workspace = true }
ruff_db = { workspace = true, features = ["os", "cache"] }
ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }

25
crates/red_knot/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Red Knot
Red Knot is an extremely fast type checker.
Currently, it is a work-in-progress and not ready for user testing.
Red Knot is designed to prioritize good type inference, even in unannotated code,
and aims to avoid false positives.
While Red Knot will produce similar results to mypy and pyright on many codebases,
100% compatibility with these tools is a non-goal.
On some codebases, Red Knot's design decisions lead to different outcomes
than you would get from running one of these more established tools.
## Contributing
Core type checking tests are written as Markdown code blocks.
They can be found in [`red_knot_python_semantic/resources/mdtest`][resources-mdtest].
See [`red_knot_test/README.md`][mdtest-readme] for more information
on the test framework itself.
The list of open issues can be found [here][open-issues].
[mdtest-readme]: ../red_knot_test/README.md
[open-issues]: https://github.com/astral-sh/ruff/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20label%3Ared-knot
[resources-mdtest]: ../red_knot_python_semantic/resources/mdtest

View File

@@ -1,7 +1,7 @@
use crate::logging::Verbosity;
use crate::python_version::PythonVersion;
use clap::{ArgAction, ArgMatches, Error, Parser};
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_python_semantic::lint;
use ruff_db::system::SystemPathBuf;
@@ -67,8 +67,8 @@ pub(crate) struct CheckCommand {
pub(crate) rules: RulesArg,
/// Use exit code 1 if there are any warning-level diagnostics.
#[arg(long, conflicts_with = "exit_zero")]
pub(crate) error_on_warning: bool,
#[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)]
pub(crate) error_on_warning: Option<bool>,
/// Always use exit code 0, even when there are error-level diagnostics.
#[arg(long)]
@@ -107,6 +107,9 @@ impl CheckCommand {
}),
..EnvironmentOptions::default()
}),
terminal: Some(TerminalOptions {
error_on_warning: self.error_on_warning,
}),
rules,
..Default::default()
}

View File

@@ -11,18 +11,17 @@ use clap::Parser;
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use red_knot_project::metadata::options::Options;
use red_knot_project::watch;
use red_knot_project::watch::ProjectWatcher;
use red_knot_project::{watch, Db};
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_server::run_server;
use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
mod args;
mod logging;
mod python_version;
mod verbosity;
mod version;
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
@@ -97,19 +96,15 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let system = OsSystem::new(cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let min_error_severity = if args.error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let cli_options = args.into_options();
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
workspace_metadata.apply_cli_options(cli_options.clone());
let mut project_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
project_metadata.apply_cli_options(cli_options.clone());
project_metadata.apply_configuration_files(&system)?;
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
let mut db = ProjectDatabase::new(project_metadata, system)?;
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -167,18 +162,10 @@ struct MainLoop {
watcher: Option<ProjectWatcher>,
cli_options: Options,
/// The minimum severity to consider an error when deciding the exit status.
///
/// TODO(micha): Get from the terminal settings.
min_error_severity: Severity,
}
impl MainLoop {
fn new(
cli_options: Options,
min_error_severity: Severity,
) -> (Self, MainLoopCancellationToken) {
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
(
@@ -187,7 +174,6 @@ impl MainLoop {
receiver,
watcher: None,
cli_options,
min_error_severity,
},
MainLoopCancellationToken { sender },
)
@@ -245,14 +231,24 @@ impl MainLoop {
result,
revision: check_revision,
} => {
let display_config = DisplayDiagnosticConfig::default()
.color(colored::control::SHOULD_COLORIZE.should_colorize());
let min_error_severity =
if db.project().settings(db).terminal().error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let failed = result
.iter()
.any(|diagnostic| diagnostic.severity() >= self.min_error_severity);
.any(|diagnostic| diagnostic.severity() >= min_error_severity);
if check_revision == revision {
#[allow(clippy::print_stdout)]
for diagnostic in result {
println!("{}", diagnostic.display(db));
println!("{}", diagnostic.display(db, &display_config));
}
} else {
tracing::debug!(

View File

@@ -40,7 +40,7 @@ impl std::fmt::Display for PythonVersion {
}
}
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
impl From<PythonVersion> for ruff_python_ast::PythonVersion {
fn from(value: PythonVersion) -> Self {
match value {
PythonVersion::Py37 => Self::PY37,
@@ -61,8 +61,8 @@ mod tests {
#[test]
fn same_default_as_python_version() {
assert_eq!(
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
red_knot_python_semantic::PythonVersion::default()
ruff_python_ast::PythonVersion::from(PythonVersion::default()),
ruff_python_ast::PythonVersion::default()
);
}
}

View File

@@ -1 +0,0 @@

View File

@@ -98,7 +98,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
])?;
// Make sure that the CLI fails when the `libs` directory is not in the search path.
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r###"
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -115,7 +115,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
----- stderr -----
"###);
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
success: true
exit_code: 0
----- stdout -----
@@ -167,7 +167,7 @@ fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Re
),
])?;
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
success: true
exit_code: 0
----- stdout -----
@@ -575,6 +575,37 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {
let case = TestCase::with_files([
("test.py", r"print(x) # [unresolved-reference]"),
(
"knot.toml",
r#"
[terminal]
error-on-warning = true
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
let case = TestCase::with_file(
@@ -686,6 +717,109 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn user_configuration() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"project/knot.toml",
r#"
[rules]
division-by-zero = "warn"
"#,
),
(
"project/main.py",
r#"
y = 4 / 0
for a in range(0, y):
x = a
print(x)
"#,
),
])?;
let config_directory = case.root().join("home/.config");
let config_env_var = if cfg!(windows) {
"APPDATA"
} else {
"XDG_CONFIG_HOME"
};
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r###"
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| - Name `x` used when possibly not defined
|
----- stderr -----
"###
);
// The user-level configuration promotes `possibly-unresolved-reference` to an error.
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
// has higher precedence.
case.write_file(
config_directory.join("knot/knot.toml"),
r#"
[rules]
division-by-zero = "error"
possibly-unresolved-reference = "error"
"#,
)?;
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
error: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
|
----- stderr -----
"###
);
Ok(())
}
struct TestCase {
_temp_dir: TempDir,
_settings_scope: SettingsBindDropGuard,
@@ -753,7 +887,7 @@ impl TestCase {
Ok(())
}
fn project_dir(&self) -> &Path {
fn root(&self) -> &Path {
&self.project_dir
}

View File

@@ -9,11 +9,14 @@ use red_knot_project::metadata::pyproject::{PyProject, Tool};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform, PythonVersion};
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform};
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::system::{
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
};
use ruff_db::Upcast;
use ruff_python_ast::PythonVersion;
struct TestCase {
db: ProjectDatabase,
@@ -220,17 +223,44 @@ where
}
trait SetupFiles {
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()>;
fn setup(self, context: &SetupContext) -> anyhow::Result<()>;
}
struct SetupContext<'a> {
system: &'a OsSystem,
root_path: &'a SystemPath,
}
impl<'a> SetupContext<'a> {
fn system(&self) -> &'a OsSystem {
self.system
}
fn join_project_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
self.project_path().join(relative)
}
fn project_path(&self) -> &SystemPath {
self.system.current_directory()
}
fn root_path(&self) -> &'a SystemPath {
self.root_path
}
fn join_root_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
self.root_path().join(relative)
}
}
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
where
P: AsRef<SystemPath>,
{
fn setup(self, _root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
for (relative_path, content) in self {
let relative_path = relative_path.as_ref();
let absolute_path = project_path.join(relative_path);
let absolute_path = context.join_project_path(relative_path);
if let Some(parent) = absolute_path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for file `{relative_path}`")
@@ -250,10 +280,10 @@ where
impl<F> SetupFiles for F
where
F: FnOnce(&SystemPath, &SystemPath) -> anyhow::Result<()>,
F: FnOnce(&SetupContext) -> anyhow::Result<()>,
{
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
self(root_path, project_path)
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
self(context)
}
}
@@ -261,13 +291,12 @@ fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
where
F: SetupFiles,
{
setup_with_options(setup_files, |_root, _project_path| None)
setup_with_options(setup_files, |_context| None)
}
// TODO: Replace with configuration?
fn setup_with_options<F>(
setup_files: F,
create_options: impl FnOnce(&SystemPath, &SystemPath) -> Option<Options>,
create_options: impl FnOnce(&SetupContext) -> Option<Options>,
) -> anyhow::Result<TestCase>
where
F: SetupFiles,
@@ -295,13 +324,17 @@ where
std::fs::create_dir_all(project_path.as_std_path())
.with_context(|| format!("Failed to create project directory `{project_path}`"))?;
let system = OsSystem::new(&project_path);
let setup_context = SetupContext {
system: &system,
root_path: &root_path,
};
setup_files
.setup(&root_path, &project_path)
.setup(&setup_context)
.context("Failed to setup test files")?;
let system = OsSystem::new(&project_path);
if let Some(options) = create_options(&root_path, &project_path) {
if let Some(options) = create_options(&setup_context) {
std::fs::write(
project_path.join("pyproject.toml").as_std_path(),
toml::to_string(&PyProject {
@@ -315,7 +348,9 @@ where
.context("Failed to write configuration")?;
}
let project = ProjectMetadata::discover(&project_path, &system)?;
let mut project = ProjectMetadata::discover(&project_path, &system)?;
project.apply_configuration_files(&system)?;
let program_settings = project.to_program_settings(&system);
for path in program_settings
@@ -789,10 +824,12 @@ fn directory_deleted() -> anyhow::Result<()> {
#[test]
fn search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
extra_paths: Some(vec![RelativePathBuf::cli(
context.join_root_path("site_packages"),
)]),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -853,10 +890,12 @@ fn add_search_path() -> anyhow::Result<()> {
#[test]
fn remove_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
extra_paths: Some(vec![RelativePathBuf::cli(
context.join_root_path("site_packages"),
)]),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -894,7 +933,7 @@ import os
print(sys.last_exc, os.getegid())
"#,
)],
|_root_path, _project_path| {
|_context| {
Some(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
@@ -942,21 +981,31 @@ print(sys.last_exc, os.getegid())
#[test]
fn changed_versions_file() -> anyhow::Result<()> {
let mut case = setup_with_options(
|root_path: &SystemPath, project_path: &SystemPath| {
std::fs::write(project_path.join("bar.py").as_std_path(), "import sub.a")?;
std::fs::create_dir_all(root_path.join("typeshed/stdlib").as_std_path())?;
std::fs::write(root_path.join("typeshed/stdlib/VERSIONS").as_std_path(), "")?;
|context: &SetupContext| {
std::fs::write(
root_path.join("typeshed/stdlib/os.pyi").as_std_path(),
context.join_project_path("bar.py").as_std_path(),
"import sub.a",
)?;
std::fs::create_dir_all(context.join_root_path("typeshed/stdlib").as_std_path())?;
std::fs::write(
context
.join_root_path("typeshed/stdlib/VERSIONS")
.as_std_path(),
"",
)?;
std::fs::write(
context
.join_root_path("typeshed/stdlib/os.pyi")
.as_std_path(),
"# not important",
)?;
Ok(())
},
|root_path, _project_path| {
|context| {
Some(Options {
environment: Some(EnvironmentOptions {
typeshed: Some(RelativePathBuf::cli(root_path.join("typeshed"))),
typeshed: Some(RelativePathBuf::cli(context.join_root_path("typeshed"))),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -1007,12 +1056,12 @@ fn changed_versions_file() -> anyhow::Result<()> {
/// we're seeing is that Windows only emits a single event, similar to Linux.
#[test]
fn hard_links_in_project() -> anyhow::Result<()> {
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
let foo_path = project.join("foo.py");
let mut case = setup(|context: &SetupContext| {
let foo_path = context.join_project_path("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = project.join("bar.py");
let bar_path = context.join_project_path("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
@@ -1078,12 +1127,12 @@ fn hard_links_in_project() -> anyhow::Result<()> {
ignore = "windows doesn't support observing changes to hard linked files."
)]
fn hard_links_to_target_outside_project() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
let foo_path = root.join("foo.py");
let mut case = setup(|context: &SetupContext| {
let foo_path = context.join_root_path("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = project.join("bar.py");
let bar_path = context.join_project_path("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
@@ -1186,9 +1235,9 @@ mod unix {
ignore = "FSEvents doesn't emit change events for symlinked directories outside of the watched paths."
)]
fn symlink_target_outside_watched_paths() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
let mut case = setup(|context: &SetupContext| {
// Set up the symlink target.
let link_target = root.join("bar");
let link_target = context.join_root_path("bar");
std::fs::create_dir_all(link_target.as_std_path())
.context("Failed to create link target directory")?;
let baz_original = link_target.join("baz.py");
@@ -1196,7 +1245,7 @@ mod unix {
.context("Failed to write link target file")?;
// Create a symlink inside the project
let bar = project.join("bar");
let bar = context.join_project_path("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
.context("Failed to create symlink to bar package")?;
@@ -1267,9 +1316,9 @@ mod unix {
/// ```
#[test]
fn symlink_inside_project() -> anyhow::Result<()> {
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
let mut case = setup(|context: &SetupContext| {
// Set up the symlink target.
let link_target = project.join("patched/bar");
let link_target = context.join_project_path("patched/bar");
std::fs::create_dir_all(link_target.as_std_path())
.context("Failed to create link target directory")?;
let baz_original = link_target.join("baz.py");
@@ -1277,7 +1326,7 @@ mod unix {
.context("Failed to write link target file")?;
// Create a symlink inside site-packages
let bar_in_project = project.join("bar");
let bar_in_project = context.join_project_path("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_project.as_std_path())
.context("Failed to create symlink to bar package")?;
@@ -1358,9 +1407,9 @@ mod unix {
#[test]
fn symlinked_module_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options(
|root: &SystemPath, project: &SystemPath| {
|context: &SetupContext| {
// Set up the symlink target.
let site_packages = root.join("site-packages");
let site_packages = context.join_root_path("site-packages");
let bar = site_packages.join("bar");
std::fs::create_dir_all(bar.as_std_path())
.context("Failed to create bar directory")?;
@@ -1369,7 +1418,8 @@ mod unix {
.context("Failed to write baz.py")?;
// Symlink the site packages in the venv to the global site packages
let venv_site_packages = project.join(".venv/lib/python3.12/site-packages");
let venv_site_packages =
context.join_project_path(".venv/lib/python3.12/site-packages");
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
.context("Failed to create .venv directory")?;
std::os::unix::fs::symlink(
@@ -1380,7 +1430,7 @@ mod unix {
Ok(())
},
|_root, _project| {
|_context| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(
@@ -1450,9 +1500,9 @@ mod unix {
#[test]
fn nested_projects_delete_root() -> anyhow::Result<()> {
let mut case = setup(|root: &SystemPath, project_root: &SystemPath| {
let mut case = setup(|context: &SetupContext| {
std::fs::write(
project_root.join("pyproject.toml").as_std_path(),
context.join_project_path("pyproject.toml").as_std_path(),
r#"
[project]
name = "inner"
@@ -1462,7 +1512,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
)?;
std::fs::write(
root.join("pyproject.toml").as_std_path(),
context.join_root_path("pyproject.toml").as_std_path(),
r#"
[project]
name = "outer"
@@ -1487,3 +1537,79 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn changes_to_user_configuration() -> anyhow::Result<()> {
let mut _config_dir_override: Option<UserConfigDirectoryOverrideGuard> = None;
let mut case = setup(|context: &SetupContext| {
std::fs::write(
context.join_project_path("pyproject.toml").as_std_path(),
r#"
[project]
name = "test"
"#,
)?;
std::fs::write(
context.join_project_path("foo.py").as_std_path(),
"a = 10 / 0",
)?;
let config_directory = context.join_root_path("home/.config");
std::fs::create_dir_all(config_directory.join("knot").as_std_path())?;
std::fs::write(
config_directory.join("knot/knot.toml").as_std_path(),
r#"
[rules]
division-by-zero = "ignore"
"#,
)?;
_config_dir_override = Some(
context
.system()
.with_user_config_directory(Some(config_directory)),
);
Ok(())
})?;
let foo = case
.system_file(case.project_path("foo.py"))
.expect("foo.py to exist");
let diagnostics = case
.db()
.check_file(foo)
.context("Failed to check project.")?;
assert!(
diagnostics.is_empty(),
"Expected no diagnostics but got: {diagnostics:#?}"
);
// Enable division-by-zero in the user configuration with warning severity
update_file(
case.root_path().join("home/.config/knot/knot.toml"),
r#"
[rules]
division-by-zero = "warn"
"#,
)?;
let changes = case.stop_watch(event_for_file("knot.toml"));
case.apply_changes(changes);
let diagnostics = case
.db()
.check_file(foo)
.context("Failed to check project.")?;
assert!(
diagnostics.len() == 1,
"Expected exactly one diagnostic but got: {diagnostics:#?}"
);
Ok(())
}

View File

@@ -13,7 +13,7 @@ license.workspace = true
[dependencies]
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["os", "cache", "serde"] }
ruff_db = { workspace = true, features = ["cache", "serde"] }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["serde"] }
ruff_text_size = { workspace = true }
@@ -24,7 +24,7 @@ anyhow = { workspace = true }
crossbeam = { workspace = true }
glob = { workspace = true }
notify = { workspace = true }
pep440_rs = { workspace = true }
pep440_rs = { workspace = true, features = ["version-ranges"] }
rayon = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }

View File

@@ -1,7 +1,8 @@
use std::{collections::HashMap, hash::BuildHasher};
use red_knot_python_semantic::{PythonPlatform, PythonVersion, SitePackages};
use red_knot_python_semantic::{PythonPlatform, SitePackages};
use ruff_db::system::SystemPathBuf;
use ruff_python_ast::PythonVersion;
/// Combine two values, preferring the values in `self`.
///

View File

@@ -114,8 +114,8 @@ impl SemanticDb for ProjectDatabase {
project.is_file_open(self, file)
}
fn rule_selection(&self) -> &RuleSelection {
self.project().rule_selection(self)
fn rule_selection(&self) -> Arc<RuleSelection> {
self.project().rules(self)
}
fn lint_registry(&self) -> &LintRegistry {
@@ -186,7 +186,6 @@ pub(crate) mod tests {
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
rule_selection: RuleSelection,
project: Option<Project>,
}
@@ -198,7 +197,6 @@ pub(crate) mod tests {
vendored: red_knot_vendored::file_system().clone(),
files: Files::default(),
events: Arc::default(),
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
project: None,
};
@@ -270,8 +268,8 @@ pub(crate) mod tests {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
fn rule_selection(&self) -> Arc<RuleSelection> {
self.project().rules(self)
}
fn lint_registry(&self) -> &LintRegistry {

View File

@@ -8,6 +8,7 @@ use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::system::walk_directory::WalkState;
use ruff_db::system::SystemPath;
use ruff_db::Db as _;
use ruff_python_ast::PySourceType;
use rustc_hash::FxHashSet;
impl ProjectDatabase {
@@ -47,7 +48,7 @@ impl ProjectDatabase {
if let Some(path) = change.system_path() {
if matches!(
path.file_name(),
Some(".gitignore" | ".ignore" | "ruff.toml" | ".ruff.toml" | "pyproject.toml")
Some(".gitignore" | ".ignore" | "knot.toml" | "pyproject.toml")
) {
// Changes to ignore files or settings can change the project structure or add/remove files.
project_changed = true;
@@ -144,6 +145,12 @@ impl ProjectDatabase {
metadata.apply_cli_options(cli_options.clone());
}
if let Err(error) = metadata.apply_configuration_files(self.system()) {
tracing::error!(
"Failed to apply configuration files, continuing without applying them: {error}"
);
}
let program_settings = metadata.to_program_settings(self.system());
let program = Program::get(self);
@@ -201,9 +208,16 @@ impl ProjectDatabase {
return WalkState::Continue;
}
let mut paths = added_paths.lock().unwrap();
if entry
.path()
.extension()
.and_then(PySourceType::try_from_extension)
.is_some()
{
let mut paths = added_paths.lock().unwrap();
paths.push(entry.into_path());
paths.push(entry.into_path());
}
WalkState::Continue
})

View File

@@ -3,18 +3,18 @@
use crate::metadata::options::OptionDiagnostic;
pub use db::{Db, ProjectDatabase};
use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings;
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::types::check_types;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity, Span};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module;
use ruff_db::source::{source_text, SourceTextError};
use ruff_db::system::walk_directory::WalkState;
use ruff_db::system::{FileType, SystemPath};
use ruff_python_ast::PySourceType;
use ruff_text_size::TextRange;
use rustc_hash::{FxBuildHasher, FxHashSet};
use salsa::Durability;
use salsa::Setter;
@@ -66,12 +66,22 @@ pub struct Project {
/// The metadata describing the project, including the unresolved options.
#[return_ref]
pub metadata: ProjectMetadata,
/// The resolved project settings.
#[return_ref]
pub settings: Settings,
/// Diagnostics that were generated when resolving the project settings.
#[return_ref]
settings_diagnostics: Vec<OptionDiagnostic>,
}
#[salsa::tracked]
impl Project {
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self {
Project::builder(metadata)
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
Project::builder(metadata, settings, settings_diagnostics)
.durability(Durability::MEDIUM)
.open_fileset_durability(Durability::LOW)
.file_set_durability(Durability::LOW)
@@ -86,30 +96,37 @@ impl Project {
self.metadata(db).name()
}
/// Returns the resolved linter rules for the project.
///
/// This is a salsa query to prevent re-computing queries if other, unrelated
/// settings change. For example, we don't want that changing the terminal settings
/// invalidates any type checking queries.
#[salsa::tracked]
pub fn rules(self, db: &dyn Db) -> Arc<RuleSelection> {
self.settings(db).to_rules()
}
pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
tracing::debug!("Reloading project");
assert_eq!(self.root(db), metadata.root());
if &metadata != self.metadata(db) {
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
if self.settings(db) != &settings {
self.set_settings(db).to(settings);
}
if self.settings_diagnostics(db) != &settings_diagnostics {
self.set_settings_diagnostics(db).to(settings_diagnostics);
}
self.set_metadata(db).to(metadata);
}
self.reload_files(db);
}
pub fn rule_selection(self, db: &dyn Db) -> &RuleSelection {
let (selection, _) = self.rule_selection_with_diagnostics(db);
selection
}
#[salsa::tracked(return_ref)]
fn rule_selection_with_diagnostics(
self,
db: &dyn Db,
) -> (RuleSelection, Vec<OptionDiagnostic>) {
self.metadata(db).options().to_rule_selection(db)
}
/// Checks all open files in the project and its dependencies.
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
let project_span = tracing::debug_span!("Project::check");
@@ -118,8 +135,7 @@ impl Project {
tracing::debug!("Checking project '{name}'", name = self.name(db));
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
diagnostics.extend(options_diagnostics.iter().map(|diagnostic| {
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
diagnostic
}));
@@ -151,9 +167,8 @@ impl Project {
}
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
let mut file_diagnostics: Vec<_> = options_diagnostics
let mut file_diagnostics: Vec<_> = self
.settings_diagnostics(db)
.iter()
.map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
@@ -329,7 +344,13 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
boxed
}));
diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start());
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.span()
.and_then(|span| span.range())
.unwrap_or_default()
.start()
});
diagnostics
}
@@ -442,12 +463,8 @@ impl Diagnostic for IOErrorDiagnostic {
self.error.to_string().into()
}
fn file(&self) -> Option<File> {
Some(self.file)
}
fn range(&self) -> Option<TextRange> {
None
fn span(&self) -> Option<Span> {
Some(Span::from(self.file))
}
fn severity(&self) -> Severity {

View File

@@ -1,3 +1,4 @@
use configuration_file::{ConfigurationFile, ConfigurationFileError};
use red_knot_python_semantic::ProgramSettings;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_python_ast::name::Name;
@@ -5,13 +6,15 @@ use std::sync::Arc;
use thiserror::Error;
use crate::combine::Combine;
use crate::metadata::pyproject::{Project, PyProject, PyProjectError};
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
use crate::metadata::value::ValueSource;
use options::KnotTomlError;
use options::Options;
mod configuration_file;
pub mod options;
pub mod pyproject;
pub mod settings;
pub mod value;
#[derive(Debug, PartialEq, Eq)]
@@ -23,6 +26,15 @@ pub struct ProjectMetadata {
/// The raw options
pub(super) options: Options,
/// Paths of configurations other than the project's configuration that were combined into [`Self::options`].
///
/// This field stores the paths of the configuration files, mainly for
/// knowing which files to watch for changes.
///
/// The path ordering doesn't imply precedence.
#[cfg_attr(test, serde(skip_serializing_if = "Vec::is_empty"))]
pub(super) extra_configuration_paths: Vec<SystemPathBuf>,
}
impl ProjectMetadata {
@@ -31,12 +43,16 @@ impl ProjectMetadata {
Self {
name,
root,
extra_configuration_paths: Vec::default(),
options: Options::default(),
}
}
/// Loads a project from a `pyproject.toml` file.
pub(crate) fn from_pyproject(pyproject: PyProject, root: SystemPathBuf) -> Self {
pub(crate) fn from_pyproject(
pyproject: PyProject,
root: SystemPathBuf,
) -> Result<Self, ResolveRequiresPythonError> {
Self::from_options(
pyproject
.tool
@@ -49,21 +65,37 @@ impl ProjectMetadata {
/// Loads a project from a set of options with an optional pyproject-project table.
pub(crate) fn from_options(
options: Options,
mut options: Options,
root: SystemPathBuf,
project: Option<&Project>,
) -> Self {
) -> Result<Self, ResolveRequiresPythonError> {
let name = project
.and_then(|project| project.name.as_ref())
.map(|name| Name::new(&***name))
.and_then(|project| project.name.as_deref())
.map(|name| Name::new(&**name))
.unwrap_or_else(|| Name::new(root.file_name().unwrap_or("root")));
// TODO(https://github.com/astral-sh/ruff/issues/15491): Respect requires-python
Self {
// If the `options` don't specify a python version but the `project.requires-python` field is set,
// use that as a lower bound instead.
if let Some(project) = project {
if !options
.environment
.as_ref()
.is_some_and(|env| env.python_version.is_some())
{
if let Some(requires_python) = project.resolve_requires_python_lower_bound()? {
let mut environment = options.environment.unwrap_or_default();
environment.python_version = Some(requires_python);
options.environment = Some(environment);
}
}
}
Ok(Self {
name,
root,
options,
}
extra_configuration_paths: Vec::new(),
})
}
/// Discovers the closest project at `path` and returns its metadata.
@@ -131,19 +163,34 @@ impl ProjectMetadata {
}
tracing::debug!("Found project at '{}'", project_root);
return Ok(ProjectMetadata::from_options(
let metadata = ProjectMetadata::from_options(
options,
project_root.to_path_buf(),
pyproject
.as_ref()
.and_then(|pyproject| pyproject.project.as_ref()),
));
)
.map_err(|err| {
ProjectDiscoveryError::InvalidRequiresPythonConstraint {
source: err,
path: pyproject_path,
}
})?;
return Ok(metadata);
}
if let Some(pyproject) = pyproject {
let has_knot_section = pyproject.knot().is_some();
let metadata =
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf());
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
.map_err(
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
source: err,
path: pyproject_path,
},
)?;
if has_knot_section {
tracing::debug!("Found project at '{}'", project_root);
@@ -191,6 +238,10 @@ impl ProjectMetadata {
&self.options
}
pub fn extra_configuration_paths(&self) -> &[SystemPathBuf] {
&self.extra_configuration_paths
}
pub fn to_program_settings(&self, system: &dyn System) -> ProgramSettings {
self.options.to_program_settings(self.root(), system)
}
@@ -200,9 +251,31 @@ impl ProjectMetadata {
self.options = options.combine(std::mem::take(&mut self.options));
}
/// Combine the project options with the user options where project options take precedence.
pub fn apply_user_options(&mut self, options: Options) {
self.options.combine_with(options);
/// Applies the options from the configuration files to the project's options.
///
/// This includes:
///
/// * The user-level configuration
pub fn apply_configuration_files(
&mut self,
system: &dyn System,
) -> Result<(), ConfigurationFileError> {
if let Some(user) = ConfigurationFile::user(system)? {
tracing::debug!(
"Applying user-level configuration loaded from `{path}`.",
path = user.path()
);
self.apply_configuration_file(user);
}
Ok(())
}
/// Applies a lower-precedence configuration files to the project's options.
fn apply_configuration_file(&mut self, options: ConfigurationFile) {
self.extra_configuration_paths
.push(options.path().to_owned());
self.options.combine_with(options.into_options());
}
}
@@ -222,16 +295,22 @@ pub enum ProjectDiscoveryError {
source: Box<KnotTomlError>,
path: SystemPathBuf,
},
#[error("Invalid `requires-python` version specifier (`{path}`): {source}")]
InvalidRequiresPythonConstraint {
source: ResolveRequiresPythonError,
path: SystemPathBuf,
},
}
#[cfg(test)]
mod tests {
//! Integration tests for project discovery
use crate::snapshot_project;
use anyhow::{anyhow, Context};
use insta::assert_ron_snapshot;
use ruff_db::system::{SystemPathBuf, TestSystem};
use ruff_python_ast::PythonVersion;
use crate::{ProjectDiscoveryError, ProjectMetadata};
@@ -250,7 +329,15 @@ mod tests {
assert_eq!(project.root(), &*root);
snapshot_project!(project);
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)
"#);
});
Ok(())
}
@@ -279,7 +366,16 @@ mod tests {
ProjectMetadata::discover(&root, &system).context("Failed to discover project")?;
assert_eq!(project.root(), &*root);
snapshot_project!(project);
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)
"#);
});
// Discovering the same package from a subdirectory should give the same result
let from_src = ProjectMetadata::discover(&root.join("db"), &system)
@@ -362,7 +458,19 @@ expected `.`, `]`
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
snapshot_project!(sub_project);
with_escaped_paths(|| {
assert_ron_snapshot!(sub_project, @r#"
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
Ok(())
}
@@ -400,7 +508,19 @@ expected `.`, `]`
let root = ProjectMetadata::discover(&root, &system)?;
snapshot_project!(root);
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
Ok(())
}
@@ -432,7 +552,15 @@ expected `.`, `]`
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
snapshot_project!(sub_project);
with_escaped_paths(|| {
assert_ron_snapshot!(sub_project, @r#"
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(),
)
"#);
});
Ok(())
}
@@ -467,7 +595,19 @@ expected `.`, `]`
let root = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
snapshot_project!(root);
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.10"),
)),
),
)
"#);
});
Ok(())
}
@@ -487,27 +627,304 @@ expected `.`, `]`
(
root.join("pyproject.toml"),
r#"
[project]
name = "super-app"
requires-python = ">=3.12"
[project]
name = "super-app"
requires-python = ">=3.12"
[tool.knot.src]
root = "this_option_is_ignored"
"#,
[tool.knot.src]
root = "this_option_is_ignored"
"#,
),
(
root.join("knot.toml"),
r#"
[src]
root = "src"
"#,
[src]
root = "src"
"#,
),
])
.context("Failed to write files")?;
let root = ProjectMetadata::discover(&root, &system)?;
snapshot_project!(root);
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("super-app"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.12"),
)),
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
Ok(())
}
#[test]
fn requires_python_major_minor() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
#[test]
fn requires_python_major_only() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::from((3, 0)))
);
Ok(())
}
/// A `requires-python` constraint with major, minor and patch can be simplified
/// to major and minor (e.g. 3.12.1 -> 3.12).
#[test]
fn requires_python_major_minor_patch() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12.8"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
#[test]
fn requires_python_beta_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">= 3.13.0b0"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY313)
);
Ok(())
}
#[test]
fn requires_python_greater_than_major_minor() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
# This is somewhat nonsensical because 3.12.1 > 3.12 is true.
# That's why simplifying the constraint to >= 3.12 is correct
requires-python = ">3.12"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
/// `python-version` takes precedence if both `requires-python` and `python-version` are configured.
#[test]
fn requires_python_and_python_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12"
[tool.knot.environment]
python-version = "3.10"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY310)
);
Ok(())
}
#[test]
fn requires_python_less_than() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = "<3.12"
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because the `requires-python` doesn't specify a lower bound (it only specifies an upper bound)."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `<3.12` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.");
Ok(())
}
#[test]
fn requires_python_no_specifiers() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ""
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because the `requires-python` specifiers are empty and don't define a lower bound."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.");
Ok(())
}
#[test]
fn requires_python_too_large_major_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=999.0"
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because of the requires-python major version that is larger than 255."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): The major version `999` is larger than the maximum supported value 255");
Ok(())
}
@@ -517,15 +934,12 @@ expected `.`, `]`
assert_eq!(error.to_string().replace('\\', "/"), message);
}
/// Snapshots a project but with all paths using unix separators.
#[macro_export]
macro_rules! snapshot_project {
($project:expr) => {{
assert_ron_snapshot!($project,{
".root" => insta::dynamic_redaction(|content, _content_path| {
content.as_str().unwrap().replace("\\", "/")
}),
fn with_escaped_paths<R>(f: impl FnOnce() -> R) -> R {
let mut settings = insta::Settings::clone_current();
settings.add_dynamic_redaction(".root", |content, _path| {
content.as_str().unwrap().replace('\\', "/")
});
}};
}
settings.bind(f)
}
}

View File

@@ -0,0 +1,69 @@
use std::sync::Arc;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use thiserror::Error;
use crate::metadata::value::ValueSource;
use super::options::{KnotTomlError, Options};
/// A `knot.toml` configuration file with the options it contains.
pub(crate) struct ConfigurationFile {
path: SystemPathBuf,
options: Options,
}
impl ConfigurationFile {
/// Loads the user-level configuration file if it exists.
///
/// Returns `None` if the file does not exist or if the concept of user-level configurations
/// doesn't exist on `system`.
pub(crate) fn user(system: &dyn System) -> Result<Option<Self>, ConfigurationFileError> {
let Some(configuration_directory) = system.user_config_directory() else {
return Ok(None);
};
let knot_toml_path = configuration_directory.join("knot").join("knot.toml");
tracing::debug!(
"Searching for a user-level configuration at `{path}`",
path = &knot_toml_path
);
let Ok(knot_toml_str) = system.read_to_string(&knot_toml_path) else {
return Ok(None);
};
match Options::from_toml_str(
&knot_toml_str,
ValueSource::File(Arc::new(knot_toml_path.clone())),
) {
Ok(options) => Ok(Some(Self {
path: knot_toml_path,
options,
})),
Err(error) => Err(ConfigurationFileError::InvalidKnotToml {
source: Box::new(error),
path: knot_toml_path,
}),
}
}
/// Returns the path to the configuration file.
pub(crate) fn path(&self) -> &SystemPath {
&self.path
}
pub(crate) fn into_options(self) -> Options {
self.options
}
}
#[derive(Debug, Error)]
pub enum ConfigurationFileError {
#[error("{path} is not a valid `knot.toml`: {source}")]
InvalidKnotToml {
source: Box<KnotTomlError>,
path: SystemPathBuf,
},
}

View File

@@ -1,20 +1,20 @@
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
use crate::Db;
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use red_knot_python_semantic::{
ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings, SitePackages,
};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::{system_path_to_file, File};
use red_knot_python_semantic::{ProgramSettings, PythonPlatform, SearchPathSettings, SitePackages};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::system_path_to_file;
use ruff_db::system::{System, SystemPath};
use ruff_macros::Combine;
use ruff_text_size::TextRange;
use ruff_python_ast::PythonVersion;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::Debug;
use thiserror::Error;
use super::settings::{Settings, TerminalSettings};
/// The options for the project.
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
@@ -30,6 +30,9 @@ pub struct Options {
/// Configures the enabled lints and their severity.
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Rules>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terminal: Option<TerminalOptions>,
}
impl Options {
@@ -110,7 +113,22 @@ impl Options {
}
#[must_use]
pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
let (rules, diagnostics) = self.to_rule_selection(db);
let mut settings = Settings::new(rules);
if let Some(terminal) = self.terminal.as_ref() {
settings.set_terminal(TerminalSettings {
error_on_warning: terminal.error_on_warning.unwrap_or_default(),
});
}
(settings, diagnostics)
}
#[must_use]
fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
let registry = db.lint_registry();
let mut diagnostics = Vec::new();
@@ -169,7 +187,14 @@ impl Options {
),
};
diagnostics.push(diagnostic.with_file(file).with_range(rule_name.range()));
let span = file.map(Span::from).map(|span| {
if let Some(range) = rule_name.range() {
span.with_range(range)
} else {
span
}
});
diagnostics.push(diagnostic.with_span(span));
}
}
}
@@ -244,6 +269,16 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TerminalOptions {
/// Use exit code 1 if there are any warning-level diagnostics.
///
/// Defaults to `false`.
pub error_on_warning: Option<bool>,
}
#[cfg(feature = "schemars")]
mod schema {
use crate::DEFAULT_LINT_REGISTRY;
@@ -318,8 +353,7 @@ pub struct OptionDiagnostic {
id: DiagnosticId,
message: String,
severity: Severity,
file: Option<File>,
range: Option<TextRange>,
span: Option<Span>,
}
impl OptionDiagnostic {
@@ -328,21 +362,13 @@ impl OptionDiagnostic {
id,
message,
severity,
file: None,
range: None,
span: None,
}
}
#[must_use]
fn with_file(mut self, file: Option<File>) -> Self {
self.file = file;
self
}
#[must_use]
fn with_range(mut self, range: Option<TextRange>) -> Self {
self.range = range;
self
fn with_span(self, span: Option<Span>) -> Self {
OptionDiagnostic { span, ..self }
}
}
@@ -355,12 +381,8 @@ impl Diagnostic for OptionDiagnostic {
Cow::Borrowed(&self.message)
}
fn file(&self) -> Option<File> {
self.file
}
fn range(&self) -> Option<TextRange> {
self.range
fn span(&self) -> Option<Span> {
self.span.clone()
}
fn severity(&self) -> Severity {

View File

@@ -1,10 +1,11 @@
use pep440_rs::{Version, VersionSpecifiers};
use serde::{Deserialize, Deserializer, Serialize};
use std::ops::Deref;
use thiserror::Error;
use crate::metadata::options::Options;
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
use pep440_rs::{release_specifiers_to_ranges, Version, VersionSpecifiers};
use ruff_python_ast::PythonVersion;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::Bound;
use std::ops::Deref;
use thiserror::Error;
/// A `pyproject.toml` as specified in PEP 517.
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
@@ -55,6 +56,73 @@ pub struct Project {
pub requires_python: Option<RangedValue<VersionSpecifiers>>,
}
impl Project {
pub(super) fn resolve_requires_python_lower_bound(
&self,
) -> Result<Option<RangedValue<PythonVersion>>, ResolveRequiresPythonError> {
let Some(requires_python) = self.requires_python.as_ref() else {
return Ok(None);
};
tracing::debug!("Resolving requires-python constraint: `{requires_python}`");
let ranges = release_specifiers_to_ranges((**requires_python).clone());
let Some((lower, _)) = ranges.bounding_range() else {
return Ok(None);
};
let version = match lower {
// Ex) `>=3.10.1` -> `>=3.10`
Bound::Included(version) => version,
// Ex) `>3.10.1` -> `>=3.10` or `>3.10` -> `>=3.10`
// The second example looks obscure at first but it is required because
// `3.10.1 > 3.10` is true but we only have two digits here. So including 3.10 is the
// right move. Overall, using `>` without a patch release is most likely bogus.
Bound::Excluded(version) => version,
// Ex) `<3.10` or ``
Bound::Unbounded => {
return Err(ResolveRequiresPythonError::NoLowerBound(
requires_python.to_string(),
))
}
};
// Take the major and minor version
let mut versions = version.release().iter().take(2);
let Some(major) = versions.next().copied() else {
return Ok(None);
};
let minor = versions.next().copied().unwrap_or_default();
tracing::debug!("Resolved requires-python constraint to: {major}.{minor}");
let major =
u8::try_from(major).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(major))?;
let minor =
u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(minor))?;
Ok(Some(
requires_python
.clone()
.map_value(|_| PythonVersion::from((major, minor))),
))
}
}
#[derive(Debug, Error)]
pub enum ResolveRequiresPythonError {
#[error("The major version `{0}` is larger than the maximum supported value 255")]
TooLargeMajor(u64),
#[error("The minor version `{0}` is larger than the maximum supported value 255")]
TooLargeMinor(u64),
#[error("value `{0}` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.")]
NoLowerBound(String),
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct Tool {

View File

@@ -0,0 +1,53 @@
use std::sync::Arc;
use red_knot_python_semantic::lint::RuleSelection;
/// The resolved [`super::Options`] for the project.
///
/// Unlike [`super::Options`], the struct has default values filled in and
/// uses representations that are optimized for reads (instead of preserving the source representation).
/// It's also not required that this structure precisely resembles the TOML schema, although
/// it's encouraged to use a similar structure.
///
/// It's worth considering to adding a salsa query for specific settings to
/// limit the blast radius when only some settings change. For example,
/// changing the terminal settings shouldn't invalidate any core type-checking queries.
/// This can be achieved by adding a salsa query for the type checking specific settings.
///
/// Settings that are part of [`red_knot_python_semantic::ProgramSettings`] are not included here.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Settings {
rules: Arc<RuleSelection>,
terminal: TerminalSettings,
}
impl Settings {
pub fn new(rules: RuleSelection) -> Self {
Self {
rules: Arc::new(rules),
terminal: TerminalSettings::default(),
}
}
pub fn rules(&self) -> &RuleSelection {
&self.rules
}
pub fn to_rules(&self) -> Arc<RuleSelection> {
self.rules.clone()
}
pub fn terminal(&self) -> &TerminalSettings {
&self.terminal
}
pub fn set_terminal(&mut self, terminal: TerminalSettings) {
self.terminal = terminal;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TerminalSettings {
pub error_on_warning: bool,
}

View File

@@ -118,6 +118,15 @@ impl<T> RangedValue<T> {
self
}
#[must_use]
pub fn map_value<R>(self, f: impl FnOnce(T) -> R) -> RangedValue<R> {
RangedValue {
value: f(self.value),
source: self.source,
range: self.range,
}
}
pub fn into_inner(self) -> T {
self.value
}

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: sub_project
---
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.10"),
)),
),
)

View File

@@ -1,9 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: sub_project
---
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(),
)

View File

@@ -1,13 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("super-app"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -1,9 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: project
---
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)

View File

@@ -1,9 +0,0 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: project
---
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)

View File

@@ -73,6 +73,13 @@ impl ProjectWatcher {
.canonicalize_path(&project_path)
.unwrap_or(project_path);
let config_paths = db
.project()
.metadata(db)
.extra_configuration_paths()
.iter()
.cloned();
// Find the non-overlapping module search paths and filter out paths that are already covered by the project.
// Module search paths are already canonicalized.
let unique_module_paths = ruff_db::system::deduplicate_nested_paths(
@@ -83,8 +90,11 @@ impl ProjectWatcher {
.map(SystemPath::to_path_buf);
// Now add the new paths, first starting with the project path and then
// adding the library search paths.
for path in std::iter::once(project_path).chain(unique_module_paths) {
// adding the library search paths, and finally the paths for configurations.
for path in std::iter::once(project_path)
.chain(unique_module_paths)
.chain(config_paths)
{
// Log a warning. It's not worth aborting if registering a single folder fails because
// Ruff otherwise stills works as expected.
if let Err(error) = self.watcher.watch(&path) {

View File

@@ -12,9 +12,9 @@ license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_index = { workspace = true }
ruff_index = { workspace = true, features = ["salsa"] }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_python_ast = { workspace = true, features = ["salsa"] }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_source_file = { workspace = true }
@@ -31,7 +31,7 @@ drop_bomb = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
ordermap = { workspace = true }
salsa = { workspace = true }
salsa = { workspace = true, features = ["compact_str"] }
thiserror = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
@@ -44,7 +44,7 @@ test-case = { workspace = true }
memchr = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["os", "testing"] }
ruff_db = { workspace = true, features = ["testing", "os"] }
ruff_python_parser = { workspace = true }
red_knot_test = { workspace = true }
red_knot_vendored = { workspace = true }
@@ -57,7 +57,7 @@ quickcheck = { version = "1.0.3", default-features = false }
quickcheck_macros = { version = "1.0.0" }
[features]
serde = ["ruff_db/serde", "dep:serde"]
serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"]
[lints]
workspace = true

View File

@@ -0,0 +1,90 @@
# Special cases for int/float/complex in annotations
In order to support common use cases, an annotation of `float` actually means `int | float`, and an
annotation of `complex` actually means `int | float | complex`. See
[the specification](https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex)
## float
An annotation of `float` means `int | float`, so `int` is assignable to it:
```py
def takes_float(x: float):
pass
def passes_int_to_float(x: int):
# no error!
takes_float(x)
```
It also applies to variable annotations:
```py
def assigns_int_to_float(x: int):
# no error!
y: float = x
```
It doesn't work the other way around:
```py
def takes_int(x: int):
pass
def passes_float_to_int(x: float):
# error: [invalid-argument-type]
takes_int(x)
def assigns_float_to_int(x: float):
# error: [invalid-assignment]
y: int = x
```
Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float`
as just `float`; we display the actual type:
```py
def f(x: float):
reveal_type(x) # revealed: int | float
```
## complex
An annotation of `complex` means `int | float | complex`, so `int` and `float` are both assignable
to it (but not the other way around):
```py
def takes_complex(x: complex):
pass
def passes_to_complex(x: float, y: int):
# no errors!
takes_complex(x)
takes_complex(y)
def assigns_to_complex(x: float, y: int):
# no errors!
a: complex = x
b: complex = y
def takes_int(x: int):
pass
def takes_float(x: float):
pass
def passes_complex(x: complex):
# error: [invalid-argument-type]
takes_int(x)
# error: [invalid-argument-type]
takes_float(x)
def assigns_complex(x: complex):
# error: [invalid-assignment]
y: int = x
# error: [invalid-assignment]
z: float = x
def f(x: complex):
reveal_type(x) # revealed: int | float | complex
```

View File

@@ -73,12 +73,12 @@ qux = (foo, bar)
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
# TODO: Infer "LiteralString"
reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types)
reveal_type(foo.join(qux)) # revealed: @Todo(decorated method)
template: LiteralString = "{}, {}"
reveal_type(template) # revealed: Literal["{}, {}"]
# TODO: Infer `LiteralString`
reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types)
reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method)
```
### Assignability

View File

@@ -9,9 +9,9 @@ from typing import Union
a: Union[int, str]
a1: Union[int, bool]
a2: Union[int, Union[float, str]]
a2: Union[int, Union[bytes, str]]
a3: Union[int, None]
a4: Union[Union[float, str]]
a4: Union[Union[bytes, str]]
a5: Union[int]
a6: Union[()]
@@ -21,11 +21,11 @@ def f():
# 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
# revealed: int | bytes | str
reveal_type(a2)
# revealed: int | None
reveal_type(a3)
# revealed: float | str
# revealed: bytes | str
reveal_type(a4)
# revealed: int
reveal_type(a5)

View File

@@ -9,7 +9,7 @@ reveal_type(x) # revealed: Literal[2]
x = 1.0
x /= 2
reveal_type(x) # revealed: float
reveal_type(x) # revealed: int | float
```
## Dunder methods
@@ -24,12 +24,12 @@ x -= 1
reveal_type(x) # revealed: str
class C:
def __iadd__(self, other: str) -> float:
return 1.0
def __iadd__(self, other: str) -> int:
return 1
x = C()
x += "Hello"
reveal_type(x) # revealed: float
reveal_type(x) # revealed: int
```
## Unsupported types
@@ -40,7 +40,7 @@ class C:
return 42
x = C()
# error: [invalid-argument-type]
# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`"
x -= 1
reveal_type(x) # revealed: int
@@ -130,10 +130,10 @@ def _(flag: bool):
if flag:
f = Foo()
else:
f = 42.0
f = 42
f += 12
reveal_type(f) # revealed: str | float
reveal_type(f) # revealed: str | Literal[54]
```
## Partially bound target union with `__add__`

View File

@@ -30,7 +30,11 @@ reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# TODO: should be `int | None`
# There is no special handling of attributes that are (directly) assigned to a declared parameter,
# which means we union with `Unknown` here, since the attribute itself is not declared. This is
# something that we might want to change in the future.
#
# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
reveal_type(c_instance.declared_only) # revealed: bytes
@@ -45,10 +49,10 @@ reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
c_instance.inferred_from_value = "value set on instance"
# This assignment is also fine:
c_instance.inferred_from_param = None
c_instance.declared_and_bound = False
# TODO: this should be an error (incompatible types in assignment)
c_instance.inferred_from_param = "incompatible"
# error: [invalid-assignment] "Object of type `Literal["incompatible"]` is not assignable to attribute `declared_and_bound` of type `bool`"
c_instance.declared_and_bound = "incompatible"
# TODO: we already show an error here but the message might be improved?
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
@@ -181,7 +185,6 @@ reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a
# TODO: Should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
# TODO: Should be `int | None`
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
reveal_type(c_instance.declared_only) # revealed: bytes
@@ -804,6 +807,67 @@ def _(flag: bool, flag1: bool, flag2: bool):
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
```
### Attribute possibly unbound on a subclass but not on a superclass
```py
def _(flag: bool):
class Foo:
x = 1
class Bar(Foo):
if flag:
x = 2
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
```
### Attribute possibly unbound on a subclass and on a superclass
```py
def _(flag: bool):
class Foo:
if flag:
x = 1
class Bar(Foo):
if flag:
x = 2
# error: [possibly-unbound-attribute]
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
```
### Attribute access on `Any`
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on
`object` -- but if the attribute *does* exist on `object`, the type of the attribute is
`<type as it exists on object> & Any`.
```py
from typing import Any
class Foo(Any): ...
reveal_type(Foo.bar) # revealed: Any
reveal_type(Foo.__repr__) # revealed: Literal[__repr__] & Any
```
Similar principles apply if `Any` appears in the middle of an inheritance hierarchy:
```py
from typing import ClassVar, Literal
class A:
x: ClassVar[Literal[1]] = 1
class B(Any): ...
class C(B, A): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A], Literal[object]]
reveal_type(C.x) # revealed: Literal[1] & Any
```
### Unions with all paths unbound
If the symbol is unbound in all elements of the union, we detect that:
@@ -941,8 +1005,8 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
Some attributes are special-cased, however:
```py
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(f.__call__) # revealed: <bound method `__call__` of `Literal[f]`>
```
### Int-literal attributes
@@ -951,7 +1015,7 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si
integers are instances of that class:
```py
reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
reveal_type((2).denominator) # revealed: @Todo(@property)
```
@@ -965,11 +1029,11 @@ reveal_type((2).real) # revealed: Literal[2]
### Bool-literal attributes
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
bols are instances of that class:
bools are instances of that class:
```py
reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method)
reveal_type(True.__and__) # revealed: @Todo(decorated method)
reveal_type(False.__or__) # revealed: @Todo(decorated method)
```
Some attributes are special-cased, however:
@@ -981,11 +1045,11 @@ reveal_type(False.real) # revealed: Literal[0]
### Bytes-literal attributes
All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`:
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
reveal_type(b"foo".join) # revealed: @Todo(bound method)
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
reveal_type(b"foo".join) # revealed: <bound method `join` of `Literal[b"foo"]`>
reveal_type(b"foo".endswith) # revealed: <bound method `endswith` of `Literal[b"foo"]`>
```
## Instance attribute edge cases
@@ -1072,6 +1136,40 @@ class C:
reveal_type(C().x) # revealed: Unknown
```
### Builtin types attributes
This test can probably be removed eventually, but we currently include it because we do not yet
understand generic bases and protocols, and we want to make sure that we can still use builtin types
in our tests in the meantime. See the corresponding TODO in `Type::static_member` for more
information.
```py
class C:
a_int: int = 1
a_str: str = "a"
a_bytes: bytes = b"a"
a_bool: bool = True
a_float: float = 1.0
a_complex: complex = 1 + 1j
a_tuple: tuple[int] = (1,)
a_range: range = range(1)
a_slice: slice = slice(1)
a_type: type = int
a_none: None = None
reveal_type(C.a_int) # revealed: int
reveal_type(C.a_str) # revealed: str
reveal_type(C.a_bytes) # revealed: bytes
reveal_type(C.a_bool) # revealed: bool
reveal_type(C.a_float) # revealed: int | float
reveal_type(C.a_complex) # revealed: int | float | complex
reveal_type(C.a_tuple) # revealed: tuple[int]
reveal_type(C.a_range) # revealed: range
reveal_type(C.a_slice) # revealed: slice
reveal_type(C.a_type) # revealed: type
reveal_type(C.a_none) # revealed: None
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@@ -56,7 +56,7 @@ def _(a: bool):
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x / a) # revealed: int | float
reveal_type(x % a) # revealed: int
def rhs_is_int(x: int):
@@ -64,7 +64,7 @@ def _(a: bool):
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a / x) # revealed: int | float
reveal_type(a % x) # revealed: int
def lhs_is_bool(x: bool):
@@ -72,7 +72,7 @@ def _(a: bool):
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x / a) # revealed: int | float
reveal_type(x % a) # revealed: int
def rhs_is_bool(x: bool):
@@ -80,7 +80,7 @@ def _(a: bool):
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a / x) # revealed: int | float
reveal_type(a % x) # revealed: int
def both_are_bool(x: bool, y: bool):
@@ -88,6 +88,6 @@ def _(a: bool):
reveal_type(x - y) # revealed: int
reveal_type(x * y) # revealed: int
reveal_type(x // y) # revealed: int
reveal_type(x / y) # revealed: float
reveal_type(x / y) # revealed: int | float
reveal_type(x % y) # revealed: int
```

View File

@@ -244,10 +244,7 @@ class B:
def __rsub__(self, other: A) -> B:
return B()
# TODO: this should be `B` (the return annotation of `B.__rsub__`),
# because `A.__sub__` is annotated as only accepting `A`,
# but `B.__rsub__` will accept `A`.
reveal_type(A() - B()) # revealed: A
reveal_type(A() - B()) # revealed: B
```
## Callable instances as dunders
@@ -263,31 +260,31 @@ class B:
__add__ = A()
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
reveal_type(B() + B()) # revealed: Unknown | int
# TODO: Should not be an error: `A` instance is not a method descriptor, don't prepend `self` arg.
# Revealed type should be `Unknown | int`.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `B` and `B`"
reveal_type(B() + B()) # revealed: Unknown
```
## Integration test: numbers from typeshed
We get less precise results from binary operations on float/complex literals due to the special case
for annotations of `float` or `complex`, which applies also to return annotations for typeshed
dunder methods. Perhaps we could have a special-case on the special-case, to exclude these typeshed
return annotations from the widening, and preserve a bit more precision here?
```py
reveal_type(3j + 3.14) # revealed: complex
reveal_type(4.2 + 42) # revealed: float
reveal_type(3j + 3) # revealed: complex
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3.14 + 3j) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(42 + 4.2) # revealed: int
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3 + 3j) # revealed: int
reveal_type(3j + 3.14) # revealed: int | float | complex
reveal_type(4.2 + 42) # revealed: int | float
reveal_type(3j + 3) # revealed: int | float | complex
reveal_type(3.14 + 3j) # revealed: int | float | complex
reveal_type(42 + 4.2) # revealed: int | float
reveal_type(3 + 3j) # revealed: int | float | complex
def _(x: bool, y: int):
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
reveal_type(4.2 + x) # revealed: int | float
reveal_type(y + 4.12) # revealed: int | float
```
## With literal types
@@ -304,8 +301,7 @@ class A:
return self
reveal_type(A() + 1) # revealed: A
# TODO should be `A` since `int.__add__` doesn't support `A` instances
reveal_type(1 + A()) # revealed: int
reveal_type(1 + A()) # revealed: A
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances

View File

@@ -10,16 +10,16 @@ reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
# formal parameter type.
reveal_type(2 + "f") # revealed: int
# TODO: Should emit `unsupported-operator` but we don't understand the bases of `str`, so we think
# it inherits `Unknown`, so we think `str.__radd__` is `Unknown` instead of nonexistent.
reveal_type(2 + "f") # revealed: Unknown
def lhs(x: int):
reveal_type(x + 1) # revealed: int
reveal_type(x - 4) # revealed: int
reveal_type(x * -1) # revealed: int
reveal_type(x // 3) # revealed: int
reveal_type(x / 3) # revealed: float
reveal_type(x / 3) # revealed: int | float
reveal_type(x % 3) # revealed: int
def rhs(x: int):
@@ -27,7 +27,7 @@ def rhs(x: int):
reveal_type(3 - x) # revealed: int
reveal_type(3 * x) # revealed: int
reveal_type(-3 // x) # revealed: int
reveal_type(-3 / x) # revealed: float
reveal_type(-3 / x) # revealed: int | float
reveal_type(5 % x) # revealed: int
def both(x: int):
@@ -35,7 +35,7 @@ def both(x: int):
reveal_type(x - x) # revealed: int
reveal_type(x * x) # revealed: int
reveal_type(x // x) # revealed: int
reveal_type(x / x) # revealed: float
reveal_type(x / x) # revealed: int | float
reveal_type(x % x) # revealed: int
```
@@ -80,24 +80,20 @@ c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
reveal_type(c) # revealed: int
# error: "Cannot divide object of type `int` by zero"
# revealed: float
reveal_type(int() / 0)
reveal_type(int() / 0) # revealed: int | float
# error: "Cannot divide object of type `Literal[1]` by zero"
# revealed: float
reveal_type(1 / False)
reveal_type(1 / False) # revealed: float
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
True / False
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
bool(1) / False
# error: "Cannot divide object of type `float` by zero"
# revealed: float
reveal_type(1.0 / 0)
reveal_type(1.0 / 0) # revealed: int | float
class MyInt(int): ...
# No error for a subclass of int
# revealed: float
reveal_type(MyInt(3) / 0)
reveal_type(MyInt(3) / 0) # revealed: int | float
```

View File

@@ -4,14 +4,14 @@
```py
class Multiplier:
def __init__(self, factor: float):
def __init__(self, factor: int):
self.factor = factor
def __call__(self, number: float) -> float:
def __call__(self, number: int) -> int:
return number * self.factor
a = Multiplier(2.0)(3.0)
reveal_type(a) # revealed: float
a = Multiplier(2)(3)
reveal_type(a) # revealed: int
class Unit: ...
@@ -52,7 +52,7 @@ class NonCallable:
__call__ = 1
a = NonCallable()
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(a()) # revealed: Unknown
```
@@ -67,8 +67,8 @@ def _(flag: bool):
def __call__(self) -> int: ...
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(a()) # revealed: int | Unknown
```
## Call binding errors
@@ -99,3 +99,26 @@ c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
reveal_type(c()) # revealed: int
```
## Union over callables
### Possibly unbound `__call__`
```py
def outer(cond1: bool):
class Test:
if cond1:
def __call__(self): ...
class Other:
def __call__(self): ...
def inner(cond2: bool):
if cond2:
a = Test()
else:
a = Other()
# error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)"
a()
```

View File

@@ -278,10 +278,10 @@ proper diagnostics in case of missing or superfluous arguments.
from typing_extensions import reveal_type
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type() # revealed: Unknown
reveal_type()
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2) # revealed: Literal[1]
reveal_type(1, 2)
```
### `static_assert`
@@ -290,7 +290,6 @@ reveal_type(1, 2) # revealed: Literal[1]
from knot_extensions import static_assert
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
# error: [static-assert-error]
static_assert()
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"

View File

@@ -0,0 +1,133 @@
# `inspect.getattr_static`
## Basic usage
`inspect.getattr_static` is a function that returns attributes of an object without invoking the
descriptor protocol (for caveats, see the [official documentation]).
Consider the following example:
```py
import inspect
class Descriptor:
def __get__(self, instance, owner) -> str:
return 1
class C:
normal: int = 1
descriptor: Descriptor = Descriptor()
```
If we access attributes on an instance of `C` as usual, the descriptor protocol is invoked, and we
get a type of `str` for the `descriptor` attribute:
```py
c = C()
reveal_type(c.normal) # revealed: int
reveal_type(c.descriptor) # revealed: str
```
However, if we use `inspect.getattr_static`, we can see the underlying `Descriptor` type:
```py
reveal_type(inspect.getattr_static(c, "normal")) # revealed: int
reveal_type(inspect.getattr_static(c, "descriptor")) # revealed: Descriptor
```
For non-existent attributes, a default value can be provided:
```py
reveal_type(inspect.getattr_static(C, "normal", "default-arg")) # revealed: int
reveal_type(inspect.getattr_static(C, "non_existent", "default-arg")) # revealed: Literal["default-arg"]
```
When a non-existent attribute is accessed without a default value, the runtime raises an
`AttributeError`. We could emit a diagnostic for this case, but that is currently not supported:
```py
# TODO: we could emit a diagnostic here
reveal_type(inspect.getattr_static(C, "non_existent")) # revealed: Never
```
We can access attributes on objects of all kinds:
```py
import sys
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[1]
```
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
```py
class D:
def __init__(self) -> None:
self.instance_attr: int = 1
reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int
```
## Error cases
We can only infer precise types if the attribute is a literal string. In all other cases, we fall
back to `Any`:
```py
import inspect
class C:
x: int = 1
def _(attr_name: str):
reveal_type(inspect.getattr_static(C(), attr_name)) # revealed: Any
reveal_type(inspect.getattr_static(C(), attr_name, 1)) # revealed: Any
```
But we still detect errors in the number or type of arguments:
```py
# error: [missing-argument] "No arguments provided for required parameters `obj`, `attr` of function `getattr_static`"
inspect.getattr_static()
# error: [missing-argument] "No argument provided for required parameter `attr`"
inspect.getattr_static(C())
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`attr`) of function `getattr_static`; expected type `str`"
inspect.getattr_static(C(), 1)
# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4"
inspect.getattr_static(C(), "x", "default-arg", "one too many")
```
## Possibly unbound attributes
```py
import inspect
def _(flag: bool):
class C:
if flag:
x: int = 1
reveal_type(inspect.getattr_static(C, "x", "default")) # revealed: int | Literal["default"]
```
## Gradual types
```py
import inspect
from typing import Any
def _(a: Any, tuple_of_any: tuple[Any]):
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
# TODO: Ideally, this would just be `Literal[index]`
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) # revealed: Literal[index] | Literal["default"]
```
[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static

View File

@@ -0,0 +1,258 @@
# Methods
## Background: Functions as descriptors
> Note: See also this related section in the descriptor guide: [Functions and methods].
Say we have a simple class `C` with a function definition `f` inside its body:
```py
class C:
def f(self, x: int) -> str:
return "a"
```
Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance
(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors
because they implement a `__get__` method. This is crucial in making sure that method calls work as
expected. In general, the signature of the `__get__` method in the descriptor protocol is
`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The
passed value for the `instance` argument depends on whether the attribute is accessed from the class
object (in which case it is `None`), or from an instance (in which case it is the instance of type
`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize:
- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)`
- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)`
Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the
function attribute. The way the special `__get__` method *on functions* works is as follows. In the
former case, if the `instance` argument is `None`, `__get__` simply returns the function itself. In
the latter case, it returns a *bound method* object:
```py
from inspect import getattr_static
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `C`>
```
In conclusion, this is why we see the following two types when accessing the `f` attribute on the
class object `C` and on an instance `C()`:
```py
reveal_type(C.f) # revealed: Literal[f]
reveal_type(C().f) # revealed: <bound method `f` of `C`>
```
A bound method is a callable object that contains a reference to the `instance` that it was called
on (can be inspected via `__self__`), and the function object that it refers to (can be inspected
via `__func__`):
```py
bound_method = C().f
reveal_type(bound_method.__self__) # revealed: C
reveal_type(bound_method.__func__) # revealed: Literal[f]
```
When we call the bound method, the `instance` is implicitly passed as the first argument (`self`):
```py
reveal_type(C().f(1)) # revealed: str
reveal_type(bound_method(1)) # revealed: str
```
When we call the function object itself, we need to pass the `instance` explicitly:
```py
C.f(1) # error: [missing-argument]
reveal_type(C.f(C(), 1)) # revealed: str
```
When we access methods from derived classes, they will be bound to instances of the derived class:
```py
class D(C):
pass
reveal_type(D().f) # revealed: <bound method `f` of `D`>
```
If we access an attribute on a bound method object itself, it will defer to `types.MethodType`:
```py
reveal_type(bound_method.__hash__) # revealed: <bound method `__hash__` of `MethodType`>
```
If an attribute is not available on the bound method object, it will be looked up on the underlying
function object. We model this explicitly, which means that we can access `__kwdefaults__` on bound
methods, even though it is not available on `types.MethodType`:
```py
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
```
## Basic method calls on class objects and instances
```py
class Base:
def method_on_base(self, x: int | None) -> str:
return "a"
class Derived(Base):
def method_on_derived(self, x: bytes) -> tuple[int, str]:
return (1, "a")
reveal_type(Base().method_on_base(1)) # revealed: str
reveal_type(Base.method_on_base(Base(), 1)) # revealed: str
Base().method_on_base("incorrect") # error: [invalid-argument-type]
Base().method_on_base() # error: [missing-argument]
Base().method_on_base(1, 2) # error: [too-many-positional-arguments]
reveal_type(Derived().method_on_base(1)) # revealed: str
reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str]
reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str
reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str]
```
## Method calls on literals
### Boolean literals
```py
reveal_type(True.bit_length()) # revealed: int
reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]]
```
### Integer literals
```py
reveal_type((42).bit_length()) # revealed: int
```
### String literals
```py
reveal_type("abcde".find("abc")) # revealed: int
reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes
"abcde".find(123) # error: [invalid-argument-type]
```
### Bytes literals
```py
reveal_type(b"abcde".startswith(b"abc")) # revealed: bool
```
## Method calls on `LiteralString`
```py
from typing_extensions import LiteralString
def f(s: LiteralString) -> None:
reveal_type(s.find("a")) # revealed: int
```
## Method calls on `tuple`
```py
def f(t: tuple[int, str]) -> None:
reveal_type(t.index("a")) # revealed: int
```
## Method calls on unions
```py
from typing import Any
class A:
def f(self) -> int:
return 1
class B:
def f(self) -> str:
return "a"
def f(a_or_b: A | B, any_or_a: Any | A):
reveal_type(a_or_b.f) # revealed: <bound method `f` of `A`> | <bound method `f` of `B`>
reveal_type(a_or_b.f()) # revealed: int | str
reveal_type(any_or_a.f) # revealed: Any | <bound method `f` of `A`>
reveal_type(any_or_a.f()) # revealed: Any | int
```
## Method calls on `KnownInstance` types
```toml
[environment]
python-version = "3.12"
```
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: <bound method `__or__` of `typing.TypeAliasType`>
```
## Error cases: Calling `__get__` for methods
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
```py
from types import FunctionType, MethodType
from typing import overload
@overload
def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
@overload
def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
```
Here, we test that this signature is enforced correctly:
```py
from inspect import getattr_static
class C:
def f(self, x: int) -> str:
return "a"
method_wrapper = getattr_static(C, "f").__get__
reveal_type(method_wrapper) # revealed: <method-wrapper `__get__` of `f`>
# All of these are fine:
method_wrapper(C(), C)
method_wrapper(C())
method_wrapper(C(), None)
method_wrapper(None, C)
# Passing `None` without an `owner` argument is an
# error: [missing-argument] "No argument provided for required parameter `owner`"
method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`); expected type `type`"
method_wrapper(None, 1)
# Passing `None` as the `owner` argument when `instance` is `None` is an
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`); expected type `type`"
method_wrapper(None, None)
# Calling `__get__` without any arguments is an
# error: [missing-argument] "No argument provided for required parameter `instance`"
method_wrapper()
# Calling `__get__` with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments: expected 2, got 3"
method_wrapper(C(), C, "one too many")
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

View File

@@ -39,8 +39,8 @@ def _(flag: bool):
else:
def f() -> int:
return 1
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
reveal_type(x) # revealed: Unknown | int
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(x) # revealed: int | Unknown
```
## Multiple non-callable elements in a union
@@ -56,8 +56,8 @@ def _(flag: bool, flag2: bool):
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
# revealed: int | Unknown
reveal_type(f())
```
@@ -72,6 +72,39 @@ def _(flag: bool):
else:
f = "foo"
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```
## Mismatching signatures
Calling a union where the arguments don't match the signature of all variants.
```py
def f1(a: int) -> int: ...
def f2(a: str) -> str: ...
def _(flag: bool):
if flag:
f = f1
else:
f = f2
# error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`"
x = f(3)
reveal_type(x) # revealed: int | str
```
## Any non-callable variant
```py
def f1(a: int): ...
def _(flag: bool):
if flag:
f = f1
else:
f = "This is a string literal"
# error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable"
x = f(3)
reveal_type(x) # revealed: Unknown
```

View File

@@ -21,8 +21,9 @@ class A:
reveal_type("hello" in A()) # revealed: bool
reveal_type("hello" not in A()) # revealed: bool
# TODO: should emit diagnostic, need to check arg type, will fail
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
reveal_type(42 in A()) # revealed: bool
# error: [unsupported-operator] "Operator `not in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
reveal_type(42 not in A()) # revealed: bool
```
@@ -126,9 +127,9 @@ class A:
reveal_type(CheckContains() in A()) # revealed: bool
# TODO: should emit diagnostic, need to check arg type,
# should not fall back to __iter__ or __getitem__
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckIter` and `A`"
reveal_type(CheckIter() in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckGetItem` and `A`"
reveal_type(CheckGetItem() in A()) # revealed: bool
class B:
@@ -154,7 +155,8 @@ class A:
def __getitem__(self, key: str) -> str:
return "foo"
# TODO should emit a diagnostic
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
reveal_type(42 in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`"
reveal_type("hello" in A()) # revealed: bool
```

View File

@@ -16,31 +16,38 @@ most common case involves implementing these methods for the same type:
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> int:
return 42
def __eq__(self, other: A) -> EqReturnType:
return EqReturnType()
def __ne__(self, other: A) -> float:
return 42.0
def __ne__(self, other: A) -> NeReturnType:
return NeReturnType()
def __lt__(self, other: A) -> str:
return "42"
def __lt__(self, other: A) -> LtReturnType:
return LtReturnType()
def __le__(self, other: A) -> bytes:
return b"42"
def __le__(self, other: A) -> LeReturnType:
return LeReturnType()
def __gt__(self, other: A) -> list:
return [42]
def __gt__(self, other: A) -> GtReturnType:
return GtReturnType()
def __ge__(self, other: A) -> set:
return {42}
def __ge__(self, other: A) -> GeReturnType:
return GeReturnType()
reveal_type(A() == A()) # revealed: int
reveal_type(A() != A()) # revealed: float
reveal_type(A() < A()) # revealed: str
reveal_type(A() <= A()) # revealed: bytes
reveal_type(A() > A()) # revealed: list
reveal_type(A() >= A()) # revealed: set
reveal_type(A() == A()) # revealed: EqReturnType
reveal_type(A() != A()) # revealed: NeReturnType
reveal_type(A() < A()) # revealed: LtReturnType
reveal_type(A() <= A()) # revealed: LeReturnType
reveal_type(A() > A()) # revealed: GtReturnType
reveal_type(A() >= A()) # revealed: GeReturnType
```
## Rich Comparison Dunder Implementations for Other Class
@@ -51,33 +58,40 @@ type:
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> int:
return 42
def __eq__(self, other: B) -> EqReturnType:
return EqReturnType()
def __ne__(self, other: B) -> float:
return 42.0
def __ne__(self, other: B) -> NeReturnType:
return NeReturnType()
def __lt__(self, other: B) -> str:
return "42"
def __lt__(self, other: B) -> LtReturnType:
return LtReturnType()
def __le__(self, other: B) -> bytes:
return b"42"
def __le__(self, other: B) -> LeReturnType:
return LeReturnType()
def __gt__(self, other: B) -> list:
return [42]
def __gt__(self, other: B) -> GtReturnType:
return GtReturnType()
def __ge__(self, other: B) -> set:
return {42}
def __ge__(self, other: B) -> GeReturnType:
return GeReturnType()
class B: ...
reveal_type(A() == B()) # revealed: int
reveal_type(A() != B()) # revealed: float
reveal_type(A() < B()) # revealed: str
reveal_type(A() <= B()) # revealed: bytes
reveal_type(A() > B()) # revealed: list
reveal_type(A() >= B()) # revealed: set
reveal_type(A() == B()) # revealed: EqReturnType
reveal_type(A() != B()) # revealed: NeReturnType
reveal_type(A() < B()) # revealed: LtReturnType
reveal_type(A() <= B()) # revealed: LeReturnType
reveal_type(A() > B()) # revealed: GtReturnType
reveal_type(A() >= B()) # revealed: GeReturnType
```
## Reflected Comparisons
@@ -89,58 +103,64 @@ these methods will be ignored here because they require a mismatched operand typ
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: B) -> int:
return 42
def __eq__(self, other: B) -> EqReturnType:
return EqReturnType()
def __ne__(self, other: B) -> float:
return 42.0
def __ne__(self, other: B) -> NeReturnType:
return NeReturnType()
def __lt__(self, other: B) -> str:
return "42"
def __lt__(self, other: B) -> LtReturnType:
return LtReturnType()
def __le__(self, other: B) -> bytes:
return b"42"
def __le__(self, other: B) -> LeReturnType:
return LeReturnType()
def __gt__(self, other: B) -> list:
return [42]
def __gt__(self, other: B) -> GtReturnType:
return GtReturnType()
def __ge__(self, other: B) -> set:
return {42}
def __ge__(self, other: B) -> GeReturnType:
return GeReturnType()
class Unrelated: ...
class B:
# To override builtins.object.__eq__ and builtins.object.__ne__
# TODO these should emit an invalid override diagnostic
def __eq__(self, other: str) -> B:
def __eq__(self, other: Unrelated) -> B:
return B()
def __ne__(self, other: str) -> B:
def __ne__(self, other: Unrelated) -> B:
return B()
# TODO: should be `int` and `float`.
# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`.
#
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
# this can only happen with an invalid override of these methods,
# but we still support it.
reveal_type(B() == A()) # revealed: B
reveal_type(B() != A()) # revealed: B
reveal_type(B() == A()) # revealed: EqReturnType
reveal_type(B() != A()) # revealed: NeReturnType
reveal_type(B() < A()) # revealed: list
reveal_type(B() <= A()) # revealed: set
reveal_type(B() < A()) # revealed: GtReturnType
reveal_type(B() <= A()) # revealed: GeReturnType
reveal_type(B() > A()) # revealed: str
reveal_type(B() >= A()) # revealed: bytes
reveal_type(B() > A()) # revealed: LtReturnType
reveal_type(B() >= A()) # revealed: LeReturnType
class C:
def __gt__(self, other: C) -> int:
def __gt__(self, other: C) -> EqReturnType:
return 42
def __ge__(self, other: C) -> float:
return 42.0
def __ge__(self, other: C) -> NeReturnType:
return NeReturnType()
reveal_type(C() < C()) # revealed: int
reveal_type(C() <= C()) # revealed: float
reveal_type(C() < C()) # revealed: EqReturnType
reveal_type(C() <= C()) # revealed: NeReturnType
```
## Reflected Comparisons with Subclasses
@@ -152,6 +172,13 @@ than `A`.
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, other: A) -> A:
return A()
@@ -172,32 +199,32 @@ class A:
return A()
class B(A):
def __eq__(self, other: A) -> int:
return 42
def __eq__(self, other: A) -> EqReturnType:
return EqReturnType()
def __ne__(self, other: A) -> float:
return 42.0
def __ne__(self, other: A) -> NeReturnType:
return NeReturnType()
def __lt__(self, other: A) -> str:
return "42"
def __lt__(self, other: A) -> LtReturnType:
return LtReturnType()
def __le__(self, other: A) -> bytes:
return b"42"
def __le__(self, other: A) -> LeReturnType:
return LeReturnType()
def __gt__(self, other: A) -> list:
return [42]
def __gt__(self, other: A) -> GtReturnType:
return GtReturnType()
def __ge__(self, other: A) -> set:
return {42}
def __ge__(self, other: A) -> GeReturnType:
return GeReturnType()
reveal_type(A() == B()) # revealed: int
reveal_type(A() != B()) # revealed: float
reveal_type(A() == B()) # revealed: EqReturnType
reveal_type(A() != B()) # revealed: NeReturnType
reveal_type(A() < B()) # revealed: list
reveal_type(A() <= B()) # revealed: set
reveal_type(A() < B()) # revealed: GtReturnType
reveal_type(A() <= B()) # revealed: GeReturnType
reveal_type(A() > B()) # revealed: str
reveal_type(A() >= B()) # revealed: bytes
reveal_type(A() > B()) # revealed: LtReturnType
reveal_type(A() >= B()) # revealed: LeReturnType
```
## Reflected Comparisons with Subclass But Falls Back to LHS
@@ -222,9 +249,8 @@ class B(A):
def __gt__(self, other: int) -> B:
return B()
# TODO: should be `A`, need to check argument type and fall back to LHS method
reveal_type(A() < B()) # revealed: B
reveal_type(A() > B()) # revealed: B
reveal_type(A() < B()) # revealed: A
reveal_type(A() > B()) # revealed: A
```
## Operations involving instances of classes inheriting from `Any`
@@ -272,9 +298,8 @@ class A:
def __ne__(self, other: int) -> A:
return A()
# TODO: it should be `bool`, need to check arg type and fall back to `is` and `is not`
reveal_type(A() == A()) # revealed: A
reveal_type(A() != A()) # revealed: A
reveal_type(A() == A()) # revealed: bool
reveal_type(A() != A()) # revealed: bool
```
## Object Comparisons with Typeshed
@@ -305,12 +330,14 @@ reveal_type(1 >= 1.0) # revealed: bool
reveal_type(1 == 2j) # revealed: bool
reveal_type(1 != 2j) # revealed: bool
# TODO: should be Unknown and emit diagnostic,
# need to check arg type and should be failed
reveal_type(1 < 2j) # revealed: bool
reveal_type(1 <= 2j) # revealed: bool
reveal_type(1 > 2j) # revealed: bool
reveal_type(1 >= 2j) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 < 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 <= 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 > 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 >= 2j) # revealed: Unknown
def f(x: bool, y: int):
reveal_type(x < y) # revealed: bool

View File

@@ -12,8 +12,8 @@ reveal_type(1 is 1) # revealed: bool
reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
# TODO: should be Unknown, and emit diagnostic, once we check call argument types
reveal_type(1 <= "" and 0 < 1) # revealed: bool
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
```
## Integer instance

View File

@@ -8,7 +8,9 @@ types, we can infer that the result for the intersection type is also true/false
```py
from typing import Literal
class Base: ...
class Base:
def __gt__(self, other) -> bool:
return False
class Child1(Base):
def __eq__(self, other) -> Literal[True]:

View File

@@ -23,6 +23,7 @@ from __future__ import annotations
class A:
def __lt__(self, other) -> A: ...
def __gt__(self, other) -> bool: ...
class B:
def __lt__(self, other) -> B: ...

View File

@@ -92,11 +92,14 @@ reveal_type(a == b) # revealed: bool
# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
reveal_type(a != b) # revealed: bool
# TODO: should be Unknown and add more informative diagnostics
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a < b) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a <= b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a > b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a >= b) # revealed: Unknown
```
However, if the lexicographic comparison completes without reaching a point where str and int are
@@ -144,33 +147,40 @@ of the dunder methods.)
```py
from __future__ import annotations
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, o: object) -> str:
return "hello"
def __eq__(self, o: object) -> EqReturnType:
return EqReturnType()
def __ne__(self, o: object) -> bytes:
return b"world"
def __ne__(self, o: object) -> NeReturnType:
return NeReturnType()
def __lt__(self, o: A) -> float:
return 3.14
def __lt__(self, o: A) -> LtReturnType:
return LtReturnType()
def __le__(self, o: A) -> complex:
return complex(0.5, -0.5)
def __le__(self, o: A) -> LeReturnType:
return LeReturnType()
def __gt__(self, o: A) -> tuple:
return (1, 2, 3)
def __gt__(self, o: A) -> GtReturnType:
return GtReturnType()
def __ge__(self, o: A) -> list:
return [1, 2, 3]
def __ge__(self, o: A) -> GeReturnType:
return GeReturnType()
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]
reveal_type(a < a) # revealed: LtReturnType | Literal[False]
reveal_type(a <= a) # revealed: LeReturnType | Literal[True]
reveal_type(a > a) # revealed: GtReturnType | Literal[False]
reveal_type(a >= a) # revealed: GeReturnType | Literal[True]
# If lexicographic comparison is finished before comparing A()
b = ("1_foo", A())
@@ -183,11 +193,13 @@ reveal_type(b <= c) # revealed: Literal[True]
reveal_type(b > c) # revealed: Literal[False]
reveal_type(b >= c) # revealed: Literal[False]
class LtReturnTypeOnB: ...
class B:
def __lt__(self, o: B) -> set:
def __lt__(self, o: B) -> LtReturnTypeOnB:
return set()
reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False]
```
#### Special Handling of Eq and NotEq in Lexicographic Comparisons

View File

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

View File

@@ -43,8 +43,7 @@ class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
# TODO: This could be a `tuple[int, int]` if we model that `y` can not be modified in the outer comprehension scope
# revealed: tuple[int, Unknown | int]
# revealed: tuple[int, int]
[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()]
```
@@ -67,8 +66,7 @@ class IterableOfIterables:
def __iter__(self) -> IteratorOfIterables:
return IteratorOfIterables()
# TODO: This could be a `tuple[int, int]` (see above)
# revealed: tuple[int, Unknown | IntIterable]
# revealed: tuple[int, IntIterable]
[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()]
```

View File

@@ -22,22 +22,26 @@ class Ten:
pass
class C:
ten = Ten()
ten: Ten = Ten()
c = C()
# TODO: this should be `Literal[10]`
reveal_type(c.ten) # revealed: Unknown | Ten
reveal_type(c.ten) # revealed: Literal[10]
# TODO: This should `Literal[10]`
reveal_type(C.ten) # revealed: Unknown | Ten
reveal_type(C.ten) # revealed: Literal[10]
# These are fine:
c.ten = 10
# TODO: This should not be an error
c.ten = 10 # error: [invalid-assignment]
C.ten = 10
# TODO: Both of these should be errors
# TODO: This should be an error (as the wrong type is being implicitly passed to `Ten.__set__`),
# but the error message is misleading.
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`"
c.ten = 11
# TODO: same as above
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`"
C.ten = 11
```
@@ -57,24 +61,86 @@ class FlexibleInt:
self._value = int(value)
class C:
flexible_int = FlexibleInt()
flexible_int: FlexibleInt = FlexibleInt()
c = C()
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
reveal_type(c.flexible_int) # revealed: int | None
# TODO: These should not be errors
# error: [invalid-assignment]
c.flexible_int = 42 # okay
# error: [invalid-assignment]
c.flexible_int = "42" # also okay!
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
reveal_type(c.flexible_int) # revealed: int | None
# TODO: should be an error
# TODO: This should be an error, but the message needs to be improved.
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `flexible_int` of type `FlexibleInt`"
c.flexible_int = None # not okay
# TODO: should be `int | None`
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
reveal_type(c.flexible_int) # revealed: int | None
```
## Data and non-data descriptors
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example\
of a data descriptor is a `property` with a setter and/or a deleter.\
Descriptors that only define `__get__`, meanwhile, are called *non-data descriptors*. Examples
include\
functions, `classmethod` or `staticmethod`).
The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3)
non-data descriptors.
```py
from typing import Literal
class DataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
return "data"
def __set__(self, instance: int, value) -> None:
pass
class NonDataDescriptor:
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
return "non-data"
class C:
data_descriptor = DataDescriptor()
non_data_descriptor = NonDataDescriptor()
def f(self):
# This explains why data descriptors come first in the precedence chain. If
# instance attributes would take priority, we would override the descriptor
# here. Instead, this calls `DataDescriptor.__set__`, i.e. it does not affect
# the type of the `data_descriptor` attribute.
self.data_descriptor = 1
# However, for non-data descriptors, instance attributes do take precedence.
# So it is possible to override them.
self.non_data_descriptor = 1
c = C()
# TODO: This should ideally be `Unknown | Literal["data"]`.
#
# - Pyright also wrongly shows `int | Literal['data']` here
# - Mypy shows Literal["data"] here, but also shows Literal["non-data"] below.
#
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data", 1]
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1]
reveal_type(C.data_descriptor) # revealed: Unknown | Literal["data"]
reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
# It is possible to override data descriptors via class objects. The following
# assignment does not call `DataDescriptor.__set__`. For this reason, we infer
# `Unknown | …` for all (descriptor) attributes.
C.data_descriptor = "something else" # This is okay
```
## Built-in `property` descriptor
@@ -101,7 +167,7 @@ c = C()
reveal_type(c._name) # revealed: str | None
# Should be `str`
reveal_type(c.name) # revealed: @Todo(bound method)
reveal_type(c.name) # revealed: @Todo(decorated method)
# Should be `builtins.property`
reveal_type(C.name) # revealed: Literal[name]
@@ -142,7 +208,7 @@ reveal_type(c1) # revealed: @Todo(return type)
reveal_type(C.get_name()) # revealed: @Todo(return type)
# TODO: should be `str`
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
reveal_type(C("42").get_name()) # revealed: @Todo(decorated method)
```
## Descriptors only work when used as class variables
@@ -160,9 +226,10 @@ class Ten:
class C:
def __init__(self):
self.ten = Ten()
self.ten: Ten = Ten()
reveal_type(C().ten) # revealed: Unknown | Ten
# TODO: Should be Ten
reveal_type(C().ten) # revealed: Literal[10]
```
## Descriptors distinguishing between class and instance access
@@ -186,13 +253,166 @@ class Descriptor:
return "called on class object"
class C:
d = Descriptor()
d: Descriptor = Descriptor()
# TODO: should be `Literal["called on class object"]
reveal_type(C.d) # revealed: Unknown | Descriptor
reveal_type(C.d) # revealed: LiteralString
# TODO: should be `Literal["called on instance"]
reveal_type(C().d) # revealed: Unknown | Descriptor
reveal_type(C().d) # revealed: LiteralString
```
## Undeclared descriptor arguments
If a descriptor attribute is not declared, we union with `Unknown`, just like for regular
attributes, since that attribute could be overwritten externally. Even a data descriptor with a
`__set__` method can be overwritten when accessed through a class object.
```py
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
def __set__(self, instance: object, value: int) -> None:
pass
class C:
descriptor = Descriptor()
C.descriptor = "something else"
# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments
reveal_type(C.descriptor) # revealed: Unknown | int
```
## Descriptors with incorrect `__get__` signature
```py
class Descriptor:
# `__get__` method with missing parameters:
def __get__(self) -> int:
return 1
class C:
descriptor: Descriptor = Descriptor()
# TODO: This should be an error
reveal_type(C.descriptor) # revealed: Descriptor
```
## Possibly-unbound `__get__` method
```py
def _(flag: bool):
class MaybeDescriptor:
if flag:
def __get__(self, instance: object, owner: type | None = None) -> int:
return 1
class C:
descriptor: MaybeDescriptor = MaybeDescriptor()
# TODO: This should be `MaybeDescriptor | int`
reveal_type(C.descriptor) # revealed: int
```
## Dunder methods
Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol:
```py
class SomeCallable:
def __call__(self, x: int) -> str:
return "a"
class Descriptor:
def __get__(self, instance: object, owner: type | None = None) -> SomeCallable:
return SomeCallable()
class B:
__call__: Descriptor = Descriptor()
b_instance = B()
reveal_type(b_instance(1)) # revealed: str
b_instance("bla") # error: [invalid-argument-type]
```
## Functions as descriptors
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure
that method calls work as expected. See [this test suite](./call/methods.md) for more information.
Here, we only demonstrate how `__get__` works on functions:
```py
from inspect import getattr_static
def f(x: object) -> str:
return "a"
reveal_type(f) # revealed: Literal[f]
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(f.__get__(None, type(f))) # revealed: Literal[f]
reveal_type(f.__get__(None, type(f))(1)) # revealed: str
wrapper_descriptor = getattr_static(f, "__get__")
reveal_type(wrapper_descriptor) # revealed: <wrapper-descriptor `__get__` of `function` objects>
reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f]
# Attribute access on the method-wrapper `f.__get__` falls back to `MethodWrapperType`:
reveal_type(f.__get__.__hash__) # revealed: <bound method `__hash__` of `MethodWrapperType`>
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
reveal_type(wrapper_descriptor.__qualname__) # revealed: @Todo(@property)
```
We can also bind the free function `f` to an instance of a class `C`:
```py
class C: ...
bound_method = wrapper_descriptor(f, C(), C)
reveal_type(bound_method) # revealed: <bound method `f` of `C`>
```
We can then call it, and the instance of `C` is implicitly passed to the first parameter of `f`
(`x`):
```py
reveal_type(bound_method()) # revealed: str
```
Finally, we test some error cases for the call to the wrapper descriptor:
```py
# Calling the wrapper descriptor without any arguments is an
# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`"
wrapper_descriptor()
# Calling it without the `instance` argument is an also an
# error: [missing-argument] "No argument provided for required parameter `instance`"
wrapper_descriptor(f)
# Calling it without the `owner` argument if `instance` is not `None` is an
# error: [missing-argument] "No argument provided for required parameter `owner`"
wrapper_descriptor(f, None)
# But calling it with an instance is fine (in this case, the `owner` argument is optional):
wrapper_descriptor(f, C())
# Calling it with something that is not a `FunctionType` as the first argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`); expected type `FunctionType`"
wrapper_descriptor(1, None, type(f))
# Calling it with something that is not a `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`); expected type `type`"
wrapper_descriptor(f, None, f)
# Calling it with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments: expected 3, got 4"
wrapper_descriptor(f, None, type(f), "one too many")
```
[descriptors]: https://docs.python.org/3/howto/descriptor.html

View File

@@ -0,0 +1,184 @@
# Invalid argument type diagnostics
<!-- snapshot-diagnostics -->
## Basic
This is a basic test demonstrating that a diagnostic points to the function definition corresponding
to the invalid argument.
```py
def foo(x: int) -> int:
return x * x
foo("hello") # error: [invalid-argument-type]
```
## Different source order
This is like the basic test, except we put the call site above the function definition.
```py
def bar():
foo("hello") # error: [invalid-argument-type]
def foo(x: int) -> int:
return x * x
```
## Different files
This tests that a diagnostic can point to a function definition in a different file in which an
invalid call site was found.
`package.py`:
```py
def foo(x: int) -> int:
return x * x
```
```py
import package
package.foo("hello") # error: [invalid-argument-type]
```
## Many parameters
This checks that a diagnostic renders reasonably when there are multiple parameters.
```py
def foo(x: int, y: int, z: int) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
## Many parameters across multiple lines
This checks that a diagnostic renders reasonably when there are multiple parameters spread out
across multiple lines.
```py
def foo(
x: int,
y: int,
z: int,
) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
## Many parameters with multiple invalid arguments
This checks that a diagnostic renders reasonably when there are multiple parameters and multiple
invalid argument types.
```py
def foo(x: int, y: int, z: int) -> int:
return x * y * z
# error: [invalid-argument-type]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
foo("a", "b", "c")
```
At present (2025-02-18), this renders three different diagnostic messages. But arguably, these could
all be folded into one diagnostic. Fixing this requires at least better support for multi-spans in
the diagnostic model and possibly also how diagnostics are emitted by the type checker itself.
## Test calling a function whose type is vendored from `typeshed`
This tests that diagnostic rendering is reasonable when the function being called is from the
standard library.
```py
import json
json.loads(5) # error: [invalid-argument-type]
```
## Tests for a variety of argument types
These tests check that diagnostic output is reasonable regardless of the kinds of arguments used in
a function definition.
### Only positional
Tests a function definition with only positional parameters.
```py
def foo(x: int, y: int, z: int, /) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
### Variadic arguments
Tests a function definition with variadic arguments.
```py
def foo(*numbers: int) -> int:
return len(numbers)
foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
```
### Keyword only arguments
Tests a function definition with keyword-only arguments.
```py
def foo(x: int, y: int, *, z: int = 0) -> int:
return x * y * z
foo(1, 2, z="hello") # error: [invalid-argument-type]
```
### One keyword argument
Tests a function definition with keyword-only arguments.
```py
def foo(x: int, y: int, z: int = 0) -> int:
return x * y * z
foo(1, 2, "hello") # error: [invalid-argument-type]
```
### Variadic keyword arguments
```py
def foo(**numbers: int) -> int:
return len(numbers)
foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
```
### Mix of arguments
Tests a function definition with multiple different kinds of arguments.
```py
def foo(x: int, /, y: int, *, z: int = 0) -> int:
return x * y * z
foo(1, 2, z="hello") # error: [invalid-argument-type]
```
### Synthetic arguments
Tests a function call with synthetic arguments.
```py
class C:
def __call__(self, x: int) -> int:
return 1
c = C()
c("wrong") # error: [invalid-argument-type]
```

View File

@@ -0,0 +1,2 @@
This directory contains user-facing documentation, but also doubles as an extended test suite that
makes sure that our documentation stays up to date.

View File

@@ -0,0 +1,125 @@
# Public type of undeclared symbols
## Summary
One major deviation from the behavior of existing Python type checkers is our handling of 'public'
types for undeclared symbols. This is best illustrated with an example:
```py
class Wrapper:
value = None
wrapper = Wrapper()
reveal_type(wrapper.value) # revealed: Unknown | None
wrapper.value = 1
```
Mypy and Pyright both infer a type of `None` for the type of `wrapper.value`. Consequently, both
tools emit an error when trying to assign `1` to `wrapper.value`. But there is nothing wrong with
this program. Emitting an error here violates the [gradual guarantee] which states that *"Removing
type annotations (making the program more dynamic) should not result in additional static type
errors."*: If `value` were annotated with `int | None` here, Mypy and Pyright would not emit any
errors.
By inferring `Unknown | None` instead, we allow arbitrary values to be assigned to `wrapper.value`.
This is a deliberate choice to prevent false positive errors on untyped code.
More generally, we infer `Unknown | T_inferred` for undeclared symbols, where `T_inferred` is the
inferred type of the right-hand side of the assignment. This gradual type represents an *unknown*
fully-static type that is *at least as large as* `T_inferred`. It accurately describes our static
knowledge about this type. In the example above, we don't know what values `wrapper.value` could
possibly contain, but we *do know* that `None` is a possibility. This allows us to catch errors
where `wrapper.value` is used in a way that is incompatible with `None`:
```py
def accepts_int(i: int) -> None:
pass
def f(w: Wrapper) -> None:
# This is fine
v: int | None = w.value
# This function call is incorrect, because `w.value` could be `None`. We therefore emit the following
# error: "`Unknown | None` cannot be assigned to parameter 1 (`i`) of function `accepts_int`; expected type `int`"
c = accepts_int(w.value)
```
## Explicit lack of knowledge
The following example demonstrates how Mypy and Pyright's type inference of fully-static types in
these situations can lead to false-negatives, even though everything appears to be (statically)
typed. To make this a bit more realistic, imagine that `OptionalInt` is imported from an external,
untyped module:
`optional_int.py`:
```py
class OptionalInt:
value = 10
def reset(o):
o.value = None
```
It is then used like this:
```py
from optional_int import OptionalInt, reset
o = OptionalInt()
reset(o) # Oh no...
# Mypy and Pyright infer a fully-static type of `int` here, which appears to make the
# subsequent division operation safe -- but it is not. We infer the following type:
reveal_type(o.value) # revealed: Unknown | Literal[10]
print(o.value // 2) # Runtime error!
```
We do not catch this mistake either, but we accurately reflect our lack of knowledge about
`o.value`. Together with a possible future type-checker mode that would detect the prevalence of
dynamic types, this could help developers catch such mistakes.
## Stricter behavior
Users can always opt in to stricter behavior by adding type annotations. For the `OptionalInt`
class, this would probably be:
```py
class OptionalInt:
value: int | None = 10
o = OptionalInt()
# The following public type is now
# revealed: int | None
reveal_type(o.value)
# Incompatible assignments are now caught:
# error: "Object of type `Literal["a"]` is not assignable to attribute `value` of type `int | None`"
o.value = "a"
```
## What is meant by 'public' type?
We apply different semantics depending on whether a symbol is accessed from the same scope in which
it was originally defined, or whether it is accessed from an external scope. External scopes will
see the symbol's "public type", which has been discussed above. But within the same scope the symbol
was defined in, we use a narrower type of `T_inferred` for undeclared symbols. This is because, from
the perspective of this scope, there is no way that the value of the symbol could have been
reassigned from external scopes. For example:
```py
class Wrapper:
value = None
# Type as seen from the same scope:
reveal_type(value) # revealed: None
# Type as seen from another scope:
reveal_type(Wrapper.value) # revealed: Unknown | None
```
[gradual guarantee]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-gradual-guarantee

View File

@@ -241,30 +241,34 @@ suites:
`except` suite ran to completion
```py
def could_raise_returns_str() -> str:
return "foo"
class A: ...
class B: ...
class C: ...
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_C() -> C:
return C()
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
x = could_raise_returns_A()
reveal_type(x) # revealed: A
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
finally:
# TODO: should be `Literal[1] | str | bytes | bool`
reveal_type(x) # revealed: str | bool
# TODO: should be `Literal[1] | A | B | C`
reveal_type(x) # revealed: A | C
x = 2
reveal_type(x) # revealed: Literal[2]
@@ -282,53 +286,56 @@ x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
x = could_raise_returns_A()
reveal_type(x) # revealed: A
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
finally:
# TODO: should be `Literal[1] | str | bytes | bool`
reveal_type(x) # revealed: str | bool
# TODO: should be `Literal[1] | A | B | C`
reveal_type(x) # revealed: A | C
reveal_type(x) # revealed: str | bool
reveal_type(x) # revealed: A | C
```
An example with multiple `except` branches and a `finally` branch:
```py
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
class D: ...
class E: ...
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_E() -> E:
return E()
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
x = could_raise_returns_A()
reveal_type(x) # revealed: A
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: str | bool | float
# TODO: should be `Literal[1] | A | B | C | D | E`
reveal_type(x) # revealed: A | C | E
reveal_type(x) # revealed: str | bool | float
reveal_type(x) # revealed: A | C | E
```
## Combining `except`, `else` and `finally` branches
@@ -338,84 +345,93 @@ control flow could have jumped to the `finally` suite from partway through the `
an exception raised *there*.
```py
def could_raise_returns_str() -> str:
return "foo"
class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_C() -> C:
return C()
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_E() -> E:
return E()
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
x = could_raise_returns_A()
reveal_type(x) # revealed: A
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
else:
reveal_type(x) # revealed: str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
reveal_type(x) # revealed: A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: bool | float
# TODO: should be `Literal[1] | A | B | C | D | E`
reveal_type(x) # revealed: C | E
reveal_type(x) # revealed: bool | float
reveal_type(x) # revealed: C | E
```
The same again, this time with multiple `except` branches:
```py
def could_raise_returns_range() -> range:
return range(42)
class F: ...
class G: ...
def could_raise_returns_slice() -> slice:
return slice(None)
def could_raise_returns_F() -> F:
return F()
def could_raise_returns_G() -> G:
return G()
x = 1
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
x = could_raise_returns_A()
reveal_type(x) # revealed: A
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
else:
reveal_type(x) # revealed: str
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
reveal_type(x) # revealed: A
x = could_raise_returns_F()
reveal_type(x) # revealed: F
x = could_raise_returns_G()
reveal_type(x) # revealed: G
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
reveal_type(x) # revealed: C | E | G
reveal_type(x) # revealed: bool | float | slice
reveal_type(x) # revealed: C | E | G
```
## Nested `try`/`except` blocks
@@ -429,92 +445,101 @@ a suite containing statements that could possibly raise exceptions, which would
jumping out of that suite prior to the suite running to completion.
```py
def could_raise_returns_str() -> str:
return "foo"
class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
class F: ...
class G: ...
class H: ...
class I: ...
class J: ...
class K: ...
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_bool() -> bool:
return True
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_C() -> C:
return C()
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_E() -> E:
return E()
def could_raise_returns_slice() -> slice:
return slice(None)
def could_raise_returns_F() -> F:
return F()
def could_raise_returns_complex() -> complex:
return 3j
def could_raise_returns_G() -> G:
return G()
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_H() -> H:
return H()
class Foo: ...
class Bar: ...
def could_raise_returns_I() -> I:
return I()
def could_raise_returns_Foo() -> Foo:
return Foo()
def could_raise_returns_J() -> J:
return J()
def could_raise_returns_Bar() -> Bar:
return Bar()
def could_raise_returns_K() -> K:
return K()
x = 1
try:
try:
reveal_type(x) # revealed: Literal[1]
x = could_raise_returns_str()
reveal_type(x) # revealed: str
x = could_raise_returns_A()
reveal_type(x) # revealed: A
except TypeError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
x = could_raise_returns_bool()
reveal_type(x) # revealed: bool
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
reveal_type(x) # revealed: Literal[1] | A
x = could_raise_returns_D()
reveal_type(x) # revealed: D
x = could_raise_returns_E()
reveal_type(x) # revealed: E
else:
reveal_type(x) # revealed: str
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
reveal_type(x) # revealed: A
x = could_raise_returns_F()
reveal_type(x) # revealed: F
x = could_raise_returns_G()
reveal_type(x) # revealed: G
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
reveal_type(x) # revealed: C | E | G
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: Literal[2]
except:
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice
x = could_raise_returns_complex()
reveal_type(x) # revealed: complex
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
reveal_type(x) # revealed: Literal[1, 2] | A | B | C | D | E | F | G
x = could_raise_returns_H()
reveal_type(x) # revealed: H
x = could_raise_returns_I()
reveal_type(x) # revealed: I
else:
reveal_type(x) # revealed: Literal[2]
x = could_raise_returns_Foo()
reveal_type(x) # revealed: Foo
x = could_raise_returns_Bar()
reveal_type(x) # revealed: Bar
x = could_raise_returns_J()
reveal_type(x) # revealed: J
x = could_raise_returns_K()
reveal_type(x) # revealed: K
finally:
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice | complex | bytearray | Foo | Bar`
reveal_type(x) # revealed: bytearray | Bar
# TODO: should be `Literal[1, 2] | A | B | C | D | E | F | G | H | I | J | K`
reveal_type(x) # revealed: I | K
# Either one `except` branch or the `else`
# must have been taken and completed to get here:
reveal_type(x) # revealed: bytearray | Bar
reveal_type(x) # revealed: I | K
```
## Nested scopes inside `try` blocks
@@ -523,50 +548,56 @@ Shadowing a variable in an inner scope has no effect on type inference of the va
in the outer scope:
```py
def could_raise_returns_str() -> str:
return "foo"
class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
def could_raise_returns_bytes() -> bytes:
return b"foo"
def could_raise_returns_A() -> A:
return A()
def could_raise_returns_range() -> range:
return range(42)
def could_raise_returns_B() -> B:
return B()
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_C() -> C:
return C()
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_D() -> D:
return D()
def could_raise_returns_E() -> E:
return E()
x = 1
try:
def foo(param=could_raise_returns_str()):
x = could_raise_returns_str()
def foo(param=could_raise_returns_A()):
x = could_raise_returns_A()
try:
reveal_type(x) # revealed: str
x = could_raise_returns_bytes()
reveal_type(x) # revealed: bytes
reveal_type(x) # revealed: A
x = could_raise_returns_B()
reveal_type(x) # revealed: B
except:
reveal_type(x) # revealed: str | bytes
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
x = could_raise_returns_float()
reveal_type(x) # revealed: float
reveal_type(x) # revealed: A | B
x = could_raise_returns_C()
reveal_type(x) # revealed: C
x = could_raise_returns_D()
reveal_type(x) # revealed: D
finally:
# TODO: should be `str | bytes | bytearray | float`
reveal_type(x) # revealed: bytes | float
reveal_type(x) # revealed: bytes | float
# TODO: should be `A | B | C | D`
reveal_type(x) # revealed: B | D
reveal_type(x) # revealed: B | D
x = foo
reveal_type(x) # revealed: Literal[foo]
except:
reveal_type(x) # revealed: Literal[1] | Literal[foo]
class Bar:
x = could_raise_returns_range()
reveal_type(x) # revealed: range
x = could_raise_returns_E()
reveal_type(x) # revealed: E
x = Bar
reveal_type(x) # revealed: Literal[Bar]

View File

@@ -0,0 +1,371 @@
# Import conventions
This document describes the conventions for importing symbols.
Reference:
- <https://typing.readthedocs.io/en/latest/spec/distributing.html#import-conventions>
## Builtins scope
When looking up for a name, red knot will fallback to using the builtins scope if the name is not
found in the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the
builtins scope, contains multiple symbols from other modules (e.g., `typing`) but those are not
re-exported.
```py
# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being
# available in the builtins scope.
# error: "Name `Literal` used when not defined"
reveal_type(Literal) # revealed: Unknown
# error: "Name `sys` used when not defined"
reveal_type(sys) # revealed: Unknown
```
## Builtins import
Similarly, trying to import the symbols from the builtins module which aren't re-exported should
also raise an error.
```py
# error: "Module `builtins` has no member `Literal`"
# error: "Module `builtins` has no member `sys`"
from builtins import Literal, sys
reveal_type(Literal) # revealed: Unknown
reveal_type(sys) # revealed: Unknown
# error: "Module `math` has no member `Iterable`"
from math import Iterable
reveal_type(Iterable) # revealed: Unknown
```
## Re-exported symbols in stub files
When a symbol is re-exported, importing it should not raise an error. This tests both `import ...`
and `from ... import ...` forms.
Note: Submodule imports in `import ...` form doesn't work because it's a syntax error. For example,
in `import os.path as os.path` the `os.path` is not a valid identifier.
```py
from b import Any, Literal, foo
reveal_type(Any) # revealed: typing.Any
reveal_type(Literal) # revealed: typing.Literal
reveal_type(foo) # revealed: <module 'foo'>
```
`b.pyi`:
```pyi
import foo as foo
from typing import Any as Any, Literal as Literal
```
`foo.py`:
```py
```
## Non-exported symbols in stub files
Here, none of the symbols are being re-exported in the stub file.
```py
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
from b import foo, Any, Literal
reveal_type(Any) # revealed: Unknown
reveal_type(Literal) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
```
`b.pyi`:
```pyi
import foo
from typing import Any, Literal
```
`foo.pyi`:
```pyi
```
## Nested non-exports
Here, a chain of modules all don't re-export an import.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
# error: "Module `b` has no member `Any`"
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Nested mixed re-export and not
But, if the symbol is being re-exported explicitly in one of the modules in the chain, it should not
raise an error at that step in the chain.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any as Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Exported as different name
The re-export convention only works when the aliased name is exactly the same as the original name.
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import AnyFoo as Foo
reveal_type(Foo) # revealed: Literal[AnyFoo]
```
`b.pyi`:
```pyi
class AnyFoo: ...
```
## Exported using `__all__`
Here, the symbol is re-exported using the `__all__` variable.
```py
# TODO: This should *not* be an error but we don't understand `__all__` yet.
# error: "Module `a` has no member `Foo`"
from a import Foo
```
`a.pyi`:
```pyi
from b import Foo
__all__ = ['Foo']
```
`b.pyi`:
```pyi
class Foo: ...
```
## Re-exports in `__init__.pyi`
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
but the inference would be `Unknown`.
```py
# error: 15 "Module `a` has no member `Foo`"
# error: 20 "Module `a` has no member `c`"
from a import Foo, c, foo
reveal_type(Foo) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(foo) # revealed: <module 'a.foo'>
```
`a/__init__.pyi`:
```pyi
from .b import c
from .foo import Foo
```
`a/foo.pyi`:
```pyi
class Foo: ...
```
`a/b/__init__.pyi`:
```pyi
```
`a/b/c.pyi`:
```pyi
```
## Conditional re-export in stub file
The following scenarios are when a re-export happens conditionally in a stub file.
### Global import
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: str
```
`a.pyi`:
```pyi
from b import Foo
def coinflip() -> bool: ...
if coinflip():
Foo: str = ...
reveal_type(Foo) # revealed: Literal[Foo] | str
```
`b.pyi`:
```pyi
class Foo: ...
```
### Both branch is an import
Here, both the branches of the condition are import statements where one of them re-exports while
the other does not.
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
else:
from b import Foo as Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`b.pyi`:
```pyi
class Foo: ...
```
### Re-export in one branch
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo as Foo
```
`b.pyi`:
```pyi
class Foo: ...
```
### Non-export in one branch
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
```
`b.pyi`:
```pyi
class Foo: ...
```

View File

@@ -183,25 +183,32 @@ for x in Test():
## Union type as iterable and union type as iterator
```py
class TestIter:
def __next__(self) -> int | Exception:
return 42
class Result1A: ...
class Result1B: ...
class Result2A: ...
class Result2B: ...
class Result3: ...
class Result4: ...
class TestIter1:
def __next__(self) -> Result1A | Result1B:
return Result1B()
class TestIter2:
def __next__(self) -> str | tuple[int, int]:
return "42"
def __next__(self) -> Result2A | Result2B:
return Result2B()
class TestIter3:
def __next__(self) -> bytes:
return b"42"
def __next__(self) -> Result3:
return Result3()
class TestIter4:
def __next__(self) -> memoryview:
return memoryview(b"42")
def __next__(self) -> Result4:
return Result4()
class Test:
def __iter__(self) -> TestIter | TestIter2:
return TestIter()
def __iter__(self) -> TestIter1 | TestIter2:
return TestIter1()
class Test2:
def __iter__(self) -> TestIter3 | TestIter4:
@@ -209,7 +216,7 @@ class Test2:
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B | Result3 | Result4
```
## Union type as iterable where one union element has no `__iter__` method
@@ -245,9 +252,10 @@ class Test2:
return 42
def _(flag: bool):
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
# error: "Object of type `Test | Test2` is not iterable"
for x in Test() if flag else Test2():
reveal_type(x) # revealed: Unknown
reveal_type(x) # revealed: int
```
## Union type as iterator where one union element has no `__next__` method
@@ -263,5 +271,5 @@ class Test:
# error: [not-iterable] "Object of type `Test` is not iterable"
for x in Test():
reveal_type(x) # revealed: Unknown
reveal_type(x) # revealed: int
```

View File

@@ -64,3 +64,39 @@ def _(flag1: bool, flag2: bool):
else:
reveal_type(x) # revealed: Literal[1]
```
## `is` for `EllipsisType` (Python 3.10+)
```toml
[environment]
python-version = "3.10"
```
```py
from types import EllipsisType
def _(x: int | EllipsisType):
if x is ...:
reveal_type(x) # revealed: EllipsisType
else:
reveal_type(x) # revealed: int
```
## `is` for `EllipsisType` (Python 3.9 and below)
```toml
[environment]
python-version = "3.9"
```
```py
def _(flag: bool):
x = ... if flag else 42
reveal_type(x) # revealed: ellipsis | Literal[42]
if x is ...:
reveal_type(x) # revealed: ellipsis
else:
reveal_type(x) # revealed: Literal[42]
```

View File

@@ -0,0 +1,15 @@
# Protocols
We do not support protocols yet, but to avoid false positives, we *partially* support some known
protocols.
## `typing.SupportsIndex`
```py
from typing import SupportsIndex, Literal
def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex):
a: SupportsIndex = some_int
b: SupportsIndex = some_literal_int
c: SupportsIndex = some_indexable
```

View File

@@ -0,0 +1,382 @@
# Eager scopes
Some scopes are executed eagerly: references to variables defined in enclosing scopes are resolved
_immediately_. This is in constrast to (for instance) function scopes, where those references are
resolved when the function is called.
## Function definitions
Function definitions are evaluated lazily.
```py
x = 1
def f():
reveal_type(x) # revealed: Unknown | Literal[2]
x = 2
```
## Class definitions
Class definitions are evaluated eagerly.
```py
def _():
x = 1
class A:
reveal_type(x) # revealed: Literal[1]
y = x
x = 2
reveal_type(A.y) # revealed: Unknown | Literal[1]
```
## List comprehensions
List comprehensions are evaluated eagerly.
```py
def _():
x = 1
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
## Set comprehensions
Set comprehensions are evaluated eagerly.
```py
def _():
x = 1
# revealed: Literal[1]
{reveal_type(x) for a in range(1)}
x = 2
```
## Dict comprehensions
Dict comprehensions are evaluated eagerly.
```py
def _():
x = 1
# revealed: Literal[1]
{a: reveal_type(x) for a in range(1)}
x = 2
```
## Generator expressions
Generator expressions don't necessarily run eagerly, but in practice usually they do, so assuming
they do is the better default.
```py
def _():
x = 1
# revealed: Literal[1]
list(reveal_type(x) for a in range(1))
x = 2
```
But that does lead to incorrect results when the generator expression isn't run immediately:
```py
def evaluated_later():
x = 1
# revealed: Literal[1]
y = (reveal_type(x) for a in range(1))
x = 2
# The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
# our inferred type.
print(next(y))
```
Though note that “the iterable expression in the leftmost `for` clause is immediately evaluated”
\[[spec][generators]\]:
```py
def iterable_evaluated_eagerly():
x = 1
# revealed: Literal[1]
y = (a for a in [reveal_type(x)])
x = 2
# Even though the generator isn't evaluated until here, the first iterable was evaluated
# immediately, so our inferred type is correct.
print(next(y))
```
## Top-level eager scopes
All of the above examples behave identically when the eager scopes are directly nested in the global
scope.
### Class definitions
```py
x = 1
class A:
reveal_type(x) # revealed: Literal[1]
y = x
x = 2
reveal_type(A.y) # revealed: Unknown | Literal[1]
```
### List comprehensions
```py
x = 1
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
### Set comprehensions
```py
x = 1
# revealed: Literal[1]
{reveal_type(x) for a in range(1)}
x = 2
```
### Dict comprehensions
```py
x = 1
# revealed: Literal[1]
{a: reveal_type(x) for a in range(1)}
x = 2
```
### Generator expressions
```py
x = 1
# revealed: Literal[1]
list(reveal_type(x) for a in range(1))
x = 2
```
`evaluated_later.py`:
```py
x = 1
# revealed: Literal[1]
y = (reveal_type(x) for a in range(1))
x = 2
# The generator isn't evaluated until here, so at runtime, `x` will evaluate to 2, contradicting
# our inferred type.
print(next(y))
```
`iterable_evaluated_eagerly.py`:
```py
x = 1
# revealed: Literal[1]
y = (a for a in [reveal_type(x)])
x = 2
# Even though the generator isn't evaluated until here, the first iterable was evaluated
# immediately, so our inferred type is correct.
print(next(y))
```
## Lazy scopes are "sticky"
As we look through each enclosing scope when resolving a reference, lookups become lazy as soon as
we encounter any lazy scope, even if there are other eager scopes that enclose it.
### Eager scope within eager scope
If we don't encounter a lazy scope, lookup remains eager. The resolved binding is not necessarily in
the immediately enclosing scope. Here, the list comprehension and class definition are both eager
scopes, and we immediately resolve the use of `x` to (only) the `x = 1` binding.
```py
def _():
x = 1
class A:
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
### Class definition bindings are not visible in nested scopes
Class definitions are eager scopes, but any bindings in them are explicitly not visible to any
nested scopes. (Those nested scopes are typically (lazy) function definitions, but the rule also
applies to nested eager scopes like comprehensions and other class definitions.)
```py
def _():
x = 1
class A:
x = 4
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
class B:
# revealed: Literal[1]
[reveal_type(x) for a in range(1)]
x = 2
```
### Eager scope within a lazy scope
The list comprehension is an eager scope, and it is enclosed within a function definition, which is
a lazy scope. Because we pass through this lazy scope before encountering any bindings or
definitions, the lookup is lazy.
```py
def _():
x = 1
def f():
# revealed: Unknown | Literal[2]
[reveal_type(x) for a in range(1)]
x = 2
```
### Lazy scope within an eager scope
The function definition is a lazy scope, and it is enclosed within a class definition, which is an
eager scope. Even though we pass through an eager scope before encountering any bindings or
definitions, the lookup remains lazy.
```py
def _():
x = 1
class A:
def f():
# revealed: Unknown | Literal[2]
reveal_type(x)
x = 2
```
### Lazy scope within a lazy scope
No matter how many lazy scopes we pass through before encountering a binding or definition, the
lookup remains lazy.
```py
def _():
x = 1
def f():
def g():
# revealed: Unknown | Literal[2]
reveal_type(x)
x = 2
```
### Eager scope within a lazy scope within another eager scope
We have a list comprehension (eager scope), enclosed within a function definition (lazy scope),
enclosed within a class definition (eager scope), all of which we must pass through before
encountering any binding of `x`. Even though the last scope we pass through is eager, the lookup is
lazy, since we encountered a lazy scope on the way.
```py
def _():
x = 1
class A:
def f():
# revealed: Unknown | Literal[2]
[reveal_type(x) for a in range(1)]
x = 2
```
## Annotations
Type annotations are sometimes deferred. When they are, the types that are referenced in an
annotation are looked up lazily, even if they occur in an eager scope.
### Eager annotations in a Python file
```py
x = int
class C:
var: x
reveal_type(C.var) # revealed: int
x = str
```
### Deferred annotations in a Python file
```py
from __future__ import annotations
x = int
class C:
var: x
reveal_type(C.var) # revealed: Unknown | str
x = str
```
### Deferred annotations in a stub file
```pyi
x = int
class C:
var: x
reveal_type(C.var) # revealed: Unknown | str
x = str
```
[generators]: https://docs.python.org/3/reference/expressions.html#generator-expressions

View File

@@ -9,7 +9,7 @@ is unbound.
```py
reveal_type(__name__) # revealed: str
reveal_type(__file__) # revealed: str | None
reveal_type(__loader__) # revealed: LoaderProtocol | None
reveal_type(__loader__) # revealed: @Todo(instance attribute on class with dynamic base) | None
reveal_type(__package__) # revealed: str | None
reveal_type(__doc__) # revealed: str | None
@@ -54,10 +54,10 @@ inside the module:
import typing
reveal_type(typing.__name__) # revealed: str
reveal_type(typing.__init__) # revealed: @Todo(bound method)
reveal_type(typing.__init__) # revealed: <bound method `__init__` of `ModuleType`>
# These come from `builtins.object`, not `types.ModuleType`:
reveal_type(typing.__eq__) # revealed: @Todo(bound method)
reveal_type(typing.__eq__) # revealed: <bound method `__eq__` of `ModuleType`>
reveal_type(typing.__class__) # revealed: Literal[ModuleType]

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Basic
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int) -> int:
2 | return x * x
3 |
4 | foo("hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:5
|
2 | return x * x
3 |
4 | foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * x
|
```

View File

@@ -0,0 +1,45 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different files
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## package.py
```
1 | def foo(x: int) -> int:
2 | return x * x
```
## mdtest_snippet.py
```
1 | import package
2 |
3 | package.foo("hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:3:13
|
1 | import package
2 |
3 | package.foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/package.py:1:9
|
1 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * x
|
```

View File

@@ -0,0 +1,43 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different source order
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def bar():
2 | foo("hello") # error: [invalid-argument-type]
3 |
4 | def foo(x: int) -> int:
5 | return x * x
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:2:9
|
1 | def bar():
2 | foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
3 |
4 | def foo(x: int) -> int:
|
::: /src/mdtest_snippet.py:4:9
|
2 | foo("hello") # error: [invalid-argument-type]
3 |
4 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
5 | return x * x
|
```

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int) -> int:
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:8
|
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -0,0 +1,46 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters across multiple lines
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(
2 | x: int,
3 | y: int,
4 | z: int,
5 | ) -> int:
6 | return x * y * z
7 |
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:8:8
|
6 | return x * y * z
7 |
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:3:5
|
1 | def foo(
2 | x: int,
3 | y: int,
| ------ info: parameter declared in function definition here
4 | z: int,
5 | ) -> int:
|
```

View File

@@ -0,0 +1,78 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters with multiple invalid arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int) -> int:
2 | return x * y * z
3 |
4 | # error: [invalid-argument-type]
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:5
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["a"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:10
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["b"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:15
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["c"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:25
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -0,0 +1,41 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Test calling a function whose type is vendored from `typeshed`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | import json
2 |
3 | json.loads(5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:3:12
|
1 | import json
2 |
3 | json.loads(5) # error: [invalid-argument-type]
| ^ Object of type `Literal[5]` cannot be assigned to parameter 1 (`s`) of function `loads`; expected type `str | bytes | bytearray`
|
::: vendored://stdlib/json/__init__.pyi:40:5
|
38 | ) -> None: ...
39 | def loads(
40 | s: str | bytes | bytearray,
| -------------------------- info: parameter declared in function definition here
41 | *,
42 | cls: type[JSONDecoder] | None = None,
|
```

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Keyword only arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:28
|
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Mix of arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:31
|
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - One keyword argument
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:25
|
1 | def foo(x: int, y: int, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Only positional
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int, /) -> int:
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:8
|
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int, /) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@@ -0,0 +1,41 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Synthetic arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C:
2 | def __call__(self, x: int) -> int:
3 | return 1
4 |
5 | c = C()
6 | c("wrong") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:6:3
|
5 | c = C()
6 | c("wrong") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`
|
::: /src/mdtest_snippet.py:2:24
|
1 | class C:
2 | def __call__(self, x: int) -> int:
| ------ info: parameter declared in function definition here
3 | return 1
|
```

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(*numbers: int) -> int:
2 | return len(numbers)
3 |
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:14
|
2 | return len(numbers)
3 |
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `*numbers` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(*numbers: int) -> int:
| ------------- info: parameter declared in function definition here
2 | return len(numbers)
|
```

View File

@@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic keyword arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(**numbers: int) -> int:
2 | return len(numbers)
3 |
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:20
|
2 | return len(numbers)
3 |
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `**numbers` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(**numbers: int) -> int:
| -------------- info: parameter declared in function definition here
2 | return len(numbers)
|
```

View File

@@ -66,6 +66,6 @@ It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to
```py
import sys
reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(Attribute access on `LiteralString` types)
reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(Attribute access on `LiteralString` types)
reveal_type(sys.platform.startswith("freebsd")) # revealed: bool
reveal_type(sys.platform.startswith("linux")) # revealed: bool
```

View File

@@ -35,7 +35,7 @@ in strict mode.
```py
def f(x: type):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(bound method)
reveal_type(x.__repr__) # revealed: <bound method `__repr__` of `type`>
class A: ...
@@ -50,7 +50,7 @@ x: type = A() # error: [invalid-assignment]
```py
def f(x: type[object]):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(bound method)
reveal_type(x.__repr__) # revealed: <bound method `__repr__` of `type`>
class A: ...

View File

@@ -54,3 +54,41 @@ from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(_NoDefaultType))
```
## `builtins.ellipsis`/`types.EllipsisType`
### All Python versions
The type of the builtin symbol `Ellipsis` is the same as the type of an ellipsis literal (`...`).
The type is not actually exposed from the standard library on Python \<3.10, but we still recognise
the type as a singleton on any Python version.
```toml
[environment]
python-version = "3.9"
```
```py
import sys
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(Ellipsis.__class__))
static_assert(is_singleton((...).__class__))
```
### Python 3.10+
On Python 3.10+, the standard library exposes the type of `...` as `types.EllipsisType`, and we also
recognise this as a singleton type when it is referenced directly:
```toml
[environment]
python-version = "3.10"
```
```py
import types
from knot_extensions import static_assert, is_singleton
static_assert(is_singleton(types.EllipsisType))
```

View File

@@ -11,11 +11,15 @@ See the [typing documentation] for more information.
- `bool` is a subtype of `int`. This is modeled after Python's runtime behavior, where `int` is a
supertype of `bool` (present in `bool`s bases and MRO).
- `int` is not a subtype of `float`/`complex`, even though `float`/`complex` can be used in place of
`int` in some contexts (see [special case for float and complex]).
- `int` is not a subtype of `float`/`complex`, although this is muddied by the
[special case for float and complex] where annotations of `float` and `complex` are interpreted
as `int | float` and `int | float | complex`, respectively.
```py
from knot_extensions import is_subtype_of, static_assert
from knot_extensions import is_subtype_of, static_assert, TypeOf
type JustFloat = TypeOf[1.0]
type JustComplex = TypeOf[1j]
static_assert(is_subtype_of(bool, bool))
static_assert(is_subtype_of(bool, int))
@@ -30,8 +34,8 @@ static_assert(not is_subtype_of(int, bool))
static_assert(not is_subtype_of(int, str))
static_assert(not is_subtype_of(object, int))
static_assert(not is_subtype_of(int, float))
static_assert(not is_subtype_of(int, complex))
static_assert(not is_subtype_of(int, JustFloat))
static_assert(not is_subtype_of(int, JustComplex))
static_assert(is_subtype_of(TypeError, Exception))
static_assert(is_subtype_of(FloatingPointError, Exception))
@@ -79,7 +83,9 @@ static_assert(is_subtype_of(C, object))
```py
from typing_extensions import Literal, LiteralString
from knot_extensions import is_subtype_of, static_assert
from knot_extensions import is_subtype_of, static_assert, TypeOf
type JustFloat = TypeOf[1.0]
# Boolean literals
static_assert(is_subtype_of(Literal[True], bool))
@@ -92,8 +98,7 @@ static_assert(is_subtype_of(Literal[1], object))
static_assert(not is_subtype_of(Literal[1], bool))
# See the note above (or link below) concerning int and float/complex
static_assert(not is_subtype_of(Literal[1], float))
static_assert(not is_subtype_of(Literal[1], JustFloat))
# String literals
static_assert(is_subtype_of(Literal["foo"], LiteralString))

View File

@@ -75,3 +75,19 @@ class Boom:
reveal_type(bool(Boom())) # revealed: bool
```
### Possibly unbound __bool__ method
```py
from typing import Literal
def flag() -> bool:
return True
class PossiblyUnboundTrue:
if flag():
def __bool__(self) -> Literal[True]:
return True
reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool
```

View File

@@ -37,6 +37,31 @@ def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None:
reveal_type(u2) # revealed: int | str
```
## `object` subsumes everything
Unions with `object` can be simplified to `object`:
```py
from typing_extensions import Never, Any
def _(
u1: int | object,
u2: object | int,
u3: Any | object,
u4: object | Any,
u5: object | Never,
u6: Never | object,
u7: int | str | object | bytes | Any,
) -> None:
reveal_type(u1) # revealed: object
reveal_type(u2) # revealed: object
reveal_type(u3) # revealed: object
reveal_type(u4) # revealed: object
reveal_type(u5) # revealed: object
reveal_type(u6) # revealed: object
reveal_type(u7) # revealed: object
```
## Flattening of nested unions
```py
@@ -45,11 +70,11 @@ from typing import Literal
def _(
u1: (int | str) | bytes,
u2: int | (str | bytes),
u3: int | (str | (bytes | complex)),
u3: int | (str | (bytes | bytearray)),
) -> None:
reveal_type(u1) # revealed: int | str | bytes
reveal_type(u2) # revealed: int | str | bytes
reveal_type(u3) # revealed: int | str | bytes | complex
reveal_type(u3) # revealed: int | str | bytes | bytearray
```
## Simplification using subtyping
@@ -120,8 +145,8 @@ Simplifications still apply when `Unknown` is present.
```py
from knot_extensions import Unknown
def _(u1: str | Unknown | int | object):
reveal_type(u1) # revealed: Unknown | object
def _(u1: int | Unknown | bool) -> None:
reveal_type(u1) # revealed: int | Unknown
```
## Union of intersections

View File

@@ -80,7 +80,7 @@ class Manager:
def __exit__(self, exc_tpe, exc_value, traceback): ...
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `int` is not callable"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
with Manager():
...
```
@@ -95,7 +95,7 @@ class Manager:
__exit__: int = 32
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`"
with Manager():
...
```
@@ -134,3 +134,19 @@ def _(flag: bool):
with Manager() as f:
reveal_type(f) # revealed: str
```
## Invalid `__enter__` signature
```py
class Manager:
def __enter__() -> str:
return "foo"
def __exit__(self, exc_type, exc_value, traceback): ...
context_expr = Manager()
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
with context_expr as f:
reveal_type(f) # revealed: str
```

View File

@@ -12,14 +12,32 @@ use ruff_db::parsed::ParsedModule;
/// Holding on to any [`AstNodeRef`] prevents the [`ParsedModule`] from being released.
///
/// ## Equality
/// Two `AstNodeRef` are considered equal if their wrapped nodes are equal.
/// Two `AstNodeRef` are considered equal if their pointer addresses are equal.
///
/// ## Usage in salsa tracked structs
/// It's important that [`AstNodeRef`] fields in salsa tracked structs are tracked fields
/// (attributed with `#[tracked`]). It prevents that the tracked struct gets a new ID
/// everytime the AST changes, which in turn, invalidates the result of any query
/// that takes said tracked struct as a query argument or returns the tracked struct as part of its result.
///
/// For example, marking the [`AstNodeRef`] as tracked on `Expression`
/// has the effect that salsa will consider the expression as "unchanged" for as long as it:
///
/// * belongs to the same file
/// * belongs to the same scope
/// * has the same kind
/// * was created in the same order
///
/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving
/// us some form of scope-stable identity for expressions. Only queries accessing the node field
/// run on every AST change. All other queries only run when the expression's identity changes.
#[derive(Clone)]
pub struct AstNodeRef<T> {
/// Owned reference to the node's [`ParsedModule`].
///
/// The node's reference is guaranteed to remain valid as long as it's enclosing
/// [`ParsedModule`] is alive.
_parsed: ParsedModule,
parsed: ParsedModule,
/// Pointer to the referenced node.
node: std::ptr::NonNull<T>,
@@ -37,7 +55,7 @@ impl<T> AstNodeRef<T> {
/// the invariant `node belongs to parsed` is upheld.
pub(super) unsafe fn new(parsed: ParsedModule, node: &T) -> Self {
Self {
_parsed: parsed,
parsed,
node: std::ptr::NonNull::from(node),
}
}
@@ -72,7 +90,14 @@ where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.node().eq(other.node())
if self.parsed == other.parsed {
// Comparing the pointer addresses is sufficient to determine equality
// if the parsed are the same.
self.node.eq(&other.node)
} else {
// Otherwise perform a deep comparison.
self.node().eq(other.node())
}
}
}
@@ -87,6 +112,20 @@ where
}
}
#[allow(unsafe_code)]
unsafe impl<T> salsa::Update for AstNodeRef<T> {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
let old_ref = &mut (*old_pointer);
if old_ref.parsed == new_value.parsed && old_ref.node.eq(&new_value.node) {
false
} else {
*old_ref = new_value;
true
}
}
}
#[allow(unsafe_code)]
unsafe impl<T> Send for AstNodeRef<T> where T: Send {}
#[allow(unsafe_code)]

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use crate::lint::{LintRegistry, RuleSelection};
use ruff_db::files::File;
use ruff_db::{Db as SourceDb, Upcast};
@@ -7,7 +9,7 @@ use ruff_db::{Db as SourceDb, Upcast};
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
fn is_file_open(&self, file: File) -> bool;
fn rule_selection(&self) -> &RuleSelection;
fn rule_selection(&self) -> Arc<RuleSelection>;
fn lint_registry(&self) -> &LintRegistry;
}
@@ -17,7 +19,6 @@ pub(crate) mod tests {
use std::sync::Arc;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
use super::Db;
@@ -27,6 +28,7 @@ pub(crate) mod tests {
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::PythonVersion;
#[salsa::db]
#[derive(Clone)]
@@ -111,8 +113,8 @@ pub(crate) mod tests {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
fn rule_selection(&self) -> Arc<RuleSelection> {
self.rule_selection.clone()
}
fn lint_registry(&self) -> &LintRegistry {

View File

@@ -9,7 +9,6 @@ pub use module_name::ModuleName;
pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module};
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
pub use python_platform::PythonPlatform;
pub use python_version::PythonVersion;
pub use semantic_model::{HasType, SemanticModel};
pub mod ast_node_ref;
@@ -20,11 +19,9 @@ mod module_resolver;
mod node_key;
mod program;
mod python_platform;
mod python_version;
pub mod semantic_index;
mod semantic_model;
pub(crate) mod site_packages;
mod stdlib;
mod suppression;
pub(crate) mod symbol;
pub mod types;

View File

@@ -109,6 +109,7 @@ pub enum KnownModule {
#[allow(dead_code)]
Abc, // currently only used in tests
Collections,
Inspect,
KnotExtensions,
}
@@ -123,6 +124,7 @@ impl KnownModule {
Self::Sys => "sys",
Self::Abc => "abc",
Self::Collections => "collections",
Self::Inspect => "inspect",
Self::KnotExtensions => "knot_extensions",
}
}
@@ -149,6 +151,7 @@ impl KnownModule {
"sys" => Some(Self::Sys),
"abc" => Some(Self::Abc),
"collections" => Some(Self::Collections),
"inspect" => Some(Self::Inspect),
"knot_extensions" => Some(Self::KnotExtensions),
_ => None,
}

View File

@@ -631,10 +631,10 @@ impl PartialEq<SearchPath> for VendoredPathBuf {
#[cfg(test)]
mod tests {
use ruff_db::Db;
use ruff_python_ast::PythonVersion;
use crate::db::tests::TestDb;
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::python_version::PythonVersion;
use super::*;

View File

@@ -6,12 +6,13 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_db::files::{File, FilePath, FileRootKind};
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
use ruff_db::vendored::{VendoredFileSystem, VendoredPath};
use ruff_python_ast::PythonVersion;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
use crate::site_packages::VirtualEnvironment;
use crate::{Program, PythonVersion, SearchPathSettings, SitePackages};
use crate::{Program, SearchPathSettings, SitePackages};
use super::module::{Module, ModuleKind};
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
@@ -133,7 +134,7 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SearchPaths {
pub struct SearchPaths {
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
/// These shouldn't ever change unless the config settings themselves change.
static_paths: Vec<SearchPath>,
@@ -724,12 +725,12 @@ mod tests {
assert_const_function_query_was_not_run, assert_function_query_was_not_run,
};
use ruff_db::Db;
use ruff_python_ast::PythonVersion;
use crate::db::tests::TestDb;
use crate::module_name::ModuleName;
use crate::module_resolver::module::ModuleKind;
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::PythonVersion;
use crate::{ProgramSettings, PythonPlatform};
use super::*;

View File

@@ -1,9 +1,9 @@
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
use ruff_db::vendored::VendoredPathBuf;
use ruff_python_ast::PythonVersion;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::{ProgramSettings, PythonPlatform, SitePackages};
/// A test case for the module resolver.

View File

@@ -4,11 +4,12 @@ use std::num::{NonZeroU16, NonZeroUsize};
use std::ops::{RangeFrom, RangeInclusive};
use std::str::FromStr;
use ruff_python_ast::PythonVersion;
use rustc_hash::FxHashMap;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::{Program, PythonVersion};
use crate::Program;
pub(in crate::module_resolver) fn vendored_typeshed_versions(db: &dyn Db) -> TypeshedVersions {
TypeshedVersions::from_str(
@@ -278,12 +279,12 @@ impl FromStr for PyVersionRange {
let mut parts = s.split('-').map(str::trim);
match (parts.next(), parts.next(), parts.next()) {
(Some(lower), Some(""), None) => {
let lower = PythonVersion::from_versions_file_string(lower)?;
let lower = python_version_from_versions_file_string(lower)?;
Ok(Self::AvailableFrom(lower..))
}
(Some(lower), Some(upper), None) => {
let lower = PythonVersion::from_versions_file_string(lower)?;
let upper = PythonVersion::from_versions_file_string(upper)?;
let lower = python_version_from_versions_file_string(lower)?;
let upper = python_version_from_versions_file_string(upper)?;
Ok(Self::AvailableWithin(lower..=upper))
}
_ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens),
@@ -302,21 +303,21 @@ impl fmt::Display for PyVersionRange {
}
}
impl PythonVersion {
fn from_versions_file_string(s: &str) -> Result<Self, TypeshedVersionsParseErrorKind> {
let mut parts = s.split('.').map(str::trim);
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
s.to_string(),
));
};
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
version: s.to_string(),
err: int_parse_error,
}
})
}
fn python_version_from_versions_file_string(
s: &str,
) -> Result<PythonVersion, TypeshedVersionsParseErrorKind> {
let mut parts = s.split('.').map(str::trim);
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
s.to_string(),
));
};
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
version: s.to_string(),
err: int_parse_error,
}
})
}
#[cfg(test)]

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