Open sharkdp opened 1 month ago
You can't specify explicit generic parameters in a function call in Python (?), so we couldn't do something like
x = conjure[int | None]()
Correct -- for now, the best you can do is x: int | None = conjure()
or x = typing.cast(int | None, conjure())
. But see https://peps.python.org/pep-0718/ for a proposal to change this. (Discussed at https://discuss.python.org/t/pep-718-subscriptable-functions/28457.)
(I edited your post to number your suggestions so it would be easier to discuss them, hope that's okay :-)
Your proposals (4) and (5) both involve injecting some sort of "magic" function into the namespace that we could use without any imports, which would then create instances of the types required for the test. My first instinct was that I didn't much like the idea, because in general I'd like the test snippets to be as close as possible to executable Python code. I think it's useful to keep a close resemblance between our test snippets and user code we'll actually be running on. I also think keeping our test snippets as close as possible to executable Python makes them much easier for us and external contributors to understand.
However, I then realised that this isn't really that different to what we already do with reveal_type
. At runtime, reveal_type
is not a builtin -- you have to import it from typing
or typing_extensions
if you want to use it in such a way that your code will not crash when you actually run your code with a Python interpreter. But we pretend it's a builtin, so that users can easily debug their type-checking results without having to add an import, and so that we can keep our test snippets concise. I argued against this when we were designing the test framework (I said we should have to explicitly import reveal_type
in order to use it in test snippets), but @carljm pushed for it, partly on the grounds that it would significantly reduce the boilerplate of our tests. In retrospect, I think he was probably right; it would be a bit of a pain to have to import reveal_type
in every test snippet.
The key differences with reveal_type
are:
reveal_type
is a builtin. If you want to do a cross-comparison of a red-knot test with how mypy or pyright infer the types, you can just copy and paste it into their playgrounds currently. But if we injected a magic one_of
or conjure
function, we'd have to remember to add those function definitions to the snippet before mypy or pyright would accept them.one_of
or conjure
into the namespace of test snippets, whereas for reveal_type
we also pretend it's a builtin when checking user code.I think I would personally prefer the simple
def f(x: MyDesiredType): …
approach, once we make that work.
Yes, I think I agree. Mypy has quite an extensive test suite that works in a similar way to our new framework, and they've managed to do without a conjure()
function or one_of()
. That doesn't mean that the idea is bad, of course! But it does suggest that it should be possible to do without it. And even if our test snippets already don't look exactly the same as executable Python code would (due to all the unimported reveal_type
usages), it's nice to limit the differences as much as possible.
One way that mypy test snippets do differ from executable Python is in their use of "fixture stubs". Rather than using their full vendored stdlib typeshed stubs (which is what they use for checking user code), in their tests they use a radically simplified version of typeshed. This speeds up their tests a lot, but it is very frequently a source of confusion for mypy developers and contributors, who often think they've fixed a bug only to realise that the type inference their users are seeing for standard-library functions is very different to the type inference they thought they had asserted in their test snippets.
Just for the sake of discussion, another possibility here is to allow "layering" files, so in a Markdown header section you can provide a file that will be shared by all sub-tests within that section. So this would let you write your own little utilities (or simply type-annotated variables in a stub file) and import/reuse them in a bunch of related tests. Downsides are less locality of tests, and more complexity in understanding the structure and behavior of a test.
I also don't want to do anything here that's specifically motivated by limitations we should lift soon, like not understanding function arguments, or unions in annotations.
I think on the whole my preference is also defining functions with typed arguments, in most cases.
This recently came up in a discussion. A lot of red-knot tests require some form of "setup" in the sense that they create variables of a particular type. This is not always straightforward. For example, to create a variable of type
int
, you need to make sure that it doesn't end up as aLiteralInt[…]
. So some tests use a pattern like this:To create a variable with a union type, a lot of tests follow a pattern like
It's unclear to me if this really requires any action, but I thought it might make sense to discuss this in a bit more detail. Here are some approaches (some less generic than others) that I could think of.
1.
def f() -> MyDesiredType; x = f()
Upsides:
MyDesiredType
directlyDownsides:
2.
def f(x: MyDesiredType): …
Upsides:
MyDesiredType
directlyDownsides:
3.
a if flag or b
(only relevant for union types)
Upsides:
flag
beforehandDownsides:
flag
into the test environment somehow.flag
?!)4. Helper functions like
one_of(a, b)
We could inject new functions, just for testing purposes. For example, we might have a function similar to
to easily create union types
Upsides(?):
x = one_of(1, None)
is slightly more readable thanx = 1 if flag else None
(but only if you know whatone_of
does)Downsides:
5. A magic
conjure
functionI'm not even sure if this is technically possible, but other languages have ways to create values of type
T
out of nothing. Not actually, of course. But for the purpose of doing interesting things at "type check time". For example, C++ hasstd::declval<T>()
. Rust haslet x: T = todo!()
. Functional languages haveabsurd :: ⊥-> T
.You can't specify explicit generic parameters in a function call in Python (?), so we couldn't do something like
x = conjure[int | None]()
, but maybe there is some way to create a construct conceptually similar toI think I would personally prefer the simple
def f(x: MyDesiredType): …
approach, once we make that work.