Skip to content

Commit

Permalink
Merge branch 'main' into dcreager/overloads
Browse files Browse the repository at this point in the history
* main:
  [red-knot] Rework `Type::to_instance()` to return `Option<Type>` (#16428)
  [red-knot] Add tests asserting that `KnownClass::to_instance()` doesn't unexpectedly fallback to `Type::Unknown` with full typeshed stubs (#16608)
  [red-knot] Handle gradual intersection types in assignability (#16611)
  [red-knot] mypy_primer: split installation and execution (#16622)
  [red-knot] mypy_primer: pipeline improvements (#16620)
  [red-knot] Infer `lambda` expression (#16547)
  [red-knot] mypy_primer: strip ANSI codes (#16604)
  [red-knot] mypy_primer: comment on PRs (#16599)
  • Loading branch information
dcreager committed Mar 11, 2025
2 parents fdc67f9 + c16237d commit dd17521
Show file tree
Hide file tree
Showing 13 changed files with 663 additions and 133 deletions.
32 changes: 28 additions & 4 deletions .github/workflows/mypy_primer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- ".github/workflows/mypy_primer.yaml"
- ".github/workflows/mypy_primer_comment.yaml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
Expand Down Expand Up @@ -42,6 +43,10 @@ jobs:
- name: Install Rust toolchain
run: rustup show

- name: Install mypy_primer
run: |
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
- name: Run mypy_primer
shell: bash
run: |
Expand All @@ -57,13 +62,32 @@ jobs:
cd ..
echo "Running mypy_primer"
uvx --from "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support" mypy_primer \
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
uvx mypy_primer \
--repo ruff \
--type-checker knot \
--old base_commit \
--new "$GITHUB_SHA" \
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow)$' \
--output concise \
--debug
--debug > mypy_primer.diff || [ $? -eq 1 ]
# Output diff with ANSI color codes
cat mypy_primer.diff
# Remove ANSI color codes before uploading
sed -ie 's/\x1b\[[0-9;]*m//g' mypy_primer.diff
echo ${{ github.event.number }} > pr-number
- name: Upload diff
uses: actions/upload-artifact@v4
with:
name: mypy_primer_diff
path: mypy_primer.diff

- name: Upload pr-number
uses: actions/upload-artifact@v4
with:
name: pr-number
path: pr-number
97 changes: 97 additions & 0 deletions .github/workflows/mypy_primer_comment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: PR comment (mypy_primer)

on: # zizmor: ignore[dangerous-triggers]
workflow_run:
workflows: [Run mypy_primer]
types: [completed]
workflow_dispatch:
inputs:
workflow_run_id:
description: The mypy_primer workflow that triggers the workflow run
required: true

jobs:
comment:
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
- uses: dawidd6/action-download-artifact@v8
name: Download PR number
with:
name: pr-number
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
if_no_artifact_found: ignore
allow_forks: true

- name: Parse pull request number
id: pr-number
run: |
if [[ -f pr-number ]]
then
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
fi
- uses: dawidd6/action-download-artifact@v8
name: "Download mypy_primer results"
id: download-mypy_primer_diff
if: steps.pr-number.outputs.pr-number
with:
name: mypy_primer_diff
workflow: mypy_primer.yaml
pr: ${{ steps.pr-number.outputs.pr-number }}
path: pr/mypy_primer_diff
workflow_conclusion: completed
if_no_artifact_found: ignore
allow_forks: true

- name: Generate comment content
id: generate-comment
if: steps.download-mypy_primer_diff.outputs.found_artifact == 'true'
run: |
# Guard against malicious mypy_primer results that symlink to a secret
# file on this runner
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]]
then
echo "Error: mypy_primer.diff cannot be a symlink"
exit 1
fi
# Note this identifier is used to find the comment to update on
# subsequent runs
echo '<!-- generated-comment mypy_primer -->' >> comment.txt
echo '## `mypy_primer` results' >> comment.txt
if [ -s "pr/mypy_primer_diff/mypy_primer.diff" ]; then
echo '<details>' >> comment.txt
echo '<summary>Changes were detected when running on open source projects</summary>' >> comment.txt
echo '' >> comment.txt
echo '```diff' >> comment.txt
cat pr/mypy_primer_diff/mypy_primer.diff >> comment.txt
echo '```' >> comment.txt
echo '</details>' >> comment.txt
else
echo 'No ecosystem changes detected ✅' >> comment.txt
fi
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
cat comment.txt >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
- name: Find existing comment
uses: peter-evans/find-comment@v3
if: steps.generate-comment.outcome == 'success'
id: find-comment
with:
issue-number: ${{ steps.pr-number.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- generated-comment mypy_primer -->"

- name: Create or update comment
if: steps.find-comment.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.pr-number }}
body-path: comment.txt
edit-mode: replace
100 changes: 100 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# `lambda` expression

## No parameters

`lambda` expressions can be defined without any parameters.

```py
reveal_type(lambda: 1) # revealed: () -> @Todo(lambda return type)

# error: [unresolved-reference]
reveal_type(lambda: a) # revealed: () -> @Todo(lambda return type)
```

## With parameters

Unlike parameters in function definition, the parameters in a `lambda` expression cannot be
annotated.

```py
reveal_type(lambda a: a) # revealed: (a) -> @Todo(lambda return type)
reveal_type(lambda a, b: a + b) # revealed: (a, b) -> @Todo(lambda return type)
```

But, it can have default values:

```py
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> @Todo(lambda return type)
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> @Todo(lambda return type)
```

And, positional-only parameters:

```py
reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> @Todo(lambda return type)
```

And, keyword-only parameters:

```py
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> @Todo(lambda return type)
```

And, variadic parameter:

```py
reveal_type(lambda *args: args) # revealed: (*args) -> @Todo(lambda return type)
```

And, keyword-varidic parameter:

```py
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> @Todo(lambda return type)
```

Mixing all of them together:

```py
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> @Todo(lambda return type)
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
```

## Parameter type

In addition to correctly inferring the `lambda` expression, the parameters should also be inferred
correctly.

Using a parameter with no default value:

```py
lambda x: reveal_type(x) # revealed: Unknown
```

Using a parameter with default value:

```py
lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1]
```

Using a variadic paramter:

```py
# TODO: should be `tuple[Unknown, ...]` (needs generics)
lambda *args: reveal_type(args) # revealed: tuple
```

Using a keyword-varidic parameter:

```py
# TODO: should be `dict[str, Unknown]` (needs generics)
lambda **kwargs: reveal_type(kwargs) # revealed: dict
```

## Nested `lambda` expressions

Here, a `lambda` expression is used as the default value for a parameter in another `lambda`
expression.

```py
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> @Todo(lambda return type)) -> @Todo(lambda return type)
```
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ static_assert(is_assignable_to(Intersection[Child1, Parent], Parent))

static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent))
static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1))
static_assert(is_assignable_to(Intersection[Child1, Unrelated, Child2], Intersection[Child1, Unrelated]))

static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1))
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent))
Expand All @@ -256,21 +257,40 @@ static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent))
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2]))
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1]))
static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2]))
static_assert(not is_assignable_to(Intersection[Child1, Child2], Intersection[Parent, Unrelated]))

static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated]))
static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]]))
# The literal `1` is not assignable to `Parent`, so the intersection of int and Parent is definitely an int that is not `1`
static_assert(is_assignable_to(Intersection[int, Parent], Intersection[int, Not[Literal[1]]]))
static_assert(not is_assignable_to(int, Not[int]))
static_assert(not is_assignable_to(int, Not[Literal[1]]))

static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
# Intersection with `Any` dominates the left hand side of intersections
static_assert(is_assignable_to(Intersection[Any, Parent], Parent))
static_assert(is_assignable_to(Intersection[Any, Child1], Parent))
static_assert(is_assignable_to(Intersection[Any, Child2, Not[Child1]], Parent))
static_assert(is_assignable_to(Intersection[Any, Parent], Unrelated))
static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Parent, Unrelated]))
static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Parent))
static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Parent, Unrelated]))

# Even Any & Not[Parent] is assignable to Parent, since it could be Never
static_assert(is_assignable_to(Intersection[Any, Not[Parent]], Parent))

# Intersection with `Any` is effectively ignored on the right hand side for the sake of assignment
static_assert(is_assignable_to(Parent, Intersection[Any, Parent]))
static_assert(is_assignable_to(Parent, Parent | Intersection[Any, Unrelated]))
static_assert(is_assignable_to(Child1, Intersection[Any, Parent]))
static_assert(not is_assignable_to(Literal[1], Intersection[Any, Parent]))
static_assert(not is_assignable_to(Unrelated, Intersection[Any, Parent]))

# Intersections with Any on both sides combine the above logic - the LHS dominates and Any is ignored on the right hand side
static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Any, Parent]))
static_assert(is_assignable_to(Intersection[Any, Unrelated], Intersection[Any, Parent]))
static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Any, Parent, Unrelated]))
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))

# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Any, int], int))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
static_assert(is_assignable_to(Intersection[Literal[1], Any], Intersection[Unrelated, Not[Any]]))
```

## General properties
Expand Down
11 changes: 8 additions & 3 deletions crates/red_knot_python_semantic/src/module_resolver/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,8 @@ impl KnownModule {
}

pub fn name(self) -> ModuleName {
let self_as_str = self.as_str();
ModuleName::new_static(self_as_str)
.unwrap_or_else(|| panic!("{self_as_str} should be a valid module name!"))
ModuleName::new_static(self.as_str())
.unwrap_or_else(|| panic!("{self} should be a valid module name!"))
}

pub(crate) fn try_from_search_path_and_name(
Expand Down Expand Up @@ -167,6 +166,12 @@ impl KnownModule {
}
}

impl std::fmt::Display for KnownModule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading

0 comments on commit dd17521

Please sign in to comment.