-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[red-knot] knot_extensions
Python API
#15103
Conversation
This comment was marked as off-topic.
This comment was marked as off-topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! I'm impressed how simple the implementation is
9795b39
to
9943e95
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really nice. Considering how much more readable this makes our tests, I'm sold that this is the way to go. As such, my review here is mostly a review/commentary of your implementation rather than of the concept itself :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More of a question than a comment: What are your thoughts on whether we should gate this behind a feature or make it testing only?
Seeing that mypy and pyre have similar public APIs, I could imagine (part of) this becoming a public API for red knot that isn't feature-gated? The Do we need to decide this right away? If not, can this stay public for now, or should we approach it very carefully and hide everything (by default)? |
I'm generally leaning toward keeping the API intentionally limited and only expose features that we want to support long term (and consider part of the public API) because users will using them and it's very easy that we forget to revisit this decision before the release (unless we note it down somewhere?) |
knot_extensions
Python API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thank you!!
A couple more minor comments below, and we need to sort out the open question of where the knot_extensions
stub will live (or it will get deleted in the next automated typeshed-sync PR). But other than that, this now looks great to me.
For what it's worth, there's nothing in the API added in this PR that I have reservations about supporting long-term as public API, in principle. Of course it's always possible that we realize we made mistakes in details of how we defined or named the API, and we want to change these things in future and potentially have to take backward-compatibility into account with those changes. But this is inevitable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really excellent, thank you! I look forward to using it, and I think red-knot users will also find it valuable.
# Predicates on types | ||
class _IsEquivalentTo[S, T]: ... | ||
class _IsSubtypeOf[S, T]: ... | ||
class _IsAssignableTo[S, T]: ... | ||
class _IsDisjointFrom[S, T]: ... | ||
class _IsFullyStatic[T]: ... | ||
class _IsSingleton[T]: ... | ||
class _IsSingleValued[T]: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the advantage of using these dedicated types vs just having them all return bool
in the stub? Either way the stub-annotated return type isn't used as-is, we have to special-case it. But this way it seems like the stub is just lying about the actual inferred return type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was expecting this question to come up 😄. Honestly, I didn't really know what to put here. Yes, bool
is the union of both possible return types that we would could infer (either Literal[True]
or Literal[False]
). But it wouldn't ever be useful for a type checker to fall back to this bool
return type annotation, since no static conclusions can be drawn from it, right? Or is that exactly why you want it to be annotated as bool
? For other type checkers that do not special case these functions?
I think I wanted to express that these are functions on types, not on objects. Using _Xyz[S, T]
as a return type was my way of saying: the return type depends on the argument types. But of course I couldn't express that the return type does not also depend on those "phantom data" objects x
/y
.
This is also why I implemented these predicates as generic classes like IsEquivalentTo[S, T]
initially, as that seemed to imply to me that a new type (IsEquivalentTo[S, T]
) is being computed from two other types (S
and T
). But I understand that that's not great either.
So I'm happy to change it to bool
. And I can also add some documentation for readers of that stub file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it makes sense how you got here :) But my feeling is that neither return type is directly useful, and bool
is both accurate and simple.
I wanted to express that these are functions on types
Yeah; I think the way to do this is would be via annotating the arguments as TypeForm
, once PEP 747 is accepted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I'm just going to mention this one last time and then I'll be quiet 😄. The current version of the API does feel good to me and I think it's more or less intuitive. I'm personally fine with merging it as is.
But upon closer inspection, there are some things that are "special".
The type predicates like is_fully_static
etc. do not work like usual Python functions: For all other functions, we call infer_expression
on the arguments in a function call. But for these predicates, we call infer_type_expression
.
Similarly, the special form TypeOf
does not work like usual special forms (although Annotated
can also contain an non-type-expression in the second argument). For all other special forms, we call infer_type_expression
for the bracketed arguments to the special form. For TypeOf
, we call infer_expression
.
For TypeOf
, we already discussed that it might be necessary to leave it as a special form, as a call expression can not occur in type annotations.
But the functions still feel a bit strange to me, and also require some special-casing in the implementation. It seems to me that even if we had PEP 747 and annotated the parameter types of these functions as TypeForm
s, we should still call infer_expression
instead of infer_type_expression
for calls to such functions (I haven't read the full PEP, but the split_union
example here seems to imply that typ
internally is of type UnionType
, for example — not an actual union type).
Again, I'm personally fine with this special-casing, but I thought it would be worth calling out one last time. The alternative would be to make these predicates special forms that could be called via IsFullyStatic[int]
or maybe these would be generic classes with a __bool__
method that would return Literal[True]
/Literal[False]
accordingly, such that we could write static_assert(IsFullyStatic[int]())
. Or potentially static_assert(IsFullyStatic[int])
if we implement it on the meta-class(?).
(the Intersection
and Not
type forms, and the static_assert
function act normal)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your functions aren't the first example of this: the existing typing.cast
function (which we haven't yet implemented support for, but will need to) also requires its first argument to be interpreted as a type expression rather than a value expression.
PEP 747 generalizes this; it requires that an expression whose "type context" is a TypeForm
type (e.g. the RHS of an annotated assignment where the annotation is a TypeForm
type, or an argument matched to a parameter annotated as a TypeForm
type) be interpreted as a type expression rather than a value expression. If the expression is a valid type expression, it is assignable to TypeForm
; if it is a valid type expression spelling a type assignable to T
, then it is assignable to TypeForm[T]
.
(The split_union
example in the PEP is not a counter-example to this. A TypeForm
type is indeed never an "actual union type"; TypeForm[T | S]
represents the set of runtime objects that can result from a valid type expression for the type T | S
. The type types.UnionType
overlaps with TypeForm[T | S]
; the type T | S
is disjoint from the type TypeForm[T | S]
.)
This kind of contextual type evaluation is something we don't support yet, but will need to. It comes up in other cases besides TypeForm
as well; Eric Traut has some good examples at https://discuss.python.org/t/pep-747-typeexpr-type-hint-for-a-type-expression/55984/67
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The call-checking support will require changes to handle contextual type evaluation; we'll need to match arguments to parameters before inferring argument types. I felt that it still made sense to do the simpler implementation now, and adjust it when we add contextual inference support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your functions aren't the first example of this: the existing
typing.cast
function (which we haven't yet implemented support for, but will need to) also requires its first argument to be interpreted as a type expression rather than a value expression.
similar for assert_type
as well
4aaacee
to
209a217
Compare
ccb639c
to
855d6db
Compare
crates/red_knot_vendored/vendor/typeshed/stdlib/knot_extensions.pyi
Outdated
Show resolved
Hide resolved
1fee970
to
541a13c
Compare
The integration/flattening looks great! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice! A couple of nitpicks on top of Carl's comments. And it still needs an edit to https://github.com/astral-sh/ruff/blob/main/.github/workflows/sync_typeshed.yaml to make sure we don't lose the knot_extensions
stub file in the next automated typeshed sync
Decided to merge this now to unblock work on #15194. Feel free to add post-merge comments and I will address them. |
* main: [`pylint`] Fix `unreachable` infinite loop (`PLW0101`) (#15278) fix invalid syntax in workflow file (#15357) [`pycodestyle`] Avoid false positives related to type aliases (`E252`) (#15356) [`flake8-builtins`] Disapply `A005` to stub files (#15350) Improve logging system using `logLevel`, avoid trace value (#15232) [`flake8-builtins`] Rename `A005` and improve its error message (#15348) Spruce up docs for pydoclint rules (#15325) [`flake8-type-checking`] Apply `TC008` more eagerly in `TYPE_CHECKING` blocks and disapply it in stubs (#15180) [red-knot] `knot_extensions` Python API (#15103) Display Union of Literals as a Literal (#14993) [red-knot] all types are assignable to object (#15332) [`ruff`] Parenthesize arguments to `int` when removing `int` would change semantics in `unnecessary-cast-to-int` (`RUF046`) (#15277) [`eradicate`] Correctly handle metadata blocks directly followed by normal blocks (`ERA001`) (#15330) Narrowing for class patterns in match statements (#15223) [red-knot] add call checking (#15200) Spruce up docs for `slice-to-remove-prefix-or-suffix` (`FURB188`) (#15328) [`internal`] Return statements in finally block point to end block for `unreachable` (`PLW0101`) (#15276) [`ruff`] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) (#15318) Use uv consistently throughout the documentation (#15302)
Summary
Adds a type-check-time Python API that allows us to create and manipulate types and to test various of their properties. For example, this can be used to write a Markdown test to make sure that
A & B
is a subtype ofA
andB
, but not of an unrelated classC
(something that requires quite a bit more code to do in Rust):I think this functionality is also helpful for interactive debugging sessions, in order to query various properties of Red Knot's type system. Which is something that otherwise requires a custom Rust unit test, some boilerplate code and constant re-compilation.
Comparison Rust ↔ Python:
is_assignable_to
testsRust
Python (grouped by topic instead of assignable/not-assignable)
Other ideas
assert_true(…)
to replacereveal_type(…) # revealed: Literal[True]
knot_extensions.Unknown
as a spelling for theUnknown
type.Should we have a way to spell the empty intersection type? LikeIt's probably okay not to support this. EmptyIntersection[()]
?Union
s orUnion
s with a single type are not supported either (even if they would have a well defined meaning).typing.assert_type
, and maybe alias it asknot_extensions.assert_type
To do
knot_extensions
module in a dedicated packageTypeOf
is needed. Maybe introduce something likeknot_extensions.ClassLiteral[…]
instead for that specific purpose?Test Plan
New Markdown tests