mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 02:12:22 +00:00
[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:
parent
0529ad67d7
commit
11cfe2ea8a
35 changed files with 967 additions and 286 deletions
|
@ -2,7 +2,9 @@
|
|||
|
||||
## Deferred annotations in stubs always resolve
|
||||
|
||||
```pyi path=mod.pyi
|
||||
`mod.pyi`:
|
||||
|
||||
```pyi
|
||||
def get_foo() -> Foo: ...
|
||||
class Foo: ...
|
||||
```
|
||||
|
|
|
@ -116,7 +116,9 @@ def union_example(
|
|||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
Literal.
|
||||
|
||||
```pyi path=other.pyi
|
||||
`other.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import _SpecialForm
|
||||
|
||||
Literal: _SpecialForm
|
||||
|
|
|
@ -25,7 +25,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
|
|||
|
||||
## Tuple annotations are understood
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Unpack
|
||||
|
||||
a: tuple[()] = ()
|
||||
|
@ -40,7 +42,9 @@ i: tuple[str | int, str | int] = (42, 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
|
||||
|
||||
reveal_type(a) # revealed: tuple[()]
|
||||
|
@ -114,7 +118,7 @@ reveal_type(x) # revealed: Foo
|
|||
|
||||
## Annotations in stub files are deferred
|
||||
|
||||
```pyi path=main.pyi
|
||||
```pyi
|
||||
x: Foo
|
||||
|
||||
class Foo: ...
|
||||
|
@ -125,7 +129,7 @@ reveal_type(x) # revealed: Foo
|
|||
|
||||
## Annotated assignments in stub files are inferred correctly
|
||||
|
||||
```pyi path=main.pyi
|
||||
```pyi
|
||||
x: int = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
|
|
@ -703,7 +703,9 @@ reveal_type(Foo.__class__) # revealed: Literal[type]
|
|||
|
||||
## Module attributes
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
global_symbol: str = "a"
|
||||
```
|
||||
|
||||
|
@ -737,13 +739,19 @@ for mod.global_symbol in IntIterable():
|
|||
|
||||
## 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 Nested:
|
||||
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
|
||||
functions are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
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:
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
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
|
||||
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).denominator) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
reveal_type((2).numerator) # 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
|
||||
bols are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
reveal_type(True.__and__) # revealed: @Todo(bound method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(bound method)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
reveal_type(True.numerator) # revealed: Literal[1]
|
||||
reveal_type(False.real) # revealed: Literal[0]
|
||||
```
|
||||
|
|
|
@ -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
|
||||
(`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below):
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import 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
|
||||
without raising an error.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import 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
|
||||
is available somehow and simply use the declared type.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
inferred types:
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import 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`
|
||||
error for both `a` and `b`:
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
seems inconsistent when compared to the case just above.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
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`,
|
||||
possibly due to the usage of an unknown name in the annotation:
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
# Undeclared:
|
||||
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
|
||||
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag:
|
||||
|
@ -255,7 +271,9 @@ a = None
|
|||
|
||||
If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
if False:
|
||||
a: int = 1
|
||||
```
|
||||
|
|
|
@ -33,7 +33,9 @@ reveal_type(a >= b) # revealed: Literal[False]
|
|||
|
||||
Even when tuples have different lengths, comparisons should be handled appropriately.
|
||||
|
||||
```py path=different_length.py
|
||||
`different_length.py`:
|
||||
|
||||
```py
|
||||
a = (1, 2, 3)
|
||||
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
|
||||
compared, Python will still produce a result based on the prior elements.
|
||||
|
||||
```py path=short_circuit.py
|
||||
`short_circuit.py`:
|
||||
|
||||
```py
|
||||
a = (1, 2)
|
||||
b = (999999, "hello")
|
||||
|
||||
|
|
|
@ -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
|
||||
*after* that redefinition.
|
||||
|
||||
```py path=union_type_inferred.py
|
||||
`union_type_inferred.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
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
|
||||
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:
|
||||
return "foo"
|
||||
|
||||
|
@ -133,7 +137,9 @@ the `except` suite:
|
|||
- At the end of `else`, `x == 3`
|
||||
- At the end of `except`, `x == 2`
|
||||
|
||||
```py path=single_except.py
|
||||
`single_except.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
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
|
||||
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:
|
||||
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
|
||||
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:
|
||||
return "foo"
|
||||
|
||||
|
@ -249,7 +259,9 @@ suites:
|
|||
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
|
||||
`except` suite ran to completion
|
||||
|
||||
```py path=redef_in_finally.py
|
||||
`redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
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`
|
||||
suite.)
|
||||
|
||||
```py path=no_redef_in_finally.py
|
||||
`no_redef_in_finally.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
|
@ -317,7 +331,9 @@ reveal_type(x) # revealed: str | bool
|
|||
|
||||
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:
|
||||
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
|
||||
an exception raised *there*.
|
||||
|
||||
```py path=single_except_branch.py
|
||||
`single_except_branch.py`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
|
@ -407,7 +425,9 @@ reveal_type(x) # revealed: bool | float
|
|||
|
||||
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:
|
||||
return "foo"
|
||||
|
||||
|
|
|
@ -54,7 +54,9 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
|
|||
|
||||
## Evaluates to builtin
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
redefined_builtin_bool: type[bool] = bool
|
||||
|
||||
def my_bool(x) -> bool:
|
||||
|
|
|
@ -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.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Seq[T]: ...
|
||||
|
||||
# TODO not error on the subscripting
|
||||
|
|
|
@ -9,7 +9,9 @@ E = D
|
|||
reveal_type(E) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
|
@ -22,7 +24,9 @@ D = b.C
|
|||
reveal_type(D) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
|
@ -34,10 +38,14 @@ import a.b
|
|||
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: ...
|
||||
```
|
||||
|
||||
|
@ -49,13 +57,19 @@ import a.b.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: ...
|
||||
```
|
||||
|
||||
|
@ -67,10 +81,14 @@ import a.b as b
|
|||
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: ...
|
||||
```
|
||||
|
||||
|
@ -82,13 +100,19 @@ import a.b.c as 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: ...
|
||||
```
|
||||
|
||||
|
@ -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`"
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
|
|
@ -29,13 +29,17 @@ builtins from the "actual" vendored typeshed:
|
|||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class Custom: ...
|
||||
|
||||
custom_builtin: Custom
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
|
@ -56,12 +60,16 @@ that point:
|
|||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
foo = bar
|
||||
bar = 1
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
## Maybe unbound
|
||||
|
||||
```py path=maybe_unbound.py
|
||||
`maybe_unbound.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
|
@ -29,7 +31,9 @@ reveal_type(y) # revealed: Unknown | Literal[3]
|
|||
|
||||
## Maybe unbound annotated
|
||||
|
||||
```py path=maybe_unbound_annotated.py
|
||||
`maybe_unbound_annotated.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
|
@ -60,7 +64,9 @@ reveal_type(y) # revealed: int
|
|||
|
||||
Importing a possibly undeclared name still gives us its declared type:
|
||||
|
||||
```py path=maybe_undeclared.py
|
||||
`maybe_undeclared.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
|
@ -76,11 +82,15 @@ reveal_type(x) # revealed: int
|
|||
|
||||
## Reimport
|
||||
|
||||
```py path=c.py
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
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
|
||||
should still be able to unify those:
|
||||
|
||||
```py path=c.pyi
|
||||
`c.pyi`:
|
||||
|
||||
```pyi
|
||||
x: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
|
|
|
@ -8,11 +8,15 @@ import a.b
|
|||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Via from/import
|
||||
|
@ -23,11 +27,15 @@ from a import b
|
|||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Via both
|
||||
|
@ -40,11 +48,15 @@ reveal_type(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
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Via both (backwards)
|
||||
|
@ -65,11 +77,15 @@ reveal_type(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
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
||||
|
|
|
@ -18,7 +18,9 @@ reveal_type(baz) # revealed: Unknown
|
|||
|
||||
## Unresolved import from resolved module
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py
|
||||
|
@ -29,7 +31,9 @@ reveal_type(thing) # revealed: Unknown
|
|||
|
||||
## Resolved import of symbol from unresolved import
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import foo as foo # error: "Cannot resolve import `foo`"
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
|
@ -46,7 +50,9 @@ reveal_type(foo) # revealed: Unknown
|
|||
|
||||
## No implicit shadowing
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
x: int
|
||||
```
|
||||
|
||||
|
@ -58,7 +64,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
|
|||
|
||||
## Import cycle
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
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]]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import A
|
||||
|
||||
class B(A): ...
|
||||
|
|
|
@ -23,9 +23,13 @@ reveal_type(b) # revealed: <module 'a.b'>
|
|||
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
|
||||
```
|
||||
|
|
|
@ -2,10 +2,14 @@
|
|||
|
||||
## 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]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
@ -13,14 +17,20 @@ reveal_type(X) # revealed: Unknown
|
|||
|
||||
## Simple
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
|
@ -28,14 +38,20 @@ reveal_type(X) # revealed: int
|
|||
|
||||
## 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
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo.bar.baz import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
|
@ -43,11 +59,15 @@ reveal_type(X) # revealed: int
|
|||
|
||||
## Bare to package
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
|
@ -55,7 +75,9 @@ reveal_type(X) # revealed: int
|
|||
|
||||
## Non-existent + bare to package
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
@ -63,19 +85,25 @@ reveal_type(X) # revealed: Unknown
|
|||
|
||||
## Dunder init
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
## Non-existent + dunder init
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
from .foo import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
@ -83,14 +111,20 @@ reveal_type(X) # revealed: Unknown
|
|||
|
||||
## Long relative import
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/subpackage/subsubpackage/bar.py
|
||||
`package/subpackage/subsubpackage/bar.py`:
|
||||
|
||||
```py
|
||||
from ...foo import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
|
@ -98,14 +132,20 @@ reveal_type(X) # revealed: int
|
|||
|
||||
## Unbound symbol
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
x # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo import x # error: [unresolved-import]
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
@ -113,14 +153,20 @@ reveal_type(x) # revealed: Unknown
|
|||
|
||||
## Bare to module
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import foo
|
||||
|
||||
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
|
||||
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]
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import foo
|
||||
import package
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ y = x
|
|||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
```py path=b.pyi
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
x: int
|
||||
```
|
||||
|
||||
|
@ -22,6 +24,8 @@ y = x
|
|||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
|
|
@ -32,10 +32,14 @@ reveal_type(a.b.C) # revealed: Literal[C]
|
|||
import a.b
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
|
@ -55,14 +59,20 @@ reveal_type(a.b) # revealed: <module 'a.b'>
|
|||
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: ...
|
||||
```
|
||||
|
||||
```py path=q.py
|
||||
`q.py`:
|
||||
|
||||
```py
|
||||
import a as a
|
||||
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'>
|
||||
```
|
||||
|
||||
```py path=sub/__init__.py
|
||||
`sub/__init__.py`:
|
||||
|
||||
```py
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=sub/b.py
|
||||
`sub/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=attr/__init__.py
|
||||
`attr/__init__.py`:
|
||||
|
||||
```py
|
||||
from . import b as _
|
||||
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=attr/b.py
|
||||
`attr/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
|
|
@ -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
|
||||
with the same name:
|
||||
|
||||
```py path=constants.py
|
||||
`constants.py`:
|
||||
|
||||
```py
|
||||
TYPE_CHECKING: bool = False
|
||||
```
|
||||
|
||||
|
|
|
@ -19,13 +19,17 @@ typeshed = "/typeshed"
|
|||
|
||||
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: ...
|
||||
|
||||
builtin_symbol: BuiltinClass
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/sys/__init__.pyi
|
||||
`/typeshed/stdlib/sys/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
version = "my custom Python"
|
||||
```
|
||||
|
||||
|
@ -54,15 +58,21 @@ python-version = "3.10"
|
|||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/old_module.pyi
|
||||
`/typeshed/stdlib/old_module.pyi`:
|
||||
|
||||
```pyi
|
||||
class OldClass: ...
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/new_module.pyi
|
||||
`/typeshed/stdlib/new_module.pyi`:
|
||||
|
||||
```pyi
|
||||
class NewClass: ...
|
||||
```
|
||||
|
||||
```text path=/typeshed/stdlib/VERSIONS
|
||||
`/typeshed/stdlib/VERSIONS`:
|
||||
|
||||
```text
|
||||
old_module: 3.0-
|
||||
new_module: 3.11-
|
||||
```
|
||||
|
@ -86,7 +96,9 @@ simple untyped definition is enough to make `reveal_type` work in tests:
|
|||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
|
|
@ -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.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
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:
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # 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
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # 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
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
|
||||
Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334).
|
||||
|
||||
```py path=base.py
|
||||
`base.py`:
|
||||
|
||||
```py
|
||||
# error: [invalid-base]
|
||||
class Base(2): ...
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
# No error here
|
||||
from base import Base
|
||||
```
|
||||
|
|
|
@ -29,7 +29,9 @@ def foo():
|
|||
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
|
||||
are excluded:
|
||||
|
||||
```py path=unbound_dunders.py
|
||||
`unbound_dunders.py`:
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
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
|
||||
we're dealing with:
|
||||
|
||||
```py path=__getattr__.py
|
||||
`__getattr__.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
# 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
|
||||
`__dict__` in the module's global namespace:
|
||||
|
||||
```py path=foo.py
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
__dict__ = "foo"
|
||||
|
||||
reveal_type(__dict__) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
```py path=bar.py
|
||||
`bar.py`:
|
||||
|
||||
```py
|
||||
import foo
|
||||
from foo import __dict__ as foo_dict
|
||||
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
|
||||
No diagnostics should be generated.
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(x: str):
|
||||
x: int = int(x)
|
||||
```
|
||||
|
||||
## Implicit error
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
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
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
f: int = 1
|
||||
|
|
|
@ -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
|
||||
version:
|
||||
|
||||
```py path=module1.py
|
||||
`module1.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
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
|
||||
`SomeFeature` is always bound, without raising any errors:
|
||||
|
||||
```py path=test1.py
|
||||
`test1.py`:
|
||||
|
||||
```py
|
||||
from module1 import SomeFeature
|
||||
|
||||
# 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
|
||||
for conditional imports:
|
||||
|
||||
```py path=module2.py
|
||||
`module2.py`:
|
||||
|
||||
```py
|
||||
class SomeType: ...
|
||||
```
|
||||
|
||||
```py path=test2.py
|
||||
`test2.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
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
|
||||
demonstrate this, since semantic index building is inherently single-module:
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class AlwaysTrue:
|
||||
|
@ -1426,7 +1436,9 @@ def f():
|
|||
|
||||
#### Always false, unbound
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if False:
|
||||
symbol = 1
|
||||
```
|
||||
|
@ -1438,7 +1450,9 @@ from module import symbol
|
|||
|
||||
#### Always true, bound
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if True:
|
||||
symbol = 1
|
||||
```
|
||||
|
@ -1450,7 +1464,9 @@ from module import symbol
|
|||
|
||||
#### Ambiguous, possibly unbound
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
|
@ -1465,7 +1481,9 @@ from module import symbol
|
|||
|
||||
#### Always false, undeclared
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if False:
|
||||
symbol: int
|
||||
```
|
||||
|
@ -1479,7 +1497,9 @@ reveal_type(symbol) # revealed: Unknown
|
|||
|
||||
#### Always true, declared
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if True:
|
||||
symbol: int
|
||||
```
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Foo[T]: ...
|
||||
|
||||
# TODO: actually is subscriptable
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
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.
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
def f(x: int = ...) -> None:
|
||||
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,
|
||||
in a stub file only.
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
y: bytes = ...
|
||||
reveal_type(y) # revealed: bytes
|
||||
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
|
||||
assignment statement:
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
x, y = ...
|
||||
reveal_type(x) # 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
|
||||
results in a diagnostic:
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
# error: [not-iterable] "Object of type `ellipsis` is not iterable"
|
||||
for a, b in ...:
|
||||
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.
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = Ellipsis) -> None: ...
|
||||
```
|
||||
|
|
|
@ -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
|
||||
`"test"` and adding `"other"` to the result of the cast.
|
||||
|
||||
```py path=nested.py
|
||||
`nested.py`:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
from typing import cast
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
```py path=package/script.py
|
||||
`package/script.py`:
|
||||
|
||||
```py
|
||||
from .sys import version_info
|
||||
|
||||
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:
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
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
|
||||
properties on instance types:
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
|
||||
|
|
|
@ -39,7 +39,9 @@ def f(c: type[A]):
|
|||
reveal_type(c) # revealed: type[A]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
```
|
||||
|
||||
|
@ -52,23 +54,31 @@ def f(c: type[a.B]):
|
|||
reveal_type(c) # revealed: type[B]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class B: ...
|
||||
```
|
||||
|
||||
## Deeply qualified class literal from another module
|
||||
|
||||
```py path=a/test.py
|
||||
`a/test.py`:
|
||||
|
||||
```py
|
||||
import a.b
|
||||
|
||||
def f(c: type[a.b.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: ...
|
||||
```
|
||||
|
||||
|
|
|
@ -28,7 +28,9 @@ reveal_type(not b) # revealed: Literal[False]
|
|||
reveal_type(not warnings) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
y = 1
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
file path (`/src/test.py`) in its in-memory file system, run a type check on that file, and then
|
||||
match the resulting diagnostics with the assertions in the test. Assertions are in the form of
|
||||
Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, it
|
||||
fails.
|
||||
file path (`/src/mdtest_snippet__1.py`) in its in-memory file system, run a type check on that file,
|
||||
and then match the resulting diagnostics with the assertions in the test. Assertions are in the form
|
||||
of Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise,
|
||||
it fails.
|
||||
|
||||
<!---
|
||||
(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
|
||||
|
||||
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
|
||||
use the default name of `/src/test.py`. Other files must explicitly specify their file name:
|
||||
blocks represent multiple embedded files. If there are multiple unnamed files, mdtest will name them
|
||||
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
|
||||
```py
|
||||
|
@ -138,7 +142,9 @@ from b import C
|
|||
reveal_type(C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
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
|
||||
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
|
||||
`/src/`, allowing `test.py` to import from `b.py` using the module name `b`.
|
||||
So the above test creates two files, `/src/mdtest_snippet__1.py` and `/src/b.py`, and sets the
|
||||
workspace root to `/src/`, allowing imports from `b.py` using the module name `b`.
|
||||
|
||||
## Multi-test suites
|
||||
|
||||
|
@ -171,7 +177,9 @@ from b import y
|
|||
x: int = y # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
y = "foo"
|
||||
```
|
||||
````
|
||||
|
@ -357,17 +365,17 @@ This is just an example, not a proposal that red-knot would ever actually output
|
|||
precisely this format:
|
||||
|
||||
```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”
|
||||
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`.
|
||||
An `output` block can have a `path=` option, to explicitly specify the Python file for which it
|
||||
asserts diagnostic output, and a `stage=` option, to specify which stage of an incremental test it
|
||||
specifies diagnostic output at. (See “incremental tests” below.)
|
||||
By default, an `output` block will specify diagnostic output for the file
|
||||
`<workspace-root>/mdtest_snippet__1.py`. An `output` block can be prefixed by a
|
||||
<code>`<path>`:</code> label as usual, to explicitly specify the Python file for which it asserts
|
||||
diagnostic output.
|
||||
|
||||
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.
|
||||
|
@ -385,39 +393,43 @@ fenced code blocks in the test:
|
|||
|
||||
## modify a file
|
||||
|
||||
Initial version of `test.py` and `b.py`:
|
||||
Initial file contents:
|
||||
|
||||
```py
|
||||
from b import x
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
Initial expected output for `test.py`:
|
||||
Initial expected output for the unnamed file:
|
||||
|
||||
```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`:
|
||||
|
||||
```py path=b.py stage=1
|
||||
`b.py`:
|
||||
|
||||
```py stage=1
|
||||
# b.py
|
||||
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
|
||||
/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
|
||||
inline-comment diagnostic assertions for `test.py` would require specifying new
|
||||
contents for `test.py` in stage 1, which we don't want to do in this test.)
|
||||
(One reason to use full-diagnostic-output blocks in this test is that updating inline-comment
|
||||
diagnostic assertions for `mdtest_snippet__1.py` would require specifying new contents for
|
||||
`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
|
||||
|
|
|
@ -109,9 +109,9 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
|||
);
|
||||
|
||||
let full_path = if embedded.path.starts_with('/') {
|
||||
SystemPathBuf::from(embedded.path)
|
||||
SystemPathBuf::from(embedded.path.clone())
|
||||
} else {
|
||||
project_root.join(embedded.path)
|
||||
project_root.join(&embedded.path)
|
||||
};
|
||||
|
||||
if let Some(ref typeshed_path) = custom_typeshed_path {
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::sync::LazyLock;
|
|||
use anyhow::bail;
|
||||
use memchr::memchr2;
|
||||
use regex::{Captures, Match, Regex};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_trivia::Cursor;
|
||||
|
@ -147,7 +147,7 @@ struct EmbeddedFileId;
|
|||
#[derive(Debug)]
|
||||
pub(crate) struct EmbeddedFile<'s> {
|
||||
section: SectionId,
|
||||
pub(crate) path: &'s str,
|
||||
pub(crate) path: String,
|
||||
pub(crate) lang: &'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.
|
||||
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`
|
||||
/// configuration items following the opening backticks (in the "tag string" of the code block).
|
||||
static CODE_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?x)
|
||||
^```(?<lang>(?-u:\w)+)?(?<config>(?:\x20+\S+)*)\s*\n
|
||||
(?<code>(?:.|\n)*?)\n?
|
||||
(?<end>```|\z)
|
||||
^(?:
|
||||
`(?<path>[^`\n]+)`[^\S\n]*:[^\S\n]*\n
|
||||
\n?
|
||||
)?
|
||||
```(?<lang>(?-u:\w)+)?\x20*(?<config>\S.*)?\n
|
||||
(?<code>[\s\S]*?)\n?
|
||||
(?<end>```\n?|\z)
|
||||
",
|
||||
)
|
||||
.unwrap()
|
||||
|
@ -210,6 +214,7 @@ struct Parser<'s> {
|
|||
|
||||
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
unnamed_file_count: usize,
|
||||
|
||||
/// The unparsed remainder of the Markdown source.
|
||||
cursor: Cursor<'s>,
|
||||
|
@ -221,7 +226,7 @@ struct Parser<'s> {
|
|||
stack: SectionStack,
|
||||
|
||||
/// 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.
|
||||
current_section_has_config: bool,
|
||||
|
@ -240,6 +245,7 @@ impl<'s> Parser<'s> {
|
|||
sections,
|
||||
source,
|
||||
files: IndexVec::default(),
|
||||
unnamed_file_count: 0,
|
||||
cursor: Cursor::new(source),
|
||||
source_len: source.text_len(),
|
||||
stack: SectionStack::new(root_section_id),
|
||||
|
@ -265,10 +271,14 @@ impl<'s> Parser<'s> {
|
|||
|
||||
fn parse_impl(&mut self) -> anyhow::Result<()> {
|
||||
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.
|
||||
if position == 0 || self.cursor.eat_char('\n') {
|
||||
// Code blocks and headers must start on a new line
|
||||
// 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() {
|
||||
'#' => {
|
||||
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
|
||||
if let Some(position) = memchr::memchr(b'\n', self.cursor.as_bytes()) {
|
||||
self.cursor.skip_bytes(position);
|
||||
self.cursor.skip_bytes(position + 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -349,26 +359,10 @@ impl<'s> Parser<'s> {
|
|||
return Err(anyhow::anyhow!("Unterminated code block at line {line}."));
|
||||
}
|
||||
|
||||
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
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."));
|
||||
}
|
||||
|
||||
let path = config.get("path").copied().unwrap_or("test.py");
|
||||
|
||||
// CODE_RE can't match without matches for 'lang' and 'code'.
|
||||
let lang = captures
|
||||
.name("lang")
|
||||
|
@ -381,26 +375,48 @@ impl<'s> Parser<'s> {
|
|||
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 {
|
||||
path,
|
||||
path: path.clone(),
|
||||
section,
|
||||
lang,
|
||||
|
||||
code,
|
||||
|
||||
md_offset: self.offset(),
|
||||
});
|
||||
|
||||
if let Some(current_files) = &mut self.current_section_files {
|
||||
if !current_files.insert(path) {
|
||||
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
|
||||
));
|
||||
}
|
||||
if !current_files.insert(path.clone()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`.",
|
||||
self.sections[section].title
|
||||
|
@ -473,7 +489,7 @@ mod tests {
|
|||
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.code, "x = 1");
|
||||
}
|
||||
|
@ -498,7 +514,7 @@ mod tests {
|
|||
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.code, "x = 1");
|
||||
}
|
||||
|
@ -518,22 +534,33 @@ mod tests {
|
|||
```py
|
||||
y = 2
|
||||
```
|
||||
|
||||
# Three
|
||||
|
||||
```pyi
|
||||
a: int
|
||||
```
|
||||
|
||||
```pyi
|
||||
b: str
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test1, test2] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected two tests");
|
||||
let [test1, test2, test3] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected three tests");
|
||||
};
|
||||
|
||||
assert_eq!(test1.name(), "file.md - One");
|
||||
assert_eq!(test2.name(), "file.md - Two");
|
||||
assert_eq!(test3.name(), "file.md - Three");
|
||||
|
||||
let [file] = test1.files().collect::<Vec<_>>()[..] else {
|
||||
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.code, "x = 1");
|
||||
|
||||
|
@ -541,9 +568,21 @@ mod tests {
|
|||
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.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]
|
||||
|
@ -552,11 +591,15 @@ mod tests {
|
|||
"
|
||||
# One
|
||||
|
||||
```py path=main.py
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo import y
|
||||
```
|
||||
|
||||
```py path=foo.py
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
|
||||
|
@ -592,7 +635,7 @@ mod tests {
|
|||
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.code, "y = 2");
|
||||
}
|
||||
|
@ -601,7 +644,9 @@ mod tests {
|
|||
fn custom_file_path() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py path=foo.py
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
|
@ -685,6 +730,90 @@ mod tests {
|
|||
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]
|
||||
fn unterminated_code_block_1() {
|
||||
let source = dedent(
|
||||
|
@ -738,83 +867,319 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_no_equals() {
|
||||
fn line_break_in_header_1() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo
|
||||
#
|
||||
Foo
|
||||
|
||||
```py
|
||||
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]
|
||||
fn invalid_config_item_too_many_equals() {
|
||||
fn line_break_in_header_2() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar=baz
|
||||
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`.");
|
||||
}
|
||||
# Foo
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_duplicate() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar foo=baz
|
||||
##
|
||||
Lorem
|
||||
|
||||
```py
|
||||
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]
|
||||
fn no_duplicate_name_files_in_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
`foo.py`:
|
||||
|
||||
```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 `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!(
|
||||
err.to_string(),
|
||||
"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.");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue