[red-knot] Enforce specifying paths for mdtest code blocks in a separate preceding line (#15890)

## Summary

Resolves #15695, rework of #15704.

This change modifies the Mdtests framework so that:

* Paths must now be specified in a separate preceding line:

	`````markdown
	`a.py`:

	```py
	x = 1
	```
	`````

If the path of a file conflicts with its `lang`, an error will be
thrown.

* Configs are no longer accepted. The pattern still take them into
account, however, to avoid "Unterminated code block" errors.
* Unnamed files are now assigned unique, `lang`-respecting paths
automatically.

Additionally, all legacy usages have been updated.

## Test Plan

Unit tests and Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
InSync 2025-02-04 14:27:17 +07:00 committed by GitHub
parent 0529ad67d7
commit 11cfe2ea8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 967 additions and 286 deletions

View file

@ -2,7 +2,9 @@
## Deferred annotations in stubs always resolve ## Deferred annotations in stubs always resolve
```pyi path=mod.pyi `mod.pyi`:
```pyi
def get_foo() -> Foo: ... def get_foo() -> Foo: ...
class Foo: ... class Foo: ...
``` ```

View file

@ -116,7 +116,9 @@ def union_example(
Only Literal that is defined in typing and typing_extension modules is detected as the special Only Literal that is defined in typing and typing_extension modules is detected as the special
Literal. Literal.
```pyi path=other.pyi `other.pyi`:
```pyi
from typing import _SpecialForm from typing import _SpecialForm
Literal: _SpecialForm Literal: _SpecialForm

View file

@ -25,7 +25,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
## Tuple annotations are understood ## Tuple annotations are understood
```py path=module.py `module.py`:
```py
from typing_extensions import Unpack from typing_extensions import Unpack
a: tuple[()] = () a: tuple[()] = ()
@ -40,7 +42,9 @@ i: tuple[str | int, str | int] = (42, 42)
j: tuple[str | int] = (42,) j: tuple[str | int] = (42,)
``` ```
```py path=script.py `script.py`:
```py
from module import a, b, c, d, e, f, g, h, i, j from module import a, b, c, d, e, f, g, h, i, j
reveal_type(a) # revealed: tuple[()] reveal_type(a) # revealed: tuple[()]
@ -114,7 +118,7 @@ reveal_type(x) # revealed: Foo
## Annotations in stub files are deferred ## Annotations in stub files are deferred
```pyi path=main.pyi ```pyi
x: Foo x: Foo
class Foo: ... class Foo: ...
@ -125,7 +129,7 @@ reveal_type(x) # revealed: Foo
## Annotated assignments in stub files are inferred correctly ## Annotated assignments in stub files are inferred correctly
```pyi path=main.pyi ```pyi
x: int = 1 x: int = 1
reveal_type(x) # revealed: Literal[1] reveal_type(x) # revealed: Literal[1]
``` ```

View file

@ -703,7 +703,9 @@ reveal_type(Foo.__class__) # revealed: Literal[type]
## Module attributes ## Module attributes
```py path=mod.py `mod.py`:
```py
global_symbol: str = "a" global_symbol: str = "a"
``` ```
@ -737,13 +739,19 @@ for mod.global_symbol in IntIterable():
## Nested attributes ## Nested attributes
```py path=outer/__init__.py `outer/__init__.py`:
```py
``` ```
```py path=outer/nested/__init__.py `outer/nested/__init__.py`:
```py
``` ```
```py path=outer/nested/inner.py `outer/nested/inner.py`:
```py
class Outer: class Outer:
class Nested: class Nested:
class Inner: class Inner:
@ -766,7 +774,9 @@ outer.nested.inner.Outer.Nested.Inner.attr = "a"
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
functions are instances of that class: functions are instances of that class:
```py path=a.py `a.py`:
```py
def f(): ... def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
@ -775,7 +785,9 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
Some attributes are special-cased, however: Some attributes are special-cased, however:
```py path=b.py `b.py`:
```py
def f(): ... def f(): ...
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions) reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
@ -787,14 +799,18 @@ reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
integers are instances of that class: integers are instances of that class:
```py path=a.py `a.py`:
```py
reveal_type((2).bit_length) # revealed: @Todo(bound method) reveal_type((2).bit_length) # revealed: @Todo(bound method)
reveal_type((2).denominator) # revealed: @Todo(@property) reveal_type((2).denominator) # revealed: @Todo(@property)
``` ```
Some attributes are special-cased, however: Some attributes are special-cased, however:
```py path=b.py `b.py`:
```py
reveal_type((2).numerator) # revealed: Literal[2] reveal_type((2).numerator) # revealed: Literal[2]
reveal_type((2).real) # revealed: Literal[2] reveal_type((2).real) # revealed: Literal[2]
``` ```
@ -804,14 +820,18 @@ reveal_type((2).real) # revealed: Literal[2]
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
bols are instances of that class: bols are instances of that class:
```py path=a.py `a.py`:
```py
reveal_type(True.__and__) # revealed: @Todo(bound method) reveal_type(True.__and__) # revealed: @Todo(bound method)
reveal_type(False.__or__) # revealed: @Todo(bound method) reveal_type(False.__or__) # revealed: @Todo(bound method)
``` ```
Some attributes are special-cased, however: Some attributes are special-cased, however:
```py path=b.py `b.py`:
```py
reveal_type(True.numerator) # revealed: Literal[1] reveal_type(True.numerator) # revealed: Literal[1]
reveal_type(False.real) # revealed: Literal[0] reveal_type(False.real) # revealed: Literal[0]
``` ```

View file

@ -36,7 +36,9 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type
(`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below): (`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below):
```py path=mod.py `mod.py`:
```py
from typing import Any from typing import Any
def any() -> Any: ... def any() -> Any: ...
@ -61,7 +63,9 @@ reveal_type(d) # revealed: int
If a symbol is declared and *possibly* unbound, we trust that other module and use the declared type If a symbol is declared and *possibly* unbound, we trust that other module and use the declared type
without raising an error. without raising an error.
```py path=mod.py `mod.py`:
```py
from typing import Any from typing import Any
def any() -> Any: ... def any() -> Any: ...
@ -93,7 +97,9 @@ reveal_type(d) # revealed: int
Similarly, if a symbol is declared but unbound, we do not raise an error. We trust that this symbol Similarly, if a symbol is declared but unbound, we do not raise an error. We trust that this symbol
is available somehow and simply use the declared type. is available somehow and simply use the declared type.
```py path=mod.py `mod.py`:
```py
from typing import Any from typing import Any
a: int a: int
@ -114,7 +120,9 @@ reveal_type(b) # revealed: Any
If a symbol is possibly undeclared but definitely bound, we use the union of the declared and If a symbol is possibly undeclared but definitely bound, we use the union of the declared and
inferred types: inferred types:
```py path=mod.py `mod.py`:
```py
from typing import Any from typing import Any
def any() -> Any: ... def any() -> Any: ...
@ -151,7 +159,9 @@ inferred types. This case is interesting because the "possibly declared" definit
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import` same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import`
error for both `a` and `b`: error for both `a` and `b`:
```py path=mod.py `mod.py`:
```py
from typing import Any from typing import Any
def flag() -> bool: ... def flag() -> bool: ...
@ -181,7 +191,9 @@ b = None
If a symbol is possibly undeclared and definitely unbound, we currently do not raise an error. This If a symbol is possibly undeclared and definitely unbound, we currently do not raise an error. This
seems inconsistent when compared to the case just above. seems inconsistent when compared to the case just above.
```py path=mod.py `mod.py`:
```py
def flag() -> bool: ... def flag() -> bool: ...
if flag(): if flag():
@ -208,7 +220,9 @@ If a symbol is *undeclared*, we use the union of `Unknown` with the inferred typ
treat this case differently from the case where a symbol is implicitly declared with `Unknown`, treat this case differently from the case where a symbol is implicitly declared with `Unknown`,
possibly due to the usage of an unknown name in the annotation: possibly due to the usage of an unknown name in the annotation:
```py path=mod.py `mod.py`:
```py
# Undeclared: # Undeclared:
a = 1 a = 1
@ -231,7 +245,9 @@ a = None
If a symbol is undeclared and *possibly* unbound, we currently do not raise an error. This seems If a symbol is undeclared and *possibly* unbound, we currently do not raise an error. This seems
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case. inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case.
```py path=mod.py `mod.py`:
```py
def flag() -> bool: ... def flag() -> bool: ...
if flag: if flag:
@ -255,7 +271,9 @@ a = None
If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error. If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
```py path=mod.py `mod.py`:
```py
if False: if False:
a: int = 1 a: int = 1
``` ```

View file

@ -33,7 +33,9 @@ reveal_type(a >= b) # revealed: Literal[False]
Even when tuples have different lengths, comparisons should be handled appropriately. Even when tuples have different lengths, comparisons should be handled appropriately.
```py path=different_length.py `different_length.py`:
```py
a = (1, 2, 3) a = (1, 2, 3)
b = (1, 2, 3, 4) b = (1, 2, 3, 4)
@ -102,7 +104,9 @@ reveal_type(a >= b) # revealed: bool
However, if the lexicographic comparison completes without reaching a point where str and int are However, if the lexicographic comparison completes without reaching a point where str and int are
compared, Python will still produce a result based on the prior elements. compared, Python will still produce a result based on the prior elements.
```py path=short_circuit.py `short_circuit.py`:
```py
a = (1, 2) a = (1, 2)
b = (999999, "hello") b = (999999, "hello")

View file

@ -29,7 +29,9 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite `x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
*after* that redefinition. *after* that redefinition.
```py path=union_type_inferred.py `union_type_inferred.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -50,7 +52,9 @@ reveal_type(x) # revealed: str | Literal[2]
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
inferred as having a union type following the `try`/`except` block: inferred as having a union type following the `try`/`except` block:
```py path=branches_unify_to_non_union_type.py `branches_unify_to_non_union_type.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -133,7 +137,9 @@ the `except` suite:
- At the end of `else`, `x == 3` - At the end of `else`, `x == 3`
- At the end of `except`, `x == 2` - At the end of `except`, `x == 2`
```py path=single_except.py `single_except.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -192,7 +198,9 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
type of `x` at the end of the example is therefore `Literal[2]`: type of `x` at the end of the example is therefore `Literal[2]`:
```py path=redef_in_finally.py `redef_in_finally.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -217,7 +225,9 @@ at this point than there were when we were inside the `finally` block.
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is (Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
still a TODO item for us.) still a TODO item for us.)
```py path=no_redef_in_finally.py `no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -249,7 +259,9 @@ suites:
exception raised in the `except` suite to cause us to jump to the `finally` suite before the exception raised in the `except` suite to cause us to jump to the `finally` suite before the
`except` suite ran to completion `except` suite ran to completion
```py path=redef_in_finally.py `redef_in_finally.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -286,7 +298,9 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
`finally` suite; these lead to the scope's termination following the conclusion of the `finally` `finally` suite; these lead to the scope's termination following the conclusion of the `finally`
suite.) suite.)
```py path=no_redef_in_finally.py `no_redef_in_finally.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -317,7 +331,9 @@ reveal_type(x) # revealed: str | bool
An example with multiple `except` branches and a `finally` branch: An example with multiple `except` branches and a `finally` branch:
```py path=multiple_except_branches.py `multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -364,7 +380,9 @@ If the exception handler has an `else` branch, we must also take into account th
control flow could have jumped to the `finally` suite from partway through the `else` suite due to control flow could have jumped to the `finally` suite from partway through the `else` suite due to
an exception raised *there*. an exception raised *there*.
```py path=single_except_branch.py `single_except_branch.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"
@ -407,7 +425,9 @@ reveal_type(x) # revealed: bool | float
The same again, this time with multiple `except` branches: The same again, this time with multiple `except` branches:
```py path=multiple_except_branches.py `multiple_except_branches.py`:
```py
def could_raise_returns_str() -> str: def could_raise_returns_str() -> str:
return "foo" return "foo"

View file

@ -54,7 +54,9 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
## Evaluates to builtin ## Evaluates to builtin
```py path=a.py `a.py`:
```py
redefined_builtin_bool: type[bool] = bool redefined_builtin_bool: type[bool] = bool
def my_bool(x) -> bool: def my_bool(x) -> bool:

View file

@ -51,7 +51,7 @@ In type stubs, classes can reference themselves in their base class definitions.
This should hold true even with generics at play. This should hold true even with generics at play.
```py path=a.pyi ```pyi
class Seq[T]: ... class Seq[T]: ...
# TODO not error on the subscripting # TODO not error on the subscripting

View file

@ -9,7 +9,9 @@ E = D
reveal_type(E) # revealed: Literal[C] reveal_type(E) # revealed: Literal[C]
``` ```
```py path=b.py `b.py`:
```py
class C: ... class C: ...
``` ```
@ -22,7 +24,9 @@ D = b.C
reveal_type(D) # revealed: Literal[C] reveal_type(D) # revealed: Literal[C]
``` ```
```py path=b.py `b.py`:
```py
class C: ... class C: ...
``` ```
@ -34,10 +38,14 @@ import a.b
reveal_type(a.b.C) # revealed: Literal[C] reveal_type(a.b.C) # revealed: Literal[C]
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b.py `a/b.py`:
```py
class C: ... class C: ...
``` ```
@ -49,13 +57,19 @@ import a.b.c
reveal_type(a.b.c.C) # revealed: Literal[C] reveal_type(a.b.c.C) # revealed: Literal[C]
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b/__init__.py `a/b/__init__.py`:
```py
``` ```
```py path=a/b/c.py `a/b/c.py`:
```py
class C: ... class C: ...
``` ```
@ -67,10 +81,14 @@ import a.b as b
reveal_type(b.C) # revealed: Literal[C] reveal_type(b.C) # revealed: Literal[C]
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b.py `a/b.py`:
```py
class C: ... class C: ...
``` ```
@ -82,13 +100,19 @@ import a.b.c as c
reveal_type(c.C) # revealed: Literal[C] reveal_type(c.C) # revealed: Literal[C]
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b/__init__.py `a/b/__init__.py`:
```py
``` ```
```py path=a/b/c.py `a/b/c.py`:
```py
class C: ... class C: ...
``` ```
@ -102,5 +126,7 @@ import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`" import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```

View file

@ -29,13 +29,17 @@ builtins from the "actual" vendored typeshed:
typeshed = "/typeshed" typeshed = "/typeshed"
``` ```
```pyi path=/typeshed/stdlib/builtins.pyi `/typeshed/stdlib/builtins.pyi`:
```pyi
class Custom: ... class Custom: ...
custom_builtin: Custom custom_builtin: Custom
``` ```
```pyi path=/typeshed/stdlib/typing_extensions.pyi `/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ... def reveal_type(obj, /): ...
``` ```
@ -56,12 +60,16 @@ that point:
typeshed = "/typeshed" typeshed = "/typeshed"
``` ```
```pyi path=/typeshed/stdlib/builtins.pyi `/typeshed/stdlib/builtins.pyi`:
```pyi
foo = bar foo = bar
bar = 1 bar = 1
``` ```
```pyi path=/typeshed/stdlib/typing_extensions.pyi `/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ... def reveal_type(obj, /): ...
``` ```

View file

@ -2,7 +2,9 @@
## Maybe unbound ## Maybe unbound
```py path=maybe_unbound.py `maybe_unbound.py`:
```py
def coinflip() -> bool: def coinflip() -> bool:
return True return True
@ -29,7 +31,9 @@ reveal_type(y) # revealed: Unknown | Literal[3]
## Maybe unbound annotated ## Maybe unbound annotated
```py path=maybe_unbound_annotated.py `maybe_unbound_annotated.py`:
```py
def coinflip() -> bool: def coinflip() -> bool:
return True return True
@ -60,7 +64,9 @@ reveal_type(y) # revealed: int
Importing a possibly undeclared name still gives us its declared type: Importing a possibly undeclared name still gives us its declared type:
```py path=maybe_undeclared.py `maybe_undeclared.py`:
```py
def coinflip() -> bool: def coinflip() -> bool:
return True return True
@ -76,11 +82,15 @@ reveal_type(x) # revealed: int
## Reimport ## Reimport
```py path=c.py `c.py`:
```py
def f(): ... def f(): ...
``` ```
```py path=b.py `b.py`:
```py
def coinflip() -> bool: def coinflip() -> bool:
return True return True
@ -102,11 +112,15 @@ reveal_type(f) # revealed: Literal[f, f]
When we have a declared type in one path and only an inferred-from-definition type in the other, we When we have a declared type in one path and only an inferred-from-definition type in the other, we
should still be able to unify those: should still be able to unify those:
```py path=c.pyi `c.pyi`:
```pyi
x: int x: int
``` ```
```py path=b.py `b.py`:
```py
def coinflip() -> bool: def coinflip() -> bool:
return True return True

View file

@ -8,11 +8,15 @@ import a.b
reveal_type(a.b) # revealed: <module 'a.b'> reveal_type(a.b) # revealed: <module 'a.b'>
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
b: int = 42 b: int = 42
``` ```
```py path=a/b.py `a/b.py`:
```py
``` ```
## Via from/import ## Via from/import
@ -23,11 +27,15 @@ from a import b
reveal_type(b) # revealed: int reveal_type(b) # revealed: int
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
b: int = 42 b: int = 42
``` ```
```py path=a/b.py `a/b.py`:
```py
``` ```
## Via both ## Via both
@ -40,11 +48,15 @@ reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'> reveal_type(a.b) # revealed: <module 'a.b'>
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
b: int = 42 b: int = 42
``` ```
```py path=a/b.py `a/b.py`:
```py
``` ```
## Via both (backwards) ## Via both (backwards)
@ -65,11 +77,15 @@ reveal_type(b) # revealed: <module 'a.b'>
reveal_type(a.b) # revealed: <module 'a.b'> reveal_type(a.b) # revealed: <module 'a.b'>
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
b: int = 42 b: int = 42
``` ```
```py path=a/b.py `a/b.py`:
```py
``` ```
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement [from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement

View file

@ -18,7 +18,9 @@ reveal_type(baz) # revealed: Unknown
## Unresolved import from resolved module ## Unresolved import from resolved module
```py path=a.py `a.py`:
```py
``` ```
```py ```py
@ -29,7 +31,9 @@ reveal_type(thing) # revealed: Unknown
## Resolved import of symbol from unresolved import ## Resolved import of symbol from unresolved import
```py path=a.py `a.py`:
```py
import foo as foo # error: "Cannot resolve import `foo`" import foo as foo # error: "Cannot resolve import `foo`"
reveal_type(foo) # revealed: Unknown reveal_type(foo) # revealed: Unknown
@ -46,7 +50,9 @@ reveal_type(foo) # revealed: Unknown
## No implicit shadowing ## No implicit shadowing
```py path=b.py `b.py`:
```py
x: int x: int
``` ```
@ -58,7 +64,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
## Import cycle ## Import cycle
```py path=a.py `a.py`:
```py
class A: ... class A: ...
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]] reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
@ -69,7 +77,9 @@ class C(b.B): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]] reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
``` ```
```py path=b.py `b.py`:
```py
from a import A from a import A
class B(A): ... class B(A): ...

View file

@ -23,9 +23,13 @@ reveal_type(b) # revealed: <module 'a.b'>
reveal_type(b.c) # revealed: int reveal_type(b.c) # revealed: int
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b.py `a/b.py`:
```py
c: int = 1 c: int = 1
``` ```

View file

@ -2,10 +2,14 @@
## Non-existent ## Non-existent
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from .foo import X # error: [unresolved-import] from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown reveal_type(X) # revealed: Unknown
@ -13,14 +17,20 @@ reveal_type(X) # revealed: Unknown
## Simple ## Simple
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/foo.py `package/foo.py`:
```py
X: int = 42 X: int = 42
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from .foo import X from .foo import X
reveal_type(X) # revealed: int reveal_type(X) # revealed: int
@ -28,14 +38,20 @@ reveal_type(X) # revealed: int
## Dotted ## Dotted
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/foo/bar/baz.py `package/foo/bar/baz.py`:
```py
X: int = 42 X: int = 42
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from .foo.bar.baz import X from .foo.bar.baz import X
reveal_type(X) # revealed: int reveal_type(X) # revealed: int
@ -43,11 +59,15 @@ reveal_type(X) # revealed: int
## Bare to package ## Bare to package
```py path=package/__init__.py `package/__init__.py`:
```py
X: int = 42 X: int = 42
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from . import X from . import X
reveal_type(X) # revealed: int reveal_type(X) # revealed: int
@ -55,7 +75,9 @@ reveal_type(X) # revealed: int
## Non-existent + bare to package ## Non-existent + bare to package
```py path=package/bar.py `package/bar.py`:
```py
from . import X # error: [unresolved-import] from . import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown reveal_type(X) # revealed: Unknown
@ -63,19 +85,25 @@ reveal_type(X) # revealed: Unknown
## Dunder init ## Dunder init
```py path=package/__init__.py `package/__init__.py`:
```py
from .foo import X from .foo import X
reveal_type(X) # revealed: int reveal_type(X) # revealed: int
``` ```
```py path=package/foo.py `package/foo.py`:
```py
X: int = 42 X: int = 42
``` ```
## Non-existent + dunder init ## Non-existent + dunder init
```py path=package/__init__.py `package/__init__.py`:
```py
from .foo import X # error: [unresolved-import] from .foo import X # error: [unresolved-import]
reveal_type(X) # revealed: Unknown reveal_type(X) # revealed: Unknown
@ -83,14 +111,20 @@ reveal_type(X) # revealed: Unknown
## Long relative import ## Long relative import
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/foo.py `package/foo.py`:
```py
X: int = 42 X: int = 42
``` ```
```py path=package/subpackage/subsubpackage/bar.py `package/subpackage/subsubpackage/bar.py`:
```py
from ...foo import X from ...foo import X
reveal_type(X) # revealed: int reveal_type(X) # revealed: int
@ -98,14 +132,20 @@ reveal_type(X) # revealed: int
## Unbound symbol ## Unbound symbol
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/foo.py `package/foo.py`:
```py
x # error: [unresolved-reference] x # error: [unresolved-reference]
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from .foo import x # error: [unresolved-import] from .foo import x # error: [unresolved-import]
reveal_type(x) # revealed: Unknown reveal_type(x) # revealed: Unknown
@ -113,14 +153,20 @@ reveal_type(x) # revealed: Unknown
## Bare to module ## Bare to module
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/foo.py `package/foo.py`:
```py
X: int = 42 X: int = 42
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from . import foo from . import foo
reveal_type(foo.X) # revealed: int reveal_type(foo.X) # revealed: int
@ -131,10 +177,14 @@ reveal_type(foo.X) # revealed: int
This test verifies that we emit an error when we try to import a symbol that is neither a submodule This test verifies that we emit an error when we try to import a symbol that is neither a submodule
nor an attribute of `package`. nor an attribute of `package`.
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from . import foo # error: [unresolved-import] from . import foo # error: [unresolved-import]
reveal_type(foo) # revealed: Unknown reveal_type(foo) # revealed: Unknown
@ -148,14 +198,20 @@ submodule when that submodule name appears in the `imported_modules` set. That m
that are imported via `from...import` are not visible to our type inference if you also access that that are imported via `from...import` are not visible to our type inference if you also access that
submodule via the attribute on its parent package. submodule via the attribute on its parent package.
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/foo.py `package/foo.py`:
```py
X: int = 42 X: int = 42
``` ```
```py path=package/bar.py `package/bar.py`:
```py
from . import foo from . import foo
import package import package

View file

@ -9,7 +9,9 @@ y = x
reveal_type(y) # revealed: int reveal_type(y) # revealed: int
``` ```
```py path=b.pyi `b.pyi`:
```pyi
x: int x: int
``` ```
@ -22,6 +24,8 @@ y = x
reveal_type(y) # revealed: int reveal_type(y) # revealed: int
``` ```
```py path=b.py `b.py`:
```py
x: int = 1 x: int = 1
``` ```

View file

@ -32,10 +32,14 @@ reveal_type(a.b.C) # revealed: Literal[C]
import a.b import a.b
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b.py `a/b.py`:
```py
class C: ... class C: ...
``` ```
@ -55,14 +59,20 @@ reveal_type(a.b) # revealed: <module 'a.b'>
reveal_type(a.b.C) # revealed: Literal[C] reveal_type(a.b.C) # revealed: Literal[C]
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b.py `a/b.py`:
```py
class C: ... class C: ...
``` ```
```py path=q.py `q.py`:
```py
import a as a import a as a
import a.b as b import a.b as b
``` ```
@ -83,18 +93,26 @@ reveal_type(sub.b) # revealed: <module 'sub.b'>
reveal_type(attr.b) # revealed: <module 'attr.b'> reveal_type(attr.b) # revealed: <module 'attr.b'>
``` ```
```py path=sub/__init__.py `sub/__init__.py`:
```py
b = 1 b = 1
``` ```
```py path=sub/b.py `sub/b.py`:
```py
``` ```
```py path=attr/__init__.py `attr/__init__.py`:
```py
from . import b as _ from . import b as _
b = 1 b = 1
``` ```
```py path=attr/b.py `attr/b.py`:
```py
``` ```

View file

@ -31,7 +31,9 @@ reveal_type(TC) # revealed: Literal[True]
Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants
with the same name: with the same name:
```py path=constants.py `constants.py`:
```py
TYPE_CHECKING: bool = False TYPE_CHECKING: bool = False
``` ```

View file

@ -19,13 +19,17 @@ typeshed = "/typeshed"
We can then place custom stub files in `/typeshed/stdlib`, for example: We can then place custom stub files in `/typeshed/stdlib`, for example:
```pyi path=/typeshed/stdlib/builtins.pyi `/typeshed/stdlib/builtins.pyi`:
```pyi
class BuiltinClass: ... class BuiltinClass: ...
builtin_symbol: BuiltinClass builtin_symbol: BuiltinClass
``` ```
```pyi path=/typeshed/stdlib/sys/__init__.pyi `/typeshed/stdlib/sys/__init__.pyi`:
```pyi
version = "my custom Python" version = "my custom Python"
``` ```
@ -54,15 +58,21 @@ python-version = "3.10"
typeshed = "/typeshed" typeshed = "/typeshed"
``` ```
```pyi path=/typeshed/stdlib/old_module.pyi `/typeshed/stdlib/old_module.pyi`:
```pyi
class OldClass: ... class OldClass: ...
``` ```
```pyi path=/typeshed/stdlib/new_module.pyi `/typeshed/stdlib/new_module.pyi`:
```pyi
class NewClass: ... class NewClass: ...
``` ```
```text path=/typeshed/stdlib/VERSIONS `/typeshed/stdlib/VERSIONS`:
```text
old_module: 3.0- old_module: 3.0-
new_module: 3.11- new_module: 3.11-
``` ```
@ -86,7 +96,9 @@ simple untyped definition is enough to make `reveal_type` work in tests:
typeshed = "/typeshed" typeshed = "/typeshed"
``` ```
```pyi path=/typeshed/stdlib/typing_extensions.pyi `/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ... def reveal_type(obj, /): ...
``` ```

View file

@ -205,7 +205,7 @@ reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop. Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
```py path=a.pyi ```pyi
class A(B): ... # error: [cyclic-class-definition] class A(B): ... # error: [cyclic-class-definition]
class B(C): ... # error: [cyclic-class-definition] class B(C): ... # error: [cyclic-class-definition]
class C(A): ... # error: [cyclic-class-definition] class C(A): ... # error: [cyclic-class-definition]

View file

@ -347,7 +347,7 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
These are invalid, but we need to be able to handle them gracefully without panicking. These are invalid, but we need to be able to handle them gracefully without panicking.
```py path=a.pyi ```pyi
class Foo(Foo): ... # error: [cyclic-class-definition] class Foo(Foo): ... # error: [cyclic-class-definition]
reveal_type(Foo) # revealed: Literal[Foo] reveal_type(Foo) # revealed: Literal[Foo]
@ -365,7 +365,7 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
These are similarly unlikely, but we still shouldn't crash: These are similarly unlikely, but we still shouldn't crash:
```py path=a.pyi ```pyi
class Foo(Bar): ... # error: [cyclic-class-definition] class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition] class Bar(Baz): ... # error: [cyclic-class-definition]
class Baz(Foo): ... # error: [cyclic-class-definition] class Baz(Foo): ... # error: [cyclic-class-definition]
@ -377,7 +377,7 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
## Classes with cycles in their MROs, and multiple inheritance ## Classes with cycles in their MROs, and multiple inheritance
```py path=a.pyi ```pyi
class Spam: ... class Spam: ...
class Foo(Bar): ... # error: [cyclic-class-definition] class Foo(Bar): ... # error: [cyclic-class-definition]
class Bar(Baz): ... # error: [cyclic-class-definition] class Bar(Baz): ... # error: [cyclic-class-definition]
@ -390,7 +390,7 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
## Classes with cycles in their MRO, and a sub-graph ## Classes with cycles in their MRO, and a sub-graph
```py path=a.pyi ```pyi
class FooCycle(BarCycle): ... # error: [cyclic-class-definition] class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
class Foo: ... class Foo: ...
class BarCycle(FooCycle): ... # error: [cyclic-class-definition] class BarCycle(FooCycle): ... # error: [cyclic-class-definition]

View file

@ -2,12 +2,16 @@
Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334). Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334).
```py path=base.py `base.py`:
```py
# error: [invalid-base] # error: [invalid-base]
class Base(2): ... class Base(2): ...
``` ```
```py path=a.py `a.py`:
```py
# No error here # No error here
from base import Base from base import Base
``` ```

View file

@ -29,7 +29,9 @@ def foo():
However, three attributes on `types.ModuleType` are not present as implicit module globals; these However, three attributes on `types.ModuleType` are not present as implicit module globals; these
are excluded: are excluded:
```py path=unbound_dunders.py `unbound_dunders.py`:
```py
# error: [unresolved-reference] # error: [unresolved-reference]
# revealed: Unknown # revealed: Unknown
reveal_type(__getattr__) reveal_type(__getattr__)
@ -70,7 +72,9 @@ Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType`
dynamic imports; but we ignore that for module-literal types where we know exactly which module dynamic imports; but we ignore that for module-literal types where we know exactly which module
we're dealing with: we're dealing with:
```py path=__getattr__.py `__getattr__.py`:
```py
import typing import typing
# error: [unresolved-attribute] # error: [unresolved-attribute]
@ -83,13 +87,17 @@ It's impossible to override the `__dict__` attribute of `types.ModuleType` insta
module; we should prioritise the attribute in the `types.ModuleType` stub over a variable named module; we should prioritise the attribute in the `types.ModuleType` stub over a variable named
`__dict__` in the module's global namespace: `__dict__` in the module's global namespace:
```py path=foo.py `foo.py`:
```py
__dict__ = "foo" __dict__ = "foo"
reveal_type(__dict__) # revealed: Literal["foo"] reveal_type(__dict__) # revealed: Literal["foo"]
``` ```
```py path=bar.py `bar.py`:
```py
import foo import foo
from foo import __dict__ as foo_dict from foo import __dict__ as foo_dict

View file

@ -5,14 +5,18 @@
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function. Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
No diagnostics should be generated. No diagnostics should be generated.
```py path=a.py `a.py`:
```py
def f(x: str): def f(x: str):
x: int = int(x) x: int = int(x)
``` ```
## Implicit error ## Implicit error
```py path=a.py `a.py`:
```py
def f(): ... def f(): ...
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional" f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
@ -20,7 +24,9 @@ f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explici
## Explicit shadowing ## Explicit shadowing
```py path=a.py `a.py`:
```py
def f(): ... def f(): ...
f: int = 1 f: int = 1

View file

@ -7,7 +7,9 @@ branches whose conditions we can statically determine to be always true or alway
useful for `sys.version_info` branches, which can make new features available based on the Python useful for `sys.version_info` branches, which can make new features available based on the Python
version: version:
```py path=module1.py `module1.py`:
```py
import sys import sys
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):
@ -17,7 +19,9 @@ if sys.version_info >= (3, 9):
If we can statically determine that the condition is always true, then we can also understand that If we can statically determine that the condition is always true, then we can also understand that
`SomeFeature` is always bound, without raising any errors: `SomeFeature` is always bound, without raising any errors:
```py path=test1.py `test1.py`:
```py
from module1 import SomeFeature from module1 import SomeFeature
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer: # SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
@ -27,11 +31,15 @@ reveal_type(SomeFeature) # revealed: str
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used
for conditional imports: for conditional imports:
```py path=module2.py `module2.py`:
```py
class SomeType: ... class SomeType: ...
``` ```
```py path=test2.py `test2.py`:
```py
import typing import typing
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -167,7 +175,9 @@ statically known conditions, but here, we show that the results are truly based
not some special handling of specific conditions in semantic index building. We use two modules to not some special handling of specific conditions in semantic index building. We use two modules to
demonstrate this, since semantic index building is inherently single-module: demonstrate this, since semantic index building is inherently single-module:
```py path=module.py `module.py`:
```py
from typing import Literal from typing import Literal
class AlwaysTrue: class AlwaysTrue:
@ -1426,7 +1436,9 @@ def f():
#### Always false, unbound #### Always false, unbound
```py path=module.py `module.py`:
```py
if False: if False:
symbol = 1 symbol = 1
``` ```
@ -1438,7 +1450,9 @@ from module import symbol
#### Always true, bound #### Always true, bound
```py path=module.py `module.py`:
```py
if True: if True:
symbol = 1 symbol = 1
``` ```
@ -1450,7 +1464,9 @@ from module import symbol
#### Ambiguous, possibly unbound #### Ambiguous, possibly unbound
```py path=module.py `module.py`:
```py
def flag() -> bool: def flag() -> bool:
return True return True
@ -1465,7 +1481,9 @@ from module import symbol
#### Always false, undeclared #### Always false, undeclared
```py path=module.py `module.py`:
```py
if False: if False:
symbol: int symbol: int
``` ```
@ -1479,7 +1497,9 @@ reveal_type(symbol) # revealed: Unknown
#### Always true, declared #### Always true, declared
```py path=module.py `module.py`:
```py
if True: if True:
symbol: int symbol: int
``` ```

