Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow assigning ellipsis literal as parameter default value #14982

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

Glyphack
Copy link
Contributor

@Glyphack Glyphack commented Dec 15, 2024

Resolves #14840

Summary

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

Test Plan

Added mdtest for both python files and stub files.

Copy link
Contributor

github-actions bot commented Dec 15, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@AlexWaygood AlexWaygood added the red-knot Multi-file analysis & type inference label Dec 15, 2024
@AlexWaygood
Copy link
Member

The pre-commit failure will go away if you merge in main or rebase :-)

@Glyphack Glyphack force-pushed the ellipsis-default-type branch from 8157331 to a7e7723 Compare December 15, 2024 21:23
@Glyphack
Copy link
Contributor Author

Wow there's a bug in the new github UI for PRs it was not showing the rebase button I thought it's already up to date.

@Glyphack Glyphack marked this pull request as ready for review December 15, 2024 21:26
@AlexWaygood
Copy link
Member

Wow there's a bug in the new github UI for PRs it was not showing the rebase button I thought it's already up to date.

Oh, that only appears for a repository if the maintainers have checked a certain box in the repository settings on GitHub. We haven't enabled it for Astral repositories because the rate of development is quite rapid, and some folks found it annoying to be constantly told that all their PRs were out of date with the main branch 😆

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!!

@Glyphack
Copy link
Contributor Author

Glyphack commented Dec 16, 2024

Thanks for the review.
I have a question, pyright behavior here is that in all files the assignment works only if the function body is also ellipsis. The link does not specify if it's stub file but pasting this into a python file locally shows the same warnings.
https://pyright-play.net/?strict=true&code=CYUwZgBGAUAeBcECWA7ALhAvBAdHglBALQB8EAcgPYojwBQEjEATiGgK7MoSx12iQYCZOiy4CxMlRqI8OIA
Mypy does the same. Is this just a limitation of them or the specs are not strictly forbidding this?

@Glyphack Glyphack force-pushed the ellipsis-default-type branch from 4d835da to 06e1551 Compare December 16, 2024 21:37
@AlexWaygood
Copy link
Member

AlexWaygood commented Dec 16, 2024

This special case is only allowed in function and method arguments. Based on what I understand from typing.readthedocs.io/en/latest/guides/writing_stubs.html#module-level-attributes and typing.readthedocs.io/en/latest/guides/writing_stubs.html#classes.

Do not unnecessarily use an assignment for module-level attributes.

