Compare commits
474 Commits
0.10.0
...
dcreager/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c976dc570 | ||
|
|
b44fb47f25 | ||
|
|
a4531bf865 | ||
|
|
be54b840e9 | ||
|
|
45b5dedee2 | ||
|
|
9ff4772a2c | ||
|
|
c077b109ce | ||
|
|
8a2dd01db4 | ||
|
|
f888e51a34 | ||
|
|
d11e959ad5 | ||
|
|
a56eef444a | ||
|
|
14ff67fd46 | ||
|
|
ada7d4da0d | ||
|
|
4cafb44ba7 | ||
|
|
1445836872 | ||
|
|
da6b68cb58 | ||
|
|
2a478ce1b2 | ||
|
|
8fe2dd5e03 | ||
|
|
0a4dec0323 | ||
|
|
454ad15aee | ||
|
|
fd3fc34a9e | ||
|
|
c550b4d565 | ||
|
|
f8061e8b99 | ||
|
|
27a315b740 | ||
|
|
08221454f6 | ||
|
|
5fec1039ed | ||
|
|
787bcd1c6a | ||
|
|
5853eb28dd | ||
|
|
84d064a14c | ||
|
|
e4e405d2a1 | ||
|
|
1918c61623 | ||
|
|
44ad201262 | ||
|
|
c7372d218d | ||
|
|
de8f4e62e2 | ||
|
|
edfa03a692 | ||
|
|
9965cee998 | ||
|
|
58807b2980 | ||
|
|
9c47b6dbb0 | ||
|
|
d2ebfd6ed7 | ||
|
|
c36f3f5304 | ||
|
|
fcd50a0496 | ||
|
|
3ada36b766 | ||
|
|
bd89838212 | ||
|
|
b32407b6f3 | ||
|
|
b4de245a5a | ||
|
|
914095d08f | ||
|
|
5350288d07 | ||
|
|
649610cc98 | ||
|
|
1a79722ee0 | ||
|
|
b67590bfde | ||
|
|
e6a2de3ac6 | ||
|
|
c7b5067ef8 | ||
|
|
5a115e750d | ||
|
|
a1f361949e | ||
|
|
13ea4e5d0e | ||
|
|
a2a7b1e268 | ||
|
|
1dedcb9e0d | ||
|
|
807a8a7a29 | ||
|
|
78dabc332d | ||
|
|
bfc17fecaa | ||
|
|
942cb9e3ad | ||
|
|
312a487ea7 | ||
|
|
cf8dc60292 | ||
|
|
ea5d5c4e29 | ||
|
|
c99d5522eb | ||
|
|
e57c83e369 | ||
|
|
1d49e71ddd | ||
|
|
f05b2d3673 | ||
|
|
79b921179c | ||
|
|
1f85e0d0a0 | ||
|
|
03adae80dc | ||
|
|
4894f52bae | ||
|
|
3b24fe5c07 | ||
|
|
b5d529e976 | ||
|
|
014bb526f4 | ||
|
|
e2a38e4c00 | ||
|
|
9bee9429de | ||
|
|
17e5b61c54 | ||
|
|
701aecb2a6 | ||
|
|
850360a0b4 | ||
|
|
dfd8eaeb32 | ||
|
|
3aa3ee8b89 | ||
|
|
f0c3abd198 | ||
|
|
e5026c0877 | ||
|
|
da32a83c9f | ||
|
|
4bfdf54d1a | ||
|
|
7d11ef1564 | ||
|
|
b79d43a852 | ||
|
|
a4d3c4bf8b | ||
|
|
f3cc287a40 | ||
|
|
bd5b8a9ec6 | ||
|
|
0caf09f5c3 | ||
|
|
47956db567 | ||
|
|
ffef71d106 | ||
|
|
7e571791c0 | ||
|
|
8e11c53310 | ||
|
|
1aad180aae | ||
|
|
1a3b73720c | ||
|
|
8b2727cf67 | ||
|
|
410aa4b8fd | ||
|
|
bd58135b0d | ||
|
|
81045758d3 | ||
|
|
0b2a5b5cd3 | ||
|
|
7d958a9ee5 | ||
|
|
7e2eb591bc | ||
|
|
ba408f4231 | ||
|
|
28b64064f5 | ||
|
|
75b15ea2d0 | ||
|
|
e7e86b8584 | ||
|
|
f84bc07d56 | ||
|
|
7186d5e9ad | ||
|
|
5b6e94981d | ||
|
|
ec74f2d522 | ||
|
|
907b6ed7b5 | ||
|
|
fd9882a1f4 | ||
|
|
66a33bfd32 | ||
|
|
5b1d8350ff | ||
|
|
4d50ee6f52 | ||
|
|
06ffeb2e09 | ||
|
|
10e44124e6 | ||
|
|
9f6913c488 | ||
|
|
5fef4d4572 | ||
|
|
144484d46c | ||
|
|
c87e3ccb2f | ||
|
|
781b653511 | ||
|
|
484a8ed36d | ||
|
|
2fbc4d577e | ||
|
|
73399029b2 | ||
|
|
ff376fc262 | ||
|
|
7c81408c54 | ||
|
|
7207c86971 | ||
|
|
6ec4c6a97e | ||
|
|
f1ba596f22 | ||
|
|
8249a72412 | ||
|
|
00e00b9ad6 | ||
|
|
9a5a86769b | ||
|
|
2cee86d807 | ||
|
|
fab7d820bd | ||
|
|
ed14dbb1a2 | ||
|
|
058439d5d3 | ||
|
|
dc02732d4d | ||
|
|
0891689d2f | ||
|
|
127a45622f | ||
|
|
b662c3ff7e | ||
|
|
97dd6d120c | ||
|
|
34e06f2d17 | ||
|
|
a388c73752 | ||
|
|
60f2e67454 | ||
|
|
cb7f56fb20 | ||
|
|
761749cb50 | ||
|
|
3657f798c9 | ||
|
|
4a4a376f02 | ||
|
|
5e0f563ee4 | ||
|
|
27ecf350d8 | ||
|
|
f1d4ba32cc | ||
|
|
83a97235ae | ||
|
|
1e9e423362 | ||
|
|
708b84fb87 | ||
|
|
6cc2d02dfa | ||
|
|
c12c76e9c8 | ||
|
|
81cf860dc8 | ||
|
|
3150812ac4 | ||
|
|
12d7fad4ef | ||
|
|
41fec5171f | ||
|
|
db69dfba45 | ||
|
|
50eb3f539e | ||
|
|
ec30f18d02 | ||
|
|
1120def16a | ||
|
|
796e7510c4 | ||
|
|
1f254ab17e | ||
|
|
10003bd4e5 | ||
|
|
85f8116b2f | ||
|
|
bc8c6a4810 | ||
|
|
22f9f1dd29 | ||
|
|
6f11470298 | ||
|
|
a038c2e662 | ||
|
|
7dc6583f9f | ||
|
|
8c51fdfeff | ||
|
|
861a9c5d6c | ||
|
|
396a7162d6 | ||
|
|
040f80ab68 | ||
|
|
67f8c30b53 | ||
|
|
e01d228a71 | ||
|
|
51bdb87fd8 | ||
|
|
ac5d220d75 | ||
|
|
73a9974d8a | ||
|
|
3dd6d0a422 | ||
|
|
4571c5f56f | ||
|
|
1c652e6b98 | ||
|
|
172af7b4b0 | ||
|
|
7e6d3838bd | ||
|
|
1a6a10b30f | ||
|
|
b3243b5e2a | ||
|
|
95d6ed40cc | ||
|
|
acc5662e8b | ||
|
|
33a56f198b | ||
|
|
5cee346744 | ||
|
|
ffa824e037 | ||
|
|
98b95c9c38 | ||
|
|
a4ba10ff0a | ||
|
|
bf0306887a | ||
|
|
4f924bb975 | ||
|
|
c2b2e42ad3 | ||
|
|
24b1b1d52c | ||
|
|
6a07dd227d | ||
|
|
8aa5686e63 | ||
|
|
e7684d3493 | ||
|
|
64e7e1aa64 | ||
|
|
45c43735e0 | ||
|
|
8a4158c5f8 | ||
|
|
fedd982fd5 | ||
|
|
a1eb834a5f | ||
|
|
c1f93a702c | ||
|
|
6e2b8f9696 | ||
|
|
ca0cce3f9c | ||
|
|
3f00010a7a | ||
|
|
d401a5440e | ||
|
|
755ece0c36 | ||
|
|
62f8d855d2 | ||
|
|
130339f3d8 | ||
|
|
e50fc049ab | ||
|
|
66355a6185 | ||
|
|
177afabe18 | ||
|
|
28c68934a4 | ||
|
|
195bb433db | ||
|
|
c2bb5d5250 | ||
|
|
cb7dae1e96 | ||
|
|
8833484b10 | ||
|
|
adeba3dca7 | ||
|
|
af988bf866 | ||
|
|
f989c2c3af | ||
|
|
c2512b4c50 | ||
|
|
6bc2b04c49 | ||
|
|
fc2a0950eb | ||
|
|
33bd08f49b | ||
|
|
5d57788328 | ||
|
|
718b0cadf4 | ||
|
|
24498e383d | ||
|
|
28c7e724e3 | ||
|
|
185bcfef1a | ||
|
|
c30e80a3f4 | ||
|
|
4e169e5f6c | ||
|
|
883b8e3870 | ||
|
|
2ca2f73ba8 | ||
|
|
90f0766210 | ||
|
|
a9527edbbe | ||
|
|
57be814acb | ||
|
|
a8a18e7171 | ||
|
|
a953373892 | ||
|
|
d382065f8a | ||
|
|
d45593288f | ||
|
|
2ae39edccf | ||
|
|
7e97910704 | ||
|
|
ae2cf91a36 | ||
|
|
e1b5b0de71 | ||
|
|
f63024843c | ||
|
|
eb3e176309 | ||
|
|
d38f6fcc55 | ||
|
|
6be0a5057d | ||
|
|
d6dcc377f7 | ||
|
|
a43b683d08 | ||
|
|
d29d4956de | ||
|
|
c74ba00219 | ||
|
|
a15404a5c1 | ||
|
|
3f63c08728 | ||
|
|
5a876ed25e | ||
|
|
49c25993eb | ||
|
|
8e6a83b33e | ||
|
|
d0c8eaa092 | ||
|
|
aa93005d8d | ||
|
|
0073fd4945 | ||
|
|
b57c62e6b3 | ||
|
|
a9dbfebc61 | ||
|
|
53cfaaebc4 | ||
|
|
3ad123bc23 | ||
|
|
a1535fbdbd | ||
|
|
4a6fa5fc27 | ||
|
|
491a51960e | ||
|
|
2d7f118f52 | ||
|
|
3d1e5676fb | ||
|
|
0b1ab8fd5a | ||
|
|
d9616e61b0 | ||
|
|
fa80e10aac | ||
|
|
30a5f69913 | ||
|
|
a192d96880 | ||
|
|
5c1fab0661 | ||
|
|
5f83a32553 | ||
|
|
6d387e01b4 | ||
|
|
ef2616446a | ||
|
|
90153e5664 | ||
|
|
08f86a5ffc | ||
|
|
4cc627ef77 | ||
|
|
244618a6f5 | ||
|
|
e068ffe929 | ||
|
|
a5f4522078 | ||
|
|
c6efa93cf7 | ||
|
|
ab1011ce70 | ||
|
|
a0819f0c51 | ||
|
|
8396d7cd63 | ||
|
|
9ae2900dc3 | ||
|
|
37a40e30f6 | ||
|
|
98438a77f2 | ||
|
|
93052331b0 | ||
|
|
e07741e553 | ||
|
|
050f332771 | ||
|
|
6b02c39321 | ||
|
|
78b0b5a3ab | ||
|
|
0e48940ea4 | ||
|
|
aca6254e82 | ||
|
|
64171744dc | ||
|
|
2e56cd3737 | ||
|
|
e37a1b02f6 | ||
|
|
9aa8dc4590 | ||
|
|
df418d94b3 | ||
|
|
b90741fb92 | ||
|
|
3acf4e716d | ||
|
|
992a1af4c2 | ||
|
|
c963b185eb | ||
|
|
4067a7e50c | ||
|
|
f9bc80ad55 | ||
|
|
142fe0d29b | ||
|
|
6ef522159d | ||
|
|
b9a7328789 | ||
|
|
640d821108 | ||
|
|
43ca85a351 | ||
|
|
338fed98a4 | ||
|
|
d70a3e6753 | ||
|
|
5697d21fca | ||
|
|
58350ec93b | ||
|
|
aae4d0f3eb | ||
|
|
807fce8069 | ||
|
|
8d16a5c8c9 | ||
|
|
4975c2f027 | ||
|
|
dd5b02aaa2 | ||
|
|
68ea2b8b5b | ||
|
|
e87fee4b3b | ||
|
|
cba197e3c5 | ||
|
|
66d0cf2a72 | ||
|
|
85b7f808e1 | ||
|
|
3a97bdf689 | ||
|
|
1bee3994aa | ||
|
|
888a910925 | ||
|
|
581b7005dc | ||
|
|
b442ba440f | ||
|
|
5aba72cdbd | ||
|
|
2711e08eb8 | ||
|
|
f5cdf23545 | ||
|
|
d98222cd14 | ||
|
|
f7b9089cb8 | ||
|
|
dfebc1cfe4 | ||
|
|
7e1484a9b1 | ||
|
|
187cac56bd | ||
|
|
890f79c4ab | ||
|
|
3899f7156f | ||
|
|
902d86e79e | ||
|
|
9fe89ddfba | ||
|
|
08a0995108 | ||
|
|
2d892bc9f7 | ||
|
|
ee51c2a389 | ||
|
|
bb07ccd783 | ||
|
|
c35f2bfe32 | ||
|
|
7fb765d9b6 | ||
|
|
0360c6b219 | ||
|
|
1cffb323bc | ||
|
|
92028efe3d | ||
|
|
7b86f54c4c | ||
|
|
e4f5fe8cf7 | ||
|
|
2baaedda6c | ||
|
|
b1deab83d9 | ||
|
|
d21d639ee0 | ||
|
|
14eb4cac88 | ||
|
|
c03c28d199 | ||
|
|
4773878ee7 | ||
|
|
2a4d835132 | ||
|
|
04a8756379 | ||
|
|
193c38199e | ||
|
|
63e78b41cd | ||
|
|
296d67a496 | ||
|
|
42cbce538b | ||
|
|
67602512b6 | ||
|
|
23382f5f8c | ||
|
|
c1971fdde2 | ||
|
|
cdafd8e32b | ||
|
|
12725943cd | ||
|
|
9d72685f8d | ||
|
|
47c4ccff5d | ||
|
|
74f64d3f96 | ||
|
|
999fd4f885 | ||
|
|
433a342656 | ||
|
|
4ed93b4311 | ||
|
|
98fdc0ebae | ||
|
|
861931795c | ||
|
|
a3f3d734a1 | ||
|
|
3a5f1d46c0 | ||
|
|
f3f3e55d97 | ||
|
|
22de00de16 | ||
|
|
f2a9960fb3 | ||
|
|
fd341bb1b2 | ||
|
|
15a6aeb998 | ||
|
|
81759be14b | ||
|
|
a69f6240cc | ||
|
|
c3d429ddd8 | ||
|
|
ab3ec4de6a | ||
|
|
a9f5dddbaa | ||
|
|
cc3ddaf070 | ||
|
|
c027979851 | ||
|
|
dcf31c9348 | ||
|
|
4ab529803f | ||
|
|
23b7df9b29 | ||
|
|
3e2cf5d7c4 | ||
|
|
c9cd0acaeb | ||
|
|
ded9c69888 | ||
|
|
433879d852 | ||
|
|
b7d232cf89 | ||
|
|
4d3a5afea5 | ||
|
|
90a8d92b2f | ||
|
|
c8bd5eeb56 | ||
|
|
bd9eab059f | ||
|
|
6883c1dde7 | ||
|
|
9291074ba6 | ||
|
|
602a27c4e3 | ||
|
|
ff548b1272 | ||
|
|
7512a71bbb | ||
|
|
7ca5f132ca | ||
|
|
743f85f1a4 | ||
|
|
b2e0ae6416 | ||
|
|
23ccb52fa6 | ||
|
|
3ccc8dbbf9 | ||
|
|
75a562d313 | ||
|
|
8d3643f409 | ||
|
|
50b66dc025 | ||
|
|
24707777af | ||
|
|
93ca4a96e0 | ||
|
|
38bfda94ce | ||
|
|
4da6936ec4 | ||
|
|
238ec39c56 | ||
|
|
b04103fa1d | ||
|
|
c100d519e9 | ||
|
|
dbdb46dcd2 | ||
|
|
2a6d43740c | ||
|
|
6f5a68608e | ||
|
|
c61d9c6bb7 | ||
|
|
43d371a1c9 | ||
|
|
5f80129112 | ||
|
|
db4ae242dc | ||
|
|
d3b5ef9f0b | ||
|
|
a7a20a3684 | ||
|
|
6fc953119f | ||
|
|
405fc7f5cf | ||
|
|
d5e045df47 | ||
|
|
adac1e6a61 | ||
|
|
3768f9cb52 | ||
|
|
01f3ef4e4f | ||
|
|
2de8455e43 | ||
|
|
1fab292ec1 | ||
|
|
c755eec91e | ||
|
|
ebcad6e641 | ||
|
|
fe275725e0 | ||
|
|
a467e7c8d3 | ||
|
|
a128ca761f | ||
|
|
3a32e56445 | ||
|
|
b9d7c36a23 | ||
|
|
ef9a825827 | ||
|
|
2bcd2b4147 | ||
|
|
eb6871d209 | ||
|
|
6311412373 | ||
|
|
4f2851982d | ||
|
|
2cd25ef641 | ||
|
|
a22d206db2 | ||
|
|
270318c2e0 | ||
|
|
d03b12e711 | ||
|
|
14c5ed5d7d | ||
|
|
595565015b | ||
|
|
2382fe1f25 |
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -18,6 +18,7 @@
|
||||
/python/py-fuzzer/ @AlexWaygood
|
||||
|
||||
# red-knot
|
||||
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp
|
||||
/crates/red_knot* @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/crates/ruff_db/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/scripts/knot_benchmark/ @carljm @MichaReiser @AlexWaygood @sharkdp @dcreager
|
||||
/crates/red_knot_python_semantic @carljm @AlexWaygood @sharkdp @dcreager
|
||||
|
||||
11
.github/renovate.json5
vendored
11
.github/renovate.json5
vendored
@@ -40,6 +40,17 @@
|
||||
enabled: true,
|
||||
},
|
||||
packageRules: [
|
||||
// Pin GitHub Actions to immutable SHAs.
|
||||
{
|
||||
matchDepTypes: ["action"],
|
||||
pinDigests: true,
|
||||
},
|
||||
// Annotate GitHub Actions SHAs with a SemVer version.
|
||||
{
|
||||
extends: ["helpers:pinGitHubActionDigests"],
|
||||
extractVersion: "^(?<version>v?\\d+\\.\\d+\\.\\d+)$",
|
||||
versioning: "regex:^v?(?<major>\\d+)(\\.(?<minor>\\d+)\\.(?<patch>\\d+))?$",
|
||||
},
|
||||
{
|
||||
// Group upload/download artifact updates, the versions are dependent
|
||||
groupName: "Artifact GitHub Actions dependencies",
|
||||
|
||||
88
.github/workflows/build-binaries.yml
vendored
88
.github/workflows/build-binaries.yml
vendored
@@ -28,7 +28,7 @@ permissions: {}
|
||||
env:
|
||||
PACKAGE_NAME: ruff
|
||||
MODULE_NAME: ruff
|
||||
PYTHON_VERSION: "3.11"
|
||||
PYTHON_VERSION: "3.13"
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -39,17 +39,17 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build sdist"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload sdist"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-sdist
|
||||
path: dist
|
||||
@@ -68,23 +68,23 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - x86_64"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-macos-x86_64
|
||||
path: dist
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-macos-x86_64
|
||||
path: |
|
||||
@@ -110,18 +110,18 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: arm64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-aarch64-apple-darwin
|
||||
path: dist
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-aarch64-apple-darwin
|
||||
path: |
|
||||
@@ -166,18 +166,18 @@ jobs:
|
||||
- target: aarch64-pc-windows-msvc
|
||||
arch: x64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: ${{ matrix.platform.arch }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --locked --out dist
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-${{ matrix.platform.target }}
|
||||
path: dist
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
|
||||
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
@@ -219,18 +219,18 @@ jobs:
|
||||
- x86_64-unknown-linux-gnu
|
||||
- i686-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-${{ matrix.target }}
|
||||
path: dist
|
||||
@@ -260,7 +260,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.target }}
|
||||
path: |
|
||||
@@ -294,24 +294,24 @@ jobs:
|
||||
arch: arm
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
docker-options: ${{ matrix.platform.maturin_docker_options }}
|
||||
args: --release --locked --out dist
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
if: matrix.platform.arch != 'ppc64'
|
||||
- uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2
|
||||
if: ${{ matrix.platform.arch != 'ppc64' && matrix.platform.arch != 'ppc64le'}}
|
||||
name: Test wheel
|
||||
with:
|
||||
arch: ${{ matrix.platform.arch == 'arm' && 'armv6' || matrix.platform.arch }}
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-${{ matrix.platform.target }}
|
||||
path: dist
|
||||
@@ -343,7 +343,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
@@ -359,25 +359,25 @@ jobs:
|
||||
- x86_64-unknown-linux-musl
|
||||
- i686-unknown-linux-musl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel"
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
uses: addnab/docker-run-action@v3
|
||||
uses: addnab/docker-run-action@4f65fabd2431ebc8d299f8e5a018d79a769ae185 # v3
|
||||
with:
|
||||
image: alpine:latest
|
||||
options: -v ${{ github.workspace }}:/io -w /io
|
||||
@@ -387,7 +387,7 @@ jobs:
|
||||
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
.venv/bin/${{ env.MODULE_NAME }} --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-${{ matrix.target }}
|
||||
path: dist
|
||||
@@ -405,7 +405,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.target }}
|
||||
path: |
|
||||
@@ -425,23 +425,23 @@ jobs:
|
||||
arch: armv7
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
args: --release --locked --out dist
|
||||
docker-options: ${{ matrix.platform.maturin_docker_options }}
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
- uses: uraimo/run-on-arch-action@ac33288c3728ca72563c97b8b88dda5a65a84448 # v2
|
||||
name: Test wheel
|
||||
with:
|
||||
arch: ${{ matrix.platform.arch }}
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
.venv/bin/${{ env.MODULE_NAME }} --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: wheels-${{ matrix.platform.target }}
|
||||
path: dist
|
||||
@@ -472,7 +472,7 @@ jobs:
|
||||
tar czvf $ARCHIVE_FILE $ARCHIVE_NAME
|
||||
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
|
||||
- name: "Upload binary"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
|
||||
36
.github/workflows/build-docker.yml
vendored
36
.github/workflows/build-docker.yml
vendored
@@ -33,14 +33,14 @@ jobs:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_TUPLE }}
|
||||
path: /tmp/digests/*
|
||||
@@ -113,17 +113,17 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -167,9 +167,9 @@ jobs:
|
||||
- debian:bookworm-slim,bookworm-slim,debian-slim
|
||||
- buildpack-deps:bookworm,bookworm,debian
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||
# ghcr.io prefers index level annotations
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
${{ env.TAG_PATTERNS }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -256,17 +256,17 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
with:
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
399
.github/workflows/ci.yaml
vendored
399
.github/workflows/ci.yaml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.12"
|
||||
PYTHON_VERSION: "3.13"
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
@@ -26,83 +26,177 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
# Flag that is raised when any code that affects parser is changed
|
||||
parser: ${{ steps.changed.outputs.parser_any_changed }}
|
||||
parser: ${{ steps.check_parser.outputs.changed }}
|
||||
# Flag that is raised when any code that affects linter is changed
|
||||
linter: ${{ steps.changed.outputs.linter_any_changed }}
|
||||
linter: ${{ steps.check_linter.outputs.changed }}
|
||||
# Flag that is raised when any code that affects formatter is changed
|
||||
formatter: ${{ steps.changed.outputs.formatter_any_changed }}
|
||||
formatter: ${{ steps.check_formatter.outputs.changed }}
|
||||
# Flag that is raised when any code is changed
|
||||
# This is superset of the linter and formatter
|
||||
code: ${{ steps.changed.outputs.code_any_changed }}
|
||||
code: ${{ steps.check_code.outputs.changed }}
|
||||
# Flag that is raised when any code that affects the fuzzer is changed
|
||||
fuzz: ${{ steps.changed.outputs.fuzz_any_changed }}
|
||||
fuzz: ${{ steps.check_fuzzer.outputs.changed }}
|
||||
# Flag that is set to "true" when code related to red-knot changes.
|
||||
red_knot: ${{ steps.check_red_knot.outputs.changed }}
|
||||
|
||||
# Flag that is set to "true" when code related to the playground changes.
|
||||
playground: ${{ steps.check_playground.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: tj-actions/changed-files@v45
|
||||
id: changed
|
||||
with:
|
||||
files_yaml: |
|
||||
parser:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- crates/ruff_python_trivia/**
|
||||
- crates/ruff_source_file/**
|
||||
- crates/ruff_text_size/**
|
||||
- crates/ruff_python_ast/**
|
||||
- crates/ruff_python_parser/**
|
||||
- python/py-fuzzer/**
|
||||
- .github/workflows/ci.yaml
|
||||
- name: Determine merge base
|
||||
id: merge_base
|
||||
env:
|
||||
BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }}
|
||||
run: |
|
||||
sha=$(git merge-base HEAD "origin/${BASE_REF}")
|
||||
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
linter:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- crates/**
|
||||
- "!crates/red_knot*/**"
|
||||
- "!crates/ruff_python_formatter/**"
|
||||
- "!crates/ruff_formatter/**"
|
||||
- "!crates/ruff_dev/**"
|
||||
- scripts/*
|
||||
- python/**
|
||||
- .github/workflows/ci.yaml
|
||||
- name: Check if the parser code changed
|
||||
id: check_parser
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
':Cargo.toml' \
|
||||
':Cargo.lock' \
|
||||
':crates/ruff_python_trivia/**' \
|
||||
':crates/ruff_source_file/**' \
|
||||
':crates/ruff_text_size/**' \
|
||||
':crates/ruff_python_ast/**' \
|
||||
':crates/ruff_python_parser/**' \
|
||||
':python/py-fuzzer/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
formatter:
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- crates/ruff_python_formatter/**
|
||||
- crates/ruff_formatter/**
|
||||
- crates/ruff_python_trivia/**
|
||||
- crates/ruff_python_ast/**
|
||||
- crates/ruff_source_file/**
|
||||
- crates/ruff_python_index/**
|
||||
- crates/ruff_text_size/**
|
||||
- crates/ruff_python_parser/**
|
||||
- crates/ruff_dev/**
|
||||
- scripts/*
|
||||
- python/**
|
||||
- .github/workflows/ci.yaml
|
||||
- name: Check if the linter code changed
|
||||
id: check_linter
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \
|
||||
':Cargo.lock' \
|
||||
':crates/**' \
|
||||
':!crates/red_knot*/**' \
|
||||
':!crates/ruff_python_formatter/**' \
|
||||
':!crates/ruff_formatter/**' \
|
||||
':!crates/ruff_dev/**' \
|
||||
':!crates/ruff_db/**' \
|
||||
':scripts/*' \
|
||||
':python/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
fuzz:
|
||||
- fuzz/Cargo.toml
|
||||
- fuzz/Cargo.lock
|
||||
- fuzz/fuzz_targets/**
|
||||
- name: Check if the formatter code changed
|
||||
id: check_formatter
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \
|
||||
':Cargo.lock' \
|
||||
':crates/ruff_python_formatter/**' \
|
||||
':crates/ruff_formatter/**' \
|
||||
':crates/ruff_python_trivia/**' \
|
||||
':crates/ruff_python_ast/**' \
|
||||
':crates/ruff_source_file/**' \
|
||||
':crates/ruff_python_index/**' \
|
||||
':crates/ruff_python_index/**' \
|
||||
':crates/ruff_text_size/**' \
|
||||
':crates/ruff_python_parser/**' \
|
||||
':scripts/*' \
|
||||
':python/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
code:
|
||||
- "**/*"
|
||||
- "!**/*.md"
|
||||
- "crates/red_knot_python_semantic/resources/mdtest/**/*.md"
|
||||
- "!docs/**"
|
||||
- "!assets/**"
|
||||
- name: Check if the fuzzer code changed
|
||||
id: check_fuzzer
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':Cargo.toml' \
|
||||
':Cargo.lock' \
|
||||
':fuzz/fuzz_targets/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if there was any code related change
|
||||
id: check_code
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
|
||||
':!**/*.md' \
|
||||
':crates/red_knot_python_semantic/resources/mdtest/**/*.md' \
|
||||
':!docs/**' \
|
||||
':!assets/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if there was any playground related change
|
||||
id: check_playground
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
':playground/**' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if the red-knot code changed
|
||||
id: check_red_knot
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
':Cargo.toml' \
|
||||
':Cargo.lock' \
|
||||
':crates/red_knot*/**' \
|
||||
':crates/ruff_db/**' \
|
||||
':crates/ruff_annotate_snippets/**' \
|
||||
':crates/ruff_python_ast/**' \
|
||||
':crates/ruff_python_parser/**' \
|
||||
':crates/ruff_python_trivia/**' \
|
||||
':crates/ruff_source_file/**' \
|
||||
':crates/ruff_text_size/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
cargo-fmt:
|
||||
name: "cargo fmt"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -116,10 +210,10 @@ jobs:
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
@@ -136,22 +230,30 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Red-knot mdtests (GitHub annotations)
|
||||
if: ${{ needs.determine_changes.outputs.red_knot == 'true' }}
|
||||
env:
|
||||
NO_COLOR: 1
|
||||
MDTEST_GITHUB_ANNOTATIONS_FORMAT: 1
|
||||
# Ignore errors if this step fails; we want to continue to later steps in the workflow anyway.
|
||||
# This step is just to get nice GitHub annotations on the PR diff in the files-changed tab.
|
||||
run: cargo test -p red_knot_python_semantic --test mdtest || true
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -170,7 +272,7 @@ jobs:
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
@@ -182,20 +284,20 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -211,14 +313,14 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -238,18 +340,18 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- name: "Test ruff_wasm"
|
||||
@@ -267,14 +369,14 @@ jobs:
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Build"
|
||||
run: cargo build --release --locked
|
||||
|
||||
@@ -285,27 +387,27 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: SebRollen/toml-action@v1.2.0
|
||||
- uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0
|
||||
id: msrv
|
||||
with:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: rustup default "${MSRV}"
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -322,16 +424,16 @@ jobs:
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
|
||||
with:
|
||||
tool: cargo-fuzz@0.11.2
|
||||
- name: "Install cargo-fuzz"
|
||||
@@ -350,11 +452,11 @@ jobs:
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
with:
|
||||
@@ -384,10 +486,10 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
@@ -416,21 +518,21 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
name: Download comparison Ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download baseline Ruff binary
|
||||
with:
|
||||
name: ruff
|
||||
@@ -518,13 +620,13 @@ jobs:
|
||||
run: |
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
name: Upload PR Number
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
name: Upload Results
|
||||
with:
|
||||
name: ecosystem-result
|
||||
@@ -536,10 +638,10 @@ jobs:
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
- uses: cargo-bins/cargo-binstall@63aaa5c1932cebabc34eceda9d92a70215dcead6 # v1.12.3
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -549,18 +651,18 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@v1
|
||||
uses: PyO3/maturin-action@aef21716ff3dcae8a1c301d23ec3e4446972a6e3 # v1.49.1
|
||||
with:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
@@ -573,22 +675,15 @@ jobs:
|
||||
|
||||
pre-commit:
|
||||
name: "pre-commit"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install pre-commit"
|
||||
run: pip install pre-commit
|
||||
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- name: "Cache pre-commit"
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
@@ -597,7 +692,7 @@ jobs:
|
||||
echo '```console' > "$GITHUB_STEP_SUMMARY"
|
||||
# Enable color output for pre-commit and remove it for the summary
|
||||
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
|
||||
exit_code="${PIPESTATUS[0]}"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -610,22 +705,22 @@ jobs:
|
||||
env:
|
||||
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -652,10 +747,10 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Run checks"
|
||||
@@ -674,21 +769,22 @@ jobs:
|
||||
- determine_changes
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- uses: extractions/setup-just@v2
|
||||
- uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
name: "Download ruff-lsp source"
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
# installation fails on 3.13 and newer
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
name: Download development ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
@@ -711,6 +807,39 @@ jobs:
|
||||
|
||||
just test
|
||||
|
||||
check-playground:
|
||||
name: "check playground"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs:
|
||||
- determine_changes
|
||||
if: ${{ (needs.determine_changes.outputs.playground == 'true') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Install Node dependencies"
|
||||
run: npm ci
|
||||
working-directory: playground
|
||||
- name: "Build playgrounds"
|
||||
run: npm run dev:wasm
|
||||
working-directory: playground
|
||||
- name: "Run TypeScript checks"
|
||||
run: npm run check
|
||||
working-directory: playground
|
||||
- name: "Check formatting"
|
||||
run: npm run fmt:check
|
||||
working-directory: playground
|
||||
|
||||
benchmarks:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: determine_changes
|
||||
@@ -718,17 +847,17 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@v2
|
||||
uses: taiki-e/install-action@09dc018eee06ae1c9e0409786563f534210ceb83 # v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -736,7 +865,7 @@ jobs:
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@v3
|
||||
uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3.5.0
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
10
.github/workflows/daily_fuzz.yaml
vendored
10
.github/workflows/daily_fuzz.yaml
vendored
@@ -31,15 +31,15 @@ jobs:
|
||||
# Don't run the cron job on forks:
|
||||
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@v5
|
||||
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: Build ruff
|
||||
# A debug build means the script runs slower once it gets started,
|
||||
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
8
.github/workflows/daily_property_tests.yaml
vendored
8
.github/workflows/daily_property_tests.yaml
vendored
@@ -30,14 +30,14 @@ jobs:
|
||||
# Don't run the cron job on forks:
|
||||
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
uses: rui314/setup-mold@e16410e7f8d9e167b74ad5697a9089a35126eb50 # v1
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: Build Red Knot
|
||||
# A release build takes longer (2 min vs 1 min), but the property tests run much faster in release
|
||||
# mode (1.5 min vs 14 min), so the overall time is shorter with a release build.
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
19
.github/workflows/mypy_primer.yaml
vendored
19
.github/workflows/mypy_primer.yaml
vendored
@@ -25,19 +25,19 @@ env:
|
||||
jobs:
|
||||
mypy_primer:
|
||||
name: Run mypy_primer
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
path: ruff
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
- name: Install Rust toolchain
|
||||
@@ -45,13 +45,15 @@ jobs:
|
||||
|
||||
- name: Install mypy_primer
|
||||
run: |
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support"
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v5"
|
||||
|
||||
- name: Run mypy_primer
|
||||
shell: bash
|
||||
run: |
|
||||
cd ruff
|
||||
|
||||
PRIMER_SELECTOR="$(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt)"
|
||||
|
||||
echo "new commit"
|
||||
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
|
||||
|
||||
@@ -62,13 +64,14 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
echo "Project selector: $PRIMER_SELECTOR"
|
||||
# Allow the exit code to be 0 or 1, only fail for actual mypy_primer crashes/bugs
|
||||
uvx mypy_primer \
|
||||
--repo ruff \
|
||||
--type-checker knot \
|
||||
--old base_commit \
|
||||
--new "$GITHUB_SHA" \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow)$' \
|
||||
--project-selector "/($PRIMER_SELECTOR)\$" \
|
||||
--output concise \
|
||||
--debug > mypy_primer.diff || [ $? -eq 1 ]
|
||||
|
||||
@@ -81,13 +84,13 @@ jobs:
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: mypy_primer_diff
|
||||
path: mypy_primer.diff
|
||||
|
||||
- name: Upload pr-number
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
8
.github/workflows/mypy_primer_comment.yaml
vendored
8
.github/workflows/mypy_primer_comment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download PR number
|
||||
with:
|
||||
name: pr-number
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: "Download mypy_primer results"
|
||||
id: download-mypy_primer_diff
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
|
||||
if: steps.generate-comment.outcome == 'success'
|
||||
id: find-comment
|
||||
with:
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
- name: Create or update comment
|
||||
if: steps.find-comment.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
|
||||
2
.github/workflows/notify-dependents.yml
vendored
2
.github/workflows/notify-dependents.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Update pre-commit mirror"
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
|
||||
script: |
|
||||
|
||||
8
.github/workflows/pr-comment.yaml
vendored
8
.github/workflows/pr-comment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download pull request number
|
||||
with:
|
||||
name: pr-number
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: "Download ecosystem results"
|
||||
id: download-ecosystem-result
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3
|
||||
if: steps.generate-comment.outcome == 'success'
|
||||
id: find-comment
|
||||
with:
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
|
||||
- name: Create or update comment
|
||||
if: steps.find-comment.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
|
||||
8
.github/workflows/publish-docs.yml
vendored
8
.github/workflows/publish-docs.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
env:
|
||||
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -61,14 +61,14 @@ jobs:
|
||||
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
|
||||
58
.github/workflows/publish-knot-playground.yml
vendored
Normal file
58
.github/workflows/publish-knot-playground.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Publish the Red Knot playground.
|
||||
name: "[Knot Playground] Release"
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "crates/red_knot*/**"
|
||||
- "crates/ruff_db/**"
|
||||
- "crates/ruff_python_ast/**"
|
||||
- "crates/ruff_python_parser/**"
|
||||
- "playground/**"
|
||||
- ".github/workflows/publish-knot-playground.yml"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Install Node dependencies"
|
||||
run: npm ci
|
||||
working-directory: playground
|
||||
- name: "Run TypeScript checks"
|
||||
run: npm run check
|
||||
working-directory: playground
|
||||
- name: "Build Knot playground"
|
||||
run: npm run build --workspace knot-playground
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
|
||||
command: pages deploy playground/knot/dist --project-name=knot-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}
|
||||
21
.github/workflows/publish-playground.yml
vendored
21
.github/workflows/publish-playground.yml
vendored
@@ -24,36 +24,31 @@ jobs:
|
||||
env:
|
||||
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: jetli/wasm-bindgen-action@v0.2.0
|
||||
- name: "Run wasm-pack"
|
||||
run: wasm-pack build --target web --out-dir ../../playground/src/pkg crates/ruff_wasm
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Install Node dependencies"
|
||||
run: npm ci
|
||||
working-directory: playground
|
||||
- name: "Run TypeScript checks"
|
||||
run: npm run check
|
||||
working-directory: playground
|
||||
- name: "Build JavaScript bundle"
|
||||
run: npm run build
|
||||
- name: "Build Ruff playground"
|
||||
run: npm run build --workspace ruff-playground
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
|
||||
command: pages deploy playground/dist --project-name=ruff-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}
|
||||
command: pages deploy playground/ruff/dist --project-name=ruff-playground --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}
|
||||
|
||||
4
.github/workflows/publish-pypi.yml
vendored
4
.github/workflows/publish-pypi.yml
vendored
@@ -22,8 +22,8 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
pattern: wheels-*
|
||||
path: wheels
|
||||
|
||||
8
.github/workflows/publish-wasm.yml
vendored
8
.github/workflows/publish-wasm.yml
vendored
@@ -29,15 +29,15 @@ jobs:
|
||||
target: [web, bundler, nodejs]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: jetli/wasm-bindgen-action@v0.2.0
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Run wasm-pack build"
|
||||
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm
|
||||
- name: "Rename generated package"
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
|
||||
mv /tmp/package.json crates/ruff_wasm/pkg
|
||||
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
@@ -1,6 +1,7 @@
|
||||
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
|
||||
# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# Copyright 2025 Astral Software Inc.
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
@@ -50,7 +51,7 @@ on:
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ (inputs.tag != 'dry-run' && inputs.tag) || '' }}
|
||||
@@ -59,16 +60,17 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.2-prerelease.3/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.4-prerelease.1/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
@@ -84,7 +86,7 @@ jobs:
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
@@ -116,23 +118,24 @@ jobs:
|
||||
- plan
|
||||
- custom-build-binaries
|
||||
- custom-build-docker
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -150,7 +153,7 @@ jobs:
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
@@ -167,22 +170,23 @@ jobs:
|
||||
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -196,7 +200,7 @@ jobs:
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
@@ -242,16 +246,17 @@ jobs:
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-wasm.result == 'skipped' || needs.custom-publish-wasm.result == 'success') }}
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
|
||||
6
.github/workflows/sync_typeshed.yaml
vendored
6
.github/workflows/sync_typeshed.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
name: Checkout Ruff
|
||||
with:
|
||||
path: ruff
|
||||
persist-credentials: true
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
name: Checkout typeshed
|
||||
with:
|
||||
repository: python/typeshed
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -18,8 +18,13 @@ exclude: |
|
||||
)$
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.23
|
||||
rev: v0.24.1
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -60,7 +65,7 @@ repos:
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.30.0
|
||||
rev: v1.31.1
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -74,7 +79,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.9
|
||||
rev: v0.11.6
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -84,7 +89,7 @@ repos:
|
||||
|
||||
# Prettier
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.5.2
|
||||
rev: v3.5.3
|
||||
hooks:
|
||||
- id: prettier
|
||||
types: [yaml]
|
||||
@@ -92,12 +97,12 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.4.1
|
||||
rev: v1.6.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.31.2
|
||||
rev: 0.33.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.10.0
|
||||
## 0.11.0
|
||||
|
||||
This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`.
|
||||
|
||||
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
|
||||
|
||||
@@ -23,6 +25,13 @@
|
||||
search for the closest `pyproject.toml` in the parent directories and use its
|
||||
`requires-python` setting.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
|
||||
|
||||
Because of a mistake in the release process, the `requires-python` inference changes are not included in this release and instead shipped as part of 0.11.0.
|
||||
You can find a description of this change in the 0.11.0 section.
|
||||
|
||||
- **Updated `TYPE_CHECKING` behavior** ([#16669](https://github.com/astral-sh/ruff/pull/16669))
|
||||
|
||||
Previously, Ruff only recognized typechecking blocks that tested the `typing.TYPE_CHECKING` symbol. Now, Ruff recognizes any local variable named `TYPE_CHECKING`. This release also removes support for the legacy `if 0:` and `if False:` typechecking checks. Use a local `TYPE_CHECKING` variable instead.
|
||||
|
||||
171
CHANGELOG.md
171
CHANGELOG.md
@@ -1,13 +1,134 @@
|
||||
# Changelog
|
||||
|
||||
## 0.10.0
|
||||
## 0.11.6
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.10.0) for a migration guide and overview of the changes!
|
||||
### Preview features
|
||||
|
||||
- Avoid adding whitespace to the end of a docstring after an escaped quote ([#17216](https://github.com/astral-sh/ruff/pull/17216))
|
||||
- \[`airflow`\] Extract `AIR311` from `AIR301` rules (`AIR301`, `AIR311`) ([#17310](https://github.com/astral-sh/ruff/pull/17310), [#17422](https://github.com/astral-sh/ruff/pull/17422))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Raise syntax error when `\` is at end of file ([#17409](https://github.com/astral-sh/ruff/pull/17409))
|
||||
|
||||
## 0.11.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Add missing `AIR302` attribute check ([#17115](https://github.com/astral-sh/ruff/pull/17115))
|
||||
- \[`airflow`\] Expand module path check to individual symbols (`AIR302`) ([#17278](https://github.com/astral-sh/ruff/pull/17278))
|
||||
- \[`airflow`\] Extract `AIR312` from `AIR302` rules (`AIR302`, `AIR312`) ([#17152](https://github.com/astral-sh/ruff/pull/17152))
|
||||
- \[`airflow`\] Update oudated `AIR301`, `AIR302` rules ([#17123](https://github.com/astral-sh/ruff/pull/17123))
|
||||
- [syntax-errors] Async comprehension in sync comprehension ([#17177](https://github.com/astral-sh/ruff/pull/17177))
|
||||
- [syntax-errors] Check annotations in annotated assignments ([#17283](https://github.com/astral-sh/ruff/pull/17283))
|
||||
- [syntax-errors] Extend annotation checks to `await` ([#17282](https://github.com/astral-sh/ruff/pull/17282))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-pie`\] Avoid false positive for multiple assignment with `auto()` (`PIE796`) ([#17274](https://github.com/astral-sh/ruff/pull/17274))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`ruff`\] Fix `RUF100` to detect unused file-level `noqa` directives with specific codes (#17042) ([#17061](https://github.com/astral-sh/ruff/pull/17061))
|
||||
- \[`flake8-pytest-style`\] Avoid false positive for legacy form of `pytest.raises` (`PT011`) ([#17231](https://github.com/astral-sh/ruff/pull/17231))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix formatting of "See Style Guide" link ([#17272](https://github.com/astral-sh/ruff/pull/17272))
|
||||
|
||||
## 0.11.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`ruff`\] Implement `invalid-rule-code` as `RUF102` ([#17138](https://github.com/astral-sh/ruff/pull/17138))
|
||||
- [syntax-errors] Detect duplicate keys in `match` mapping patterns ([#17129](https://github.com/astral-sh/ruff/pull/17129))
|
||||
- [syntax-errors] Detect duplicate attributes in `match` class patterns ([#17186](https://github.com/astral-sh/ruff/pull/17186))
|
||||
- [syntax-errors] Detect invalid syntax in annotations ([#17101](https://github.com/astral-sh/ruff/pull/17101))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [syntax-errors] Fix multiple assignment error for class fields in `match` patterns ([#17184](https://github.com/astral-sh/ruff/pull/17184))
|
||||
- Don't skip visiting non-tuple slice in `typing.Annotated` subscripts ([#17201](https://github.com/astral-sh/ruff/pull/17201))
|
||||
|
||||
## 0.11.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Add more autofixes for `AIR302` ([#16876](https://github.com/astral-sh/ruff/pull/16876), [#16977](https://github.com/astral-sh/ruff/pull/16977), [#16976](https://github.com/astral-sh/ruff/pull/16976), [#16965](https://github.com/astral-sh/ruff/pull/16965))
|
||||
- \[`airflow`\] Move `AIR301` to `AIR002` ([#16978](https://github.com/astral-sh/ruff/pull/16978))
|
||||
- \[`airflow`\] Move `AIR302` to `AIR301` and `AIR303` to `AIR302` ([#17151](https://github.com/astral-sh/ruff/pull/17151))
|
||||
- \[`flake8-bandit`\] Mark `str` and `list[str]` literals as trusted input (`S603`) ([#17136](https://github.com/astral-sh/ruff/pull/17136))
|
||||
- \[`ruff`\] Support slices in `RUF005` ([#17078](https://github.com/astral-sh/ruff/pull/17078))
|
||||
- [syntax-errors] Start detecting compile-time syntax errors ([#16106](https://github.com/astral-sh/ruff/pull/16106))
|
||||
- [syntax-errors] Duplicate type parameter names ([#16858](https://github.com/astral-sh/ruff/pull/16858))
|
||||
- [syntax-errors] Irrefutable `case` pattern before final case ([#16905](https://github.com/astral-sh/ruff/pull/16905))
|
||||
- [syntax-errors] Multiple assignments in `case` pattern ([#16957](https://github.com/astral-sh/ruff/pull/16957))
|
||||
- [syntax-errors] Single starred assignment target ([#17024](https://github.com/astral-sh/ruff/pull/17024))
|
||||
- [syntax-errors] Starred expressions in `return`, `yield`, and `for` ([#17134](https://github.com/astral-sh/ruff/pull/17134))
|
||||
- [syntax-errors] Store to or delete `__debug__` ([#16984](https://github.com/astral-sh/ruff/pull/16984))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Error instead of `panic!` when running Ruff from a deleted directory (#16903) ([#17054](https://github.com/astral-sh/ruff/pull/17054))
|
||||
- [syntax-errors] Fix false positive for parenthesized tuple index ([#16948](https://github.com/astral-sh/ruff/pull/16948))
|
||||
|
||||
### CLI
|
||||
|
||||
- Check `pyproject.toml` correctly when it is passed via stdin ([#16971](https://github.com/astral-sh/ruff/pull/16971))
|
||||
|
||||
### Configuration
|
||||
|
||||
- \[`flake8-import-conventions`\] Add import `numpy.typing as npt` to default `flake8-import-conventions.aliases` ([#17133](https://github.com/astral-sh/ruff/pull/17133))
|
||||
|
||||
### Documentation
|
||||
|
||||
- \[`refurb`\] Document why `UserDict`, `UserList`, and `UserString` are preferred over `dict`, `list`, and `str` (`FURB189`) ([#16927](https://github.com/astral-sh/ruff/pull/16927))
|
||||
|
||||
## 0.11.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- [syntax-errors] Fix false-positive syntax errors emitted for annotations on variadic parameters before Python 3.11 ([#16878](https://github.com/astral-sh/ruff/pull/16878))
|
||||
|
||||
## 0.11.1
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Add `chain`, `chain_linear` and `cross_downstream` for `AIR302` ([#16647](https://github.com/astral-sh/ruff/pull/16647))
|
||||
- [syntax-errors] Improve error message and range for pre-PEP-614 decorator syntax errors ([#16581](https://github.com/astral-sh/ruff/pull/16581))
|
||||
- [syntax-errors] PEP 701 f-strings before Python 3.12 ([#16543](https://github.com/astral-sh/ruff/pull/16543))
|
||||
- [syntax-errors] Parenthesized context managers before Python 3.9 ([#16523](https://github.com/astral-sh/ruff/pull/16523))
|
||||
- [syntax-errors] Star annotations before Python 3.11 ([#16545](https://github.com/astral-sh/ruff/pull/16545))
|
||||
- [syntax-errors] Star expression in index before Python 3.11 ([#16544](https://github.com/astral-sh/ruff/pull/16544))
|
||||
- [syntax-errors] Unparenthesized assignment expressions in sets and indexes ([#16404](https://github.com/astral-sh/ruff/pull/16404))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Server: Allow `FixAll` action in presence of version-specific syntax errors ([#16848](https://github.com/astral-sh/ruff/pull/16848))
|
||||
- \[`flake8-bandit`\] Allow raw strings in `suspicious-mark-safe-usage` (`S308`) #16702 ([#16770](https://github.com/astral-sh/ruff/pull/16770))
|
||||
- \[`refurb`\] Avoid panicking `unwrap` in `verbose-decimal-constructor` (`FURB157`) ([#16777](https://github.com/astral-sh/ruff/pull/16777))
|
||||
- \[`refurb`\] Fix starred expressions fix (`FURB161`) ([#16550](https://github.com/astral-sh/ruff/pull/16550))
|
||||
- Fix `--statistics` reporting for unsafe fixes ([#16756](https://github.com/astral-sh/ruff/pull/16756))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-executables`\] Allow `uv run` in shebang line for `shebang-missing-python` (`EXE003`) ([#16849](https://github.com/astral-sh/ruff/pull/16849),[#16855](https://github.com/astral-sh/ruff/pull/16855))
|
||||
|
||||
### CLI
|
||||
|
||||
- Add `--exit-non-zero-on-format` ([#16009](https://github.com/astral-sh/ruff/pull/16009))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update Ruff tutorial to avoid non-existent fix in `__init__.py` ([#16818](https://github.com/astral-sh/ruff/pull/16818))
|
||||
- \[`flake8-gettext`\] Swap `format-` and `printf-in-get-text-func-call` examples (`INT002`, `INT003`) ([#16769](https://github.com/astral-sh/ruff/pull/16769))
|
||||
|
||||
## 0.11.0
|
||||
|
||||
This is a follow-up to release 0.10.0. Because of a mistake in the release process, the `requires-python` inference changes were not included in that release. Ruff 0.11.0 now includes this change as well as the stabilization of the preview behavior for `PGH004`.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
|
||||
|
||||
In previous versions of Ruff, you could specify your Python version with:
|
||||
@@ -29,13 +150,36 @@ See also, the "Remapped rules" section which may result in disabled rules.
|
||||
search for the closest `pyproject.toml` in the parent directories and use its
|
||||
`requires-python` setting.
|
||||
|
||||
### Stabilization
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`blanket-noqa`](https://docs.astral.sh/ruff/rules/blanket-noqa/) (`PGH004`): Also detect blanked file-level noqa comments (and not just line level comments).
|
||||
|
||||
### Preview features
|
||||
|
||||
- [syntax-errors] Tuple unpacking in `for` statement iterator clause before Python 3.9 ([#16558](https://github.com/astral-sh/ruff/pull/16558))
|
||||
|
||||
## 0.10.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.10.0) for a migration guide and overview of the changes!
|
||||
|
||||
### Breaking changes
|
||||
|
||||
See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
- **Changes to how the Python version is inferred when a `target-version` is not specified** ([#16319](https://github.com/astral-sh/ruff/pull/16319))
|
||||
|
||||
Because of a mistake in the release process, the `requires-python` inference changes are not included in this release and instead shipped as part of 0.11.0.
|
||||
You can find a description of this change in the 0.11.0 section.
|
||||
|
||||
- **Updated `TYPE_CHECKING` behavior** ([#16669](https://github.com/astral-sh/ruff/pull/16669))
|
||||
|
||||
Previously, Ruff only recognized typechecking blocks that tested the `typing.TYPE_CHECKING` symbol. Now, Ruff recognizes any local variable named `TYPE_CHECKING`. This release also removes support for the legacy `if 0:` and `if False:` typechecking checks. Use a local `TYPE_CHECKING` variable instead.
|
||||
|
||||
- **More robust noqa parsing** ([#16483](https://github.com/astral-sh/ruff/pull/16483))
|
||||
|
||||
The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [_Error suppression_](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification.
|
||||
The syntax for both file-level and in-line suppression comments has been unified and made more robust to certain errors. In most cases, this will result in more suppression comments being read by Ruff, but there are a few instances where previously read comments will now log an error to the user instead. Please refer to the documentation on [*Error suppression*](https://docs.astral.sh/ruff/linter/#error-suppression) for the full specification.
|
||||
|
||||
- **Avoid unnecessary parentheses around with statements with a single context manager and a trailing comment** ([#14005](https://github.com/astral-sh/ruff/pull/14005))
|
||||
|
||||
@@ -86,7 +230,6 @@ The following behaviors have been stabilized:
|
||||
|
||||
- [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`) [`invalid-first-argument-name-for-class-method`](https://docs.astral.sh/ruff/rules/invalid-first-argument-name-for-class-method/) (`N804`): `__new__` methods are now no longer flagged by `invalid-first-argument-name-for-class-method` (`N804`) but instead by `bad-staticmethod-argument` (`PLW0211`)
|
||||
- [`bad-str-strip-call`](https://docs.astral.sh/ruff/rules/bad-str-strip-call/) (`PLE1310`): The rule now applies to objects which are known to have type `str` or `bytes`.
|
||||
- [`blanket-noqa`](https://docs.astral.sh/ruff/rules/blanket-noqa/) (`PGH004`): Also detect blanked file-level noqa comments (and not just line level comments).
|
||||
- [`custom-type-var-for-self`](https://docs.astral.sh/ruff/rules/custom-type-var-for-self/) (`PYI019`): More accurate detection of custom `TypeVars` replaceable by `Self`. The range of the diagnostic is now the full function header rather than just the return annotation.
|
||||
- [`invalid-argument-name`](https://docs.astral.sh/ruff/rules/invalid-argument-name/) (`N803`): Ignore argument names of functions decorated with `typing.override`
|
||||
- [`invalid-envvar-default`](https://docs.astral.sh/ruff/rules/invalid-envvar-default/) (`PLW1508`): Detect default value arguments to `os.environ.get` with invalid type.
|
||||
@@ -1362,11 +1505,11 @@ The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`cancel-scope-no-checkpoint`](https://docs.astral.sh/ruff/rules/cancel-scope-no-checkpoint/) (`ASYNC100`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-function-with-timeout`](https://docs.astral.sh/ruff/rules/async-function-with-timeout/) (`ASYNC109`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-busy-wait`](https://docs.astral.sh/ruff/rules/async-busy-wait/) (`ASYNC110`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-zero-sleep`](https://docs.astral.sh/ruff/rules/async-zero-sleep/) (`ASYNC115`): Support `anyio` context mangers.
|
||||
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever/) (`ASYNC116`): Support `anyio` context mangers.
|
||||
- [`cancel-scope-no-checkpoint`](https://docs.astral.sh/ruff/rules/cancel-scope-no-checkpoint/) (`ASYNC100`): Support `asyncio` and `anyio` context managers.
|
||||
- [`async-function-with-timeout`](https://docs.astral.sh/ruff/rules/async-function-with-timeout/) (`ASYNC109`): Support `asyncio` and `anyio` context managers.
|
||||
- [`async-busy-wait`](https://docs.astral.sh/ruff/rules/async-busy-wait/) (`ASYNC110`): Support `asyncio` and `anyio` context managers.
|
||||
- [`async-zero-sleep`](https://docs.astral.sh/ruff/rules/async-zero-sleep/) (`ASYNC115`): Support `anyio` context managers.
|
||||
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever/) (`ASYNC116`): Support `anyio` context managers.
|
||||
|
||||
The following fixes have been stabilized:
|
||||
|
||||
@@ -1448,7 +1591,7 @@ The following fixes have been stabilized:
|
||||
|
||||
## 0.5.6
|
||||
|
||||
Ruff 0.5.6 automatically enables linting and formatting of notebooks in _preview mode_.
|
||||
Ruff 0.5.6 automatically enables linting and formatting of notebooks in *preview mode*.
|
||||
You can opt-out of this behavior by adding `*.ipynb` to the `extend-exclude` setting.
|
||||
|
||||
```toml
|
||||
@@ -2201,7 +2344,7 @@ To setup `ruff server` with your editor, refer to the [README.md](https://github
|
||||
|
||||
### Server
|
||||
|
||||
_This section is devoted to updates for our new language server, written in Rust._
|
||||
*This section is devoted to updates for our new language server, written in Rust.*
|
||||
|
||||
- Enable ruff-specific source actions ([#10916](https://github.com/astral-sh/ruff/pull/10916))
|
||||
- Refreshes diagnostics for open files when file configuration is changed ([#10988](https://github.com/astral-sh/ruff/pull/10988))
|
||||
@@ -3608,7 +3751,7 @@ Read Ruff's new [versioning policy](https://docs.astral.sh/ruff/versioning/).
|
||||
- \[`refurb`\] Add `single-item-membership-test` (`FURB171`) ([#7815](https://github.com/astral-sh/ruff/pull/7815))
|
||||
- \[`pylint`\] Add `and-or-ternary` (`R1706`) ([#7811](https://github.com/astral-sh/ruff/pull/7811))
|
||||
|
||||
_New rules are added in [preview](https://docs.astral.sh/ruff/preview/)._
|
||||
*New rules are added in [preview](https://docs.astral.sh/ruff/preview/).*
|
||||
|
||||
### Configuration
|
||||
|
||||
|
||||
569
Cargo.lock
generated
569
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -4,7 +4,7 @@ resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.83"
|
||||
rust-version = "1.84"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -38,10 +38,11 @@ ruff_text_size = { path = "crates/ruff_text_size" }
|
||||
red_knot_vendored = { path = "crates/red_knot_vendored" }
|
||||
ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot_ide = { path = "crates/red_knot_ide" }
|
||||
red_knot_project = { path = "crates/red_knot_project", default-features = false }
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_server = { path = "crates/red_knot_server" }
|
||||
red_knot_test = { path = "crates/red_knot_test" }
|
||||
red_knot_project = { path = "crates/red_knot_project", default-features = false }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
anstream = { version = "0.6.18" }
|
||||
@@ -54,7 +55,6 @@ bitflags = { version = "2.5.0" }
|
||||
bstr = { version = "1.9.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
camino = { version = "1.1.7" }
|
||||
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.5.3", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.6.0" }
|
||||
clearscreen = { version = "4.0.0" }
|
||||
@@ -63,7 +63,7 @@ colored = { version = "3.0.0" }
|
||||
console_error_panic_hook = { version = "0.1.7" }
|
||||
console_log = { version = "1.0.0" }
|
||||
countme = { version = "3.0.1" }
|
||||
compact_str = "0.8.0"
|
||||
compact_str = "0.9.0"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam = { version = "0.8.4" }
|
||||
dashmap = { version = "6.0.1" }
|
||||
@@ -71,7 +71,7 @@ dir-test = { version = "0.4.0" }
|
||||
dunce = { version = "1.0.5" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
etcetera = { version = "0.8.0" }
|
||||
etcetera = { version = "0.10.0" }
|
||||
fern = { version = "0.7.0" }
|
||||
filetime = { version = "0.2.23" }
|
||||
getrandom = { version = "0.3.1" }
|
||||
@@ -94,6 +94,7 @@ insta-cmd = { version = "0.6.0" }
|
||||
is-macro = { version = "0.3.5" }
|
||||
is-wsl = { version = "0.4.0" }
|
||||
itertools = { version = "0.14.0" }
|
||||
jiff = { version = "0.2.0" }
|
||||
js-sys = { version = "0.3.69" }
|
||||
jod-thread = { version = "0.1.2" }
|
||||
libc = { version = "0.2.153" }
|
||||
@@ -123,7 +124,7 @@ rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "095d8b2b8115c3cf8bf31914dd9ea74648bb7cf9" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "87bf6b6c2d5f6479741271da73bd9d30c2580c26" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -154,6 +155,7 @@ toml = { version = "0.8.11" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-flame = { version = "0.2.0" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-log = { version = "0.2.0" }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
@@ -207,6 +209,7 @@ must_use_candidate = "allow"
|
||||
similar_names = "allow"
|
||||
single_match_else = "allow"
|
||||
too_many_lines = "allow"
|
||||
needless_continue = "allow" # An explicit continue can be more readable, especially if the alternative is an empty block.
|
||||
# Without the hashes we run into a `rustfmt` bug in some snapshot tests, see #13250
|
||||
needless_raw_string_hashes = "allow"
|
||||
# Disallowed restriction lints
|
||||
@@ -269,7 +272,7 @@ inherits = "release"
|
||||
# Config for 'dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.25.2-prerelease.3"
|
||||
cargo-dist-version = "0.28.4-prerelease.1"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
@@ -326,3 +329,12 @@ github-custom-job-permissions = { "build-docker" = { packages = "write", content
|
||||
install-updater = false
|
||||
# Path that installers should place binaries in
|
||||
install-path = ["$XDG_BIN_HOME/", "$XDG_DATA_HOME/../bin", "~/.local/bin"]
|
||||
|
||||
[workspace.metadata.dist.github-custom-runners]
|
||||
global = "depot-ubuntu-latest-4"
|
||||
|
||||
[workspace.metadata.dist.github-action-commits]
|
||||
"actions/checkout" = "11bd71901bbe5b1630ceea73d27597364c9af683" # v4
|
||||
"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2
|
||||
"actions/download-artifact" = "95815c38cf2ff2164869cbab79da8d1f422bc89e" # v4.2.1
|
||||
"actions/attest-build-provenance" = "c074443f1aee8d4aeeae555aebba3282517141b2" #v2.2.3
|
||||
|
||||
@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.10.0/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.10.0/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.11.6/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.6/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.10.0
|
||||
rev: v0.11.6
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -20,12 +20,12 @@ ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help"] }
|
||||
colored = { workspace = true }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
crossbeam = { workspace = true }
|
||||
ctrlc = { version = "3.4.4" }
|
||||
jiff = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true, features = ["release_max_level_debug"] }
|
||||
|
||||
@@ -31,7 +31,7 @@ mypy_primer \
|
||||
```
|
||||
|
||||
This will show the diagnostics diff for the `black` project between the `main` branch and your `my/feature` branch. To run the
|
||||
diff for all projects, you currently need to copy the project-selector regex from the CI pipeline in `.github/workflows/mypy_primer.yaml`.
|
||||
diff for all projects we currently enable in CI, use `--project-selector "/($(paste -s -d'|' crates/red_knot_python_semantic/resources/primer/good.txt))\$"`.
|
||||
|
||||
You can also take a look at the [full list of ecosystem projects]. Note that some of them might still need a `knot_paths` configuration
|
||||
option to work correctly.
|
||||
|
||||
@@ -50,6 +50,8 @@ pub(crate) struct CheckCommand {
|
||||
|
||||
/// Path to the Python installation from which Red Knot resolves type information and third-party dependencies.
|
||||
///
|
||||
/// If not specified, Red Knot will look at the `VIRTUAL_ENV` environment variable.
|
||||
///
|
||||
/// Red Knot will search in the path's `site-packages` directories for type information and
|
||||
/// third-party imports.
|
||||
///
|
||||
@@ -69,12 +71,29 @@ pub(crate) struct CheckCommand {
|
||||
#[arg(long, value_name = "VERSION", alias = "target-version")]
|
||||
pub(crate) python_version: Option<PythonVersion>,
|
||||
|
||||
/// Target platform to assume when resolving types.
|
||||
///
|
||||
/// This is used to specialize the type of `sys.platform` and will affect the visibility
|
||||
/// of platform-specific functions and attributes. If the value is set to `all`, no
|
||||
/// assumptions are made about the target platform. If unspecified, the current system's
|
||||
/// platform will be used.
|
||||
#[arg(long, value_name = "PLATFORM", alias = "platform")]
|
||||
pub(crate) python_platform: Option<String>,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) verbosity: Verbosity,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) rules: RulesArg,
|
||||
|
||||
/// The format to use for printing diagnostic messages.
|
||||
#[arg(long)]
|
||||
pub(crate) output_format: Option<OutputFormat>,
|
||||
|
||||
/// Control when colored output is used.
|
||||
#[arg(long, value_name = "WHEN")]
|
||||
pub(crate) color: Option<TerminalColor>,
|
||||
|
||||
/// Use exit code 1 if there are any warning-level diagnostics.
|
||||
#[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)]
|
||||
pub(crate) error_on_warning: Option<bool>,
|
||||
@@ -106,6 +125,9 @@ impl CheckCommand {
|
||||
python_version: self
|
||||
.python_version
|
||||
.map(|version| RangedValue::cli(version.into())),
|
||||
python_platform: self
|
||||
.python_platform
|
||||
.map(|platform| RangedValue::cli(platform.into())),
|
||||
python: self.python.map(RelativePathBuf::cli),
|
||||
typeshed: self.typeshed.map(RelativePathBuf::cli),
|
||||
extra_paths: self.extra_search_path.map(|extra_search_paths| {
|
||||
@@ -114,9 +136,11 @@ impl CheckCommand {
|
||||
.map(RelativePathBuf::cli)
|
||||
.collect()
|
||||
}),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
terminal: Some(TerminalOptions {
|
||||
output_format: self
|
||||
.output_format
|
||||
.map(|output_format| RangedValue::cli(output_format.into())),
|
||||
error_on_warning: self.error_on_warning,
|
||||
}),
|
||||
rules,
|
||||
@@ -211,3 +235,46 @@ impl clap::Args for RulesArg {
|
||||
Self::augment_args(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
/// The diagnostic output format.
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum OutputFormat {
|
||||
/// Print diagnostics verbosely, with context and helpful hints.
|
||||
///
|
||||
/// Diagnostic messages may include additional context and
|
||||
/// annotations on the input to help understand the message.
|
||||
#[default]
|
||||
#[value(name = "full")]
|
||||
Full,
|
||||
/// Print diagnostics concisely, one per line.
|
||||
///
|
||||
/// This will guarantee that each diagnostic is printed on
|
||||
/// a single line. Only the most important or primary aspects
|
||||
/// of the diagnostic are included. Contextual information is
|
||||
/// dropped.
|
||||
#[value(name = "concise")]
|
||||
Concise,
|
||||
}
|
||||
|
||||
impl From<OutputFormat> for ruff_db::diagnostic::DiagnosticFormat {
|
||||
fn from(format: OutputFormat) -> ruff_db::diagnostic::DiagnosticFormat {
|
||||
match format {
|
||||
OutputFormat::Full => Self::Full,
|
||||
OutputFormat::Concise => Self::Concise,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Control when colored output is used.
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub(crate) enum TerminalColor {
|
||||
/// Display colors if the output goes to an interactive terminal.
|
||||
#[default]
|
||||
Auto,
|
||||
|
||||
/// Always display colors.
|
||||
Always,
|
||||
|
||||
/// Never display colors.
|
||||
Never,
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ where
|
||||
let ansi = writer.has_ansi_escapes();
|
||||
|
||||
if self.display_timestamp {
|
||||
let timestamp = chrono::Local::now()
|
||||
.format("%Y-%m-%d %H:%M:%S.%f")
|
||||
let timestamp = jiff::Zoned::now()
|
||||
.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
.to_string();
|
||||
if ansi {
|
||||
write!(writer, "{} ", timestamp.dimmed())?;
|
||||
@@ -199,7 +199,7 @@ where
|
||||
write!(
|
||||
writer,
|
||||
"{} ",
|
||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f")
|
||||
jiff::Zoned::now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::process::{ExitCode, Termination};
|
||||
use anyhow::Result;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::args::{Args, CheckCommand, Command};
|
||||
use crate::args::{Args, CheckCommand, Command, TerminalColor};
|
||||
use crate::logging::setup_tracing;
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
@@ -15,7 +15,7 @@ use red_knot_project::watch::ProjectWatcher;
|
||||
use red_knot_project::{watch, Db};
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_server::run_server;
|
||||
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait, Severity};
|
||||
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
@@ -76,10 +76,14 @@ pub(crate) fn version() -> Result<()> {
|
||||
}
|
||||
|
||||
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
set_colored_override(args.color);
|
||||
|
||||
let verbosity = args.verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
|
||||
tracing::debug!("Version: {}", version::version());
|
||||
|
||||
// The base path to which all CLI arguments are relative to.
|
||||
let cwd = {
|
||||
let cwd = std::env::current_dir().context("Failed to get the current working directory")?;
|
||||
@@ -255,15 +259,16 @@ impl MainLoop {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let terminal_settings = db.project().settings(db).terminal();
|
||||
let display_config = DisplayDiagnosticConfig::default()
|
||||
.format(terminal_settings.output_format)
|
||||
.color(colored::control::SHOULD_COLORIZE.should_colorize());
|
||||
|
||||
let min_error_severity =
|
||||
if db.project().settings(db).terminal().error_on_warning {
|
||||
Severity::Warning
|
||||
} else {
|
||||
Severity::Error
|
||||
};
|
||||
let min_error_severity = if terminal_settings.error_on_warning {
|
||||
Severity::Warning
|
||||
} else {
|
||||
Severity::Error
|
||||
};
|
||||
|
||||
if check_revision == revision {
|
||||
if db.project().files(db).is_empty() {
|
||||
@@ -283,7 +288,7 @@ impl MainLoop {
|
||||
let diagnostics_count = result.len();
|
||||
|
||||
for diagnostic in result {
|
||||
writeln!(stdout, "{}", diagnostic.display(db, &display_config))?;
|
||||
write!(stdout, "{}", diagnostic.display(db, &display_config))?;
|
||||
|
||||
failed |= diagnostic.severity() >= min_error_severity;
|
||||
}
|
||||
@@ -354,9 +359,27 @@ enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted {
|
||||
/// The diagnostics that were found during the check.
|
||||
result: Vec<Box<dyn OldDiagnosticTrait>>,
|
||||
result: Vec<Diagnostic>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn set_colored_override(color: Option<TerminalColor>) {
|
||||
let Some(color) = color else {
|
||||
return;
|
||||
};
|
||||
|
||||
match color {
|
||||
TerminalColor::Auto => {
|
||||
colored::control::unset_override();
|
||||
}
|
||||
TerminalColor::Always => {
|
||||
colored::control::set_override(true);
|
||||
}
|
||||
TerminalColor::Never => {
|
||||
colored::control::set_override(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Specifying an option on the CLI should take precedence over the same setting in the
|
||||
/// project's configuration.
|
||||
/// project's configuration. Here, this is tested for the Python version.
|
||||
#[test]
|
||||
fn config_override() -> anyhow::Result<()> {
|
||||
fn config_override_python_version() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
@@ -57,6 +57,67 @@ fn config_override() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as above, but for the Python platform.
|
||||
#[test]
|
||||
fn config_override_python_platform() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.knot.environment]
|
||||
python-platform = "linux"
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"test.py",
|
||||
r#"
|
||||
import sys
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
reveal_type(sys.platform)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> <temp_dir>/test.py:5:1
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal["linux"]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type: Revealed type
|
||||
--> <temp_dir>/test.py:5:1
|
||||
|
|
||||
3 | from typing_extensions import reveal_type
|
||||
4 |
|
||||
5 | reveal_type(sys.platform)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `LiteralString`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paths specified on the CLI are relative to the current working directory and not the project root.
|
||||
///
|
||||
/// We test this by adding an extra search path from the CLI to the libs directory when
|
||||
@@ -191,7 +252,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, y):
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
@@ -210,7 +271,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
warning: lint:possibly-unresolved-reference
|
||||
@@ -219,7 +280,7 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| - Name `x` used when possibly not defined
|
||||
| ^ Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
@@ -244,9 +305,9 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -267,7 +328,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, y):
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
@@ -297,7 +358,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
4 | y = 4 / 0
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
5 |
|
||||
6 | for a in range(0, y):
|
||||
6 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
warning: lint:possibly-unresolved-reference
|
||||
@@ -306,7 +367,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
7 | x = a
|
||||
8 |
|
||||
9 | print(x) # possibly-unresolved-reference
|
||||
| - Name `x` used when possibly not defined
|
||||
| ^ Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 3 diagnostics
|
||||
@@ -331,7 +392,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
| ------------- Cannot resolve import `does_not_exit`
|
||||
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
@@ -342,9 +403,9 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
2 | import does_not_exit
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
5 |
|
||||
6 | for a in range(0, y):
|
||||
6 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
@@ -365,7 +426,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, y):
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x) # possibly-unresolved-reference
|
||||
@@ -384,7 +445,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
warning: lint:possibly-unresolved-reference
|
||||
@@ -393,7 +454,7 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| - Name `x` used when possibly not defined
|
||||
| ^ Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
@@ -419,9 +480,9 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -456,7 +517,7 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
|
|
||||
2 | [tool.knot.rules]
|
||||
3 | division-by-zer = "warn" # incorrect rule name
|
||||
| --------------- Unknown lint rule `division-by-zer`
|
||||
| ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -498,7 +559,7 @@ fn exit_code_only_warnings() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
| ^ Name `x` used when not defined
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -523,12 +584,12 @@ fn exit_code_only_info() -> anyhow::Result<()> {
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> <temp_dir>/test.py:3:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| -------------- info: Revealed type is `Literal[1]`
|
||||
| ^^^^^^^^^^^^^^ `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -553,12 +614,12 @@ fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> <temp_dir>/test.py:3:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| -------------- info: Revealed type is `Literal[1]`
|
||||
| ^^^^^^^^^^^^^^ `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -581,7 +642,7 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
| ^ Name `x` used when not defined
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -613,7 +674,7 @@ fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> any
|
||||
--> <temp_dir>/test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
| ^ Name `x` used when not defined
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -642,7 +703,7 @@ fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
| ^ Name `x` used when not defined
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
|
||||
@@ -680,7 +741,7 @@ fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::
|
||||
--> <temp_dir>/test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
| ^ Name `x` used when not defined
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
|
||||
@@ -718,7 +779,7 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
| ^ Name `x` used when not defined
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
|
||||
@@ -753,7 +814,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
r#"
|
||||
y = 4 / 0
|
||||
|
||||
for a in range(0, y):
|
||||
for a in range(0, int(y)):
|
||||
x = a
|
||||
|
||||
print(x)
|
||||
@@ -778,9 +839,9 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/project/main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
warning: lint:possibly-unresolved-reference
|
||||
@@ -789,7 +850,7 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x)
|
||||
| - Name `x` used when possibly not defined
|
||||
| ^ Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
Found 2 diagnostics
|
||||
@@ -820,9 +881,9 @@ fn user_configuration() -> anyhow::Result<()> {
|
||||
--> <temp_dir>/project/main.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
4 | for a in range(0, int(y)):
|
||||
|
|
||||
|
||||
error: lint:possibly-unresolved-reference
|
||||
@@ -967,6 +1028,63 @@ fn check_non_existing_path() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concise_diagnostics() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:unresolved-reference] <temp_dir>/test.py:2:7: Name `x` used when not defined
|
||||
error[lint:non-subscriptable] <temp_dir>/test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This tests the diagnostic format for revealed type.
|
||||
///
|
||||
/// This test was introduced because changes were made to
|
||||
/// how the revealed type diagnostic was constructed and
|
||||
/// formatted in "verbose" mode. But it required extra
|
||||
/// logic to ensure the concise version didn't regress on
|
||||
/// information content. So this test was introduced to
|
||||
/// capture that.
|
||||
#[test]
|
||||
fn concise_revealed_type() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
x = "hello"
|
||||
reveal_type(x)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info[revealed-type] <temp_dir>/test.py:5:1: Revealed type: `Literal["hello"]`
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
_temp_dir: TempDir,
|
||||
_settings_scope: SettingsBindDropGuard,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::disallowed_names)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::io::Write;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -14,9 +12,9 @@ use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform};
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{
|
||||
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
|
||||
file_time_now, OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
|
||||
};
|
||||
use ruff_db::Upcast;
|
||||
use ruff_db::{Db as _, Upcast};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
struct TestCase {
|
||||
@@ -464,7 +462,7 @@ fn update_file(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()
|
||||
|
||||
std::thread::sleep(Duration::from_nanos(10));
|
||||
|
||||
filetime::set_file_handle_times(&file, None, Some(filetime::FileTime::now()))?;
|
||||
filetime::set_file_handle_times(&file, None, Some(file_time_now()))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,11 +1125,11 @@ print(sys.last_exc, os.getegid())
|
||||
|
||||
assert_eq!(diagnostics.len(), 2);
|
||||
assert_eq!(
|
||||
diagnostics[0].message(),
|
||||
diagnostics[0].primary_message(),
|
||||
"Type `<module 'sys'>` has no attribute `last_exc`"
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics[1].message(),
|
||||
diagnostics[1].primary_message(),
|
||||
"Type `<module 'os'>` has no attribute `getegid`"
|
||||
);
|
||||
|
||||
@@ -1790,3 +1788,82 @@ fn changes_to_user_configuration() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that renaming a file from `lib.py` to `Lib.py` is correctly reflected.
|
||||
///
|
||||
/// This test currently fails on case-insensitive systems because `Files` is case-sensitive
|
||||
/// but the `System::metadata` call isn't. This means that
|
||||
/// Red Knot considers both `Lib.py` and `lib.py` to exist when only `lib.py` does
|
||||
///
|
||||
/// The incoming change events then are no-ops because they don't change either file's
|
||||
/// status nor does it update their last modified time (renaming a file doesn't bump it's
|
||||
/// last modified timestamp).
|
||||
///
|
||||
/// Fixing this requires to either make `Files` case-insensitive and store the
|
||||
/// real-case path (if it differs) on `File` or make `Files` use a
|
||||
/// case-sensitive `System::metadata` call. This does open the question if all
|
||||
/// `System` calls should be case sensitive. This would be the most consistent
|
||||
/// but might be hard to pull off.
|
||||
///
|
||||
/// What the right solution is also depends on if Red Knot itself should be case
|
||||
/// sensitive or not. E.g. should `include="src"` be case sensitive on all systems
|
||||
/// or only on case-sensitive systems?
|
||||
///
|
||||
/// Lastly, whatever solution we pick must also work well with VS Code which,
|
||||
/// unfortunately ,doesn't propagate casing-only renames.
|
||||
/// <https://github.com/rust-lang/rust-analyzer/issues/9581>
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn rename_files_casing_only() -> anyhow::Result<()> {
|
||||
let mut case = setup([("lib.py", "class Foo: ...")])?;
|
||||
|
||||
assert!(
|
||||
resolve_module(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
|
||||
"Expected `lib` module to exist."
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()),
|
||||
None,
|
||||
"Expected `Lib` module not to exist"
|
||||
);
|
||||
|
||||
// Now rename `lib.py` to `Lib.py`
|
||||
if case.db().system().case_sensitivity().is_case_sensitive() {
|
||||
std::fs::rename(
|
||||
case.project_path("lib.py").as_std_path(),
|
||||
case.project_path("Lib.py").as_std_path(),
|
||||
)
|
||||
.context("Failed to rename `lib.py` to `Lib.py`")?;
|
||||
} else {
|
||||
// On case-insensitive file systems, renaming a file to a different casing is a no-op.
|
||||
// Rename to a different name first
|
||||
std::fs::rename(
|
||||
case.project_path("lib.py").as_std_path(),
|
||||
case.project_path("temp.py").as_std_path(),
|
||||
)
|
||||
.context("Failed to rename `lib.py` to `temp.py`")?;
|
||||
|
||||
std::fs::rename(
|
||||
case.project_path("temp.py").as_std_path(),
|
||||
case.project_path("Lib.py").as_std_path(),
|
||||
)
|
||||
.context("Failed to rename `temp.py` to `Lib.py`")?;
|
||||
}
|
||||
|
||||
let changes = case.stop_watch(event_for_file("Lib.py"));
|
||||
case.apply_changes(changes);
|
||||
|
||||
// Resolving `lib` should now fail but `Lib` should now succeed
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), &ModuleName::new("lib").unwrap()),
|
||||
None,
|
||||
"Expected `lib` module to no longer exist."
|
||||
);
|
||||
|
||||
assert!(
|
||||
resolve_module(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
|
||||
"Expected `Lib` module to exist"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
31
crates/red_knot_ide/Cargo.toml
Normal file
31
crates/red_knot_ide/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "red_knot_ide"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
138
crates/red_knot_ide/src/db.rs
Normal file
138
crates/red_knot_ide/src/db.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use red_knot_python_semantic::Db as SemanticDb;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
pub trait Db: SemanticDb + Upcast<dyn SemanticDb> + Upcast<dyn SourceDb> {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::Db;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb, Program};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
events: Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
events: Arc::default(),
|
||||
files: Files::default(),
|
||||
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||
let inner = Arc::get_mut(&mut self.events).expect("no pending salsa snapshots");
|
||||
|
||||
let events = inner.get_mut().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
|
||||
/// Clears the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are any pending salsa snapshots.
|
||||
pub(crate) fn clear_salsa_events(&mut self) {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SemanticDb> for TestDb {
|
||||
fn upcast(&self) -> &(dyn SemanticDb + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn upcast_mut(&mut self) -> &mut dyn SemanticDb {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for TestDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
default_lint_registry()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: &dyn Fn() -> salsa::Event) {
|
||||
let event = event();
|
||||
tracing::trace!("event: {event:?}");
|
||||
let mut events = self.events.lock().unwrap();
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
crates/red_knot_ide/src/find_node.rs
Normal file
106
crates/red_knot_ide/src/find_node.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
/// Returns the node with a minimal range that fully contains `range`.
|
||||
///
|
||||
/// If `range` is empty and falls within a parser *synthesized* node generated during error recovery,
|
||||
/// then the first node with the given range is returned.
|
||||
///
|
||||
/// ## Panics
|
||||
/// Panics if `range` is not contained within `root`.
|
||||
pub(crate) fn covering_node(root: AnyNodeRef, range: TextRange) -> CoveringNode {
|
||||
struct Visitor<'a> {
|
||||
range: TextRange,
|
||||
found: bool,
|
||||
ancestors: Vec<AnyNodeRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> SourceOrderVisitor<'a> for Visitor<'a> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
||||
// If the node fully contains the range, than it is a possible match but traverse into its children
|
||||
// to see if there's a node with a narrower range.
|
||||
if !self.found && node.range().contains_range(self.range) {
|
||||
self.ancestors.push(node);
|
||||
TraversalSignal::Traverse
|
||||
} else {
|
||||
TraversalSignal::Skip
|
||||
}
|
||||
}
|
||||
|
||||
fn leave_node(&mut self, node: AnyNodeRef<'a>) {
|
||||
if !self.found && self.ancestors.last() == Some(&node) {
|
||||
self.found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
root.range().contains_range(range),
|
||||
"Range is not contained within root"
|
||||
);
|
||||
|
||||
let mut visitor = Visitor {
|
||||
range,
|
||||
found: false,
|
||||
ancestors: Vec::new(),
|
||||
};
|
||||
|
||||
root.visit_source_order(&mut visitor);
|
||||
|
||||
let minimal = visitor.ancestors.pop().unwrap_or(root);
|
||||
CoveringNode {
|
||||
node: minimal,
|
||||
ancestors: visitor.ancestors,
|
||||
}
|
||||
}
|
||||
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
pub(crate) struct CoveringNode<'a> {
|
||||
/// The node with a minimal range that fully contains the search range.
|
||||
node: AnyNodeRef<'a>,
|
||||
|
||||
/// The node's ancestor (the spine up to the root).
|
||||
ancestors: Vec<AnyNodeRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> CoveringNode<'a> {
|
||||
pub(crate) fn node(&self) -> AnyNodeRef<'a> {
|
||||
self.node
|
||||
}
|
||||
|
||||
/// Returns the node's parent.
|
||||
pub(crate) fn parent(&self) -> Option<AnyNodeRef<'a>> {
|
||||
self.ancestors.last().copied()
|
||||
}
|
||||
|
||||
/// Finds the minimal node that fully covers the range and fulfills the given predicate.
|
||||
pub(crate) fn find(mut self, f: impl Fn(AnyNodeRef<'a>) -> bool) -> Result<Self, Self> {
|
||||
if f(self.node) {
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
match self.ancestors.iter().rposition(|node| f(*node)) {
|
||||
Some(index) => {
|
||||
let node = self.ancestors[index];
|
||||
self.ancestors.truncate(index);
|
||||
|
||||
Ok(Self {
|
||||
node,
|
||||
ancestors: self.ancestors,
|
||||
})
|
||||
}
|
||||
None => Err(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CoveringNode<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("NodeWithAncestors")
|
||||
.field(&self.node)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
851
crates/red_knot_ide/src/goto.rs
Normal file
851
crates/red_knot_ide/src/goto.rs
Normal file
@@ -0,0 +1,851 @@
|
||||
use crate::find_node::covering_node;
|
||||
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
||||
use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::{HasType, SemanticModel};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::{parsed_module, ParsedModule};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
pub fn goto_type_definition(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
offset: TextSize,
|
||||
) -> Option<RangedValue<NavigationTargets>> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
|
||||
let model = SemanticModel::new(db.upcast(), file);
|
||||
let ty = goto_target.inferred_type(&model)?;
|
||||
|
||||
tracing::debug!(
|
||||
"Inferred type of covering node is {}",
|
||||
ty.display(db.upcast())
|
||||
);
|
||||
|
||||
let navigation_targets = ty.navigation_targets(db);
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: navigation_targets,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum GotoTarget<'a> {
|
||||
Expression(ast::ExprRef<'a>),
|
||||
FunctionDef(&'a ast::StmtFunctionDef),
|
||||
ClassDef(&'a ast::StmtClassDef),
|
||||
Parameter(&'a ast::Parameter),
|
||||
Alias(&'a ast::Alias),
|
||||
|
||||
/// Go to on the module name of an import from
|
||||
/// ```py
|
||||
/// from foo import bar
|
||||
/// ^^^
|
||||
/// ```
|
||||
ImportedModule(&'a ast::StmtImportFrom),
|
||||
|
||||
/// Go to on the exception handler variable
|
||||
/// ```py
|
||||
/// try: ...
|
||||
/// except Exception as e: ...
|
||||
/// ^
|
||||
/// ```
|
||||
ExceptVariable(&'a ast::ExceptHandlerExceptHandler),
|
||||
|
||||
/// Go to on a keyword argument
|
||||
/// ```py
|
||||
/// test(a = 1)
|
||||
/// ^
|
||||
/// ```
|
||||
KeywordArgument(&'a ast::Keyword),
|
||||
|
||||
/// Go to on the rest parameter of a pattern match
|
||||
///
|
||||
/// ```py
|
||||
/// match x:
|
||||
/// case {"a": a, "b": b, **rest}: ...
|
||||
/// ^^^^
|
||||
/// ```
|
||||
PatternMatchRest(&'a ast::PatternMatchMapping),
|
||||
|
||||
/// Go to on a keyword argument of a class pattern
|
||||
///
|
||||
/// ```py
|
||||
/// match Point3D(0, 0, 0):
|
||||
/// case Point3D(x=0, y=0, z=0): ...
|
||||
/// ^ ^ ^
|
||||
/// ```
|
||||
PatternKeywordArgument(&'a ast::PatternKeyword),
|
||||
|
||||
/// Go to on a pattern star argument
|
||||
///
|
||||
/// ```py
|
||||
/// match array:
|
||||
/// case [*args]: ...
|
||||
/// ^^^^
|
||||
PatternMatchStarName(&'a ast::PatternMatchStar),
|
||||
|
||||
/// Go to on the name of a pattern match as pattern
|
||||
///
|
||||
/// ```py
|
||||
/// match x:
|
||||
/// case [x] as y: ...
|
||||
/// ^
|
||||
PatternMatchAsName(&'a ast::PatternMatchAs),
|
||||
|
||||
/// Go to on the name of a type variable
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[T: int = bool] = list[T]
|
||||
/// ^
|
||||
/// ```
|
||||
TypeParamTypeVarName(&'a ast::TypeParamTypeVar),
|
||||
|
||||
/// Go to on the name of a type param spec
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[**P = [int, str]] = Callable[P, int]
|
||||
/// ^
|
||||
/// ```
|
||||
TypeParamParamSpecName(&'a ast::TypeParamParamSpec),
|
||||
|
||||
/// Go to on the name of a type var tuple
|
||||
///
|
||||
/// ```py
|
||||
/// type Alias[*Ts = ()] = tuple[*Ts]
|
||||
/// ^^
|
||||
/// ```
|
||||
TypeParamTypeVarTupleName(&'a ast::TypeParamTypeVarTuple),
|
||||
|
||||
NonLocal {
|
||||
identifier: &'a ast::Identifier,
|
||||
},
|
||||
Globals {
|
||||
identifier: &'a ast::Identifier,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> GotoTarget<'db> {
|
||||
pub(crate) fn inferred_type(self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
||||
let ty = match self {
|
||||
GotoTarget::Expression(expression) => expression.inferred_type(model),
|
||||
GotoTarget::FunctionDef(function) => function.inferred_type(model),
|
||||
GotoTarget::ClassDef(class) => class.inferred_type(model),
|
||||
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
|
||||
GotoTarget::Alias(alias) => alias.inferred_type(model),
|
||||
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
||||
GotoTarget::KeywordArgument(argument) => {
|
||||
// TODO: Pyright resolves the declared type of the matching parameter. This seems more accurate
|
||||
// than using the inferred value.
|
||||
argument.value.inferred_type(model)
|
||||
}
|
||||
// TODO: Support identifier targets
|
||||
GotoTarget::PatternMatchRest(_)
|
||||
| GotoTarget::PatternKeywordArgument(_)
|
||||
| GotoTarget::PatternMatchStarName(_)
|
||||
| GotoTarget::PatternMatchAsName(_)
|
||||
| GotoTarget::ImportedModule(_)
|
||||
| GotoTarget::TypeParamTypeVarName(_)
|
||||
| GotoTarget::TypeParamParamSpecName(_)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||
| GotoTarget::NonLocal { .. }
|
||||
| GotoTarget::Globals { .. } => return None,
|
||||
};
|
||||
|
||||
Some(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for GotoTarget<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => expression.range(),
|
||||
GotoTarget::FunctionDef(function) => function.name.range,
|
||||
GotoTarget::ClassDef(class) => class.name.range,
|
||||
GotoTarget::Parameter(parameter) => parameter.name.range,
|
||||
GotoTarget::Alias(alias) => alias.name.range,
|
||||
GotoTarget::ImportedModule(module) => module.module.as_ref().unwrap().range,
|
||||
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
|
||||
GotoTarget::KeywordArgument(keyword) => keyword.arg.as_ref().unwrap().range,
|
||||
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
|
||||
GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range,
|
||||
GotoTarget::PatternMatchStarName(star) => star.name.as_ref().unwrap().range,
|
||||
GotoTarget::PatternMatchAsName(as_name) => as_name.name.as_ref().unwrap().range,
|
||||
GotoTarget::TypeParamTypeVarName(type_var) => type_var.name.range,
|
||||
GotoTarget::TypeParamParamSpecName(spec) => spec.name.range,
|
||||
GotoTarget::TypeParamTypeVarTupleName(tuple) => tuple.name.range,
|
||||
GotoTarget::NonLocal { identifier, .. } => identifier.range,
|
||||
GotoTarget::Globals { identifier, .. } => identifier.range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
|
||||
let token = parsed
|
||||
.tokens()
|
||||
.at_offset(offset)
|
||||
.max_by_key(|token| match token.kind() {
|
||||
TokenKind::Name
|
||||
| TokenKind::String
|
||||
| TokenKind::Complex
|
||||
| TokenKind::Float
|
||||
| TokenKind::Int => 1,
|
||||
_ => 0,
|
||||
})?;
|
||||
|
||||
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
||||
.find(|node| node.is_identifier() || node.is_expression())
|
||||
.ok()?;
|
||||
|
||||
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
||||
|
||||
match covering_node.node() {
|
||||
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
||||
Some(AnyNodeRef::StmtFunctionDef(function)) => Some(GotoTarget::FunctionDef(function)),
|
||||
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
|
||||
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
|
||||
Some(AnyNodeRef::Alias(alias)) => Some(GotoTarget::Alias(alias)),
|
||||
Some(AnyNodeRef::StmtImportFrom(from)) => Some(GotoTarget::ImportedModule(from)),
|
||||
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
|
||||
Some(GotoTarget::ExceptVariable(handler))
|
||||
}
|
||||
Some(AnyNodeRef::Keyword(keyword)) => Some(GotoTarget::KeywordArgument(keyword)),
|
||||
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
||||
Some(GotoTarget::PatternMatchRest(mapping))
|
||||
}
|
||||
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
||||
Some(GotoTarget::PatternKeywordArgument(keyword))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
||||
Some(GotoTarget::PatternMatchStarName(star))
|
||||
}
|
||||
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
||||
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVar(var)) => Some(GotoTarget::TypeParamTypeVarName(var)),
|
||||
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
||||
Some(GotoTarget::TypeParamParamSpecName(bound))
|
||||
}
|
||||
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
||||
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
||||
}
|
||||
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
||||
Some(GotoTarget::Expression(attribute.into()))
|
||||
}
|
||||
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
||||
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
||||
None => None,
|
||||
Some(parent) => {
|
||||
tracing::debug!(
|
||||
"Missing `GoToTarget` for identifier with parent {:?}",
|
||||
parent.kind()
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
|
||||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{cursor_test, CursorTest, IntoDiagnostic};
|
||||
use crate::{goto_type_definition, NavigationTarget};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_class_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Test: ...
|
||||
|
||||
a<CURSOR>b = Test()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:19
|
||||
|
|
||||
2 | class Test: ...
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | class Test: ...
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
| ^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_function_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
ab = foo
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | ab = foo
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:6:13
|
||||
|
|
||||
4 | ab = foo
|
||||
5 |
|
||||
6 | ab
|
||||
| ^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_union_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
|
||||
def bar(a, b): ...
|
||||
|
||||
if random.choice():
|
||||
a = foo
|
||||
else:
|
||||
a = bar
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:3:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
| ^^^
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:5:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
| ^^^
|
||||
6 |
|
||||
7 | if random.choice():
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
lib<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /lib.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | import lib
|
||||
3 |
|
||||
4 | lib
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "test"
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:438:7
|
||||
|
|
||||
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
437 |
|
||||
438 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
439 | @overload
|
||||
440 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | a: str = "test"
|
||||
3 |
|
||||
4 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_node() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "te<CURSOR>st"
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:438:7
|
||||
|
|
||||
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
437 |
|
||||
438 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
439 | @overload
|
||||
440 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:2:22
|
||||
|
|
||||
2 | a: str = "test"
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:24
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:2:46
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_param_spec() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type param specs
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_tuple() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type var tuples
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= "123")
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:438:7
|
||||
|
|
||||
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
437 |
|
||||
438 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
439 | @overload
|
||||
440 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= "123")
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= 123)
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should jump to `str` and not `int` because
|
||||
// the keyword is typed as a string. It's only the passed argument that
|
||||
// is an int. Navigating to `str` would match pyright's behavior.
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:231:7
|
||||
|
|
||||
229 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
230 |
|
||||
231 | class int:
|
||||
| ^^^
|
||||
232 | @overload
|
||||
233 | def __new__(cls, x: ConvertibleToInt = ..., /) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= 123)
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_kwargs() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def f(name: str): ...
|
||||
|
||||
kwargs = { "name": "test"}
|
||||
|
||||
f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:1086:7
|
||||
|
|
||||
1084 | def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
|
||||
1085 |
|
||||
1086 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
1087 | # __init__ should be kept roughly in line with `collections.UserDict.__init__`, which has similar semantics
|
||||
1088 | # Also multiprocessing.managers.SyncManager.dict()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:6:5
|
||||
|
|
||||
4 | kwargs = { "name": "test"}
|
||||
5 |
|
||||
6 | f(**kwargs)
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_builtin() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:438:7
|
||||
|
|
||||
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
437 |
|
||||
438 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
439 | @overload
|
||||
440 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_definition_cursor_between_object_and_attribute() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class X:
|
||||
def foo(a, b): ...
|
||||
|
||||
x = X()
|
||||
|
||||
x<CURSOR>.foo()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:19
|
||||
|
|
||||
2 | class X:
|
||||
| ^
|
||||
3 | def foo(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:7:13
|
||||
|
|
||||
5 | x = X()
|
||||
6 |
|
||||
7 | x.foo()
|
||||
| ^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_between_call_arguments() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
foo<CURSOR>()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> /main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | foo()
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
3 |
|
||||
4 | foo()
|
||||
| ^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_narrowing() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
print(a<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:438:7
|
||||
|
|
||||
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
437 |
|
||||
438 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
439 | @overload
|
||||
440 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:4:27
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | if a is not None:
|
||||
4 | print(a)
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_none() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/types.pyi:671:11
|
||||
|
|
||||
669 | if sys.version_info >= (3, 10):
|
||||
670 | @final
|
||||
671 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
672 | def __bool__(self) -> Literal[False]: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:438:7
|
||||
|
|
||||
436 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
437 |
|
||||
438 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
439 | @overload
|
||||
440 | def __new__(cls, object: object = ...) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_type_definition(&self) -> String {
|
||||
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No type definitions found".to_string();
|
||||
}
|
||||
|
||||
let source = targets.range;
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoTypeDefinitionDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoTypeDefinitionDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(Severity::Info, "Source");
|
||||
source.annotate(Annotation::primary(
|
||||
Span::from(self.source.file()).with_range(self.source.range()),
|
||||
));
|
||||
|
||||
let mut main = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("goto-type-definition")),
|
||||
Severity::Info,
|
||||
"Type definition".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
||||
602
crates/red_knot_ide/src/hover.rs
Normal file
602
crates/red_knot_ide/src/hover.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
use crate::goto::{find_goto_target, GotoTarget};
|
||||
use crate::{Db, MarkupKind, RangedValue};
|
||||
use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::SemanticModel;
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option<RangedValue<Hover>> {
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
|
||||
if let GotoTarget::Expression(expr) = goto_target {
|
||||
if expr.is_literal_expr() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let model = SemanticModel::new(db.upcast(), file);
|
||||
let ty = goto_target.inferred_type(&model)?;
|
||||
|
||||
tracing::debug!(
|
||||
"Inferred type of covering node is {}",
|
||||
ty.display(db.upcast())
|
||||
);
|
||||
|
||||
// TODO: Add documentation of the symbol (not the type's definition).
|
||||
// TODO: Render the symbol's signature instead of just its type.
|
||||
let contents = vec![HoverContent::Type(ty)];
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: Hover { contents },
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Hover<'db> {
|
||||
contents: Vec<HoverContent<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> Hover<'db> {
|
||||
/// Renders the hover to a string using the specified markup kind.
|
||||
pub const fn display<'a>(&'a self, db: &'a dyn Db, kind: MarkupKind) -> DisplayHover<'a> {
|
||||
DisplayHover {
|
||||
db,
|
||||
hover: self,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> std::slice::Iter<'_, HoverContent<'db>> {
|
||||
self.contents.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> IntoIterator for Hover<'db> {
|
||||
type Item = HoverContent<'db>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.contents.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a Hover<'db> {
|
||||
type Item = &'a HoverContent<'db>;
|
||||
type IntoIter = std::slice::Iter<'a, HoverContent<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayHover<'a> {
|
||||
db: &'a dyn Db,
|
||||
hover: &'a Hover<'a>,
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayHover<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let mut first = true;
|
||||
for content in &self.hover.contents {
|
||||
if !first {
|
||||
self.kind.horizontal_line().fmt(f)?;
|
||||
}
|
||||
|
||||
content.display(self.db, self.kind).fmt(f)?;
|
||||
first = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum HoverContent<'db> {
|
||||
Type(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> HoverContent<'db> {
|
||||
fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> {
|
||||
DisplayHoverContent {
|
||||
db,
|
||||
content: self,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayHoverContent<'a, 'db> {
|
||||
db: &'db dyn Db,
|
||||
content: &'a HoverContent<'db>,
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayHoverContent<'_, '_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.content {
|
||||
HoverContent::Type(ty) => self
|
||||
.kind
|
||||
.fenced_code_block(ty.display(self.db.upcast()), "text")
|
||||
.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{cursor_test, CursorTest};
|
||||
use crate::{hover, MarkupKind};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
|
||||
Severity, Span,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
#[test]
|
||||
fn hover_basic() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a = 10
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Literal[10]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[10]
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:9
|
||||
|
|
||||
2 | a = 10
|
||||
3 |
|
||||
4 | a
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_member() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Foo:
|
||||
a: int = 10
|
||||
|
||||
def __init__(a: int, b: str):
|
||||
self.a = a
|
||||
self.b: str = b
|
||||
|
||||
foo = Foo()
|
||||
foo.<CURSOR>a
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
int
|
||||
---------------------------------------------
|
||||
```text
|
||||
int
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:10:9
|
||||
|
|
||||
9 | foo = Foo()
|
||||
10 | foo.a
|
||||
| ^^^^-
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_function_typed_variable() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
foo<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r###"
|
||||
def foo(a, b) -> Unknown
|
||||
---------------------------------------------
|
||||
```text
|
||||
def foo(a, b) -> Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
3 |
|
||||
4 | foo
|
||||
| ^^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_binary_expression() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: int, b: int, c: int):
|
||||
a + b ==<CURSOR> c
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
bool
|
||||
---------------------------------------------
|
||||
```text
|
||||
bool
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: int, b: int, c: int):
|
||||
3 | a + b == c
|
||||
| ^^^^^^^^-^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_keyword_parameter() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: int): ...
|
||||
|
||||
test(a<CURSOR>= 123)
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should reveal `int` because the user hovers over the parameter and not the value.
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Literal[123]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[123]
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:18
|
||||
|
|
||||
2 | def test(a: int): ...
|
||||
3 |
|
||||
4 | test(a= 123)
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_union() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
|
||||
def bar(a, b): ...
|
||||
|
||||
if random.choice([True, False]):
|
||||
a = foo
|
||||
else:
|
||||
a = bar
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r###"
|
||||
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
|
||||
---------------------------------------------
|
||||
```text
|
||||
(def foo(a, b) -> Unknown) | (def bar(a, b) -> Unknown)
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
li<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<module 'lib'>
|
||||
---------------------------------------------
|
||||
```text
|
||||
<module 'lib'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:13
|
||||
|
|
||||
2 | import lib
|
||||
3 |
|
||||
4 | lib
|
||||
| ^^-
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_of_expression_with_type_var_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
T
|
||||
---------------------------------------------
|
||||
```text
|
||||
T
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:2:46
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_of_expression_with_type_param_spec() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```text
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:2:53
|
||||
|
|
||||
2 | type Alias[**P = [int, str]] = Callable[P, int]
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_of_expression_with_type_var_tuple() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```text
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:2:43
|
||||
|
|
||||
2 | type Alias[*Ts = ()] = tuple[*Ts]
|
||||
| ^^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_class_member_declaration() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Foo:
|
||||
a<CURSOR>: int
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should be int and not `Never`, https://github.com/astral-sh/ruff/issues/17122
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Never
|
||||
---------------------------------------------
|
||||
```text
|
||||
Never
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:3:13
|
||||
|
|
||||
2 | class Foo:
|
||||
3 | a: int
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_type_narrowing() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
print(a<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
str
|
||||
---------------------------------------------
|
||||
```text
|
||||
str
|
||||
```
|
||||
---------------------------------------------
|
||||
info: lint:hover: Hovered content is
|
||||
--> /main.py:4:27
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | if a is not None:
|
||||
4 | print(a)
|
||||
| ^- Cursor offset
|
||||
| |
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_whitespace() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class C:
|
||||
<CURSOR>
|
||||
foo: str = 'bar'
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_literal_int() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
print(
|
||||
0 + 1<CURSOR>
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_literal_ellipsis() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
print(
|
||||
.<CURSOR>..
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_docstring() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def f():
|
||||
"""Lorem ipsum dolor sit amet.<CURSOR>"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn hover(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let Some(hover) = hover(&self.db, self.file, self.cursor_offset) else {
|
||||
return "Hover provided no content".to_string();
|
||||
};
|
||||
|
||||
let source = hover.range;
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
write!(
|
||||
&mut buf,
|
||||
"{plaintext}{line}{markdown}{line}",
|
||||
plaintext = hover.display(&self.db, MarkupKind::PlainText),
|
||||
line = MarkupKind::PlainText.horizontal_line(),
|
||||
markdown = hover.display(&self.db, MarkupKind::Markdown),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.color(false)
|
||||
.format(DiagnosticFormat::Full);
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("hover")),
|
||||
Severity::Info,
|
||||
"Hovered content is",
|
||||
);
|
||||
diagnostic.annotate(
|
||||
Annotation::primary(Span::from(source.file()).with_range(source.range()))
|
||||
.message("source"),
|
||||
);
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(
|
||||
Span::from(source.file()).with_range(TextRange::empty(self.cursor_offset)),
|
||||
)
|
||||
.message("Cursor offset"),
|
||||
);
|
||||
|
||||
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
}
|
||||
279
crates/red_knot_ide/src/inlay_hints.rs
Normal file
279
crates/red_knot_ide/src/inlay_hints.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
use crate::Db;
|
||||
use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::{HasType, SemanticModel};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::{AnyNodeRef, Expr, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct InlayHint<'db> {
|
||||
pub position: TextSize,
|
||||
pub content: InlayHintContent<'db>,
|
||||
}
|
||||
|
||||
impl<'db> InlayHint<'db> {
|
||||
pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> {
|
||||
self.content.display(db)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum InlayHintContent<'db> {
|
||||
Type(Type<'db>),
|
||||
ReturnType(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> InlayHintContent<'db> {
|
||||
pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> {
|
||||
DisplayInlayHint { db, hint: self }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayInlayHint<'a, 'db> {
|
||||
db: &'db dyn Db,
|
||||
hint: &'a InlayHintContent<'db>,
|
||||
}
|
||||
|
||||
impl fmt::Display for DisplayInlayHint<'_, '_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.hint {
|
||||
InlayHintContent::Type(ty) => {
|
||||
write!(f, ": {}", ty.display(self.db.upcast()))
|
||||
}
|
||||
InlayHintContent::ReturnType(ty) => {
|
||||
write!(f, " -> {}", ty.display(self.db.upcast()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inlay_hints(db: &dyn Db, file: File, range: TextRange) -> Vec<InlayHint<'_>> {
|
||||
let mut visitor = InlayHintVisitor::new(db, file, range);
|
||||
|
||||
let ast = parsed_module(db.upcast(), file);
|
||||
|
||||
visitor.visit_body(ast.suite());
|
||||
|
||||
visitor.hints
|
||||
}
|
||||
|
||||
struct InlayHintVisitor<'db> {
|
||||
model: SemanticModel<'db>,
|
||||
hints: Vec<InlayHint<'db>>,
|
||||
in_assignment: bool,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl<'db> InlayHintVisitor<'db> {
|
||||
fn new(db: &'db dyn Db, file: File, range: TextRange) -> Self {
|
||||
Self {
|
||||
model: SemanticModel::new(db.upcast(), file),
|
||||
hints: Vec::new(),
|
||||
in_assignment: false,
|
||||
range,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) {
|
||||
self.hints.push(InlayHint {
|
||||
position,
|
||||
content: InlayHintContent::Type(ty),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal {
|
||||
if self.range.intersect(node.range()).is_some() {
|
||||
TraversalSignal::Traverse
|
||||
} else {
|
||||
TraversalSignal::Skip
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
let node = AnyNodeRef::from(stmt);
|
||||
|
||||
if !self.enter_node(node).is_traverse() {
|
||||
return;
|
||||
}
|
||||
|
||||
match stmt {
|
||||
Stmt::Assign(assign) => {
|
||||
self.in_assignment = true;
|
||||
for target in &assign.targets {
|
||||
self.visit_expr(target);
|
||||
}
|
||||
self.in_assignment = false;
|
||||
|
||||
return;
|
||||
}
|
||||
// TODO
|
||||
Stmt::FunctionDef(_) => {}
|
||||
Stmt::For(_) => {}
|
||||
Stmt::Expr(_) => {
|
||||
// Don't traverse into expression statements because we don't show any hints.
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
source_order::walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'_ Expr) {
|
||||
if !self.in_assignment {
|
||||
return;
|
||||
}
|
||||
|
||||
match expr {
|
||||
Expr::Name(name) => {
|
||||
if name.ctx.is_store() {
|
||||
let ty = expr.inferred_type(&self.model);
|
||||
self.add_type_hint(expr.range().end(), ty);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
source_order::walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::{
|
||||
files::{system_path_to_file, File},
|
||||
source::source_text,
|
||||
};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use red_knot_python_semantic::{
|
||||
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest {
|
||||
const START: &str = "<START>";
|
||||
const END: &str = "<END>";
|
||||
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let start = source.find(START);
|
||||
let end = source
|
||||
.find(END)
|
||||
.map(|x| if start.is_some() { x - START.len() } else { x })
|
||||
.unwrap_or(source.len());
|
||||
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(start.unwrap_or_default()).unwrap(),
|
||||
TextSize::try_from(end).unwrap(),
|
||||
);
|
||||
|
||||
let source = source.replace(START, "");
|
||||
let source = source.replace(END, "");
|
||||
|
||||
db.write_file("main.py", source)
|
||||
.expect("write to memory file system to be successful");
|
||||
|
||||
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::latest(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![SystemPathBuf::from("/")],
|
||||
custom_typeshed: None,
|
||||
python_path: PythonPath::KnownSitePackages(vec![]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("Default settings to be valid");
|
||||
|
||||
InlayHintTest { db, file, range }
|
||||
}
|
||||
|
||||
pub(super) struct InlayHintTest {
|
||||
pub(super) db: TestDb,
|
||||
pub(super) file: File,
|
||||
pub(super) range: TextRange,
|
||||
}
|
||||
|
||||
impl InlayHintTest {
|
||||
fn inlay_hints(&self) -> String {
|
||||
let hints = inlay_hints(&self.db, self.file, self.range);
|
||||
|
||||
let mut buf = source_text(&self.db, self.file).as_str().to_string();
|
||||
|
||||
let mut offset = 0;
|
||||
|
||||
for hint in hints {
|
||||
let end_position = (hint.position.to_u32() as usize) + offset;
|
||||
let hint_str = format!("[{}]", hint.display(&self.db));
|
||||
buf.insert_str(end_position, &hint_str);
|
||||
offset += hint_str.len();
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assign_statement() {
|
||||
let test = inlay_hint_test("x = 1");
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
x[: Literal[1]] = 1
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_assignment() {
|
||||
let test = inlay_hint_test("x, y = (1, 'abc')");
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
x[: Literal[1]], y[: Literal["abc"]] = (1, 'abc')
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_tuple_assignment() {
|
||||
let test = inlay_hint_test("x, (y, z) = (1, ('abc', 2))");
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
x[: Literal[1]], (y[: Literal["abc"]], z[: Literal[2]]) = (1, ('abc', 2))
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assign_statement_with_type_annotation() {
|
||||
let test = inlay_hint_test("x: int = 1");
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
x: int = 1
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assign_statement_out_of_range() {
|
||||
let test = inlay_hint_test("<START>x = 1<END>\ny = 2");
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
x[: Literal[1]] = 1
|
||||
y = 2
|
||||
");
|
||||
}
|
||||
}
|
||||
296
crates/red_knot_ide/src/lib.rs
Normal file
296
crates/red_knot_ide/src/lib.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
mod db;
|
||||
mod find_node;
|
||||
mod goto;
|
||||
mod hover;
|
||||
mod inlay_hints;
|
||||
mod markup;
|
||||
|
||||
pub use db::Db;
|
||||
pub use goto::goto_type_definition;
|
||||
pub use hover::hover;
|
||||
pub use inlay_hints::inlay_hints;
|
||||
pub use markup::MarkupKind;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use red_knot_python_semantic::types::{Type, TypeDefinition};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
/// Information associated with a text range.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct RangedValue<T> {
|
||||
pub range: FileRange,
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
impl<T> RangedValue<T> {
|
||||
pub fn file_range(&self) -> FileRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for RangedValue<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for RangedValue<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for RangedValue<T>
|
||||
where
|
||||
T: IntoIterator,
|
||||
{
|
||||
type Item = T::Item;
|
||||
type IntoIter = T::IntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.value.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Target to which the editor can navigate to.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NavigationTarget {
|
||||
file: File,
|
||||
|
||||
/// The range that should be focused when navigating to the target.
|
||||
///
|
||||
/// This is typically not the full range of the node. For example, it's the range of the class's name in a class definition.
|
||||
///
|
||||
/// The `focus_range` must be fully covered by `full_range`.
|
||||
focus_range: TextRange,
|
||||
|
||||
/// The range covering the entire target.
|
||||
full_range: TextRange,
|
||||
}
|
||||
|
||||
impl NavigationTarget {
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
pub fn focus_range(&self) -> TextRange {
|
||||
self.focus_range
|
||||
}
|
||||
|
||||
pub fn full_range(&self) -> TextRange {
|
||||
self.full_range
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NavigationTargets(smallvec::SmallVec<[NavigationTarget; 1]>);
|
||||
|
||||
impl NavigationTargets {
|
||||
fn single(target: NavigationTarget) -> Self {
|
||||
Self(smallvec::smallvec![target])
|
||||
}
|
||||
|
||||
fn empty() -> Self {
|
||||
Self(smallvec::SmallVec::new())
|
||||
}
|
||||
|
||||
fn unique(targets: impl IntoIterator<Item = NavigationTarget>) -> Self {
|
||||
let unique: FxHashSet<_> = targets.into_iter().collect();
|
||||
if unique.is_empty() {
|
||||
Self::empty()
|
||||
} else {
|
||||
let mut targets = unique.into_iter().collect::<Vec<_>>();
|
||||
targets.sort_by_key(|target| (target.file, target.focus_range.start()));
|
||||
Self(targets.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> std::slice::Iter<'_, NavigationTarget> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for NavigationTargets {
|
||||
type Item = NavigationTarget;
|
||||
type IntoIter = smallvec::IntoIter<[NavigationTarget; 1]>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a NavigationTargets {
|
||||
type Item = &'a NavigationTarget;
|
||||
type IntoIter = std::slice::Iter<'a, NavigationTarget>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<NavigationTarget> for NavigationTargets {
|
||||
fn from_iter<T: IntoIterator<Item = NavigationTarget>>(iter: T) -> Self {
|
||||
Self::unique(iter)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasNavigationTargets {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets;
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for Type<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
match self {
|
||||
Type::Union(union) => union
|
||||
.iter(db.upcast())
|
||||
.flat_map(|target| target.navigation_targets(db))
|
||||
.collect(),
|
||||
|
||||
Type::Intersection(intersection) => {
|
||||
// Only consider the positive elements because the negative elements are mainly from narrowing constraints.
|
||||
let mut targets = intersection
|
||||
.iter_positive(db.upcast())
|
||||
.filter(|ty| !ty.is_unknown());
|
||||
|
||||
let Some(first) = targets.next() else {
|
||||
return NavigationTargets::empty();
|
||||
};
|
||||
|
||||
match targets.next() {
|
||||
Some(_) => {
|
||||
// If there are multiple types in the intersection, we can't navigate to a single one
|
||||
// because the type is the intersection of all those types.
|
||||
NavigationTargets::empty()
|
||||
}
|
||||
None => first.navigation_targets(db),
|
||||
}
|
||||
}
|
||||
|
||||
ty => ty
|
||||
.definition(db.upcast())
|
||||
.map(|definition| definition.navigation_targets(db))
|
||||
.unwrap_or_else(NavigationTargets::empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for TypeDefinition<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let full_range = self.full_range(db.upcast());
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: full_range.file(),
|
||||
focus_range: self.focus_range(db.upcast()).unwrap_or(full_range).range(),
|
||||
full_range: full_range.range(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use red_knot_python_semantic::{
|
||||
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
pub(super) fn cursor_test(source: &str) -> CursorTest {
|
||||
let mut db = TestDb::new();
|
||||
let cursor_offset = source.find("<CURSOR>").expect(
|
||||
"`source`` should contain a `<CURSOR>` marker, indicating the position of the cursor.",
|
||||
);
|
||||
|
||||
let mut content = source[..cursor_offset].to_string();
|
||||
content.push_str(&source[cursor_offset + "<CURSOR>".len()..]);
|
||||
|
||||
db.write_file("main.py", &content)
|
||||
.expect("write to memory file system to be successful");
|
||||
|
||||
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::latest(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_roots: vec![SystemPathBuf::from("/")],
|
||||
custom_typeshed: None,
|
||||
python_path: PythonPath::KnownSitePackages(vec![]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("Default settings to be valid");
|
||||
|
||||
let mut insta_settings = insta::Settings::clone_current();
|
||||
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
||||
// Filter out TODO types because they are different between debug and release builds.
|
||||
insta_settings.add_filter(r"@Todo\(.+\)", "@Todo");
|
||||
|
||||
let insta_settings_guard = insta_settings.bind_to_scope();
|
||||
|
||||
CursorTest {
|
||||
db,
|
||||
cursor_offset: TextSize::try_from(cursor_offset)
|
||||
.expect("source to be smaller than 4GB"),
|
||||
file,
|
||||
_insta_settings_guard: insta_settings_guard,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct CursorTest {
|
||||
pub(super) db: TestDb,
|
||||
pub(super) cursor_offset: TextSize,
|
||||
pub(super) file: File,
|
||||
_insta_settings_guard: SettingsBindDropGuard,
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
pub(super) fn write_file(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: &str,
|
||||
) -> std::io::Result<()> {
|
||||
self.db.write_file(path, content)
|
||||
}
|
||||
|
||||
pub(super) fn render_diagnostics<I, D>(&self, diagnostics: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = D>,
|
||||
D: IntoDiagnostic,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.color(false)
|
||||
.format(DiagnosticFormat::Full);
|
||||
for diagnostic in diagnostics {
|
||||
let diag = diagnostic.into_diagnostic();
|
||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait IntoDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic;
|
||||
}
|
||||
}
|
||||
66
crates/red_knot_ide/src/markup.rs
Normal file
66
crates/red_knot_ide/src/markup.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum MarkupKind {
|
||||
PlainText,
|
||||
Markdown,
|
||||
}
|
||||
|
||||
impl MarkupKind {
|
||||
pub(crate) const fn fenced_code_block<T>(self, code: T, language: &str) -> FencedCodeBlock<T>
|
||||
where
|
||||
T: fmt::Display,
|
||||
{
|
||||
FencedCodeBlock {
|
||||
language,
|
||||
code,
|
||||
kind: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn horizontal_line(self) -> HorizontalLine {
|
||||
HorizontalLine { kind: self }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FencedCodeBlock<'a, T> {
|
||||
language: &'a str,
|
||||
code: T,
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl<T> fmt::Display for FencedCodeBlock<'_, T>
|
||||
where
|
||||
T: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.kind {
|
||||
MarkupKind::PlainText => self.code.fmt(f),
|
||||
MarkupKind::Markdown => write!(
|
||||
f,
|
||||
"```{language}\n{code}\n```",
|
||||
language = self.language,
|
||||
code = self.code
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct HorizontalLine {
|
||||
kind: MarkupKind,
|
||||
}
|
||||
|
||||
impl fmt::Display for HorizontalLine {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.kind {
|
||||
MarkupKind::PlainText => {
|
||||
f.write_str("\n---------------------------------------------\n")
|
||||
}
|
||||
MarkupKind::Markdown => {
|
||||
write!(f, "\n---\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,9 @@ ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["cache", "serde"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["serde"] }
|
||||
ruff_python_formatter = { workspace = true, optional = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
red_knot_ide = { workspace = true }
|
||||
red_knot_python_semantic = { workspace = true, features = ["serde"] }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
@@ -42,8 +44,13 @@ insta = { workspace = true, features = ["redactions", "ron"] }
|
||||
[features]
|
||||
default = ["zstd"]
|
||||
deflate = ["red_knot_vendored/deflate"]
|
||||
schemars = ["dep:schemars", "ruff_db/schemars", "red_knot_python_semantic/schemars"]
|
||||
schemars = [
|
||||
"dep:schemars",
|
||||
"ruff_db/schemars",
|
||||
"red_knot_python_semantic/schemars",
|
||||
]
|
||||
zstd = ["red_knot_vendored/zstd"]
|
||||
format = ["ruff_python_formatter"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/17215
|
||||
# panicked in commit 1a6a10b30
|
||||
# error message:
|
||||
# dependency graph cycle querying all_narrowing_constraints_for_expression(Id(8591))
|
||||
|
||||
def f(a: A, b: B, c: C):
|
||||
unknown_a: UA = make_unknown()
|
||||
unknown_b: UB = make_unknown()
|
||||
unknown_c: UC = make_unknown()
|
||||
unknown_d: UD = make_unknown()
|
||||
|
||||
if unknown_a and unknown_b:
|
||||
if unknown_c:
|
||||
if unknown_d:
|
||||
return a, b, c
|
||||
@@ -0,0 +1,22 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/17215
|
||||
# panicked in commit 1a6a10b30
|
||||
# error message:
|
||||
# dependency graph cycle querying all_negative_narrowing_constraints_for_expression(Id(859f))
|
||||
|
||||
def f(f1: bool, f2: bool, f3: bool, f4: bool):
|
||||
o1: UnknownClass = make_o()
|
||||
o2: UnknownClass = make_o()
|
||||
o3: UnknownClass = make_o()
|
||||
o4: UnknownClass = make_o()
|
||||
|
||||
if f1 and f2 and f3 and f4:
|
||||
if o1 == o2:
|
||||
return None
|
||||
if o2 == o3:
|
||||
return None
|
||||
if o3 == o4:
|
||||
return None
|
||||
if o4 == o1:
|
||||
return None
|
||||
|
||||
return o1, o2, o3, o4
|
||||
@@ -43,7 +43,7 @@ use ruff_python_ast::PythonVersion;
|
||||
/// resolved extra search path of `["b", "c", "a"]`, which means `a` will be tried last.
|
||||
///
|
||||
/// There's an argument here that the user should be able to specify the order of the paths,
|
||||
/// because only then is the user in full control of where to insert the path when specyifing `extra-paths`
|
||||
/// because only then is the user in full control of where to insert the path when specifying `extra-paths`
|
||||
/// in multiple sources.
|
||||
///
|
||||
/// ## Macro
|
||||
|
||||
@@ -3,9 +3,10 @@ use std::sync::Arc;
|
||||
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use red_knot_ide::Db as IdeDb;
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
@@ -55,13 +56,12 @@ impl ProjectDatabase {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub fn check(&self) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
pub fn check(&self) -> Result<Vec<Diagnostic>, Cancelled> {
|
||||
self.with_db(|db| db.project().check(db))
|
||||
}
|
||||
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn OldDiagnosticTrait>>, Cancelled> {
|
||||
let _span = tracing::debug_span!("check_file", file=%file.path(self)).entered();
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Diagnostic>, Cancelled> {
|
||||
self.with_db(|db| self.project().check_file(db, file))
|
||||
}
|
||||
|
||||
@@ -104,6 +104,19 @@ impl Upcast<dyn SourceDb> for ProjectDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn IdeDb> for ProjectDatabase {
|
||||
fn upcast(&self) -> &(dyn IdeDb + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn upcast_mut(&mut self) -> &mut (dyn IdeDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl IdeDb for ProjectDatabase {}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for ProjectDatabase {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
@@ -136,6 +149,10 @@ impl SourceDb for ProjectDatabase {
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
@@ -146,7 +163,7 @@ impl salsa::Database for ProjectDatabase {
|
||||
}
|
||||
|
||||
let event = event();
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation { .. }) {
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,6 +178,32 @@ impl Db for ProjectDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "format")]
|
||||
mod format {
|
||||
use crate::ProjectDatabase;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::Upcast;
|
||||
use ruff_python_formatter::{Db as FormatDb, PyFormatOptions};
|
||||
|
||||
#[salsa::db]
|
||||
impl FormatDb for ProjectDatabase {
|
||||
fn format_options(&self, file: File) -> PyFormatOptions {
|
||||
let source_ty = file.source_type(self);
|
||||
PyFormatOptions::from_source_type(source_ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn FormatDb> for ProjectDatabase {
|
||||
fn upcast(&self) -> &(dyn FormatDb + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn upcast_mut(&mut self) -> &mut (dyn FormatDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
@@ -168,7 +211,7 @@ pub(crate) mod tests {
|
||||
use salsa::Event;
|
||||
|
||||
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
|
||||
use red_knot_python_semantic::Db as SemanticDb;
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
@@ -242,6 +285,10 @@ pub(crate) mod tests {
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
fn python_version(&self) -> ruff_python_ast::PythonVersion {
|
||||
Program::get(self).python_version(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SemanticDb> for TestDb {
|
||||
|
||||
@@ -187,7 +187,7 @@ impl ProjectDatabase {
|
||||
let program = Program::get(self);
|
||||
if let Err(error) = program.update_from_settings(self, program_settings) {
|
||||
tracing::error!("Failed to update the program settings, keeping the old program settings: {error}");
|
||||
};
|
||||
}
|
||||
|
||||
if metadata.root() == project.root(self) {
|
||||
tracing::debug!("Reloading project after structural change");
|
||||
|
||||
@@ -9,7 +9,10 @@ pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
|
||||
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
|
||||
use red_knot_python_semantic::register_lints;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, OldParseDiagnostic, Severity, Span};
|
||||
use ruff_db::diagnostic::{
|
||||
create_parse_diagnostic, create_unsupported_syntax_diagnostic, Annotation, Diagnostic,
|
||||
DiagnosticId, Severity, Span,
|
||||
};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{source_text, SourceTextError};
|
||||
@@ -17,7 +20,6 @@ use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use rustc_hash::FxHashSet;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -163,24 +165,27 @@ impl Project {
|
||||
}
|
||||
|
||||
/// Checks all open files in the project and its dependencies.
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Diagnostic> {
|
||||
let project_span = tracing::debug_span!("Project::check");
|
||||
let _span = project_span.enter();
|
||||
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
}));
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
diagnostics.extend(
|
||||
self.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(OptionDiagnostic::to_diagnostic),
|
||||
);
|
||||
|
||||
let files = ProjectFiles::new(db, self);
|
||||
|
||||
diagnostics.extend(files.diagnostics().iter().cloned().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic);
|
||||
diagnostic
|
||||
}));
|
||||
diagnostics.extend(
|
||||
files
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.map(IOErrorDiagnostic::to_diagnostic),
|
||||
);
|
||||
|
||||
let result = Arc::new(std::sync::Mutex::new(diagnostics));
|
||||
let inner_result = Arc::clone(&result);
|
||||
@@ -195,7 +200,8 @@ impl Project {
|
||||
let project_span = project_span.clone();
|
||||
|
||||
scope.spawn(move |_| {
|
||||
let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", file=%file.path(&db));
|
||||
let check_file_span =
|
||||
tracing::debug_span!(parent: &project_span, "check_file", ?file);
|
||||
let _entered = check_file_span.entered();
|
||||
|
||||
let file_diagnostics = check_file_impl(&db, file);
|
||||
@@ -207,14 +213,11 @@ impl Project {
|
||||
Arc::into_inner(result).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
let mut file_diagnostics: Vec<_> = self
|
||||
.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(|diagnostic| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
})
|
||||
.map(OptionDiagnostic::to_diagnostic)
|
||||
.collect();
|
||||
|
||||
let check_diagnostics = check_file_impl(db, file);
|
||||
@@ -325,7 +328,7 @@ impl Project {
|
||||
self.files(db).contains(&file)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
#[tracing::instrument(level = "debug", skip(self, db))]
|
||||
pub fn remove_file(self, db: &mut dyn Db, file: File) {
|
||||
tracing::debug!(
|
||||
"Removing file `{}` from project `{}`",
|
||||
@@ -397,35 +400,43 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn OldDiagnosticTrait>> {
|
||||
let mut diagnostics: Vec<Box<dyn OldDiagnosticTrait>> = Vec::new();
|
||||
fn check_file_impl(db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
if let Some(read_error) = source.read_error() {
|
||||
diagnostics.push(Box::new(IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}));
|
||||
diagnostics.push(
|
||||
IOErrorDiagnostic {
|
||||
file: Some(file),
|
||||
error: read_error.clone().into(),
|
||||
}
|
||||
.to_diagnostic(),
|
||||
);
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
diagnostics.extend(parsed.errors().iter().map(|error| {
|
||||
let diagnostic: Box<dyn OldDiagnosticTrait> =
|
||||
Box::new(OldParseDiagnostic::new(file, error.clone()));
|
||||
diagnostic
|
||||
}));
|
||||
diagnostics.extend(
|
||||
parsed
|
||||
.errors()
|
||||
.iter()
|
||||
.map(|error| create_parse_diagnostic(file, error)),
|
||||
);
|
||||
|
||||
diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
|
||||
let boxed: Box<dyn OldDiagnosticTrait> = Box::new(diagnostic.clone());
|
||||
boxed
|
||||
}));
|
||||
diagnostics.extend(
|
||||
parsed
|
||||
.unsupported_syntax_errors()
|
||||
.iter()
|
||||
.map(|error| create_unsupported_syntax_diagnostic(file, error)),
|
||||
);
|
||||
|
||||
diagnostics.extend(check_types(db.upcast(), file).into_iter().cloned());
|
||||
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||
diagnostic
|
||||
.span()
|
||||
.primary_span()
|
||||
.and_then(|span| span.range())
|
||||
.unwrap_or_default()
|
||||
.start()
|
||||
@@ -493,21 +504,13 @@ pub struct IOErrorDiagnostic {
|
||||
error: IOErrorKind,
|
||||
}
|
||||
|
||||
impl OldDiagnosticTrait for IOErrorDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
DiagnosticId::Io
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
self.file.map(Span::from)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
impl IOErrorDiagnostic {
|
||||
fn to_diagnostic(&self) -> Diagnostic {
|
||||
let mut diag = Diagnostic::new(DiagnosticId::Io, Severity::Error, &self.error);
|
||||
if let Some(file) = self.file {
|
||||
diag.annotate(Annotation::primary(Span::from(file)));
|
||||
}
|
||||
diag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,12 +528,13 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{check_file_impl, ProjectMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::OldDiagnosticTrait;
|
||||
use red_knot_python_semantic::{Program, ProgramSettings, PythonPlatform, SearchPathSettings};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
#[test]
|
||||
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
||||
@@ -538,6 +542,16 @@ mod tests {
|
||||
let mut db = TestDb::new(project);
|
||||
let path = SystemPath::new("test.py");
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
python_platform: PythonPlatform::default(),
|
||||
search_paths: SearchPathSettings::new(vec![SystemPathBuf::from(".")]),
|
||||
},
|
||||
)
|
||||
.expect("Failed to configure program settings");
|
||||
|
||||
db.write_file(path, "x = 10")?;
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
@@ -549,7 +563,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Failed to read file: No such file or directory".to_string()]
|
||||
);
|
||||
@@ -565,7 +579,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
check_file_impl(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.map(|diagnostic| diagnostic.primary_message().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![] as Vec<String>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ impl ProjectMetadata {
|
||||
}
|
||||
|
||||
/// Loads a project from a set of options with an optional pyproject-project table.
|
||||
pub(crate) fn from_options(
|
||||
pub fn from_options(
|
||||
mut options: Options,
|
||||
root: SystemPathBuf,
|
||||
project: Option<&Project>,
|
||||
|
||||
@@ -2,14 +2,13 @@ use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSou
|
||||
use crate::Db;
|
||||
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
|
||||
use red_knot_python_semantic::{ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings};
|
||||
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, Severity, Span};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -37,30 +36,43 @@ pub struct Options {
|
||||
|
||||
impl Options {
|
||||
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, KnotTomlError> {
|
||||
let _guard = ValueSourceGuard::new(source);
|
||||
let _guard = ValueSourceGuard::new(source, true);
|
||||
let options = toml::from_str(content)?;
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
pub fn deserialize_with<'de, D>(source: ValueSource, deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let _guard = ValueSourceGuard::new(source, false);
|
||||
Self::deserialize(deserializer)
|
||||
}
|
||||
|
||||
pub(crate) fn to_program_settings(
|
||||
&self,
|
||||
project_root: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> ProgramSettings {
|
||||
let (python_version, python_platform) = self
|
||||
let python_version = self
|
||||
.environment
|
||||
.as_ref()
|
||||
.map(|env| {
|
||||
(
|
||||
env.python_version.as_deref().copied(),
|
||||
env.python_platform.as_deref(),
|
||||
)
|
||||
})
|
||||
.and_then(|env| env.python_version.as_deref().copied())
|
||||
.unwrap_or_default();
|
||||
|
||||
let python_platform = self
|
||||
.environment
|
||||
.as_ref()
|
||||
.and_then(|env| env.python_platform.as_deref().cloned())
|
||||
.unwrap_or_else(|| {
|
||||
let default = PythonPlatform::default();
|
||||
tracing::info!(
|
||||
"Defaulting to default python version for this platform: '{default}'",
|
||||
);
|
||||
default
|
||||
});
|
||||
ProgramSettings {
|
||||
python_version: python_version.unwrap_or_default(),
|
||||
python_platform: python_platform.cloned().unwrap_or_default(),
|
||||
python_version,
|
||||
python_platform,
|
||||
search_paths: self.to_search_path_settings(project_root, system),
|
||||
}
|
||||
}
|
||||
@@ -106,9 +118,14 @@ impl Options {
|
||||
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
|
||||
python_path: python
|
||||
.map(|python_path| {
|
||||
PythonPath::SysPrefix(python_path.absolute(project_root, system))
|
||||
PythonPath::from_cli_flag(python_path.absolute(project_root, system))
|
||||
})
|
||||
.unwrap_or(PythonPath::KnownSitePackages(vec![])),
|
||||
.or_else(|| {
|
||||
std::env::var("VIRTUAL_ENV")
|
||||
.ok()
|
||||
.map(PythonPath::from_virtual_env_var)
|
||||
})
|
||||
.unwrap_or_else(|| PythonPath::Discover(project_root.to_path_buf())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +137,11 @@ impl Options {
|
||||
|
||||
if let Some(terminal) = self.terminal.as_ref() {
|
||||
settings.set_terminal(TerminalSettings {
|
||||
output_format: terminal
|
||||
.output_format
|
||||
.as_deref()
|
||||
.copied()
|
||||
.unwrap_or_default(),
|
||||
error_on_warning: terminal.error_on_warning.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
@@ -207,7 +229,7 @@ impl Options {
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct EnvironmentOptions {
|
||||
/// Specifies the version of Python that will be used to execute the source code.
|
||||
/// Specifies the version of Python that will be used to analyze the source code.
|
||||
/// The version should be specified as a string in the format `M.m` where `M` is the major version
|
||||
/// and `m` is the minor (e.g. "3.0" or "3.6").
|
||||
/// If a version is provided, knot will generate errors if the source code makes use of language features
|
||||
@@ -216,11 +238,16 @@ pub struct EnvironmentOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub python_version: Option<RangedValue<PythonVersion>>,
|
||||
|
||||
/// Specifies the target platform that will be used to execute the source code.
|
||||
/// Specifies the target platform that will be used to analyze the source code.
|
||||
/// If specified, Red Knot will tailor its use of type stub files,
|
||||
/// which conditionalize type definitions based on the platform.
|
||||
///
|
||||
/// If no platform is specified, knot will use `all` or the current platform in the LSP use case.
|
||||
/// If no platform is specified, knot will use the current platform:
|
||||
/// - `win32` for Windows
|
||||
/// - `darwin` for macOS
|
||||
/// - `android` for Android
|
||||
/// - `ios` for iOS
|
||||
/// - `linux` for everything else
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub python_platform: Option<RangedValue<PythonPlatform>>,
|
||||
|
||||
@@ -277,6 +304,11 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TerminalOptions {
|
||||
/// The format to use for printing diagnostic messages.
|
||||
///
|
||||
/// Defaults to `full`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<RangedValue<DiagnosticFormat>>,
|
||||
/// Use exit code 1 if there are any warning-level diagnostics.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
@@ -374,22 +406,14 @@ impl OptionDiagnostic {
|
||||
fn with_span(self, span: Option<Span>) -> Self {
|
||||
OptionDiagnostic { span, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl OldDiagnosticTrait for OptionDiagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
Cow::Borrowed(&self.message)
|
||||
}
|
||||
|
||||
fn span(&self) -> Option<Span> {
|
||||
self.span.clone()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
self.severity
|
||||
pub(crate) fn to_diagnostic(&self) -> Diagnostic {
|
||||
if let Some(ref span) = self.span {
|
||||
let mut diag = Diagnostic::new(self.id, self.severity, "");
|
||||
diag.annotate(Annotation::primary(span.clone()).message(&self.message));
|
||||
diag
|
||||
} else {
|
||||
Diagnostic::new(self.id, self.severity, &self.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ impl PyProject {
|
||||
content: &str,
|
||||
source: ValueSource,
|
||||
) -> Result<Self, PyProjectError> {
|
||||
let _guard = ValueSourceGuard::new(source);
|
||||
let _guard = ValueSourceGuard::new(source, true);
|
||||
toml::from_str(content).map_err(PyProjectError::TomlSyntax)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use red_knot_python_semantic::lint::RuleSelection;
|
||||
use ruff_db::diagnostic::DiagnosticFormat;
|
||||
|
||||
/// The resolved [`super::Options`] for the project.
|
||||
///
|
||||
@@ -49,5 +50,6 @@ impl Settings {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct TerminalSettings {
|
||||
pub output_format: DiagnosticFormat,
|
||||
pub error_on_warning: bool,
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ pub enum ValueSource {
|
||||
/// Ideally, we'd use [`ruff_db::files::File`] but we can't because the database hasn't been
|
||||
/// created when loading the configuration.
|
||||
File(Arc<SystemPathBuf>),
|
||||
|
||||
/// The value comes from a CLI argument, while it's left open if specified using a short argument,
|
||||
/// long argument (`--extra-paths`) or `--config key=value`.
|
||||
Cli,
|
||||
@@ -41,18 +42,18 @@ thread_local! {
|
||||
/// Use the [`ValueSourceGuard`] to initialize the thread local before calling into any
|
||||
/// deserialization code. It ensures that the thread local variable gets cleaned up
|
||||
/// once deserialization is done (once the guard gets dropped).
|
||||
static VALUE_SOURCE: RefCell<Option<ValueSource>> = const { RefCell::new(None) };
|
||||
static VALUE_SOURCE: RefCell<Option<(ValueSource, bool)>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Guard to safely change the [`VALUE_SOURCE`] for the current thread.
|
||||
#[must_use]
|
||||
pub(super) struct ValueSourceGuard {
|
||||
prev_value: Option<ValueSource>,
|
||||
prev_value: Option<(ValueSource, bool)>,
|
||||
}
|
||||
|
||||
impl ValueSourceGuard {
|
||||
pub(super) fn new(source: ValueSource) -> Self {
|
||||
let prev = VALUE_SOURCE.replace(Some(source));
|
||||
pub(super) fn new(source: ValueSource, is_toml: bool) -> Self {
|
||||
let prev = VALUE_SOURCE.replace(Some((source, is_toml)));
|
||||
Self { prev_value: prev }
|
||||
}
|
||||
}
|
||||
@@ -265,18 +266,24 @@ where
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
|
||||
let span = spanned.span();
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(span.start).expect("Configuration file to be smaller than 4GB"),
|
||||
TextSize::try_from(span.end).expect("Configuration file to be smaller than 4GB"),
|
||||
);
|
||||
VALUE_SOURCE.with_borrow(|source| {
|
||||
let (source, has_span) = source.clone().unwrap();
|
||||
|
||||
Ok(VALUE_SOURCE.with_borrow(|source| {
|
||||
let source = source.clone().unwrap();
|
||||
if has_span {
|
||||
let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
|
||||
let span = spanned.span();
|
||||
let range = TextRange::new(
|
||||
TextSize::try_from(span.start)
|
||||
.expect("Configuration file to be smaller than 4GB"),
|
||||
TextSize::try_from(span.end)
|
||||
.expect("Configuration file to be smaller than 4GB"),
|
||||
);
|
||||
|
||||
Self::with_range(spanned.into_inner(), source, range)
|
||||
}))
|
||||
Ok(Self::with_range(spanned.into_inner(), source, range))
|
||||
} else {
|
||||
Ok(Self::new(T::deserialize(deserializer)?, source))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};
|
||||
use ruff_python_ast::visitor::source_order;
|
||||
use ruff_python_ast::visitor::source_order::SourceOrderVisitor;
|
||||
use ruff_python_ast::{self as ast, Alias, Expr, Parameter, ParameterWithDefault, Stmt};
|
||||
use ruff_python_ast::{
|
||||
self as ast, Alias, Comprehension, Expr, Parameter, ParameterWithDefault, Stmt,
|
||||
};
|
||||
|
||||
fn setup_db(project_root: &SystemPath, system: TestSystem) -> anyhow::Result<ProjectDatabase> {
|
||||
let project = ProjectMetadata::discover(project_root, &system)?;
|
||||
@@ -258,6 +260,14 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
source_order::walk_expr(self, expr);
|
||||
}
|
||||
|
||||
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
|
||||
self.visit_expr(&comprehension.iter);
|
||||
self.visit_target(&comprehension.target);
|
||||
for if_expr in &comprehension.ifs {
|
||||
self.visit_expr(if_expr);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_parameter(&mut self, parameter: &Parameter) {
|
||||
let _ty = parameter.inferred_type(&self.model);
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ class MDTestRunner:
|
||||
|
||||
def watch(self) -> Never:
|
||||
self._recompile_tests("Compiling tests...", message_on_success=False)
|
||||
self._run_mdtest()
|
||||
self.console.print("[dim]Ready to watch for changes...[/dim]")
|
||||
|
||||
for changes in watch(CRATE_ROOT):
|
||||
|
||||
@@ -29,7 +29,7 @@ It is invalid to parameterize `Annotated` with less than two arguments.
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
|
||||
def _(x: Annotated):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
@@ -39,11 +39,11 @@ def _(flag: bool):
|
||||
else:
|
||||
X = bool
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
|
||||
def f(y: X):
|
||||
reveal_type(y) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
|
||||
def _(x: Annotated | bool):
|
||||
reveal_type(x) # revealed: Unknown | bool
|
||||
|
||||
@@ -73,12 +73,10 @@ Inheriting from `Annotated[T, ...]` is equivalent to inheriting from `T` itself.
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# TODO: False positive
|
||||
# error: [invalid-base]
|
||||
class C(Annotated[int, "foo"]): ...
|
||||
|
||||
# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], @Todo(Inference of subscript on special form), Literal[object]]
|
||||
```
|
||||
|
||||
### Not parameterized
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
References:
|
||||
|
||||
- <https://typing.readthedocs.io/en/latest/spec/callables.html#callable>
|
||||
- <https://typing.python.org/en/latest/spec/callables.html#callable>
|
||||
|
||||
TODO: Use `collections.abc` as importing from `typing` is deprecated but this requires support for
|
||||
`*` imports. See: <https://docs.python.org/3/library/typing.html#deprecated-aliases>.
|
||||
Note that `typing.Callable` is deprecated at runtime, in favour of `collections.abc.Callable` (see:
|
||||
<https://docs.python.org/3/library/typing.html#deprecated-aliases>). However, removal of
|
||||
`typing.Callable` is not currently planned, and the canonical location of the stub for the symbol in
|
||||
typeshed is still `typing.pyi`.
|
||||
|
||||
## Invalid forms
|
||||
|
||||
@@ -47,8 +49,10 @@ def _(c: Callable[42, str]):
|
||||
Or, when one of the parameter type is invalid in the list:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
# error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
|
||||
def _(c: Callable[[int, 42, str, False], None]):
|
||||
# revealed: (int, @Todo(number literal in type expression), str, @Todo(boolean literal in type expression), /) -> None
|
||||
# revealed: (int, Unknown, str, Unknown, /) -> None
|
||||
reveal_type(c)
|
||||
```
|
||||
|
||||
@@ -61,7 +65,7 @@ from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[[int, str]]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> Unknown
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or, an ellipsis:
|
||||
@@ -72,6 +76,18 @@ def _(c: Callable[...]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or something else that's invalid in a type expression generally:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
{1, 2} # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
]
|
||||
):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### More than two arguments
|
||||
|
||||
We can't reliably infer the callable type if there are more then 2 arguments because we don't know
|
||||
@@ -85,6 +101,48 @@ def _(c: Callable[[int], str, str]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### List as the second argument
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# fmt: off
|
||||
|
||||
def _(c: Callable[
|
||||
int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
[str] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
]
|
||||
):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### List as both arguments
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
def _(c: Callable[[int], [str]]):
|
||||
reveal_type(c) # revealed: (int, /) -> Unknown
|
||||
```
|
||||
|
||||
### Three list arguments
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# fmt: off
|
||||
|
||||
|
||||
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
[int],
|
||||
[str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
[bytes] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
]
|
||||
):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
## Simple
|
||||
|
||||
A simple `Callable` with multiple parameters and a return type:
|
||||
@@ -96,6 +154,39 @@ def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> int
|
||||
```
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
from typing import Callable, Union
|
||||
|
||||
def _(
|
||||
c: Callable[[Union[int, str]], int] | None,
|
||||
d: None | Callable[[Union[int, str]], int],
|
||||
e: None | Callable[[Union[int, str]], int] | int,
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) | None
|
||||
reveal_type(d) # revealed: None | ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: None | ((int | str, /) -> int) | int
|
||||
```
|
||||
|
||||
## Intersection
|
||||
|
||||
```py
|
||||
from typing import Callable, Union
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
def _(
|
||||
c: Intersection[Callable[[Union[int, str]], int], int],
|
||||
d: Intersection[int, Callable[[Union[int, str]], int]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], str],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, str]]]],
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) & int
|
||||
reveal_type(d) # revealed: int & ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & str
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & str)
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
A nested `Callable` as one of the parameter types:
|
||||
@@ -146,6 +237,11 @@ def _(c: Callable[[Concatenate[int, str, ...], int], int]):
|
||||
|
||||
## Using `typing.ParamSpec`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Using a `ParamSpec` in a `Callable` annotation:
|
||||
|
||||
```py
|
||||
@@ -198,7 +294,7 @@ def _(c: Callable[[int, Unpack[Ts]], int]):
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[int], int]):
|
||||
reveal_type(c.__init__) # revealed: Literal[__init__]
|
||||
reveal_type(c.__init__) # revealed: def __init__(self) -> None
|
||||
reveal_type(c.__class__) # revealed: type
|
||||
|
||||
# TODO: The member lookup for `Callable` uses `object` which does not have a `__call__`
|
||||
@@ -208,4 +304,4 @@ def _(c: Callable[[int], int]):
|
||||
reveal_type(c.__call__) # revealed: Unknown
|
||||
```
|
||||
|
||||
[gradual form]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-gradual-form
|
||||
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
|
||||
|
||||
@@ -45,3 +45,135 @@ class Foo: ...
|
||||
|
||||
reveal_type(get_foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred self-reference annotations in a class definition
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Foo:
|
||||
this: Foo
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Foo
|
||||
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Baz = Foo
|
||||
```
|
||||
|
||||
## Non-deferred self-reference annotations in a class definition
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
# error: [unresolved-reference]
|
||||
this: Foo
|
||||
ok: "Foo"
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
# error: [unresolved-reference]
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Qux = Foo
|
||||
|
||||
def _():
|
||||
class C:
|
||||
# error: [unresolved-reference]
|
||||
def f(self) -> C:
|
||||
return self
|
||||
```
|
||||
|
||||
## Base class references
|
||||
|
||||
### Not deferred by __future__.annotations
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A(B): # error: [unresolved-reference]
|
||||
pass
|
||||
|
||||
class B:
|
||||
pass
|
||||
```
|
||||
|
||||
### Deferred in stub files
|
||||
|
||||
```pyi
|
||||
class A(B): ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
In order to support common use cases, an annotation of `float` actually means `int | float`, and an
|
||||
annotation of `complex` actually means `int | float | complex`. See
|
||||
[the specification](https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex)
|
||||
[the specification](https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex)
|
||||
|
||||
## float
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import typing
|
||||
from knot_extensions import AlwaysTruthy, AlwaysFalsy
|
||||
from typing_extensions import Literal, Never
|
||||
|
||||
class A: ...
|
||||
|
||||
def _(
|
||||
a: type[int],
|
||||
b: AlwaysTruthy,
|
||||
@@ -18,28 +20,95 @@ def _(
|
||||
f: Literal[b"foo"],
|
||||
g: tuple[int, str],
|
||||
h: Never,
|
||||
i: int,
|
||||
j: A,
|
||||
):
|
||||
def foo(): ...
|
||||
def invalid(
|
||||
i: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression"
|
||||
j: b, # error: [invalid-type-form]
|
||||
k: c, # error: [invalid-type-form]
|
||||
l: d, # error: [invalid-type-form]
|
||||
m: e, # error: [invalid-type-form]
|
||||
n: f, # error: [invalid-type-form]
|
||||
o: g, # error: [invalid-type-form]
|
||||
p: h, # error: [invalid-type-form]
|
||||
q: typing, # error: [invalid-type-form]
|
||||
r: foo, # error: [invalid-type-form]
|
||||
a_: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression"
|
||||
b_: b, # error: [invalid-type-form]
|
||||
c_: c, # error: [invalid-type-form]
|
||||
d_: d, # error: [invalid-type-form]
|
||||
e_: e, # error: [invalid-type-form]
|
||||
f_: f, # error: [invalid-type-form]
|
||||
g_: g, # error: [invalid-type-form]
|
||||
h_: h, # error: [invalid-type-form]
|
||||
i_: typing, # error: [invalid-type-form]
|
||||
j_: foo, # error: [invalid-type-form]
|
||||
k_: i, # error: [invalid-type-form] "Variable of type `int` is not allowed in a type expression"
|
||||
l_: j, # error: [invalid-type-form] "Variable of type `A` is not allowed in a type expression"
|
||||
):
|
||||
reveal_type(i) # revealed: Unknown
|
||||
reveal_type(j) # revealed: Unknown
|
||||
reveal_type(k) # revealed: Unknown
|
||||
reveal_type(l) # revealed: Unknown
|
||||
reveal_type(m) # revealed: Unknown
|
||||
reveal_type(n) # revealed: Unknown
|
||||
reveal_type(o) # revealed: Unknown
|
||||
reveal_type(p) # revealed: Unknown
|
||||
reveal_type(q) # revealed: Unknown
|
||||
reveal_type(r) # revealed: Unknown
|
||||
reveal_type(a_) # revealed: Unknown
|
||||
reveal_type(b_) # revealed: Unknown
|
||||
reveal_type(c_) # revealed: Unknown
|
||||
reveal_type(d_) # revealed: Unknown
|
||||
reveal_type(e_) # revealed: Unknown
|
||||
reveal_type(f_) # revealed: Unknown
|
||||
reveal_type(g_) # revealed: Unknown
|
||||
reveal_type(h_) # revealed: Unknown
|
||||
reveal_type(i_) # revealed: Unknown
|
||||
reveal_type(j_) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Invalid AST nodes
|
||||
|
||||
```py
|
||||
def bar() -> None:
|
||||
return None
|
||||
|
||||
def _(
|
||||
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
|
||||
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
|
||||
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
|
||||
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
|
||||
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
|
||||
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
|
||||
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
|
||||
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
|
||||
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
|
||||
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
|
||||
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
|
||||
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
|
||||
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
|
||||
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
|
||||
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
|
||||
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
|
||||
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
reveal_type(e) # revealed: int | Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
reveal_type(h) # revealed: Unknown
|
||||
reveal_type(i) # revealed: Unknown
|
||||
reveal_type(j) # revealed: Unknown
|
||||
reveal_type(k) # revealed: Unknown
|
||||
reveal_type(p) # revealed: Unknown
|
||||
reveal_type(q) # revealed: int | Unknown
|
||||
reveal_type(r) # revealed: @Todo(unknown type subscript)
|
||||
```
|
||||
|
||||
## Invalid Collection based AST nodes
|
||||
|
||||
```py
|
||||
def _(
|
||||
a: {1: 2}, # error: [invalid-type-form] "Dict literals are not allowed in type expressions"
|
||||
b: {1, 2}, # error: [invalid-type-form] "Set literals are not allowed in type expressions"
|
||||
c: {k: v for k, v in [(1, 2)]}, # error: [invalid-type-form] "Dict comprehensions are not allowed in type expressions"
|
||||
d: [k for k in [1, 2]], # error: [invalid-type-form] "List comprehensions are not allowed in type expressions"
|
||||
e: {k for k in [1, 2]}, # error: [invalid-type-form] "Set comprehensions are not allowed in type expressions"
|
||||
f: (k for k in [1, 2]), # error: [invalid-type-form] "Generator expressions are not allowed in type expressions"
|
||||
g: [int, str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Literal
|
||||
|
||||
<https://typing.readthedocs.io/en/latest/spec/literal.html#literals>
|
||||
<https://typing.python.org/en/latest/spec/literal.html#literals>
|
||||
|
||||
## Parameterization
|
||||
|
||||
@@ -36,7 +36,7 @@ def f():
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Unknown | Literal[0]
|
||||
reveal_type(b1) # revealed: @Todo(Attribute access on enum classes)
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
@@ -68,7 +68,7 @@ def x(
|
||||
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
|
||||
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
|
||||
):
|
||||
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(a1) # revealed: Literal[1, 2, 3, 5, "foo"] | None
|
||||
reveal_type(a2) # revealed: Literal["w", "r"]
|
||||
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
|
||||
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
|
||||
@@ -108,7 +108,7 @@ def union_example(
|
||||
None,
|
||||
],
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
|
||||
reveal_type(x) # revealed: Unknown | Literal[-1, 0, 1, "A", "B", "foo", "bar", b"A", b"\x00", b"\x07", True] | None
|
||||
```
|
||||
|
||||
## Detecting Literal outside typing and typing_extensions
|
||||
@@ -127,10 +127,17 @@ Literal: _SpecialForm
|
||||
```py
|
||||
from other import Literal
|
||||
|
||||
# TODO: can we add a subdiagnostic here saying something like:
|
||||
#
|
||||
# `other.Literal` and `typing.Literal` have similar names, but are different symbols and don't have the same semantics
|
||||
#
|
||||
# ?
|
||||
#
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: @Todo(generics)
|
||||
reveal_type(a1) # revealed: @Todo(unknown type subscript)
|
||||
```
|
||||
|
||||
## Detecting typing_extensions.Literal
|
||||
@@ -149,7 +156,7 @@ def f():
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
# error: [invalid-type-form] "`Literal` requires at least one argument when used in a type expression"
|
||||
# error: [invalid-type-form] "`typing.Literal` requires at least one argument when used in a type expression"
|
||||
def _(x: Literal):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -72,13 +72,11 @@ reveal_type(baz) # revealed: Literal["bazfoo"]
|
||||
qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
# TODO: Infer "LiteralString"
|
||||
reveal_type(foo.join(qux)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(foo.join(qux)) # revealed: LiteralString
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
# TODO: Infer `LiteralString`
|
||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(template.format(foo, bar)) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Assignability
|
||||
@@ -147,4 +145,4 @@ def f():
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
```
|
||||
|
||||
[1]: https://typing.readthedocs.io/en/latest/spec/literal.html#literalstring
|
||||
[1]: https://typing.python.org/en/latest/spec/literal.html#literalstring
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# NewType
|
||||
|
||||
Currently, red-knot doesn't support `typing.NewType` in type annotations.
|
||||
|
||||
## Valid forms
|
||||
|
||||
```py
|
||||
from typing_extensions import NewType
|
||||
from types import GenericAlias
|
||||
|
||||
X = GenericAlias(type, ())
|
||||
A = NewType("A", int)
|
||||
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
|
||||
# to be compatible with `type`
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `type`, found `NewType`"
|
||||
B = GenericAlias(A, ())
|
||||
|
||||
def _(
|
||||
a: A,
|
||||
b: B,
|
||||
):
|
||||
reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions)
|
||||
reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
|
||||
```
|
||||
@@ -45,3 +45,13 @@ def f():
|
||||
# revealed: int | None
|
||||
reveal_type(a)
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
# error: [invalid-type-form] "`typing.Optional` requires exactly one argument when used in a type expression"
|
||||
def f(x: Optional) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Starred expression annotations
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
Type annotations for `*args` can be starred expressions themselves:
|
||||
|
||||
```py
|
||||
|
||||
@@ -67,22 +67,24 @@ import typing
|
||||
|
||||
####################
|
||||
### Built-ins
|
||||
####################
|
||||
|
||||
class ListSubclass(typing.List): ...
|
||||
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(protocol), Literal[object]]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||
reveal_type(ListSubclass.__mro__)
|
||||
|
||||
class DictSubclass(typing.Dict): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[DictSubclass], Literal[dict], Unknown, Literal[object]]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||
reveal_type(DictSubclass.__mro__)
|
||||
|
||||
class SetSubclass(typing.Set): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Unknown, Literal[object]]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||
reveal_type(SetSubclass.__mro__)
|
||||
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
@@ -93,11 +95,12 @@ reveal_type(FrozenSetSubclass.__mro__)
|
||||
|
||||
####################
|
||||
### `collections`
|
||||
####################
|
||||
|
||||
class ChainMapSubclass(typing.ChainMap): ...
|
||||
|
||||
# TODO: Should be (ChainMapSubclass, ChainMap, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Unknown, Literal[object]]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||
reveal_type(ChainMapSubclass.__mro__)
|
||||
|
||||
class CounterSubclass(typing.Counter): ...
|
||||
@@ -114,8 +117,8 @@ reveal_type(DefaultDictSubclass.__mro__)
|
||||
|
||||
class DequeSubclass(typing.Deque): ...
|
||||
|
||||
# TODO: Should be (DequeSubclass, deque, MutableSequence, Sequence, Reversible, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Unknown, Literal[object]]
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
|
||||
reveal_type(DequeSubclass.__mro__)
|
||||
|
||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||
|
||||
@@ -105,7 +105,7 @@ def f1(
|
||||
from typing import Literal
|
||||
|
||||
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
|
||||
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
|
||||
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Union` can be used to construct union types same as `|` operator.
|
||||
`typing.Union` can be used to construct union types in the same way as the `|` operator.
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
@@ -59,3 +59,30 @@ def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
# error: [invalid-type-form] "`typing.Union` requires at least one argument when used in a type expression"
|
||||
def f(x: Union) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Implicit type aliases using new-style unions
|
||||
|
||||
We don't recognise these as type aliases yet, but we also don't emit false-positive diagnostics if
|
||||
you use them in type expressions:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
X = int | str
|
||||
|
||||
def f(y: X):
|
||||
reveal_type(y) # revealed: @Todo(Support for `types.UnionType` instances in type expressions)
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
reveal_type(Alias) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`)
|
||||
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def h() -> TypeIs[int]: ...
|
||||
@@ -29,13 +29,35 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
|
||||
# TODO: not an error; remove once `call` is implemented for `Callable`
|
||||
# error: [call-non-callable]
|
||||
return callback(42, *args, **kwargs)
|
||||
|
||||
class Foo:
|
||||
def method(self, x: Self):
|
||||
reveal_type(x) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`)
|
||||
reveal_type(x) # revealed: @Todo(Support for `typing.Self`)
|
||||
```
|
||||
|
||||
## Type expressions
|
||||
|
||||
One thing that is supported is error messages for using special forms in type expressions.
|
||||
|
||||
```py
|
||||
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic
|
||||
|
||||
def _(
|
||||
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
|
||||
b: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression"
|
||||
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
|
||||
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
|
||||
e: ParamSpec,
|
||||
f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions"
|
||||
) -> None:
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
|
||||
def foo(a_: e) -> None:
|
||||
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
@@ -44,7 +66,7 @@ You can't inherit from most of these. `typing.Callable` is an exception.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
|
||||
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic
|
||||
|
||||
class A(Self): ... # error: [invalid-base]
|
||||
class B(Unpack): ... # error: [invalid-base]
|
||||
@@ -52,12 +74,18 @@ class C(TypeGuard): ... # error: [invalid-base]
|
||||
class D(TypeIs): ... # error: [invalid-base]
|
||||
class E(Concatenate): ... # error: [invalid-base]
|
||||
class F(Callable): ...
|
||||
class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`"
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
|
||||
```
|
||||
|
||||
## Subscriptability
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Some of these are not subscriptable:
|
||||
|
||||
```py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Unsupported type qualifiers
|
||||
|
||||
## Not yet supported
|
||||
## Not yet fully supported
|
||||
|
||||
Several type qualifiers are unsupported by red-knot currently. However, we also don't emit
|
||||
false-positive errors if you use one in an annotation:
|
||||
@@ -19,6 +19,33 @@ class Bar(TypedDict):
|
||||
z: ReadOnly[bytes]
|
||||
```
|
||||
|
||||
## Type expressions
|
||||
|
||||
One thing that is supported is error messages for using type qualifiers in type expressions.
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
||||
|
||||
def _(
|
||||
a: (
|
||||
Final # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
|
||||
| int
|
||||
),
|
||||
b: (
|
||||
ClassVar # error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
|
||||
| int
|
||||
),
|
||||
c: Required, # error: [invalid-type-form] "Type qualifier `typing.Required` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
||||
d: NotRequired, # error: [invalid-type-form] "Type qualifier `typing.NotRequired` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
||||
e: ReadOnly, # error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
||||
) -> None:
|
||||
reveal_type(a) # revealed: Unknown | int
|
||||
reveal_type(b) # revealed: Unknown | int
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
You can't inherit from a type qualifier.
|
||||
|
||||
@@ -25,6 +25,11 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
|
||||
|
||||
## Tuple annotations are understood
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
@@ -52,13 +57,11 @@ reveal_type(b) # revealed: tuple[int]
|
||||
reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogeneous tuples, PEP-646 tuples
|
||||
# TODO: homogeneous tuples, PEP-646 tuples, generics
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
# TODO: support more kinds of type expressions in annotations
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)]
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
|
||||
@@ -10,6 +10,10 @@ reveal_type(x) # revealed: Literal[2]
|
||||
x = 1.0
|
||||
x /= 2
|
||||
reveal_type(x) # revealed: int | float
|
||||
|
||||
x = (1, 2)
|
||||
x += (3, 4)
|
||||
reveal_type(x) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
|
||||
```
|
||||
|
||||
## Dunder methods
|
||||
@@ -161,3 +165,18 @@ def f(flag: bool, flag2: bool):
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
## Implicit dunder calls on class objects
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
def __iadd__(cls, other: int) -> str:
|
||||
return ""
|
||||
|
||||
class C(metaclass=Meta): ...
|
||||
|
||||
cls = C
|
||||
cls += 1
|
||||
|
||||
reveal_type(cls) # revealed: str
|
||||
```
|
||||
|
||||
@@ -41,8 +41,7 @@ reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||
|
||||
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
|
||||
# mypy and pyright do not show an error here.
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
|
||||
|
||||
# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`.
|
||||
@@ -303,7 +302,7 @@ class C:
|
||||
|
||||
c_instance = C()
|
||||
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in for-loop (unpacking)
|
||||
@@ -339,8 +338,10 @@ class C:
|
||||
for self.z in NonIterable():
|
||||
pass
|
||||
|
||||
# Iterable might be empty
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C().x) # revealed: Unknown | int
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C().y) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
@@ -396,21 +397,33 @@ class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
class TupleIterator:
|
||||
def __next__(self) -> tuple[int, str]:
|
||||
return (1, "a")
|
||||
|
||||
class TupleIterable:
|
||||
def __iter__(self) -> TupleIterator:
|
||||
return TupleIterator()
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
[... for self.a in IntIterable()]
|
||||
[... for (self.b, self.c) in TupleIterable()]
|
||||
[... for self.d in IntIterable() for self.e in IntIterable()]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# TODO: Should be `Unknown | int`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # revealed: Unknown
|
||||
reveal_type(c_instance.a) # revealed: Unknown | int
|
||||
reveal_type(c_instance.b) # revealed: Unknown | int
|
||||
reveal_type(c_instance.c) # revealed: Unknown | str
|
||||
reveal_type(c_instance.d) # revealed: Unknown | int
|
||||
reveal_type(c_instance.e) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
#### Conditionally declared / bound attributes
|
||||
|
||||
We currently do not raise a diagnostic or change behavior if an attribute is only conditionally
|
||||
defined. This is consistent with what mypy and pyright do.
|
||||
Attributes are possibly unbound if they, or the method to which they are added are conditionally
|
||||
declared / bound.
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
@@ -428,9 +441,13 @@ class C:
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(c_instance.a1) # revealed: str | None
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(c_instance.a2) # revealed: str | None
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(c_instance.b1) # revealed: Unknown | Literal[1]
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(c_instance.b2) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
@@ -539,10 +556,88 @@ class C:
|
||||
if (2 + 3) < 4:
|
||||
self.x: str = "a"
|
||||
|
||||
# TODO: Ideally, this would result in a `unresolved-attribute` error. But mypy and pyright
|
||||
# do not support this either (for conditions that can only be resolved to `False` in type
|
||||
# inference), so it does not seem to be particularly important.
|
||||
reveal_type(C().x) # revealed: str
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, cond: bool) -> None:
|
||||
if True:
|
||||
self.a = 1
|
||||
else:
|
||||
self.a = "a"
|
||||
|
||||
if False:
|
||||
self.b = 2
|
||||
|
||||
if cond:
|
||||
return
|
||||
|
||||
self.c = 3
|
||||
|
||||
self.d = 4
|
||||
self.d = 5
|
||||
|
||||
def set_c(self, c: str) -> None:
|
||||
self.c = c
|
||||
if False:
|
||||
def set_e(self, e: str) -> None:
|
||||
self.e = e
|
||||
|
||||
reveal_type(C(True).a) # revealed: Unknown | Literal[1]
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C(True).b) # revealed: Unknown
|
||||
reveal_type(C(True).c) # revealed: Unknown | Literal[3] | str
|
||||
# TODO: this attribute is possibly unbound
|
||||
reveal_type(C(True).d) # revealed: Unknown | Literal[5]
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C(True).e) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes considered always bound
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, cond: bool):
|
||||
self.x = 1
|
||||
if cond:
|
||||
raise ValueError("Something went wrong")
|
||||
|
||||
# We consider this attribute is always bound.
|
||||
# This is because, it is not possible to access a partially-initialized object by normal means.
|
||||
self.y = 2
|
||||
|
||||
reveal_type(C(False).x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(C(False).y) # revealed: Unknown | Literal[2]
|
||||
|
||||
class C:
|
||||
def __init__(self, b: bytes) -> None:
|
||||
self.b = b
|
||||
|
||||
try:
|
||||
s = b.decode()
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError("Invalid UTF-8 sequence")
|
||||
|
||||
self.s = s
|
||||
|
||||
reveal_type(C(b"abc").b) # revealed: Unknown | bytes
|
||||
reveal_type(C(b"abc").s) # revealed: Unknown | str
|
||||
|
||||
class C:
|
||||
def __init__(self, iter) -> None:
|
||||
self.x = 1
|
||||
|
||||
for _ in iter:
|
||||
pass
|
||||
|
||||
# The for-loop may not stop,
|
||||
# but we consider the subsequent attributes to be definitely-bound.
|
||||
self.y = 2
|
||||
|
||||
reveal_type(C([]).x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(C([]).y) # revealed: Unknown | Literal[2]
|
||||
```
|
||||
|
||||
#### Diagnostics are reported for the right-hand side of attribute assignments
|
||||
@@ -551,6 +646,7 @@ reveal_type(C().x) # revealed: str
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
self.x: int = len(1, 2, 3)
|
||||
```
|
||||
|
||||
@@ -697,10 +793,10 @@ class Base:
|
||||
self.defined_in_init: str | None = "value in base"
|
||||
|
||||
class Intermediate(Base):
|
||||
# Re-declaring base class attributes with the *same *type is fine:
|
||||
# Redeclaring base class attributes with the *same *type is fine:
|
||||
base_class_attribute_1: str | None = None
|
||||
|
||||
# Re-declaring them with a *narrower type* is unsound, because modifications
|
||||
# Redeclaring them with a *narrower type* is unsound, because modifications
|
||||
# through a `Base` reference could violate that constraint.
|
||||
#
|
||||
# Mypy does not report an error here, but pyright does: "… overrides symbol
|
||||
@@ -712,7 +808,7 @@ class Intermediate(Base):
|
||||
# TODO: This should be an error
|
||||
base_class_attribute_2: str
|
||||
|
||||
# Re-declaring attributes with a *wider type* directly violates LSP.
|
||||
# Redeclaring attributes with a *wider type* directly violates LSP.
|
||||
#
|
||||
# In this case, both mypy and pyright report an error.
|
||||
#
|
||||
@@ -818,40 +914,74 @@ def _(flag: bool):
|
||||
if flag:
|
||||
class C1:
|
||||
x = 1
|
||||
y: int = 1
|
||||
|
||||
else:
|
||||
class C1:
|
||||
x = 2
|
||||
y: int | str = "b"
|
||||
|
||||
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(C1.y) # revealed: int | str
|
||||
|
||||
C1.y = 100
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `Literal[C1, C1]`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
x = 3
|
||||
y: int = 3
|
||||
else:
|
||||
x = 4
|
||||
y: int | str = "d"
|
||||
|
||||
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
|
||||
reveal_type(C2.y) # revealed: int | str
|
||||
|
||||
C2.y = 100
|
||||
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
|
||||
C2.y = None
|
||||
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
|
||||
C2.y = "problematic"
|
||||
|
||||
if flag:
|
||||
class Meta3(type):
|
||||
x = 5
|
||||
y: int = 5
|
||||
|
||||
else:
|
||||
class Meta3(type):
|
||||
x = 6
|
||||
y: int | str = "f"
|
||||
|
||||
class C3(metaclass=Meta3): ...
|
||||
reveal_type(C3.x) # revealed: Unknown | Literal[5, 6]
|
||||
reveal_type(C3.y) # revealed: int | str
|
||||
|
||||
C3.y = 100
|
||||
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
|
||||
C3.y = None
|
||||
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
|
||||
C3.y = "problematic"
|
||||
|
||||
class Meta4(type):
|
||||
if flag:
|
||||
x = 7
|
||||
y: int = 7
|
||||
else:
|
||||
x = 8
|
||||
y: int | str = "h"
|
||||
|
||||
class C4(metaclass=Meta4): ...
|
||||
reveal_type(C4.x) # revealed: Unknown | Literal[7, 8]
|
||||
reveal_type(C4.y) # revealed: int | str
|
||||
|
||||
C4.y = 100
|
||||
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `y` of type `int | str`"
|
||||
C4.y = None
|
||||
# TODO: should be an error, needs more sophisticated union handling in `validate_attribute_assignment`
|
||||
C4.y = "problematic"
|
||||
```
|
||||
|
||||
## Unions with possibly unbound paths
|
||||
@@ -875,8 +1005,14 @@ def _(flag1: bool, flag2: bool):
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `Literal[C1, C2, C3]`"
|
||||
C.x = 100
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`"
|
||||
C().x = 100
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
@@ -901,10 +1037,16 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
C.x = 100
|
||||
|
||||
# Note: we might want to consider ignoring possibly-unbound diagnostics for instance attributes eventually,
|
||||
# see the "Possibly unbound/undeclared instance attribute" section below.
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `C1 | C2 | C3` is possibly unbound"
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
C().x = 100
|
||||
```
|
||||
|
||||
### Possibly-unbound within gradual types
|
||||
@@ -922,6 +1064,9 @@ def _(flag: bool):
|
||||
x: int
|
||||
|
||||
reveal_type(Derived().x) # revealed: int | Any
|
||||
|
||||
Derived().x = 1
|
||||
Derived().x = "a"
|
||||
```
|
||||
|
||||
### Attribute possibly unbound on a subclass but not on a superclass
|
||||
@@ -936,8 +1081,10 @@ def _(flag: bool):
|
||||
x = 2
|
||||
|
||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||
Bar.x = 3
|
||||
|
||||
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
|
||||
Bar().x = 3
|
||||
```
|
||||
|
||||
### Attribute possibly unbound on a subclass and on a superclass
|
||||
@@ -955,8 +1102,14 @@ def _(flag: bool):
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
Bar.x = 3
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Bar().x) # revealed: Unknown | Literal[2, 1]
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
Bar().x = 3
|
||||
```
|
||||
|
||||
### Possibly unbound/undeclared instance attribute
|
||||
@@ -975,6 +1128,9 @@ def _(flag: bool):
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Foo().x) # revealed: int | Unknown
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
Foo().x = 1
|
||||
```
|
||||
|
||||
#### Possibly unbound
|
||||
@@ -985,10 +1141,18 @@ def _(flag: bool):
|
||||
def __init(self):
|
||||
if flag:
|
||||
self.x = 1
|
||||
self.y = "a"
|
||||
else:
|
||||
self.y = "b"
|
||||
|
||||
# Emitting a diagnostic in a case like this is not something we support, and it's unclear
|
||||
# if we ever will (or want to)
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(Foo().x) # revealed: Unknown | Literal[1]
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
Foo().x = 2
|
||||
|
||||
reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"]
|
||||
Foo().y = "c"
|
||||
```
|
||||
|
||||
### Unions with all paths unbound
|
||||
@@ -1003,6 +1167,11 @@ def _(flag: bool):
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: This should ideally be a `unresolved-attribute` error. We need better union
|
||||
# handling in `validate_attribute_assignment` for this.
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `x` on type `Literal[C1, C2]`"
|
||||
C.x = 1
|
||||
```
|
||||
|
||||
## Inherited class attributes
|
||||
@@ -1017,6 +1186,8 @@ class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(C.X) # revealed: Unknown | Literal["foo"]
|
||||
|
||||
C.X = "bar"
|
||||
```
|
||||
|
||||
### Multiple inheritance
|
||||
@@ -1040,6 +1211,8 @@ reveal_type(A.__mro__)
|
||||
|
||||
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
|
||||
reveal_type(A.X) # revealed: Unknown | Literal[42]
|
||||
|
||||
A.X = 100
|
||||
```
|
||||
|
||||
## Intersections of attributes
|
||||
@@ -1057,9 +1230,13 @@ class B: ...
|
||||
def _(a_and_b: Intersection[A, B]):
|
||||
reveal_type(a_and_b.x) # revealed: int
|
||||
|
||||
a_and_b.x = 2
|
||||
|
||||
# Same for class objects
|
||||
def _(a_and_b: Intersection[type[A], type[B]]):
|
||||
reveal_type(a_and_b.x) # revealed: int
|
||||
|
||||
a_and_b.x = 2
|
||||
```
|
||||
|
||||
### Attribute available on both elements
|
||||
@@ -1069,6 +1246,7 @@ from knot_extensions import Intersection
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R(P, Q): ...
|
||||
|
||||
class A:
|
||||
x: P = P()
|
||||
@@ -1078,10 +1256,12 @@ class B:
|
||||
|
||||
def _(a_and_b: Intersection[A, B]):
|
||||
reveal_type(a_and_b.x) # revealed: P & Q
|
||||
a_and_b.x = R()
|
||||
|
||||
# Same for class objects
|
||||
def _(a_and_b: Intersection[type[A], type[B]]):
|
||||
reveal_type(a_and_b.x) # revealed: P & Q
|
||||
a_and_b.x = R()
|
||||
```
|
||||
|
||||
### Possible unboundness
|
||||
@@ -1091,6 +1271,7 @@ from knot_extensions import Intersection
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R(P, Q): ...
|
||||
|
||||
def _(flag: bool):
|
||||
class A1:
|
||||
@@ -1102,11 +1283,17 @@ def _(flag: bool):
|
||||
def inner1(a_and_b: Intersection[A1, B1]):
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(a_and_b.x) # revealed: P
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
a_and_b.x = R()
|
||||
# Same for class objects
|
||||
def inner1_class(a_and_b: Intersection[type[A1], type[B1]]):
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(a_and_b.x) # revealed: P
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
a_and_b.x = R()
|
||||
|
||||
class A2:
|
||||
if flag:
|
||||
x: P = P()
|
||||
@@ -1116,6 +1303,11 @@ def _(flag: bool):
|
||||
|
||||
def inner2(a_and_b: Intersection[A2, B1]):
|
||||
reveal_type(a_and_b.x) # revealed: P & Q
|
||||
|
||||
# TODO: this should not be an error, we need better intersection
|
||||
# handling in `validate_attribute_assignment` for this
|
||||
# error: [possibly-unbound-attribute]
|
||||
a_and_b.x = R()
|
||||
# Same for class objects
|
||||
def inner2_class(a_and_b: Intersection[type[A2], type[B1]]):
|
||||
reveal_type(a_and_b.x) # revealed: P & Q
|
||||
@@ -1131,21 +1323,33 @@ def _(flag: bool):
|
||||
def inner3(a_and_b: Intersection[A3, B3]):
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(a_and_b.x) # revealed: P & Q
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
a_and_b.x = R()
|
||||
# Same for class objects
|
||||
def inner3_class(a_and_b: Intersection[type[A3], type[B3]]):
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(a_and_b.x) # revealed: P & Q
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
a_and_b.x = R()
|
||||
|
||||
class A4: ...
|
||||
class B4: ...
|
||||
|
||||
def inner4(a_and_b: Intersection[A4, B4]):
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a_and_b.x) # revealed: Unknown
|
||||
|
||||
# error: [invalid-assignment]
|
||||
a_and_b.x = R()
|
||||
# Same for class objects
|
||||
def inner4_class(a_and_b: Intersection[type[A4], type[B4]]):
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(a_and_b.x) # revealed: Unknown
|
||||
|
||||
# error: [invalid-assignment]
|
||||
a_and_b.x = R()
|
||||
```
|
||||
|
||||
### Intersection of implicit instance attributes
|
||||
@@ -1181,7 +1385,7 @@ from typing import Any
|
||||
class Foo(Any): ...
|
||||
|
||||
reveal_type(Foo.bar) # revealed: Any
|
||||
reveal_type(Foo.__repr__) # revealed: Literal[__repr__] & Any
|
||||
reveal_type(Foo.__repr__) # revealed: (def __repr__(self) -> str) & Any
|
||||
```
|
||||
|
||||
Similar principles apply if `Any` appears in the middle of an inheritance hierarchy:
|
||||
@@ -1291,6 +1495,59 @@ def _(ns: argparse.Namespace):
|
||||
reveal_type(ns.whatever) # revealed: Any
|
||||
```
|
||||
|
||||
## Classes with custom `__setattr__` methods
|
||||
|
||||
### Basic
|
||||
|
||||
If a type provides a custom `__setattr__` method, we use the parameter type of that method as the
|
||||
type to validate attribute assignments. Consider the following `CustomSetAttr` class:
|
||||
|
||||
```py
|
||||
class CustomSetAttr:
|
||||
def __setattr__(self, name: str, value: int) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
We can set arbitrary attributes on instances of this class:
|
||||
|
||||
```py
|
||||
c = CustomSetAttr()
|
||||
|
||||
c.whatever = 42
|
||||
```
|
||||
|
||||
### Type of the `name` parameter
|
||||
|
||||
If the `name` parameter of the `__setattr__` method is annotated with a (union of) literal type(s),
|
||||
we only consider the attribute assignment to be valid if the assigned attribute is one of them:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Date:
|
||||
def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None:
|
||||
pass
|
||||
|
||||
date = Date()
|
||||
date.day = 8
|
||||
date.month = 4
|
||||
date.year = 2025
|
||||
|
||||
# error: [unresolved-attribute] "Can not assign object of `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
|
||||
date.tz = "UTC"
|
||||
```
|
||||
|
||||
### `argparse.Namespace`
|
||||
|
||||
A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`:
|
||||
|
||||
```py
|
||||
import argparse
|
||||
|
||||
def _(ns: argparse.Namespace):
|
||||
ns.whatever = 42
|
||||
```
|
||||
|
||||
## Objects of all types have a `__class__` method
|
||||
|
||||
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
||||
@@ -1420,14 +1677,14 @@ functions are instances of that class:
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
|
||||
reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
|
||||
reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py
|
||||
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
reveal_type(f.__call__) # revealed: <bound method `__call__` of `Literal[f]`>
|
||||
reveal_type(f.__call__) # revealed: <method-wrapper `__call__` of `f`>
|
||||
```
|
||||
|
||||
### Int-literal attributes
|
||||
@@ -1436,8 +1693,8 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si
|
||||
integers are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
|
||||
reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
reveal_type((2).bit_length) # revealed: bound method Literal[2].bit_length() -> int
|
||||
reveal_type((2).denominator) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -1453,8 +1710,10 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
|
||||
bools are instances of that class:
|
||||
|
||||
```py
|
||||
reveal_type(True.__and__) # revealed: <bound method `__and__` of `Literal[True]`>
|
||||
reveal_type(False.__or__) # revealed: <bound method `__or__` of `Literal[False]`>
|
||||
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
|
||||
reveal_type(True.__and__)
|
||||
# revealed: Overload[(value: bool, /) -> bool, (value: int, /) -> int]
|
||||
reveal_type(False.__or__)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
@@ -1469,8 +1728,10 @@ reveal_type(False.real) # revealed: Literal[0]
|
||||
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
|
||||
|
||||
```py
|
||||
reveal_type(b"foo".join) # revealed: <bound method `join` of `Literal[b"foo"]`>
|
||||
reveal_type(b"foo".endswith) # revealed: <bound method `endswith` of `Literal[b"foo"]`>
|
||||
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: @Todo(specialized non-generic class), /) -> bytes
|
||||
reveal_type(b"foo".join)
|
||||
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`), start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool
|
||||
reveal_type(b"foo".endswith)
|
||||
```
|
||||
|
||||
## Instance attribute edge cases
|
||||
@@ -1571,6 +1832,89 @@ def f(never: Never):
|
||||
never.another_attribute = never
|
||||
```
|
||||
|
||||
### Cyclic implicit attributes
|
||||
|
||||
Inferring types for undeclared implicit attributes can be cyclic:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "C"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute:
|
||||
|
||||
```py
|
||||
class D:
|
||||
def copy(self, other: "D"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(D().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If there is an annotation for a name, we don't try to infer any type from the RHS of assignments to
|
||||
that name, so these cases don't trigger any cycle:
|
||||
|
||||
```py
|
||||
class E:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
|
||||
def copy(self, other: "E"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(E().x) # revealed: int
|
||||
|
||||
class F:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "F"):
|
||||
self.x: int = other.x
|
||||
|
||||
reveal_type(F().x) # revealed: int
|
||||
|
||||
class G:
|
||||
def copy(self, other: "G"):
|
||||
self.x: int = other.x
|
||||
|
||||
reveal_type(G().x) # revealed: int
|
||||
```
|
||||
|
||||
We can even handle cycles involving multiple classes:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "B"):
|
||||
self.x = other.x
|
||||
|
||||
class B:
|
||||
def copy(self, other: "A"):
|
||||
self.x = other.x
|
||||
|
||||
reveal_type(B().x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(A().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
This case additionally tests our union/intersection simplification logic:
|
||||
|
||||
```py
|
||||
class H:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "H"):
|
||||
self.x = other.x or self.x
|
||||
```
|
||||
|
||||
### Builtin types attributes
|
||||
|
||||
This test can probably be removed eventually, but we currently include it because we do not yet
|
||||
@@ -1605,6 +1949,23 @@ reveal_type(C.a_type) # revealed: type
|
||||
reveal_type(C.a_none) # revealed: None
|
||||
```
|
||||
|
||||
## Enum classes
|
||||
|
||||
Enums are not supported yet; attribute access on an enum class is inferred as `Todo`.
|
||||
|
||||
```py
|
||||
import enum
|
||||
|
||||
reveal_type(enum.Enum.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||
|
||||
class Foo(enum.Enum):
|
||||
BAR = 1
|
||||
|
||||
reveal_type(Foo.BAR) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
|
||||
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
@@ -1612,5 +1973,5 @@ Some of the tests in the *Class and instance variables* section draw inspiration
|
||||
|
||||
[descriptor protocol tests]: descriptor_protocol.md
|
||||
[pyright's documentation]: https://microsoft.github.io/pyright/#/type-concepts-advanced?id=class-and-instance-variables
|
||||
[typing spec on `classvar`]: https://typing.readthedocs.io/en/latest/spec/class-compat.html#classvar
|
||||
[typing spec on `classvar`]: https://typing.python.org/en/latest/spec/class-compat.html#classvar
|
||||
[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar
|
||||
|
||||
@@ -350,30 +350,30 @@ reveal_type(no() + no()) # revealed: Unknown
|
||||
def f():
|
||||
pass
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f + f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f - f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f * f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f @ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f / f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f % f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f**f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f << f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f >> f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f | f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f ^ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f & f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f // f) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -14,43 +14,43 @@ We support inference for all Python's binary operators: `+`, `-`, `*`, `@`, `/`,
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> A:
|
||||
def __add__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __sub__(self, other) -> A:
|
||||
def __sub__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __mul__(self, other) -> A:
|
||||
def __mul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __matmul__(self, other) -> A:
|
||||
def __matmul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __truediv__(self, other) -> A:
|
||||
def __truediv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __floordiv__(self, other) -> A:
|
||||
def __floordiv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __mod__(self, other) -> A:
|
||||
def __mod__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __pow__(self, other) -> A:
|
||||
def __pow__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __lshift__(self, other) -> A:
|
||||
def __lshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rshift__(self, other) -> A:
|
||||
def __rshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __and__(self, other) -> A:
|
||||
def __and__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __xor__(self, other) -> A:
|
||||
def __xor__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __or__(self, other) -> A:
|
||||
def __or__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
@@ -76,43 +76,43 @@ We also support inference for reflected operations:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __radd__(self, other) -> A:
|
||||
def __radd__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rsub__(self, other) -> A:
|
||||
def __rsub__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rmul__(self, other) -> A:
|
||||
def __rmul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rmatmul__(self, other) -> A:
|
||||
def __rmatmul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rtruediv__(self, other) -> A:
|
||||
def __rtruediv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rfloordiv__(self, other) -> A:
|
||||
def __rfloordiv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rmod__(self, other) -> A:
|
||||
def __rmod__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rpow__(self, other) -> A:
|
||||
def __rpow__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rlshift__(self, other) -> A:
|
||||
def __rlshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rrshift__(self, other) -> A:
|
||||
def __rrshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rand__(self, other) -> A:
|
||||
def __rand__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rxor__(self, other) -> A:
|
||||
def __rxor__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __ror__(self, other) -> A:
|
||||
def __ror__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
@@ -157,11 +157,11 @@ the right-hand side is not a subtype of the left-hand side, `lhs.__add__` will t
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other: B) -> int:
|
||||
def __add__(self, other: "B") -> int:
|
||||
return 42
|
||||
|
||||
class B:
|
||||
def __radd__(self, other: A) -> str:
|
||||
def __radd__(self, other: "A") -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(A() + B()) # revealed: int
|
||||
@@ -169,10 +169,10 @@ reveal_type(A() + B()) # revealed: int
|
||||
# Edge case: C is a subtype of C, *but* if the two sides are of *equal* types,
|
||||
# the lhs *still* takes precedence
|
||||
class C:
|
||||
def __add__(self, other: C) -> int:
|
||||
def __add__(self, other: "C") -> int:
|
||||
return 42
|
||||
|
||||
def __radd__(self, other: C) -> str:
|
||||
def __radd__(self, other: "C") -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(C() + C()) # revealed: int
|
||||
@@ -237,11 +237,11 @@ well.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __sub__(self, other: A) -> A:
|
||||
def __sub__(self, other: "A") -> "A":
|
||||
return A()
|
||||
|
||||
class B:
|
||||
def __rsub__(self, other: A) -> B:
|
||||
def __rsub__(self, other: A) -> "B":
|
||||
return B()
|
||||
|
||||
reveal_type(A() - B()) # revealed: B
|
||||
@@ -300,19 +300,17 @@ its instance super-type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> A:
|
||||
def __add__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __radd__(self, other) -> A:
|
||||
def __radd__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
reveal_type(A() + 1) # revealed: A
|
||||
reveal_type(1 + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type("foo" + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
@@ -320,16 +318,14 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(() + A()) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(literal_string_instance + A()) # revealed: A
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
@@ -363,7 +359,7 @@ reveal_type(X() + Y()) # revealed: int
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
@@ -371,6 +367,39 @@ a = NotBoolable()
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Operations on class objects
|
||||
|
||||
When operating on class objects, the corresponding dunder methods are looked up on the metaclass.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Meta(type):
|
||||
def __add__(self, other: Meta) -> int:
|
||||
return 1
|
||||
|
||||
def __lt__(self, other: Meta) -> bool:
|
||||
return True
|
||||
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return "a"
|
||||
|
||||
class A(metaclass=Meta): ...
|
||||
class B(metaclass=Meta): ...
|
||||
|
||||
reveal_type(A + B) # revealed: int
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A - B) # revealed: Unknown
|
||||
|
||||
reveal_type(A < B) # revealed: bool
|
||||
reveal_type(A > B) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A <= B) # revealed: Unknown
|
||||
|
||||
reveal_type(A[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### Dunder as instance attribute
|
||||
@@ -433,7 +462,7 @@ the unreflected dunder of the left-hand operand. For context, see
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __radd__(self, other: Foo) -> Foo:
|
||||
def __radd__(self, other: "Foo") -> "Foo":
|
||||
return self
|
||||
|
||||
# error: [unsupported-operator]
|
||||
|
||||
@@ -50,9 +50,23 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||
reveal_type(2**largest_u32) # revealed: int
|
||||
|
||||
def variable(x: int):
|
||||
reveal_type(x**2) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(2**x) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(x**x) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(x**2) # revealed: int
|
||||
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||
reveal_type(2**x) # revealed: int
|
||||
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||
reveal_type(x**x) # revealed: int
|
||||
```
|
||||
|
||||
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
|
||||
the second argument is >=0, an `int` is still returned:
|
||||
|
||||
```py
|
||||
reveal_type(1**0) # revealed: Literal[1]
|
||||
reveal_type(0**1) # revealed: Literal[0]
|
||||
reveal_type(0**0) # revealed: Literal[1]
|
||||
reveal_type((-1) ** 2) # revealed: Literal[1]
|
||||
reveal_type(2 ** (-1)) # revealed: float
|
||||
reveal_type((-1) ** (-1)) # revealed: float
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Binary operations on tuples
|
||||
|
||||
## Concatenation for heterogeneous tuples
|
||||
|
||||
```py
|
||||
reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
|
||||
reveal_type(() + (1, 2)) # revealed: tuple[Literal[1], Literal[2]]
|
||||
reveal_type((1, 2) + ()) # revealed: tuple[Literal[1], Literal[2]]
|
||||
reveal_type(() + ()) # revealed: tuple[()]
|
||||
|
||||
def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
|
||||
reveal_type(x + y) # revealed: tuple[int, str, None, tuple[int]]
|
||||
reveal_type(y + x) # revealed: tuple[None, tuple[int], int, str]
|
||||
```
|
||||
|
||||
## Concatenation for homogeneous tuples
|
||||
|
||||
```py
|
||||
def _(x: tuple[int, ...], y: tuple[str, ...]):
|
||||
reveal_type(x + y) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(x + (1, 2)) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
@@ -49,3 +49,11 @@ def f4(x: float, y: float):
|
||||
reveal_type(x // y) # revealed: int | float
|
||||
reveal_type(x % y) # revealed: int | float
|
||||
```
|
||||
|
||||
If any of the union elements leads to a division by zero, we will report an error:
|
||||
|
||||
```py
|
||||
def f5(m: int, n: Literal[-1, 0, 1]):
|
||||
# error: [division-by-zero] "Cannot divide object of type `int` by zero"
|
||||
return m / n
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ if True and (x := 1):
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||
flag or (x := 1) or reveal_type(x) # revealed: Never
|
||||
|
||||
# error: [unresolved-reference]
|
||||
flag or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# `typing.Callable`
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[], int]):
|
||||
reveal_type(c()) # revealed: int
|
||||
|
||||
def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c(1, "a")) # revealed: int
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
|
||||
reveal_type(c("a", 1)) # revealed: int
|
||||
```
|
||||
|
||||
The `Callable` annotation can only be used to describe positional-only parameters.
|
||||
|
||||
```py
|
||||
def _(c: Callable[[int, str], None]):
|
||||
# error: [unknown-argument] "Argument `a` does not match any known parameter"
|
||||
# error: [unknown-argument] "Argument `b` does not match any known parameter"
|
||||
# error: [missing-argument] "No arguments provided for required parameters 1, 2"
|
||||
reveal_type(c(a=1, b="b")) # revealed: None
|
||||
```
|
||||
|
||||
If the annotation uses a gradual form (`...`) for the parameter list, then it can accept any kind of
|
||||
parameter with any type.
|
||||
|
||||
```py
|
||||
def _(c: Callable[..., int]):
|
||||
reveal_type(c()) # revealed: int
|
||||
reveal_type(c(1)) # revealed: int
|
||||
reveal_type(c(1, "str", False, a=[1, 2], b=(3, 4))) # revealed: int
|
||||
```
|
||||
|
||||
An invalid `Callable` form can accept any parameters and will return `Unknown`.
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form]
|
||||
def _(c: Callable[42, str]):
|
||||
reveal_type(c()) # revealed: Unknown
|
||||
```
|
||||
@@ -26,7 +26,11 @@ reveal_type(type(1)) # revealed: Literal[int]
|
||||
But a three-argument call to type creates a dynamic instance of the `type` class:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
reveal_type(type("Foo", (), {})) # revealed: type
|
||||
|
||||
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
|
||||
```
|
||||
|
||||
Other numbers of arguments are invalid
|
||||
@@ -38,3 +42,60 @@ type("Foo", ())
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", (), {}, weird_other_arg=42)
|
||||
```
|
||||
|
||||
The following calls are also invalid, due to incorrect argument types:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type(b"Foo", (), {})
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", Base, {})
|
||||
|
||||
# TODO: this should be an error
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# TODO: this should be an error
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
## Calls to `str()`
|
||||
|
||||
### Valid calls
|
||||
|
||||
```py
|
||||
str()
|
||||
str("")
|
||||
str(b"")
|
||||
str(1)
|
||||
str(object=1)
|
||||
|
||||
str(b"M\xc3\xbcsli", "utf-8")
|
||||
str(b"M\xc3\xbcsli", "utf-8", "replace")
|
||||
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16")
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore")
|
||||
|
||||
str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8")
|
||||
str(bytearray(), "utf-8")
|
||||
|
||||
str(encoding="utf-8", object=b"M\xc3\xbcsli")
|
||||
str(b"", errors="replace")
|
||||
str(encoding="utf-8")
|
||||
str(errors="replace")
|
||||
```
|
||||
|
||||
### Invalid calls
|
||||
|
||||
```py
|
||||
str(1, 2) # error: [no-matching-overload]
|
||||
str(o=1) # error: [no-matching-overload]
|
||||
|
||||
# First argument is not a bytes-like object:
|
||||
str("Müsli", "utf-8") # error: [no-matching-overload]
|
||||
|
||||
# Second argument is not a valid encoding:
|
||||
str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload]
|
||||
```
|
||||
|
||||
@@ -71,7 +71,7 @@ def _(flag: bool):
|
||||
|
||||
a = NonCallable()
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: int | Unknown
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Call binding errors
|
||||
@@ -85,7 +85,7 @@ class C:
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(c("foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -99,7 +99,7 @@ class C:
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of bound method `__call__`; expected type `int`"
|
||||
# error: 13 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `C`"
|
||||
reveal_type(c()) # revealed: int
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,325 @@
|
||||
# Constructor
|
||||
|
||||
When classes are instantiated, Python calls the meta-class `__call__` method, which can either be
|
||||
customized by the user or `type.__call__` is used.
|
||||
|
||||
The latter calls the `__new__` method of the class, which is responsible for creating the instance
|
||||
and then calls the `__init__` method on the resulting instance to initialize it with the same
|
||||
arguments.
|
||||
|
||||
Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then
|
||||
called as an implicit static, rather than bound method with `cls` passed as the first argument.
|
||||
`__init__` has no special handling, it is fetched as bound method and is called just like any other
|
||||
dunder method.
|
||||
|
||||
`type.__call__` does other things too, but this is not yet handled by us.
|
||||
|
||||
Since every class has `object` in it's MRO, the default implementations are `object.__new__` and
|
||||
`object.__init__`. They have some special behavior, namely:
|
||||
|
||||
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`)
|
||||
\- no arguments are accepted and `TypeError` is raised if any are passed.
|
||||
- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments!
|
||||
|
||||
As of today there are a number of behaviors that we do not support:
|
||||
|
||||
- `__new__` is assumed to return an instance of the class on which it is called
|
||||
- User defined `__call__` on metaclass is ignored
|
||||
|
||||
## Creating an instance of the `object` class itself
|
||||
|
||||
Test the behavior of the `object` class itself. As implementation has to ignore `object` own methods
|
||||
as defined in typeshed due to behavior not expressible in typeshed (see above how `__init__` behaves
|
||||
differently depending on whether `__new__` is defined or not), we have to test the behavior of
|
||||
`object` itself.
|
||||
|
||||
```py
|
||||
reveal_type(object()) # revealed: object
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to class `object`: expected 0, got 1"
|
||||
reveal_type(object(1)) # revealed: object
|
||||
```
|
||||
|
||||
## No init or new
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__new__` present on the class itself
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __new__(cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__new__` present on a superclass
|
||||
|
||||
If the `__new__` method is defined on a superclass, we can still infer the signature of the
|
||||
constructor from it.
|
||||
|
||||
```py
|
||||
from typing_extensions import Self
|
||||
|
||||
class Base:
|
||||
def __new__(cls, x: int) -> Self: ...
|
||||
|
||||
class Foo(Base): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## Conditional `__new__`
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Foo:
|
||||
if flag:
|
||||
def __new__(cls, x: int): ...
|
||||
else:
|
||||
def __new__(cls, x: int, y: int = 1): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`"
|
||||
reveal_type(Foo("1")) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## A descriptor in place of `__new__`
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
def __call__(self, cls, x: int) -> "Foo":
|
||||
obj = object.__new__(cls)
|
||||
obj.x = x
|
||||
return obj
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class Foo:
|
||||
__new__: Descriptor = Descriptor()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## A callable instance in place of `__new__`
|
||||
|
||||
### Bound
|
||||
|
||||
```py
|
||||
class Callable:
|
||||
def __call__(self, cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
class Foo:
|
||||
__new__ = Callable()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
### Possibly Unbound
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Callable:
|
||||
if flag:
|
||||
def __call__(self, cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
class Foo:
|
||||
__new__ = Callable()
|
||||
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__init__` present on the class itself
|
||||
|
||||
If the class has an `__init__` method, we can infer the signature of the constructor from it.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__init__` present on a superclass
|
||||
|
||||
If the `__init__` method is defined on a superclass, we can still infer the signature of the
|
||||
constructor from it.
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
class Foo(Base): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## Conditional `__init__`
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Foo:
|
||||
if flag:
|
||||
def __init__(self, x: int): ...
|
||||
else:
|
||||
def __init__(self, x: int, y: int = 1): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`"
|
||||
reveal_type(Foo("1")) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## A descriptor in place of `__init__`
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
# TODO: at runtime `__init__` is checked to return `None` and
|
||||
# a `TypeError` is raised if it doesn't. However, apparently
|
||||
# this is not true when the descriptor is used as `__init__`.
|
||||
# However, we may still want to check this.
|
||||
def __call__(self, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class Foo:
|
||||
__init__: Descriptor = Descriptor()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## A callable instance in place of `__init__`
|
||||
|
||||
### Bound
|
||||
|
||||
```py
|
||||
class Callable:
|
||||
def __call__(self, x: int) -> None:
|
||||
pass
|
||||
|
||||
class Foo:
|
||||
__init__ = Callable()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
### Possibly Unbound
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Callable:
|
||||
if flag:
|
||||
def __call__(self, x: int) -> None:
|
||||
pass
|
||||
|
||||
class Foo:
|
||||
__init__ = Callable()
|
||||
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__new__` and `__init__` both present
|
||||
|
||||
### Identical signatures
|
||||
|
||||
A common case is to have `__new__` and `__init__` with identical signatures (except for the first
|
||||
argument). We report errors for both `__new__` and `__init__` if the arguments are incorrect.
|
||||
|
||||
At runtime `__new__` is called first and will fail without executing `__init__` if the arguments are
|
||||
incorrect. However, we decided that it is better to report errors for both methods, since after
|
||||
fixing the `__new__` method, the user may forget to fix the `__init__` method.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __new__(cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
```
|
||||
|
||||
### Compatible signatures
|
||||
|
||||
But they can also be compatible, but not identical. We should correctly report errors only for the
|
||||
mthod that would fail.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: int) -> None:
|
||||
self.x = x
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
@@ -204,6 +204,28 @@ def _(flag: bool):
|
||||
reveal_type(d[0]) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Calling a union of types without dunder methods
|
||||
|
||||
We add instance attributes here to make sure that we don't treat the implicit dunder calls here like
|
||||
regular method calls.
|
||||
|
||||
```py
|
||||
def external_getitem(instance, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class NotSubscriptable1:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
class NotSubscriptable2:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
def _(union: NotSubscriptable1 | NotSubscriptable2):
|
||||
# error: [non-subscriptable]
|
||||
union[0]
|
||||
```
|
||||
|
||||
## Calling a possibly-unbound dunder method
|
||||
|
||||
```py
|
||||
|
||||
@@ -21,6 +21,11 @@ reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
|
||||
|
||||
## Generic
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
def get_int[T]() -> int:
|
||||
return 42
|
||||
@@ -37,16 +42,13 @@ def foo() -> int:
|
||||
return 42
|
||||
|
||||
def decorator(func) -> Callable[[], int]:
|
||||
# TODO: no error
|
||||
# error: [invalid-return-type]
|
||||
return foo
|
||||
|
||||
@decorator
|
||||
def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(bar()) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(bar()) # revealed: int
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
@@ -75,7 +77,7 @@ def _(flag: bool):
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -85,7 +87,7 @@ reveal_type(f("foo")) # revealed: int
|
||||
def f(x: int, /) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -95,7 +97,7 @@ reveal_type(f("foo")) # revealed: int
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `*args` of function `f`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -105,7 +107,7 @@ reveal_type(f("foo")) # revealed: int
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -115,7 +117,7 @@ reveal_type(f(x="foo")) # revealed: int
|
||||
def f(*, x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -125,7 +127,7 @@ reveal_type(f(x="foo")) # revealed: int
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter `**kwargs` of function `f`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -135,8 +137,8 @@ reveal_type(f(x="foo")) # revealed: int
|
||||
def f(x: int = 1, y: str = "foo") -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal[2]` cannot be assigned to parameter `y` of function `f`; expected type `str`"
|
||||
# error: 20 [invalid-argument-type] "Object of type `Literal["bar"]` cannot be assigned to parameter `x` of function `f`; expected type `int`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[2]`"
|
||||
# error: 20 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["bar"]`"
|
||||
reveal_type(f(y=2, x="bar")) # revealed: int
|
||||
```
|
||||
|
||||
|
||||
@@ -56,10 +56,11 @@ We can access attributes on objects of all kinds:
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
|
||||
reveal_type(inspect.getattr_static(sys, "dont_write_bytecode")) # revealed: bool
|
||||
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = ellipsis) -> Any
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static"))
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[real]
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
|
||||
```
|
||||
|
||||
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
|
||||
@@ -114,7 +115,7 @@ inspect.getattr_static()
|
||||
# error: [missing-argument] "No argument provided for required parameter `attr`"
|
||||
inspect.getattr_static(C())
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`attr`) of function `getattr_static`; expected type `str`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
|
||||
inspect.getattr_static(C(), 1)
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4"
|
||||
@@ -143,8 +144,9 @@ from typing import Any
|
||||
def _(a: Any, tuple_of_any: tuple[Any]):
|
||||
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
|
||||
|
||||
# TODO: Ideally, this would just be `Literal[index]`
|
||||
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) # revealed: Literal[index] | Literal["default"]
|
||||
# TODO: Ideally, this would just be `def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int`
|
||||
# revealed: (def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int) | Literal["default"]
|
||||
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))
|
||||
```
|
||||
|
||||
[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static
|
||||
|
||||
@@ -24,7 +24,7 @@ to the valid order:
|
||||
def f(**kw: int, x: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `str`"
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
|
||||
reveal_type(f(1)) # revealed: int
|
||||
```
|
||||
|
||||
@@ -38,7 +38,7 @@ def f(x: int = 1, y: str) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(y="foo")) # revealed: int
|
||||
# error: [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 1 (`x`) of function `f`; expected type `int`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
# error: [missing-argument] "No argument provided for required parameter `y` of function `f`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
@@ -32,20 +32,20 @@ the latter case, it returns a *bound method* object:
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
|
||||
reveal_type(getattr_static(C, "f")) # revealed: def f(self, x: int) -> str
|
||||
|
||||
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f]
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `C`>
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f(self, x: int) -> str
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method C.f(x: int) -> str
|
||||
```
|
||||
|
||||
In conclusion, this is why we see the following two types when accessing the `f` attribute on the
|
||||
class object `C` and on an instance `C()`:
|
||||
|
||||
```py
|
||||
reveal_type(C.f) # revealed: Literal[f]
|
||||
reveal_type(C().f) # revealed: <bound method `f` of `C`>
|
||||
reveal_type(C.f) # revealed: def f(self, x: int) -> str
|
||||
reveal_type(C().f) # revealed: bound method C.f(x: int) -> str
|
||||
```
|
||||
|
||||
A bound method is a callable object that contains a reference to the `instance` that it was called
|
||||
@@ -56,7 +56,7 @@ via `__func__`):
|
||||
bound_method = C().f
|
||||
|
||||
reveal_type(bound_method.__self__) # revealed: C
|
||||
reveal_type(bound_method.__func__) # revealed: Literal[f]
|
||||
reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str
|
||||
```
|
||||
|
||||
When we call the bound method, the `instance` is implicitly passed as the first argument (`self`):
|
||||
@@ -80,13 +80,13 @@ When we access methods from derived classes, they will be bound to instances of
|
||||
class D(C):
|
||||
pass
|
||||
|
||||
reveal_type(D().f) # revealed: <bound method `f` of `D`>
|
||||
reveal_type(D().f) # revealed: bound method D.f(x: int) -> str
|
||||
```
|
||||
|
||||
If we access an attribute on a bound method object itself, it will defer to `types.MethodType`:
|
||||
|
||||
```py
|
||||
reveal_type(bound_method.__hash__) # revealed: <bound method `__hash__` of `MethodType`>
|
||||
reveal_type(bound_method.__hash__) # revealed: bound method MethodType.__hash__() -> int
|
||||
```
|
||||
|
||||
If an attribute is not available on the bound method object, it will be looked up on the underlying
|
||||
@@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd
|
||||
methods, even though it is not available on `types.MethodType`:
|
||||
|
||||
```py
|
||||
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None
|
||||
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
|
||||
```
|
||||
|
||||
## Basic method calls on class objects and instances
|
||||
@@ -181,10 +181,10 @@ class B:
|
||||
return "a"
|
||||
|
||||
def f(a_or_b: A | B, any_or_a: Any | A):
|
||||
reveal_type(a_or_b.f) # revealed: <bound method `f` of `A`> | <bound method `f` of `B`>
|
||||
reveal_type(a_or_b.f) # revealed: (bound method A.f() -> int) | (bound method B.f() -> str)
|
||||
reveal_type(a_or_b.f()) # revealed: int | str
|
||||
|
||||
reveal_type(any_or_a.f) # revealed: Any | <bound method `f` of `A`>
|
||||
reveal_type(any_or_a.f) # revealed: Any | (bound method A.f() -> int)
|
||||
reveal_type(any_or_a.f()) # revealed: Any | int
|
||||
```
|
||||
|
||||
@@ -198,7 +198,7 @@ python-version = "3.12"
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__or__) # revealed: <bound method `__or__` of `typing.TypeAliasType`>
|
||||
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm
|
||||
```
|
||||
|
||||
## Error cases: Calling `__get__` for methods
|
||||
@@ -270,7 +270,7 @@ class Meta(type):
|
||||
class C(metaclass=Meta):
|
||||
pass
|
||||
|
||||
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(C.f) # revealed: bound method Literal[C].f(arg: int) -> str
|
||||
reveal_type(C.f(1)) # revealed: str
|
||||
```
|
||||
|
||||
@@ -322,8 +322,8 @@ class C:
|
||||
def f(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(C().f) # revealed: <bound method `f` of `type[C]`>
|
||||
reveal_type(C.f) # revealed: bound method Literal[C].f(x: int) -> str
|
||||
reveal_type(C().f) # revealed: bound method type[C].f(x: int) -> str
|
||||
```
|
||||
|
||||
The `cls` method argument is then implicitly passed as the first argument when calling the method:
|
||||
@@ -350,7 +350,7 @@ class D:
|
||||
# This function is wrongly annotated, it should be `type[D]` instead of `D`
|
||||
pass
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[D]` cannot be assigned to parameter 1 (`cls`) of bound method `f`; expected type `D`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `D`, found `Literal[D]`"
|
||||
D.f()
|
||||
```
|
||||
|
||||
@@ -360,8 +360,8 @@ When a class method is accessed on a derived class, it is bound to that derived
|
||||
class Derived(C):
|
||||
pass
|
||||
|
||||
reveal_type(Derived.f) # revealed: <bound method `f` of `Literal[Derived]`>
|
||||
reveal_type(Derived().f) # revealed: <bound method `f` of `type[Derived]`>
|
||||
reveal_type(Derived.f) # revealed: bound method Literal[Derived].f(x: int) -> str
|
||||
reveal_type(Derived().f) # revealed: bound method type[Derived].f(x: int) -> str
|
||||
|
||||
reveal_type(Derived.f(1)) # revealed: str
|
||||
reveal_type(Derived().f(1)) # revealed: str
|
||||
@@ -379,26 +379,31 @@ class C:
|
||||
@classmethod
|
||||
def f(cls): ...
|
||||
|
||||
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
|
||||
reveal_type(getattr_static(C, "f")) # revealed: def f(cls) -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
```
|
||||
|
||||
But we correctly model how the `classmethod` descriptor works:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: <bound method `f` of `type[C]`>
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: bound method type[C].f() -> Unknown
|
||||
```
|
||||
|
||||
The `owner` argument takes precedence over the `instance` argument:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
```
|
||||
|
||||
### Classmethods mixed with other decorators
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
|
||||
class method:
|
||||
|
||||
@@ -419,14 +424,10 @@ class C:
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: We do not support decorators yet (only limited special cases). Eventually,
|
||||
# these should all return `str`:
|
||||
|
||||
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f1(1)) # revealed: @Todo(return type of decorated function)
|
||||
|
||||
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C().f2(1)) # revealed: @Todo(return type of decorated function)
|
||||
reveal_type(C.f1(1)) # revealed: str
|
||||
reveal_type(C().f1(1)) # revealed: str
|
||||
reveal_type(C.f2(1)) # revealed: str
|
||||
reveal_type(C().f2(1)) # revealed: str
|
||||
```
|
||||
|
||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# `str.startswith`
|
||||
|
||||
We special-case `str.startswith` to allow inference of precise Boolean literal types, because those
|
||||
are used in [`sys.platform` checks].
|
||||
|
||||
```py
|
||||
reveal_type("abc".startswith("")) # revealed: Literal[True]
|
||||
reveal_type("abc".startswith("a")) # revealed: Literal[True]
|
||||
reveal_type("abc".startswith("ab")) # revealed: Literal[True]
|
||||
reveal_type("abc".startswith("abc")) # revealed: Literal[True]
|
||||
|
||||
reveal_type("abc".startswith("abcd")) # revealed: Literal[False]
|
||||
reveal_type("abc".startswith("bc")) # revealed: Literal[False]
|
||||
|
||||
reveal_type("AbC".startswith("")) # revealed: Literal[True]
|
||||
reveal_type("AbC".startswith("A")) # revealed: Literal[True]
|
||||
reveal_type("AbC".startswith("Ab")) # revealed: Literal[True]
|
||||
reveal_type("AbC".startswith("AbC")) # revealed: Literal[True]
|
||||
|
||||
reveal_type("AbC".startswith("a")) # revealed: Literal[False]
|
||||
reveal_type("AbC".startswith("aB")) # revealed: Literal[False]
|
||||
|
||||
reveal_type("".startswith("")) # revealed: Literal[True]
|
||||
|
||||
reveal_type("".startswith(" ")) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
Make sure that we fall back to `bool` for more complex cases:
|
||||
|
||||
```py
|
||||
reveal_type("abc".startswith("b", 1)) # revealed: bool
|
||||
reveal_type("abc".startswith("bc", 1, 3)) # revealed: bool
|
||||
|
||||
reveal_type("abc".startswith(("a", "x"))) # revealed: bool
|
||||
```
|
||||
|
||||
And similiarly, we should still infer `bool` if the instance or the prefix are not string literals:
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
def _(string_instance: str, literalstring: LiteralString):
|
||||
reveal_type(string_instance.startswith("a")) # revealed: bool
|
||||
reveal_type(literalstring.startswith("a")) # revealed: bool
|
||||
|
||||
reveal_type("a".startswith(string_instance)) # revealed: bool
|
||||
reveal_type("a".startswith(literalstring)) # revealed: bool
|
||||
```
|
||||
|
||||
[`sys.platform` checks]: https://docs.python.org/3/library/sys.html#sys.platform
|
||||
@@ -20,9 +20,11 @@ class C:
|
||||
def _(subclass_of_c: type[C]):
|
||||
reveal_type(subclass_of_c(1)) # revealed: C
|
||||
|
||||
# TODO: Those should all be errors
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
reveal_type(subclass_of_c("a")) # revealed: C
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(subclass_of_c()) # revealed: C
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(subclass_of_c(1, 2)) # revealed: C
|
||||
```
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def _(flag: bool):
|
||||
def f() -> int:
|
||||
return 1
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
@@ -58,7 +58,7 @@ def _(flag: bool, flag2: bool):
|
||||
return 1
|
||||
# TODO we should mention all non-callable elements of the union
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
# revealed: int | Unknown
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
```
|
||||
|
||||
@@ -94,7 +94,7 @@ def _(flag: bool):
|
||||
else:
|
||||
f = f2
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[3]`"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
@@ -148,3 +148,68 @@ def _(flag: bool):
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union including a special-cased function
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = str
|
||||
else:
|
||||
f = repr
|
||||
reveal_type(str("string")) # revealed: Literal["string"]
|
||||
reveal_type(repr("string")) # revealed: Literal["'string'"]
|
||||
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
|
||||
```
|
||||
|
||||
## Cannot use an argument as both a value and a type form
|
||||
|
||||
```py
|
||||
from knot_extensions import is_fully_static
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = repr
|
||||
else:
|
||||
f = is_fully_static
|
||||
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
|
||||
reveal_type(f(int)) # revealed: str | Literal[True]
|
||||
```
|
||||
|
||||
## Size limit on unions of literals
|
||||
|
||||
Beyond a certain size, large unions of literal types collapse to their nearest super-type (`int`,
|
||||
`bytes`, `str`).
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(literals_2: Literal[0, 1], b: bool, flag: bool):
|
||||
literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3]
|
||||
literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15]
|
||||
literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63]
|
||||
literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127]
|
||||
|
||||
# Going beyond the MAX_UNION_LITERALS limit (currently 200):
|
||||
literals_256 = 16 * literals_16 + literals_16
|
||||
reveal_type(literals_256) # revealed: int
|
||||
|
||||
# Going beyond the limit when another type is already part of the union
|
||||
bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127]
|
||||
literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255]
|
||||
|
||||
# Now union the two:
|
||||
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
|
||||
```
|
||||
|
||||
## Simplifying gradually-equivalent types
|
||||
|
||||
If two types are gradually equivalent, we can keep just one of them in a union:
|
||||
|
||||
```py
|
||||
from typing import Any, Union
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
|
||||
reveal_type(x) # revealed: Any & ~int
|
||||
```
|
||||
|
||||
410
crates/red_knot_python_semantic/resources/mdtest/class/super.md
Normal file
410
crates/red_knot_python_semantic/resources/mdtest/class/super.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# Super
|
||||
|
||||
Python defines the terms *bound super object* and *unbound super object*.
|
||||
|
||||
An **unbound super object** is created when `super` is called with only one argument. (e.g.
|
||||
`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is
|
||||
rarely used in practice.
|
||||
|
||||
A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the
|
||||
implicit form `super()`, where both the pivot class and the owner are inferred. This is the most
|
||||
common usage.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Explicit Super Object
|
||||
|
||||
`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the
|
||||
specified pivot class.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def a(self): ...
|
||||
aa: int = 1
|
||||
|
||||
class B(A):
|
||||
def b(self): ...
|
||||
bb: int = 2
|
||||
|
||||
class C(B):
|
||||
def c(self): ...
|
||||
cc: int = 3
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||
|
||||
super(C, C()).a
|
||||
super(C, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
|
||||
super(C, C()).c
|
||||
|
||||
super(B, C()).a
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
|
||||
super(B, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
|
||||
super(B, C()).c
|
||||
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
|
||||
super(A, C()).a
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
|
||||
super(A, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
|
||||
super(A, C()).c
|
||||
|
||||
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
|
||||
reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
|
||||
reveal_type(super(C, C()).aa) # revealed: int
|
||||
reveal_type(super(C, C()).bb) # revealed: int
|
||||
```
|
||||
|
||||
### Implicit Super Object
|
||||
|
||||
The implicit form `super()` is same as `super(__class__, <first argument>)`. The `__class__` refers
|
||||
to the class that contains the function where `super()` is used. The first argument refers to the
|
||||
current method’s first parameter (typically `self` or `cls`).
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __init__(self, a: int): ...
|
||||
@classmethod
|
||||
def f(cls): ...
|
||||
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
# TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
super().__init__(a)
|
||||
|
||||
@classmethod
|
||||
def f(cls):
|
||||
# TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
super().f()
|
||||
|
||||
super(B, B(42)).__init__(42)
|
||||
super(B, B).f()
|
||||
```
|
||||
|
||||
### Unbound Super Object
|
||||
|
||||
Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as
|
||||
a plain `super` instance and does not support name lookup via the MRO.
|
||||
|
||||
```py
|
||||
class A:
|
||||
a: int = 42
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(super(B)) # revealed: super
|
||||
|
||||
# error: [unresolved-attribute] "Type `super` has no attribute `a`"
|
||||
super(B).a
|
||||
```
|
||||
|
||||
## Attribute Assignment
|
||||
|
||||
`super()` objects do not allow attribute assignment — even if the attribute is resolved
|
||||
successfully.
|
||||
|
||||
```py
|
||||
class A:
|
||||
a: int = 3
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(super(B, B()).a) # revealed: int
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
|
||||
super(B, B()).a = 3
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
|
||||
super(B).a = 5
|
||||
```
|
||||
|
||||
## Dynamic Types
|
||||
|
||||
If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a
|
||||
member, it should effectively behave like a dynamic type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
a: int = 1
|
||||
|
||||
def f(x):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
reveal_type(super(x, x)) # revealed: <super: Unknown, Unknown>
|
||||
reveal_type(super(A, x)) # revealed: <super: Literal[A], Unknown>
|
||||
reveal_type(super(x, A())) # revealed: <super: Unknown, A>
|
||||
|
||||
reveal_type(super(x, x).a) # revealed: Unknown
|
||||
reveal_type(super(A, x).a) # revealed: Unknown
|
||||
reveal_type(super(x, A()).a) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Implicit `super()` in Complex Structure
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[A], Unknown>
|
||||
|
||||
class B:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
|
||||
class C(A.B):
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[C], Unknown>
|
||||
|
||||
def inner(t: C):
|
||||
reveal_type(super()) # revealed: <super: Literal[B], C>
|
||||
lambda x: reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
```
|
||||
|
||||
## Built-ins and Literals
|
||||
|
||||
```py
|
||||
reveal_type(super(bool, True)) # revealed: <super: Literal[bool], bool>
|
||||
reveal_type(super(bool, bool())) # revealed: <super: Literal[bool], bool>
|
||||
reveal_type(super(int, bool())) # revealed: <super: Literal[int], bool>
|
||||
reveal_type(super(int, 3)) # revealed: <super: Literal[int], int>
|
||||
reveal_type(super(str, "")) # revealed: <super: Literal[str], str>
|
||||
```
|
||||
|
||||
## Descriptor Behavior with Super
|
||||
|
||||
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can
|
||||
differ depending on whether the second argument to `super` is a class or an instance.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def a1(self): ...
|
||||
@classmethod
|
||||
def a2(cls): ...
|
||||
|
||||
class B(A): ...
|
||||
|
||||
# A.__dict__["a1"].__get__(B(), B)
|
||||
reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown
|
||||
# A.__dict__["a2"].__get__(B(), B)
|
||||
reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown
|
||||
|
||||
# A.__dict__["a1"].__get__(None, B)
|
||||
reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown
|
||||
# A.__dict__["a2"].__get__(None, B)
|
||||
reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown
|
||||
```
|
||||
|
||||
## Union of Supers
|
||||
|
||||
When the owner is a union type, `super()` is built separately for each branch, and the resulting
|
||||
super objects are combined into a union.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
class B:
|
||||
b: int = 42
|
||||
|
||||
class C(A, B): ...
|
||||
class D(B, A): ...
|
||||
|
||||
def f(x: C | D):
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]
|
||||
|
||||
s = super(A, x)
|
||||
reveal_type(s) # revealed: <super: Literal[A], C> | <super: Literal[A], D>
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
|
||||
s.b
|
||||
|
||||
def f(flag: bool):
|
||||
x = str() if flag else str("hello")
|
||||
reveal_type(x) # revealed: Literal["", "hello"]
|
||||
reveal_type(super(str, x)) # revealed: <super: Literal[str], str>
|
||||
|
||||
def f(x: int | str):
|
||||
# error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
|
||||
super(int, x)
|
||||
```
|
||||
|
||||
Even when `super()` is constructed separately for each branch of a union, it should behave correctly
|
||||
in all cases.
|
||||
|
||||
```py
|
||||
def f(flag: bool):
|
||||
if flag:
|
||||
class A:
|
||||
x = 1
|
||||
y: int = 1
|
||||
|
||||
a: str = "hello"
|
||||
|
||||
class B(A): ...
|
||||
s = super(B, B())
|
||||
else:
|
||||
class C:
|
||||
x = 2
|
||||
y: int | str = "test"
|
||||
|
||||
class D(C): ...
|
||||
s = super(D, D())
|
||||
|
||||
reveal_type(s) # revealed: <super: Literal[B], B> | <super: Literal[D], D>
|
||||
|
||||
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(s.y) # revealed: int | str
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
|
||||
reveal_type(s.a) # revealed: str
|
||||
```
|
||||
|
||||
## Supers with Generic Classes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||
|
||||
class A[T]:
|
||||
def f(self, a: T) -> T:
|
||||
return a
|
||||
|
||||
class B[T](A[T]):
|
||||
def f(self, b: T) -> T:
|
||||
return super().f(b)
|
||||
```
|
||||
|
||||
## Invalid Usages
|
||||
|
||||
### Unresolvable `super()` Calls
|
||||
|
||||
If an appropriate class and argument cannot be found, a runtime error will occur.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
reveal_type(super()) # revealed: Unknown
|
||||
|
||||
def f():
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
super()
|
||||
|
||||
# No first argument in its scope
|
||||
class A:
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
s = super()
|
||||
|
||||
def f(self):
|
||||
def g():
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
super()
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
lambda: super()
|
||||
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
(super() for _ in range(10))
|
||||
|
||||
@staticmethod
|
||||
def h():
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
super()
|
||||
```
|
||||
|
||||
### Failing Condition Checks
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`super()` requires its first argument to be a valid class, and its second argument to be either an
|
||||
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
|
||||
runtime.
|
||||
|
||||
```py
|
||||
def f(x: int):
|
||||
# error: [invalid-super-argument] "`int` is not a valid class"
|
||||
super(x, x)
|
||||
|
||||
type IntAlias = int
|
||||
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
|
||||
super(IntAlias, 0)
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(int, str()))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(int, str))
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, A()))
|
||||
|
||||
# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, object()))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, A))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, object))
|
||||
|
||||
super(object, object()).__class__
|
||||
```
|
||||
|
||||
### Instance Member Access via `super`
|
||||
|
||||
Accessing instance members through `super()` is not allowed.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __init__(self, a: int):
|
||||
self.a = a
|
||||
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
super().__init__(a)
|
||||
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
|
||||
super().a
|
||||
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
|
||||
super(B, B(42)).a
|
||||
```
|
||||
|
||||
### Dunder Method Resolution
|
||||
|
||||
Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super
|
||||
object itself. In other words, `super` should not be treated as if it inherits attributes of the
|
||||
`owner`.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A()[0]) # revealed: int
|
||||
reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int
|
||||
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
|
||||
super(B, B())[0]
|
||||
```
|
||||
@@ -191,7 +191,7 @@ It may also be more appropriate to use `unsupported-operator` as the error code.
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
class WithContains:
|
||||
def __contains__(self, item) -> NotBoolable:
|
||||
|
||||
@@ -355,7 +355,7 @@ element) of a chained comparison.
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, item) -> NotBoolable:
|
||||
|
||||
@@ -50,13 +50,17 @@ reveal_type(x) # revealed: LiteralString
|
||||
if x != "abc":
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
||||
|
||||
reveal_type(x == "abc") # revealed: Literal[False]
|
||||
reveal_type("abc" == x) # revealed: Literal[False]
|
||||
# TODO: This should be `Literal[False]`
|
||||
reveal_type(x == "abc") # revealed: bool
|
||||
# TODO: This should be `Literal[False]`
|
||||
reveal_type("abc" == x) # revealed: bool
|
||||
reveal_type(x == "something else") # revealed: bool
|
||||
reveal_type("something else" == x) # revealed: bool
|
||||
|
||||
reveal_type(x != "abc") # revealed: Literal[True]
|
||||
reveal_type("abc" != x) # revealed: Literal[True]
|
||||
# TODO: This should be `Literal[True]`
|
||||
reveal_type(x != "abc") # revealed: bool
|
||||
# TODO: This should be `Literal[True]`
|
||||
reveal_type("abc" != x) # revealed: bool
|
||||
reveal_type(x != "something else") # revealed: bool
|
||||
reveal_type("something else" != x) # revealed: bool
|
||||
|
||||
@@ -79,10 +83,10 @@ def _(x: int):
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 1) # revealed: bool
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 1) # revealed: bool
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ def compute_chained_comparison():
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 5
|
||||
__bool__: int = 5
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, other) -> NotBoolable:
|
||||
@@ -382,13 +382,13 @@ arbitrary objects to a `bool`, but a comparison of tuples will fail if the resul
|
||||
pair of elements at equivalent positions cannot be converted to a `bool`:
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
class A:
|
||||
def __eq__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
(A(),) == (A(),)
|
||||
```
|
||||
|
||||
@@ -40,7 +40,7 @@ def _(flag: bool):
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
3 if NotBoolable() else 4
|
||||
|
||||
@@ -152,7 +152,7 @@ def _(flag: bool):
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Pattern matching
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
## With wildcard
|
||||
|
||||
```py
|
||||
@@ -44,11 +49,245 @@ def _(target: int):
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Value match
|
||||
|
||||
A value pattern matches based on equality: the first `case` branch here will be taken if `subject`
|
||||
is equal to `2`, even if `subject` is not an instance of `int`. We can't know whether `C` here has a
|
||||
custom `__eq__` implementation that might cause it to compare equal to `2`, so we have to consider
|
||||
the possibility that the `case` branch might be taken even though the type `C` is disjoint from the
|
||||
type `Literal[2]`.
|
||||
|
||||
This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` statement, rather than
|
||||
`Literal[1]`:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class C:
|
||||
pass
|
||||
|
||||
def _(subject: C):
|
||||
y = 1
|
||||
match subject:
|
||||
case 2:
|
||||
y = 3
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Class match
|
||||
|
||||
A `case` branch with a class pattern is taken if the subject is an instance of the given class, and
|
||||
all subpatterns in the class pattern match.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
class FooSub(Foo):
|
||||
pass
|
||||
|
||||
class Bar:
|
||||
pass
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Bar():
|
||||
y = 3
|
||||
case Foo():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3, 4]
|
||||
|
||||
def _(target: FooSub | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3, 4]
|
||||
```
|
||||
|
||||
## Singleton match
|
||||
|
||||
Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(target: Literal[True, False]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: bool):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[4]
|
||||
|
||||
def _(target: None | Literal[True]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 4]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
|
||||
# bool is an int subclass
|
||||
def _(target: int):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Or match
|
||||
|
||||
A `|` pattern matches if any of the subpatterns match.
|
||||
|
||||
```py
|
||||
from typing import Literal, final
|
||||
|
||||
def _(target: Literal["foo", "baz"]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case "foo" | "bar":
|
||||
y = 2
|
||||
case "baz":
|
||||
y = 3
|
||||
|
||||
# TODO: with exhaustiveness, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case "foo" | 4 | True:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2]
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: int | None | float):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case Baz():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
|
||||
def _(target: None | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz() | True | False:
|
||||
y = 2
|
||||
case int():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Guard with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
def _(target: int, flag: NotBoolable):
|
||||
y = 1
|
||||
|
||||
731
crates/red_knot_python_semantic/resources/mdtest/dataclasses.md
Normal file
731
crates/red_knot_python_semantic/resources/mdtest/dataclasses.md
Normal file
@@ -0,0 +1,731 @@
|
||||
# Dataclasses
|
||||
|
||||
## Basic
|
||||
|
||||
Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`,
|
||||
`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass`
|
||||
decorator. By default, only the three mentioned methods are generated.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
alice1 = Person("Alice", 30)
|
||||
alice2 = Person(name="Alice", age=30)
|
||||
alice3 = Person(age=30, name="Alice")
|
||||
alice4 = Person("Alice", age=30)
|
||||
|
||||
reveal_type(alice1) # revealed: Person
|
||||
reveal_type(type(alice1)) # revealed: type[Person]
|
||||
|
||||
reveal_type(alice1.name) # revealed: str
|
||||
reveal_type(alice1.age) # revealed: int | None
|
||||
|
||||
reveal_type(repr(alice1)) # revealed: str
|
||||
|
||||
reveal_type(alice1 == alice2) # revealed: bool
|
||||
reveal_type(alice1 == "Alice") # revealed: bool
|
||||
|
||||
bob = Person("Bob")
|
||||
bob2 = Person("Bob", None)
|
||||
bob3 = Person(name="Bob")
|
||||
bob4 = Person(name="Bob", age=None)
|
||||
```
|
||||
|
||||
The signature of the `__init__` method is generated based on the classes attributes. The following
|
||||
calls are not valid:
|
||||
|
||||
```py
|
||||
# error: [missing-argument]
|
||||
Person()
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
Person("Eve", 20, "too many arguments")
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
Person("Eve", "string instead of int")
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
Person(20, "Eve")
|
||||
```
|
||||
|
||||
## Signature of `__init__`
|
||||
|
||||
TODO: All of the following tests are missing the `self` argument in the `__init__` signature.
|
||||
|
||||
Declarations in the class body are used to generate the signature of the `__init__` method. If the
|
||||
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
|
||||
the default value.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
y: str = "default"
|
||||
z: int | None = 1 + 2
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
|
||||
```
|
||||
|
||||
This also works if the declaration and binding are split:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int | None
|
||||
x = None
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int | None = None) -> None
|
||||
```
|
||||
|
||||
Non-fully static types are handled correctly:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
x: Any
|
||||
y: int | Any
|
||||
z: tuple[int, Any]
|
||||
|
||||
reveal_type(C.__init__) # revealed: (x: Any, y: int | Any, z: tuple[int, Any]) -> None
|
||||
```
|
||||
|
||||
Variables without annotations are ignored:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
y = 1
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
If attributes without default values are declared after attributes with default values, a
|
||||
`TypeError` will be raised at runtime. Ideally, we would emit a diagnostic in that case:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int = 1
|
||||
# TODO: this should be an error: field without default defined after field with default
|
||||
y: str
|
||||
```
|
||||
|
||||
Pure class attributes (`ClassVar`) are not included in the signature of `__init__`:
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
y: ClassVar[str] = "default"
|
||||
z: bool
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int, z: bool) -> None
|
||||
|
||||
d = D(1, True)
|
||||
reveal_type(d.x) # revealed: int
|
||||
reveal_type(d.y) # revealed: str
|
||||
reveal_type(d.z) # revealed: bool
|
||||
```
|
||||
|
||||
Function declarations do not affect the signature of `__init__`:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
|
||||
def y(self) -> str:
|
||||
return ""
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
And neither do nested class declarations:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
|
||||
class Nested:
|
||||
y: str
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
But if there is a variable annotation with a function or class literal type, the signature of
|
||||
`__init__` will include this field:
|
||||
|
||||
```py
|
||||
from knot_extensions import TypeOf
|
||||
|
||||
class SomeClass: ...
|
||||
|
||||
def some_function() -> None: ...
|
||||
@dataclass
|
||||
class D:
|
||||
function_literal: TypeOf[some_function]
|
||||
class_literal: TypeOf[SomeClass]
|
||||
class_subtype_of: type[SomeClass]
|
||||
|
||||
# revealed: (function_literal: def some_function() -> None, class_literal: Literal[SomeClass], class_subtype_of: type[SomeClass]) -> None
|
||||
reveal_type(D.__init__)
|
||||
```
|
||||
|
||||
More realistically, dataclasses can have `Callable` attributes:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
c: Callable[[int], str]
|
||||
|
||||
reveal_type(D.__init__) # revealed: (c: (int, /) -> str) -> None
|
||||
```
|
||||
|
||||
Implicit instance attributes do not affect the signature of `__init__`:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
|
||||
def f(self, y: str) -> None:
|
||||
self.y: str = y
|
||||
|
||||
reveal_type(D(1).y) # revealed: str
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't
|
||||
be included in the signature of `__init__`. This is a case that we currently don't detect:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
# (x) is an expression, not a "simple name"
|
||||
(x): int = 1
|
||||
|
||||
# TODO: should ideally not include a `x` parameter
|
||||
reveal_type(D.__init__) # revealed: (x: int = Literal[1]) -> None
|
||||
```
|
||||
|
||||
## `@dataclass` calls with arguments
|
||||
|
||||
The `@dataclass` decorator can take several arguments to customize the existence of the generated
|
||||
methods. The following test makes sure that we still treat the class as a dataclass if (the default)
|
||||
arguments are passed in:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=True, repr=True, eq=True)
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
alice = Person("Alice", 30)
|
||||
reveal_type(repr(alice)) # revealed: str
|
||||
reveal_type(alice == alice) # revealed: bool
|
||||
```
|
||||
|
||||
If `init` is set to `False`, no `__init__` method is generated:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=False)
|
||||
class C:
|
||||
x: int
|
||||
|
||||
C() # Okay
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
C(1)
|
||||
|
||||
repr(C())
|
||||
|
||||
C() == C()
|
||||
```
|
||||
|
||||
## Other dataclass parameters
|
||||
|
||||
### `repr`
|
||||
|
||||
A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but
|
||||
in that case `__repr__` is still available via `object.__repr__`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(repr=False)
|
||||
class WithoutRepr:
|
||||
x: int
|
||||
|
||||
reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str
|
||||
```
|
||||
|
||||
### `eq`
|
||||
|
||||
The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but
|
||||
`__eq__` is still available via `object.__eq__`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(eq=False)
|
||||
class WithoutEq:
|
||||
x: int
|
||||
|
||||
reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
|
||||
```
|
||||
|
||||
### `order`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
|
||||
methods will be generated:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class WithoutOrder:
|
||||
x: int
|
||||
|
||||
WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator]
|
||||
WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator]
|
||||
WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator]
|
||||
WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator]
|
||||
|
||||
@dataclass(order=True)
|
||||
class WithOrder:
|
||||
x: int
|
||||
|
||||
WithOrder(1) < WithOrder(2)
|
||||
WithOrder(1) <= WithOrder(2)
|
||||
WithOrder(1) > WithOrder(2)
|
||||
WithOrder(1) >= WithOrder(2)
|
||||
```
|
||||
|
||||
Comparisons are only allowed for `WithOrder` instances:
|
||||
|
||||
```py
|
||||
WithOrder(1) < 2 # error: [unsupported-operator]
|
||||
WithOrder(1) <= 2 # error: [unsupported-operator]
|
||||
WithOrder(1) > 2 # error: [unsupported-operator]
|
||||
WithOrder(1) >= 2 # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
This also works for generic dataclasses:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(order=True)
|
||||
class GenericWithOrder[T]:
|
||||
x: T
|
||||
|
||||
GenericWithOrder[int](1) < GenericWithOrder[int](1)
|
||||
|
||||
GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
If a class already defines one of the comparison methods, a `TypeError` is raised at runtime.
|
||||
Ideally, we would emit a diagnostic in that case:
|
||||
|
||||
```py
|
||||
@dataclass(order=True)
|
||||
class AlreadyHasCustomDunderLt:
|
||||
x: int
|
||||
|
||||
# TODO: Ideally, we would emit a diagnostic here
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return False
|
||||
```
|
||||
|
||||
### `unsafe_hash`
|
||||
|
||||
To do
|
||||
|
||||
### `frozen`
|
||||
|
||||
To do
|
||||
|
||||
### `match_args`
|
||||
|
||||
To do
|
||||
|
||||
### `kw_only`
|
||||
|
||||
To do
|
||||
|
||||
### `slots`
|
||||
|
||||
To do
|
||||
|
||||
### `weakref_slot`
|
||||
|
||||
To do
|
||||
|
||||
## Inheritance
|
||||
|
||||
### Normal class inheriting from a dataclass
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: int
|
||||
|
||||
class Derived(Base): ...
|
||||
|
||||
d = Derived(1) # OK
|
||||
reveal_type(d.x) # revealed: int
|
||||
```
|
||||
|
||||
### Dataclass inheriting from normal class
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
class Base:
|
||||
x: int = 1
|
||||
|
||||
@dataclass
|
||||
class Derived(Base):
|
||||
y: str
|
||||
|
||||
d = Derived("a")
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
Derived(1, "a")
|
||||
```
|
||||
|
||||
### Dataclass inheriting from another dataclass
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: int
|
||||
y: str
|
||||
|
||||
@dataclass
|
||||
class Derived(Base):
|
||||
z: bool
|
||||
|
||||
d = Derived(1, "a", True) # OK
|
||||
|
||||
reveal_type(d.x) # revealed: int
|
||||
reveal_type(d.y) # revealed: str
|
||||
reveal_type(d.z) # revealed: bool
|
||||
|
||||
# error: [missing-argument]
|
||||
Derived(1, "a")
|
||||
|
||||
# error: [missing-argument]
|
||||
Derived(True)
|
||||
```
|
||||
|
||||
### Overwriting attributes from base class
|
||||
|
||||
The following example comes from the
|
||||
[Python documentation](https://docs.python.org/3/library/dataclasses.html#inheritance). The `x`
|
||||
attribute appears just once in the `__init__` signature, and the default value is taken from the
|
||||
derived class
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: Any = 15.0
|
||||
y: int = 0
|
||||
|
||||
@dataclass
|
||||
class C(Base):
|
||||
z: int = 10
|
||||
x: int = 15
|
||||
|
||||
reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
|
||||
```
|
||||
|
||||
## Generic dataclasses
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class DataWithDescription[T]:
|
||||
data: T
|
||||
description: str
|
||||
|
||||
reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]]
|
||||
|
||||
d_int = DataWithDescription[int](1, "description") # OK
|
||||
reveal_type(d_int.data) # revealed: int
|
||||
reveal_type(d_int.description) # revealed: str
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
DataWithDescription[int](None, "description")
|
||||
```
|
||||
|
||||
## Descriptor-typed fields
|
||||
|
||||
### Same type in `__get__` and `__set__`
|
||||
|
||||
For the following descriptor, the return type of `__get__` and the type of the `value` parameter in
|
||||
`__set__` are the same. The generated `__init__` method takes an argument of this type (instead of
|
||||
the type of the descriptor), and the default value is also of this type:
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
from dataclasses import dataclass
|
||||
|
||||
class UppercaseString:
|
||||
_value: str = ""
|
||||
|
||||
def __get__(self, instance: object, owner: None | type) -> str:
|
||||
return self._value
|
||||
|
||||
def __set__(self, instance: object, value: str) -> None:
|
||||
self._value = value.upper()
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
upper: UppercaseString = UppercaseString()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (upper: str = str) -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.upper) # revealed: str
|
||||
|
||||
# This is also okay:
|
||||
C()
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
C(1)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
C("a", "b")
|
||||
```
|
||||
|
||||
### Different types in `__get__` and `__set__`
|
||||
|
||||
In general, the type of the `__init__` parameter is determined by the `value` parameter type of the
|
||||
`__set__` method (`str` in the example below). However, the default value is generated by calling
|
||||
the descriptor's `__get__` method as if it had been called on the class itself, i.e. passing `None`
|
||||
for the `instance` argument.
|
||||
|
||||
```py
|
||||
from typing import Literal, overload
|
||||
from dataclasses import dataclass
|
||||
|
||||
class ConvertToLength:
|
||||
_len: int = 0
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type) -> Literal[""]: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None) -> int: ...
|
||||
def __get__(self, instance: object | None, owner: type | None) -> str | int:
|
||||
if instance is None:
|
||||
return ""
|
||||
|
||||
return self._len
|
||||
|
||||
def __set__(self, instance, value: str) -> None:
|
||||
self._len = len(value)
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
converter: ConvertToLength = ConvertToLength()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.converter) # revealed: int
|
||||
|
||||
# This is also okay:
|
||||
C()
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
C(1)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
C("a", "b")
|
||||
```
|
||||
|
||||
### With overloaded `__set__` method
|
||||
|
||||
If the `__set__` method is overloaded, we determine the type for the `__init__` parameter as the
|
||||
union of all possible `value` parameter types:
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
from dataclasses import dataclass
|
||||
|
||||
class AcceptsStrAndInt:
|
||||
def __get__(self, instance, owner) -> int:
|
||||
return 0
|
||||
|
||||
@overload
|
||||
def __set__(self, instance: object, value: str) -> None: ...
|
||||
@overload
|
||||
def __set__(self, instance: object, value: int) -> None: ...
|
||||
def __set__(self, instance: object, value) -> None:
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
field: AcceptsStrAndInt = AcceptsStrAndInt()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (field: str | int = int) -> None
|
||||
```
|
||||
|
||||
## `dataclasses.field`
|
||||
|
||||
To do
|
||||
|
||||
## Other special cases
|
||||
|
||||
### `dataclasses.dataclass`
|
||||
|
||||
We also understand dataclasses if they are decorated with the fully qualified name:
|
||||
|
||||
```py
|
||||
import dataclasses
|
||||
|
||||
@dataclasses.dataclass
|
||||
class C:
|
||||
x: str
|
||||
|
||||
reveal_type(C.__init__) # revealed: (x: str) -> None
|
||||
```
|
||||
|
||||
### Dataclass with custom `__init__` method
|
||||
|
||||
If a class already defines `__init__`, it is not replaced by the `dataclass` decorator.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=True)
|
||||
class C:
|
||||
x: str
|
||||
|
||||
def __init__(self, x: int) -> None:
|
||||
self.x = str(x)
|
||||
|
||||
C(1) # OK
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
C("a")
|
||||
```
|
||||
|
||||
Similarly, if we set `init=False`, we still recognize the custom `__init__` method:
|
||||
|
||||
```py
|
||||
@dataclass(init=False)
|
||||
class D:
|
||||
def __init__(self, x: int) -> None:
|
||||
self.x = str(x)
|
||||
|
||||
D(1) # OK
|
||||
D() # error: [missing-argument]
|
||||
```
|
||||
|
||||
### Accessing instance attributes on the class itself
|
||||
|
||||
Just like for normal classes, accessing instance attributes on the class itself is not allowed:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
x: int
|
||||
|
||||
# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
C.x
|
||||
```
|
||||
|
||||
### Return type of `dataclass(...)`
|
||||
|
||||
A call like `dataclass(order=True)` returns a callable itself, which is then used as the decorator.
|
||||
We can store the callable in a variable and later use it as a decorator:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
dataclass_with_order = dataclass(order=True)
|
||||
|
||||
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclasses.dataclass>
|
||||
|
||||
@dataclass_with_order
|
||||
class C:
|
||||
x: int
|
||||
|
||||
C(1) < C(2) # ok
|
||||
```
|
||||
|
||||
### Using `dataclass` as a function
|
||||
|
||||
To do
|
||||
|
||||
## Internals
|
||||
|
||||
The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`,
|
||||
and attributes like the MRO are unchanged:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
reveal_type(type(Person)) # revealed: Literal[type]
|
||||
reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
|
||||
```
|
||||
|
||||
The generated methods have the following signatures:
|
||||
|
||||
```py
|
||||
# TODO: `self` is missing here
|
||||
reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None
|
||||
|
||||
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
|
||||
|
||||
reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool
|
||||
```
|
||||
237
crates/red_knot_python_semantic/resources/mdtest/decorators.md
Normal file
237
crates/red_knot_python_semantic/resources/mdtest/decorators.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Decorators
|
||||
|
||||
Decorators are a way to modify function and class behavior. A decorator is a callable that takes the
|
||||
function or class as an argument and returns a modified version of it.
|
||||
|
||||
## Basic example
|
||||
|
||||
A decorated function definition is conceptually similar to `def f(x): ...` followed by
|
||||
`f = decorator(f)`. This means that the type of a decorated function is the same as the return type
|
||||
of the decorator (which does not necessarily need to be a callable type):
|
||||
|
||||
```py
|
||||
def custom_decorator(f) -> int:
|
||||
return 1
|
||||
|
||||
@custom_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: int
|
||||
```
|
||||
|
||||
## Type-annotated decorator
|
||||
|
||||
More commonly, a decorator returns a modified callable type:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_positive(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x > 0
|
||||
|
||||
@ensure_positive
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(4)) # revealed: bool
|
||||
```
|
||||
|
||||
## Decorators which take arguments
|
||||
|
||||
Decorators can be arbitrary expressions. This is often useful when the decorator itself takes
|
||||
arguments:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_larger_than(lower_bound: int) -> Callable[[Callable[[int], bool]], Callable[[int], bool]]:
|
||||
def decorator(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x >= lower_bound
|
||||
return decorator
|
||||
|
||||
@ensure_larger_than(10)
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(14)) # revealed: bool
|
||||
```
|
||||
|
||||
## Multiple decorators
|
||||
|
||||
Multiple decorators can be applied to a single function. They are applied in "bottom-up" order,
|
||||
meaning that the decorator closest to the function definition is applied first:
|
||||
|
||||
```py
|
||||
def maps_to_str(f) -> str:
|
||||
return "a"
|
||||
|
||||
def maps_to_int(f) -> int:
|
||||
return 1
|
||||
|
||||
def maps_to_bytes(f) -> bytes:
|
||||
return b"a"
|
||||
|
||||
@maps_to_str
|
||||
@maps_to_int
|
||||
@maps_to_bytes
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Decorating with a class
|
||||
|
||||
When a function is decorated with a class-based decorator, the decorated function turns into an
|
||||
instance of the class (see also: [properties](properties.md)). Attributes of the class can be
|
||||
accessed on the decorated function.
|
||||
|
||||
```py
|
||||
class accept_strings:
|
||||
custom_attribute: str = "a"
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __call__(self, x: str | int) -> bool:
|
||||
return self.f(int(x))
|
||||
|
||||
@accept_strings
|
||||
def even(x: int) -> bool:
|
||||
return x > 0
|
||||
|
||||
reveal_type(even) # revealed: accept_strings
|
||||
reveal_type(even.custom_attribute) # revealed: str
|
||||
reveal_type(even("1")) # revealed: bool
|
||||
reveal_type(even(1)) # revealed: bool
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
even(None)
|
||||
```
|
||||
|
||||
## Common decorator patterns
|
||||
|
||||
### `functools.wraps`
|
||||
|
||||
This test mainly makes sure that we do not emit any diagnostics in a case where the decorator is
|
||||
implemented using `functools.wraps`.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from functools import wraps
|
||||
|
||||
def custom_decorator(f) -> Callable[[int], str]:
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
print("Calling decorated function")
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@custom_decorator
|
||||
def f(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
reveal_type(f) # revealed: (int, /) -> str
|
||||
```
|
||||
|
||||
### `functools.cache`
|
||||
|
||||
```py
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def f(x: int) -> int:
|
||||
return x**2
|
||||
|
||||
# TODO: Should be `_lru_cache_wrapper[int]`
|
||||
reveal_type(f) # revealed: @Todo(specialized non-generic class)
|
||||
|
||||
# TODO: Should be `int`
|
||||
reveal_type(f(1)) # revealed: @Todo(specialized non-generic class)
|
||||
```
|
||||
|
||||
## Lambdas as decorators
|
||||
|
||||
```py
|
||||
@lambda f: f
|
||||
def g(x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: This should be `Literal[g]` or `(int, /) -> str`
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
### Unknown decorator
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference] "Name `unknown_decorator` used when not defined"
|
||||
@unknown_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Error in the decorator expression
|
||||
|
||||
```py
|
||||
# error: [unsupported-operator]
|
||||
@(1 + "a")
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Non-callable decorator
|
||||
|
||||
```py
|
||||
non_callable = 1
|
||||
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
@non_callable
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
#### Wrong argument type
|
||||
|
||||
Here, we emit a diagnostic since `wrong_signature` takes an `int` instead of a callable type as the
|
||||
first argument:
|
||||
|
||||
```py
|
||||
def wrong_signature(f: int) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `def f(x) -> Unknown`"
|
||||
@wrong_signature
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
#### Wrong number of arguments
|
||||
|
||||
Decorators need to be callable with a single argument. If they are not, we emit a diagnostic:
|
||||
|
||||
```py
|
||||
def takes_two_arguments(f, g) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `g` of function `takes_two_arguments`"
|
||||
@takes_two_arguments
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
|
||||
def takes_no_argument() -> str:
|
||||
return "a"
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `takes_no_argument`: expected 0, got 1"
|
||||
@takes_no_argument
|
||||
def g(x): ...
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user