View file

@ -5,7 +5,7 @@
In type stubs, classes can reference themselves in their base class definitions. For example, in In type stubs, classes can reference themselves in their base class definitions. For example, in
`typeshed`, we have `class str(Sequence[str]): ...`. `typeshed`, we have `class str(Sequence[str]): ...`.
```py path=a.pyi ```pyi
class Foo[T]: ... class Foo[T]: ...
# TODO: actually is subscriptable # TODO: actually is subscriptable

View file

@ -5,7 +5,7 @@
The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a
stub file only, regardless of the type of the parameter. stub file only, regardless of the type of the parameter.
```py path=test.pyi ```pyi
def f(x: int = ...) -> None: def f(x: int = ...) -> None:
reveal_type(x) # revealed: int reveal_type(x) # revealed: int
@ -18,7 +18,7 @@ def f2(x: str = ...) -> None:
The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type, The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type,
in a stub file only. in a stub file only.
```py path=test.pyi ```pyi
y: bytes = ... y: bytes = ...
reveal_type(y) # revealed: bytes reveal_type(y) # revealed: bytes
x = ... x = ...
@ -35,7 +35,7 @@ reveal_type(Foo.y) # revealed: int
No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an
assignment statement: assignment statement:
```py path=test.pyi ```pyi
x, y = ... x, y = ...
reveal_type(x) # revealed: Unknown reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: Unknown reveal_type(y) # revealed: Unknown
@ -46,7 +46,7 @@ reveal_type(y) # revealed: Unknown
Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and
results in a diagnostic: results in a diagnostic:
```py path=test.pyi ```pyi
# error: [not-iterable] "Object of type `ellipsis` is not iterable" # error: [not-iterable] "Object of type `ellipsis` is not iterable"
for a, b in ...: for a, b in ...:
reveal_type(a) # revealed: Unknown reveal_type(a) # revealed: Unknown
@ -72,7 +72,7 @@ reveal_type(b) # revealed: ellipsis
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals. There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
```py path=test.pyi ```pyi
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`" # error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
def f(x: int = Ellipsis) -> None: ... def f(x: int = Ellipsis) -> None: ...
``` ```

View file

@ -37,7 +37,9 @@ child expression now suppresses errors in the outer expression.
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
`"test"` and adding `"other"` to the result of the cast. `"test"` and adding `"other"` to the result of the cast.
```py path=nested.py `nested.py`:
```py
# fmt: off # fmt: off
from typing import cast from typing import cast

View file

@ -86,14 +86,20 @@ reveal_type(bar >= (3, 9)) # revealed: Literal[True]
Only comparisons with the symbol `version_info` from the `sys` module produce literal types: Only comparisons with the symbol `version_info` from the `sys` module produce literal types:
```py path=package/__init__.py `package/__init__.py`:
```py
``` ```
```py path=package/sys.py `package/sys.py`:
```py
version_info: tuple[int, int] = (4, 2) version_info: tuple[int, int] = (4, 2)
``` ```
```py path=package/script.py `package/script.py`:
```py
from .sys import version_info from .sys import version_info
reveal_type(version_info >= (3, 9)) # revealed: bool reveal_type(version_info >= (3, 9)) # revealed: bool
@ -103,7 +109,9 @@ reveal_type(version_info >= (3, 9)) # revealed: bool
The fields of `sys.version_info` can be accessed by name: The fields of `sys.version_info` can be accessed by name:
```py path=a.py `a.py`:
```py
import sys import sys
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True] reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
@ -114,7 +122,9 @@ reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
properties on instance types: properties on instance types:
```py path=b.py `b.py`:
```py
import sys import sys
reveal_type(sys.version_info.micro) # revealed: @Todo(@property) reveal_type(sys.version_info.micro) # revealed: @Todo(@property)

View file

@ -39,7 +39,9 @@ def f(c: type[A]):
reveal_type(c) # revealed: type[A] reveal_type(c) # revealed: type[A]
``` ```
```py path=a.py `a.py`:
```py
class A: ... class A: ...
``` ```
@ -52,23 +54,31 @@ def f(c: type[a.B]):
reveal_type(c) # revealed: type[B] reveal_type(c) # revealed: type[B]
``` ```
```py path=a.py `a.py`:
```py
class B: ... class B: ...
``` ```
## Deeply qualified class literal from another module ## Deeply qualified class literal from another module
```py path=a/test.py `a/test.py`:
```py
import a.b import a.b
def f(c: type[a.b.C]): def f(c: type[a.b.C]):
reveal_type(c) # revealed: type[C] reveal_type(c) # revealed: type[C]
``` ```
```py path=a/__init__.py `a/__init__.py`:
```py
``` ```
```py path=a/b.py `a/b.py`:
```py
class C: ... class C: ...
``` ```

View file

@ -28,7 +28,9 @@ reveal_type(not b) # revealed: Literal[False]
reveal_type(not warnings) # revealed: Literal[False] reveal_type(not warnings) # revealed: Literal[False]
``` ```
```py path=b.py `b.py`:
```py
y = 1 y = 1
``` ```

View file

@ -20,10 +20,10 @@ reveal_type(1) # revealed: Literal[1]
```` ````
When running this test, the mdtest framework will write a file with these contents to the default When running this test, the mdtest framework will write a file with these contents to the default
file path (`/src/test.py`) in its in-memory file system, run a type check on that file, and then file path (`/src/mdtest_snippet__1.py`) in its in-memory file system, run a type check on that file,
match the resulting diagnostics with the assertions in the test. Assertions are in the form of and then match the resulting diagnostics with the assertions in the test. Assertions are in the form
Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, it of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise,
fails. it fails.
<!--- <!---
(If you are reading this document in raw Markdown source rather than rendered Markdown, note that (If you are reading this document in raw Markdown source rather than rendered Markdown, note that
@ -129,8 +129,12 @@ assertion as the line of source code on which the matched diagnostics are emitte
## Multi-file tests ## Multi-file tests
Some tests require multiple files, with imports from one file into another. Multiple fenced code Some tests require multiple files, with imports from one file into another. Multiple fenced code
blocks represent multiple embedded files. Since files must have unique names, at most one file can blocks represent multiple embedded files. If there are multiple unnamed files, mdtest will name them
use the default name of `/src/test.py`. Other files must explicitly specify their file name: according to the numbered scheme `/src/mdtest_snippet__1.py`, `/src/mdtest_snippet__2.py`, etc. (If
they are `pyi` files, they will be named with a `pyi` extension instead.)
Tests should not rely on these default names. If a test must import from a file, then it should
explicitly specify the file name:
````markdown ````markdown
```py ```py
@ -138,7 +142,9 @@ from b import C
reveal_type(C) # revealed: Literal[C] reveal_type(C) # revealed: Literal[C]
``` ```
```py path=b.py `b.py`:
```py
class C: pass class C: pass
``` ```
```` ````
@ -149,8 +155,8 @@ is, the equivalent of a runtime entry on `sys.path`).
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
this is a feature we will want to add in the future. this is a feature we will want to add in the future.
So the above test creates two files, `/src/test.py` and `/src/b.py`, and sets the workspace root to So the above test creates two files, `/src/mdtest_snippet__1.py` and `/src/b.py`, and sets the
`/src/`, allowing `test.py` to import from `b.py` using the module name `b`. workspace root to `/src/`, allowing imports from `b.py` using the module name `b`.
## Multi-test suites ## Multi-test suites
@ -171,7 +177,9 @@ from b import y
x: int = y # error: [invalid-assignment] x: int = y # error: [invalid-assignment]
``` ```
```py path=b.py `b.py`:
```py
y = "foo" y = "foo"
``` ```
```` ````
@ -357,17 +365,17 @@ This is just an example, not a proposal that red-knot would ever actually output
precisely this format: precisely this format:
```output ```output
test.py, line 1, col 1: revealed type is 'Literal[1]' mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
``` ```
```` ````
We will want to build tooling to automatically capture and update these “full diagnostic output” We will want to build tooling to automatically capture and update these “full diagnostic output”
blocks, when tests are run in an update-output mode (probably specified by an environment variable.) blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
By default, an `output` block will specify diagnostic output for the file `<workspace-root>/test.py`. By default, an `output` block will specify diagnostic output for the file
An `output` block can have a `path=` option, to explicitly specify the Python file for which it `<workspace-root>/mdtest_snippet__1.py`. An `output` block can be prefixed by a
asserts diagnostic output, and a `stage=` option, to specify which stage of an incremental test it <code>`&lt;path>`:</code> label as usual, to explicitly specify the Python file for which it asserts
specifies diagnostic output at. (See “incremental tests” below.) diagnostic output.
It is an error for an `output` block to exist, if there is no `py` or `python` block in the same It is an error for an `output` block to exist, if there is no `py` or `python` block in the same
test for the same file path. test for the same file path.
@ -385,39 +393,43 @@ fenced code blocks in the test:
## modify a file ## modify a file
Initial version of `test.py` and `b.py`: Initial file contents:
```py ```py
from b import x from b import x
reveal_type(x) reveal_type(x)
``` ```
```py path=b.py `b.py`:
```py
x = 1 x = 1
``` ```
Initial expected output for `test.py`: Initial expected output for the unnamed file:
```output ```output
/src/test.py, line 1, col 1: revealed type is 'Literal[1]' /src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[1]'
``` ```
Now in our first incremental stage, modify the contents of `b.py`: Now in our first incremental stage, modify the contents of `b.py`:
```py path=b.py stage=1 `b.py`:
```py stage=1
# b.py # b.py
x = 2 x = 2
``` ```
And this is our updated expected output for `test.py` at stage 1: And this is our updated expected output for the unnamed file at stage 1:
```output stage=1 ```output stage=1
/src/test.py, line 1, col 1: revealed type is 'Literal[2]' /src/mdtest_snippet__1.py, line 1, col 1: revealed type is 'Literal[2]'
``` ```
(One reason to use full-diagnostic-output blocks in this test is that updating (One reason to use full-diagnostic-output blocks in this test is that updating inline-comment
inline-comment diagnostic assertions for `test.py` would require specifying new diagnostic assertions for `mdtest_snippet__1.py` would require specifying new contents for
contents for `test.py` in stage 1, which we don't want to do in this test.) `mdtest_snippet__1.py` in stage 1, which we don't want to do in this test.)
```` ````
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies It will be possible to provide any number of stages in an incremental test. If a stage re-specifies

View file

@ -109,9 +109,9 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
); );
let full_path = if embedded.path.starts_with('/') { let full_path = if embedded.path.starts_with('/') {
SystemPathBuf::from(embedded.path) SystemPathBuf::from(embedded.path.clone())
} else { } else {
project_root.join(embedded.path) project_root.join(&embedded.path)
}; };
if let Some(ref typeshed_path) = custom_typeshed_path { if let Some(ref typeshed_path) = custom_typeshed_path {

View file

@ -3,7 +3,7 @@ use std::sync::LazyLock;
use anyhow::bail; use anyhow::bail;
use memchr::memchr2; use memchr::memchr2;
use regex::{Captures, Match, Regex}; use regex::{Captures, Match, Regex};
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::FxHashSet;
use ruff_index::{newtype_index, IndexVec}; use ruff_index::{newtype_index, IndexVec};
use ruff_python_trivia::Cursor; use ruff_python_trivia::Cursor;
@ -147,7 +147,7 @@ struct EmbeddedFileId;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct EmbeddedFile<'s> { pub(crate) struct EmbeddedFile<'s> {
section: SectionId, section: SectionId,
pub(crate) path: &'s str, pub(crate) path: String,
pub(crate) lang: &'s str, pub(crate) lang: &'s str,
pub(crate) code: &'s str, pub(crate) code: &'s str,
@ -157,16 +157,20 @@ pub(crate) struct EmbeddedFile<'s> {
/// Matches a sequence of `#` characters, followed by a title heading, followed by a newline. /// Matches a sequence of `#` characters, followed by a title heading, followed by a newline.
static HEADER_RE: LazyLock<Regex> = static HEADER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(?<level>#+)\s+(?<title>.+)\s*\n").unwrap()); LazyLock::new(|| Regex::new(r"^(?<level>#+)[^\S\n]+(?<title>.+)[^\S\n]*\n").unwrap());
/// Matches a code block fenced by triple backticks, possibly with language and `key=val` /// Matches a code block fenced by triple backticks, possibly with language and `key=val`
/// configuration items following the opening backticks (in the "tag string" of the code block). /// configuration items following the opening backticks (in the "tag string" of the code block).
static CODE_RE: LazyLock<Regex> = LazyLock::new(|| { static CODE_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new( Regex::new(
r"(?x) r"(?x)
^```(?<lang>(?-u:\w)+)?(?<config>(?:\x20+\S+)*)\s*\n ^(?:
(?<code>(?:.|\n)*?)\n? `(?<path>[^`\n]+)`[^\S\n]*:[^\S\n]*\n
(?<end>```|\z) \n?
)?
```(?<lang>(?-u:\w)+)?\x20*(?<config>\S.*)?\n
(?<code>[\s\S]*?)\n?
(?<end>```\n?|\z)
", ",
) )
.unwrap() .unwrap()
@ -210,6 +214,7 @@ struct Parser<'s> {
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`]. /// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>, files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
unnamed_file_count: usize,
/// The unparsed remainder of the Markdown source. /// The unparsed remainder of the Markdown source.
cursor: Cursor<'s>, cursor: Cursor<'s>,
@ -221,7 +226,7 @@ struct Parser<'s> {
stack: SectionStack, stack: SectionStack,
/// Names of embedded files in current active section. /// Names of embedded files in current active section.
current_section_files: Option<FxHashSet<&'s str>>, current_section_files: Option<FxHashSet<String>>,
/// Whether or not the current section has a config block. /// Whether or not the current section has a config block.
current_section_has_config: bool, current_section_has_config: bool,
@ -240,6 +245,7 @@ impl<'s> Parser<'s> {
sections, sections,
source, source,
files: IndexVec::default(), files: IndexVec::default(),
unnamed_file_count: 0,
cursor: Cursor::new(source), cursor: Cursor::new(source),
source_len: source.text_len(), source_len: source.text_len(),
stack: SectionStack::new(root_section_id), stack: SectionStack::new(root_section_id),
@ -265,10 +271,14 @@ impl<'s> Parser<'s> {
fn parse_impl(&mut self) -> anyhow::Result<()> { fn parse_impl(&mut self) -> anyhow::Result<()> {
while let Some(position) = memchr2(b'`', b'#', self.cursor.as_bytes()) { while let Some(position) = memchr2(b'`', b'#', self.cursor.as_bytes()) {
self.cursor.skip_bytes(position.saturating_sub(1)); self.cursor.skip_bytes(position.saturating_sub(2));
// code blocks and headers must start on a new line. // Code blocks and headers must start on a new line
if position == 0 || self.cursor.eat_char('\n') { // and preceded by at least one blank line.
if position == 0 && self.cursor.first() == '#'
|| position == 1 && self.cursor.eat_char('\n')
|| self.cursor.eat_char('\n') && self.cursor.eat_char('\n')
{
match self.cursor.first() { match self.cursor.first() {
'#' => { '#' => {
if let Some(find) = HEADER_RE.find(self.cursor.as_str()) { if let Some(find) = HEADER_RE.find(self.cursor.as_str()) {
@ -290,7 +300,7 @@ impl<'s> Parser<'s> {
// Skip to the end of the line // Skip to the end of the line
if let Some(position) = memchr::memchr(b'\n', self.cursor.as_bytes()) { if let Some(position) = memchr::memchr(b'\n', self.cursor.as_bytes()) {
self.cursor.skip_bytes(position); self.cursor.skip_bytes(position + 1);
} else { } else {
break; break;
} }
@ -349,26 +359,10 @@ impl<'s> Parser<'s> {
return Err(anyhow::anyhow!("Unterminated code block at line {line}.")); return Err(anyhow::anyhow!("Unterminated code block at line {line}."));
} }
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default(); if captures.name("config").is_some() {
return Err(anyhow::anyhow!("Trailing code-block metadata is not supported. Only the code block language can be specified."));
if let Some(config_match) = captures.name("config") {
for item in config_match.as_str().split_whitespace() {
let mut parts = item.split('=');
let key = parts.next().unwrap();
let Some(val) = parts.next() else {
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
};
if parts.next().is_some() {
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
}
if config.insert(key, val).is_some() {
return Err(anyhow::anyhow!("Duplicate config item `{}`.", item));
}
}
} }
let path = config.get("path").copied().unwrap_or("test.py");
// CODE_RE can't match without matches for 'lang' and 'code'. // CODE_RE can't match without matches for 'lang' and 'code'.
let lang = captures let lang = captures
.name("lang") .name("lang")
@ -381,26 +375,48 @@ impl<'s> Parser<'s> {
return self.parse_config(code); return self.parse_config(code);
} }
let explicit_path = captures.name("path").map(|it| it.as_str());
if let Some(explicit_path) = explicit_path {
if !lang.is_empty()
&& lang != "text"
&& explicit_path.contains('.')
&& !explicit_path.ends_with(&format!(".{lang}"))
{
return Err(anyhow::anyhow!(
"File ending of test file path `{explicit_path}` does not match `lang={lang}` of code block"
));
}
}
let path = match explicit_path {
Some(path) => path.to_string(),
None => {
self.unnamed_file_count += 1;
match lang {
"py" | "pyi" => format!("mdtest_snippet__{}.{lang}", self.unnamed_file_count),
"" => format!("mdtest_snippet__{}.py", self.unnamed_file_count),
_ => {
return Err(anyhow::anyhow!(
"Cannot generate name for `lang={}`: Unsupported extension",
lang
))
}
}
}
};
self.files.push(EmbeddedFile { self.files.push(EmbeddedFile {
path, path: path.clone(),
section, section,
lang, lang,
code, code,
md_offset: self.offset(), md_offset: self.offset(),
}); });
if let Some(current_files) = &mut self.current_section_files { if let Some(current_files) = &mut self.current_section_files {
if !current_files.insert(path) { if !current_files.insert(path.clone()) {
if path == "test.py" {
return Err(anyhow::anyhow!(
"Test `{}` has duplicate files named `{path}`. \
(This is the default filename; \
consider giving some files an explicit name with `path=...`.)",
self.sections[section].title
));
}
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Test `{}` has duplicate files named `{path}`.", "Test `{}` has duplicate files named `{path}`.",
self.sections[section].title self.sections[section].title
@ -473,7 +489,7 @@ mod tests {
panic!("expected one file"); panic!("expected one file");
}; };
assert_eq!(file.path, "test.py"); assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py"); assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1"); assert_eq!(file.code, "x = 1");
} }
@ -498,7 +514,7 @@ mod tests {
panic!("expected one file"); panic!("expected one file");
}; };
assert_eq!(file.path, "test.py"); assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py"); assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1"); assert_eq!(file.code, "x = 1");
} }
@ -518,22 +534,33 @@ mod tests {
```py ```py
y = 2 y = 2
``` ```
# Three
```pyi
a: int
```
```pyi
b: str
```
", ",
); );
let mf = super::parse("file.md", &source).unwrap(); let mf = super::parse("file.md", &source).unwrap();
let [test1, test2] = &mf.tests().collect::<Vec<_>>()[..] else { let [test1, test2, test3] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected two tests"); panic!("expected three tests");
}; };
assert_eq!(test1.name(), "file.md - One"); assert_eq!(test1.name(), "file.md - One");
assert_eq!(test2.name(), "file.md - Two"); assert_eq!(test2.name(), "file.md - Two");
assert_eq!(test3.name(), "file.md - Three");
let [file] = test1.files().collect::<Vec<_>>()[..] else { let [file] = test1.files().collect::<Vec<_>>()[..] else {
panic!("expected one file"); panic!("expected one file");
}; };
assert_eq!(file.path, "test.py"); assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py"); assert_eq!(file.lang, "py");
assert_eq!(file.code, "x = 1"); assert_eq!(file.code, "x = 1");
@ -541,9 +568,21 @@ mod tests {
panic!("expected one file"); panic!("expected one file");
}; };
assert_eq!(file.path, "test.py"); assert_eq!(file.path, "mdtest_snippet__2.py");
assert_eq!(file.lang, "py"); assert_eq!(file.lang, "py");
assert_eq!(file.code, "y = 2"); assert_eq!(file.code, "y = 2");
let [file_1, file_2] = test3.files().collect::<Vec<_>>()[..] else {
panic!("expected two files");
};
assert_eq!(file_1.path, "mdtest_snippet__3.pyi");
assert_eq!(file_1.lang, "pyi");
assert_eq!(file_1.code, "a: int");
assert_eq!(file_2.path, "mdtest_snippet__4.pyi");
assert_eq!(file_2.lang, "pyi");
assert_eq!(file_2.code, "b: str");
} }
#[test] #[test]
@ -552,11 +591,15 @@ mod tests {
" "
# One # One
```py path=main.py `main.py`:
```py
from foo import y from foo import y
``` ```
```py path=foo.py `foo.py`:
```py
y = 2 y = 2
``` ```
@ -592,7 +635,7 @@ mod tests {
panic!("expected one file"); panic!("expected one file");
}; };
assert_eq!(file.path, "test.py"); assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.lang, "py"); assert_eq!(file.lang, "py");
assert_eq!(file.code, "y = 2"); assert_eq!(file.code, "y = 2");
} }
@ -601,7 +644,9 @@ mod tests {
fn custom_file_path() { fn custom_file_path() {
let source = dedent( let source = dedent(
" "
```py path=foo.py `foo.py`:
```py
x = 1 x = 1
``` ```
", ",
@ -685,6 +730,90 @@ mod tests {
assert_eq!(file.code, "x = 10"); assert_eq!(file.code, "x = 10");
} }
#[test]
fn cannot_generate_name_for_lang() {
let source = dedent(
"
```json
{}
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Cannot generate name for `lang=json`: Unsupported extension"
);
}
#[test]
fn mismatching_lang() {
let source = dedent(
"
`a.py`:
```pyi
x = 1
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"File ending of test file path `a.py` does not match `lang=pyi` of code block"
);
}
#[test]
fn files_with_no_extension_can_have_any_lang() {
let source = dedent(
"
`lorem`:
```foo
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "lorem");
assert_eq!(file.code, "x = 1");
}
#[test]
fn files_with_lang_text_can_have_any_paths() {
let source = dedent(
"
`lorem.yaml`:
```text
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "lorem.yaml");
assert_eq!(file.code, "x = 1");
}
#[test] #[test]
fn unterminated_code_block_1() { fn unterminated_code_block_1() {
let source = dedent( let source = dedent(
@ -738,83 +867,319 @@ mod tests {
} }
#[test] #[test]
fn invalid_config_item_no_equals() { fn line_break_in_header_1() {
let source = dedent( let source = dedent(
" "
```py foo #
Foo
```py
x = 1 x = 1
``` ```
", ",
); );
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Invalid config item `foo`."); let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(test.section.title, "file.md");
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
} }
#[test] #[test]
fn invalid_config_item_too_many_equals() { fn line_break_in_header_2() {
let source = dedent( let source = dedent(
" "
```py foo=bar=baz # Foo
x = 1
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Invalid config item `foo=bar=baz`.");
}
#[test] ##
fn invalid_config_item_duplicate() { Lorem
let source = dedent(
" ```py
```py foo=bar foo=baz
x = 1 x = 1
``` ```
", ",
); );
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Duplicate config item `foo=baz`."); let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(test.section.title, "Foo");
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
} }
#[test] #[test]
fn no_duplicate_name_files_in_test() { fn no_duplicate_name_files_in_test() {
let source = dedent( let source = dedent(
" "
`foo.py`:
```py ```py
x = 1 x = 1
``` ```
`foo.py`:
```py ```py
y = 2 y = 2
``` ```
", ",
); );
let err = super::parse("file.md", &source).expect_err("Should fail to parse"); let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Test `file.md` has duplicate files named `test.py`. \
(This is the default filename; consider giving some files an explicit name \
with `path=...`.)"
);
}
#[test]
fn no_duplicate_name_files_in_test_non_default() {
let source = dedent(
"
```py path=foo.py
x = 1
```
```py path=foo.py
y = 2
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!( assert_eq!(
err.to_string(), err.to_string(),
"Test `file.md` has duplicate files named `foo.py`." "Test `file.md` has duplicate files named `foo.py`."
); );
} }
#[test]
fn no_duplicate_name_files_in_test_2() {
let source = dedent(
"
`mdtest_snippet__1.py`:
```py
x = 1
```
```py
y = 2
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Test `file.md` has duplicate files named `mdtest_snippet__1.py`."
);
}
#[test]
fn separate_path() {
let source = dedent(
"
`foo.py`:
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn separate_path_whitespace_1() {
let source = dedent(
"
`foo.py` :
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn separate_path_whitespace_2() {
let source = dedent(
"
`foo.py`:
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "foo.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn path_with_space() {
let source = dedent(
"
`foo bar.py`:
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "foo bar.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn path_with_line_break() {
let source = dedent(
"
`foo
.py`:
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn path_with_backtick() {
let source = dedent(
"
`foo`bar.py`:
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn path_colon_on_next_line() {
let source = dedent(
"
`foo.py`
:
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn random_trailing_backtick_quoted() {
let source = dedent(
"
A long sentence that forces a line break
`int`:
```py
x = 1
```
",
);
let mf = super::parse("file.md", &source).unwrap();
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
panic!("expected one test");
};
let [file] = test.files().collect::<Vec<_>>()[..] else {
panic!("expected one file");
};
assert_eq!(file.path, "mdtest_snippet__1.py");
assert_eq!(file.code, "x = 1");
}
#[test]
fn config_no_longer_allowed() {
let source = dedent(
"
```py foo=bar
x = 1
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Trailing code-block metadata is not supported. Only the code block language can be specified.");
}
} }