Ah, I think there's a small misunderstanding here. There are two halves of typing.readthedocs.io:

  • One half of the website (https://typing.readthedocs.io/en/latest/spec/) is a formal typing spec: this is a set of rules that type checkers must follow if they claim to support all features of Python's typing system
  • The other half of the website (https://typing.readthedocs.io/en/latest/guides/) is a set of guides for users on how they should use annotations in their own runtime code and stub files. These are guides to best practices, but they aren't necessarily things that type checkers need to enforce

The two pages you link to there are both in the "second half" of the website: those paragraphs describe best practices for people writing stub files, but these aren't necessarily rules that type checkers have to enforce regarding stub files. The typing spec part of typing.readthedocs.io is here: https://typing.readthedocs.io/en/latest/spec/distributing.html#value-expressions. It states that type checkers should allow ... as a value in many more places, even if the "guides" part of typing.readthedocs.io states that it wouldn't necessarily be best practice for users to do so. Here's what the spec has to say:

In locations where value expressions can appear, such as the right-hand side of assignment statements and function parameter defaults, type checkers should support the following expressions:

  • The ellipsis literal, ..., which can stand in for any value
    -Any value that is a legal parameter for typing.Literal
  • Floating point literals, such as 3.14
  • Complex literals, such as 1 + 2j

And later:

Type checkers should support module-level variable annotations, with and without assignments:

x: int
x: int = 0
x = 0  # type: int
x = ...  # type: int

@AlexWaygood
Copy link
Member

So I think we should allow ... as a value for any annotated symbol in stub files, in any context, not just parameter defaults

@carljm
Copy link
Contributor

carljm commented Dec 16, 2024

Thanks for that clarification, Alex! I was surprised that x: int = ... wouldn't be supported in a stub, and I read the links without noticing they weren't part of the spec.

I feel like the wording of the spec:

In locations where value expressions can appear... type checkers should support the following expressions: ... The ellipsis literal, ..., which can stand in for any value

Suggests a totally different implementation approach here, which is that in a stub file we should simply infer literal ... expressions as type Any, which is assignable to anything. That seems better than trying to handle this at each point where we enforce assignability.

@carljm
Copy link
Contributor

carljm commented Dec 16, 2024

I have a question, pyright behavior here is that in all files the assignment works only if the function body is also ellipsis ... Mypy does the same

That's very interesting, good observation! I don't actually know why that is. It may be in order to support ellipsis defaults in overloads and protocol methods. I'm a little surprised that this appears to be handled by allowing it based on the body being ..., rather than just allowing it contextually for overloads and protocols.

I'm not convinced we want to follow suit here; it seems incorrect to do this based on a body of ..., considering that is a valid body for a regular Python function.

So I would say let's implement this only for stubs for now, and consider the best approach when we add overloads/protocols support.

@AlexWaygood
Copy link
Member

Suggests a totally different implementation approach here, which is that in a stub file we should simply infer literal ... expressions as type Any, which is assignable to anything. That seems better than trying to handle this at each point where we enforce assignability.

That seems okay to me as long as we don't start inferring unions with Any for things imported from stub files... but I don't think we will, because declared types take precedence over inferred types for things imported from other modules, right?

@carljm
Copy link
Contributor

carljm commented Dec 16, 2024

That seems okay to me as long as we don't start inferring unions with Any for things imported from stub files... but I don't think we will, because declared types take precedence over inferred types for things imported from other modules, right?

Yes, that's right.

@Glyphack
Copy link
Contributor Author

Thanks let me do try to change this to infer the type of ellipsis literal as Any instead of adding this special case.

@carljm
Copy link
Contributor

carljm commented Dec 16, 2024

Suggests a totally different implementation approach here, which is that in a stub file we should simply infer literal ... expressions as type Any, which is assignable to anything. That seems better than trying to handle this at each point where we enforce assignability.

Ok, after further discussion (thanks @AlexWaygood!) I think this is not a good idea, and might cause undesirable behavior in examples like this:

class Foo: ...

X: int = ...
Y: Foo = X

Where this should be flagged as an error, even in a type stub, but my suggestion (combined with our current handling of inferred dynamic type) would not consider it an error. (It might be worth including this as a test case.)

So I think the better approach is in fact to special-case handling of literal ... in three different places: in function parameters, where you're doing it now; in infer_annotated_assignment_definition, and in infer_assignment_definition.

In the first two cases, if we see a literal ... as the default-value or RHS, in a stub file, we should just discard that inference and record the declared type as the inferred type. In the third case, there is no declaratiion and we should record Unknown.

@Glyphack Glyphack force-pushed the ellipsis-default-type branch from 06e1551 to 9689544 Compare December 19, 2024 18:06
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! A few comments.

TargetKind::Name => value_ty,
TargetKind::Name => {
if self.file().is_stub(self.db().upcast()) && value.is_ellipsis_literal_expr() {
Type::Any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use Type::Unknown rather than Type::Any here. In our model, Type::Any is reserved for explicit uses of typing.Any as an annotation. Any dynamic type arising from lack of an annotation uses Type::Unknown.

Also we should add a test for this case (un-annotated assignment in a stub file, with ... as RHS.)

@@ -1846,7 +1852,13 @@ impl<'db> TypeInferenceBuilder<'db> {

unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be unusual, but I think valid according to the spec to do FOO, BAR = ... in a stub file. So I think we also should add a special case for ... (only if file.is_stub(db) of course) in infer_unpack_types (where we infer the value_ty from the RHS expression). And a test for this.

Copy link
Contributor Author

@Glyphack Glyphack Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied this change inside the unpacker since that was the first place that value type was being inferred. Also there was a match to check the expression being used. I hope this is what you meant as well.

But if I understand correctly we don't want the unpacking to happen in for loops so I'm checking that unpack value is of type UnpackerValue::Assign.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent some time to see if I can pull out the code for special case to infer_unpack_types itself. But I think we want the whole unpacking behavior to happen.
This could be done if we only want to treat the case where LHS is a tuple and RHS is .... But I feel this behavior would not be consistent. I also checked Pyright.

For the following .pyi file Pyright infers all variables the same.

a, b = ...
reveal_type(a)
reveal_type(b)

[c] = ...
reveal_type(c)

I can't decide if we should only allow a, b = ... or other forms of unpacking as well so please let me know if you think the other way is better.

annotation_ty,
value_ty,
);
if self.file().is_stub(self.db().upcast()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we've forgotten the condition "and the value expression is a literal ..." here. If that doesn't fail any test, we should definitely add a test with e.g. x: int = 1 in a stub file showing we still infer Literal[1] for x, not Unknown.

Copy link
Contributor Author

@Glyphack Glyphack Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it was not failing. Added a new test for it in mdtest/assignment/annotations.md.

crates/red_knot_python_semantic/src/types/infer.rs Outdated Show resolved Hide resolved
@@ -1846,7 +1852,13 @@ impl<'db> TypeInferenceBuilder<'db> {

unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
}
TargetKind::Name => value_ty,
TargetKind::Name => {
if self.file().is_stub(self.db().upcast()) && value.is_ellipsis_literal_expr() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It might be worth introducing a is_stub function on InferenceBuilder that does the self.file().is_stub(self.db().upcast())

@Glyphack Glyphack marked this pull request as draft December 20, 2024 09:57
@Glyphack Glyphack force-pushed the ellipsis-default-type branch from acf3072 to d30649c Compare December 30, 2024 11:45
@Glyphack
Copy link
Contributor Author

Glyphack commented Dec 30, 2024

Out of curiosity I checked what pyright and mypy do when they encounter ... while unpacking RHS(this comment) in assignment.

With this test.pyi(in .py files this is invalid):

x, y = ...

Pyright:

0 errors, 0 warnings, 0 informations

Mypy:

test.pyi:1: error: "EllipsisType" object is not iterable  [misc]
Found 1 error in 1 file (checked 1 source file)

@@ -76,6 +76,11 @@ impl<'db> UnpackValue<'db> {
matches!(self, UnpackValue::Iterable(_))
}

/// Returns `true` if the value is being assigned to a target.
pub(crate) const fn is_assign(self) -> bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep this consistent with is_iterable so I created this. I think https://docs.rs/is-macro/latest/is_macro/ also works here.

@Glyphack Glyphack marked this pull request as ready for review December 31, 2024 14:56
Glyphack and others added 25 commits December 31, 2024 15:57
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
…s.md

Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Carl Meyer <carl@oddbird.net>
@Glyphack Glyphack force-pushed the ellipsis-default-type branch from d499b43 to 2be530d Compare December 31, 2024 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[red-knot] ... should be treated as a valid parameter default for any annotation in a stub file
5 participants