Compare commits
139 Commits
0.11.3
...
david/test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48a026bd2b | ||
|
|
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 |
78
.github/workflows/build-binaries.yml
vendored
78
.github/workflows/build-binaries.yml
vendored
@@ -39,17 +39,17 @@ jobs:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.target }}
|
||||
path: |
|
||||
@@ -294,17 +294,17 @@ jobs:
|
||||
arch: arm
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
@@ -359,18 +359,18 @@ jobs:
|
||||
- x86_64-unknown-linux-musl
|
||||
- i686-unknown-linux-musl
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.target }}
|
||||
path: |
|
||||
@@ -425,17 +425,17 @@ jobs:
|
||||
arch: armv7
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: artifacts-${{ matrix.platform.target }}
|
||||
path: |
|
||||
|
||||
8
.github/workflows/build-docker.yml
vendored
8
.github/workflows/build-docker.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_TUPLE }}
|
||||
path: /tmp/digests/*
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
|
||||
122
.github/workflows/ci.yaml
vendored
122
.github/workflows/ci.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
# Flag that is set to "true" when code related to the playground changes.
|
||||
playground: ${{ steps.check_playground.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**/*' \
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
|
||||
':!**/*.md' \
|
||||
':crates/red_knot_python_semantic/resources/mdtest/**/*.md' \
|
||||
':!docs/**' \
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -210,10 +210,10 @@ jobs:
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
@@ -230,20 +230,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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Red-knot mdtests (GitHub annotations)
|
||||
@@ -272,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
@@ -284,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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -313,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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # 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@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -340,13 +340,13 @@ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
@@ -369,10 +369,10 @@ jobs:
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -387,7 +387,7 @@ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
with:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
@@ -403,11 +403,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -424,10 +424,10 @@ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -452,11 +452,11 @@ jobs:
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
with:
|
||||
@@ -486,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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # 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
|
||||
@@ -518,14 +518,14 @@ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
name: Download comparison Ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
@@ -620,13 +620,13 @@ jobs:
|
||||
run: |
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
name: Upload PR Number
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
name: Upload Results
|
||||
with:
|
||||
name: ecosystem-result
|
||||
@@ -638,7 +638,7 @@ jobs:
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
@@ -651,18 +651,18 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # 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@22fe573c6ed0c03ab9b84e631cbfa49bddf6e20e # v1.47.3
|
||||
uses: PyO3/maturin-action@44479ae1b6b1a57f561e03add8832e62c185eb17 # v1.48.1
|
||||
with:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
@@ -678,12 +678,12 @@ jobs:
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
||||
- name: "Cache pre-commit"
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
@@ -705,13 +705,13 @@ jobs:
|
||||
env:
|
||||
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # 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@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
@@ -720,7 +720,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -747,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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Run checks"
|
||||
@@ -773,18 +773,18 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
# installation fails on 3.13 and newer
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
name: Download development ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
@@ -815,13 +815,13 @@ jobs:
|
||||
- determine_changes
|
||||
if: ${{ (needs.determine_changes.outputs.playground == 'true') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- 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
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
@@ -847,17 +847,17 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@6aca1cfa12ef3a6b98ee8c70e0171bfa067604f5 # v2
|
||||
uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc # v2
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
|
||||
6
.github/workflows/daily_fuzz.yaml
vendored
6
.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- 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
|
||||
|
||||
4
.github/workflows/daily_property_tests.yaml
vendored
4
.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@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- 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.
|
||||
|
||||
14
.github/workflows/mypy_primer.yaml
vendored
14
.github/workflows/mypy_primer.yaml
vendored
@@ -28,16 +28,16 @@ jobs:
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
||||
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
- name: Install Rust toolchain
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
- name: Install mypy_primer
|
||||
run: |
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v2"
|
||||
uv tool install "git+https://github.com/astral-sh/mypy_primer.git@add-red-knot-support-v4"
|
||||
|
||||
- name: Run mypy_primer
|
||||
shell: bash
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
--type-checker knot \
|
||||
--old base_commit \
|
||||
--new "$GITHUB_SHA" \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy)$' \
|
||||
--project-selector '/(mypy_primer|black|pyp|git-revise|zipp|arrow|isort|itsdangerous|rich|packaging|pybind11|pyinstrument|typeshed-stats|scrapy|werkzeug|bidict|async-utils)$' \
|
||||
--output concise \
|
||||
--debug > mypy_primer.diff || [ $? -eq 1 ]
|
||||
|
||||
@@ -81,13 +81,13 @@ jobs:
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
6
.github/workflows/publish-docs.yml
vendored
6
.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
|
||||
@@ -30,12 +30,12 @@ jobs:
|
||||
env:
|
||||
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
|
||||
4
.github/workflows/publish-playground.yml
vendored
4
.github/workflows/publish-playground.yml
vendored
@@ -24,12 +24,12 @@ jobs:
|
||||
env:
|
||||
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
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@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
|
||||
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
pattern: wheels-*
|
||||
path: wheels
|
||||
|
||||
4
.github/workflows/publish-wasm.yml
vendored
4
.github/workflows/publish-wasm.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
target: [web, bundler, nodejs]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -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@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
35
.github/workflows/release.yml
vendored
35
.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:
|
||||
@@ -59,16 +60,17 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
@@ -121,18 +123,19 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # 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@95815c38cf2ff2164869cbab79da8d1f422bc89e # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
@@ -171,18 +174,19 @@ jobs:
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # 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@95815c38cf2ff2164869cbab79da8d1f422bc89e # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
@@ -246,12 +250,13 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 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@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
|
||||
4
.github/workflows/sync_typeshed.yaml
vendored
4
.github/workflows/sync_typeshed.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
name: Checkout Ruff
|
||||
with:
|
||||
path: ruff
|
||||
persist-credentials: true
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
name: Checkout typeshed
|
||||
with:
|
||||
repository: python/typeshed
|
||||
|
||||
@@ -18,6 +18,11 @@ 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.24.1
|
||||
hooks:
|
||||
@@ -60,7 +65,7 @@ repos:
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.31.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.11.2
|
||||
rev: v0.11.4
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,5 +1,44 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
75
Cargo.lock
generated
75
Cargo.lock
generated
@@ -207,9 +207,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "boxcar"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225450ee9328e1e828319b48a89726cffc1b0ad26fd9211ad435de9fa376acae"
|
||||
checksum = "6740c6e2fc6360fa57c35214c7493826aee95993926092606f27c983b40837be"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
@@ -334,9 +334,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.34"
|
||||
version = "4.5.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
|
||||
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -344,9 +344,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.34"
|
||||
version = "4.5.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
|
||||
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -695,9 +695,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.5"
|
||||
version = "3.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
|
||||
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -894,9 +894,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.7"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -918,7 +918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1390,9 +1390,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.2",
|
||||
@@ -1499,7 +1499,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1659,9 +1659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.40"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502"
|
||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1797,9 +1797,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.44"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1"
|
||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -1973,9 +1973,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordermap"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e98f974336ceffd5b1b1f4fcbb89a23c8dcd334adc4b8612f11b7fa99944535"
|
||||
checksum = "7d31b8b7a99f71bdff4235faf9ce9eada0ad3562c8fbeb7d607d9f41a6ec569d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
]
|
||||
@@ -2514,6 +2514,7 @@ dependencies = [
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
@@ -2537,6 +2538,7 @@ dependencies = [
|
||||
"ruff_db",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
@@ -2607,7 +2609,6 @@ dependencies = [
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
@@ -2670,6 +2671,7 @@ dependencies = [
|
||||
"red_knot_python_semantic",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_formatter",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -2754,7 +2756,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.3"
|
||||
version = "0.11.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2989,7 +2991,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.11.3"
|
||||
version = "0.11.5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3133,6 +3135,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_formatter",
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
@@ -3141,6 +3144,7 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3313,7 +3317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.11.3"
|
||||
version = "0.11.5"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3407,7 +3411,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3420,7 +3424,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.3",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3438,7 +3442,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str 0.8.1",
|
||||
@@ -3454,18 +3458,19 @@ dependencies = [
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
"smallvec",
|
||||
"thin-vec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d758691ba17ee1a60c5356ea90888d529e1782ad#d758691ba17ee1a60c5356ea90888d529e1782ad"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=87bf6b6c2d5f6479741271da73bd9d30c2580c26#87bf6b6c2d5f6479741271da73bd9d30c2580c26"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3680,9 +3685,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "snapbox"
|
||||
@@ -3805,7 +3810,7 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 1.0.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3878,6 +3883,12 @@ dependencies = [
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
||||
16
Cargo.toml
16
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"
|
||||
@@ -124,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 = "d758691ba17ee1a60c5356ea90888d529e1782ad" }
|
||||
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"] }
|
||||
@@ -209,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
|
||||
@@ -271,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
|
||||
@@ -328,9 +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"]
|
||||
# Temporarily allow changes to the `release` workflow, in which we pin actions
|
||||
# to a SHA instead of a tag (https://github.com/astral-sh/uv/issues/12253)
|
||||
allow-dirty = ["ci"]
|
||||
|
||||
[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.11.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.3/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.11.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.11.5/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.11.3
|
||||
rev: v0.11.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -71,6 +71,15 @@ 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,
|
||||
|
||||
@@ -116,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| {
|
||||
@@ -124,7 +136,6 @@ impl CheckCommand {
|
||||
.map(RelativePathBuf::cli)
|
||||
.collect()
|
||||
}),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
terminal: Some(TerminalOptions {
|
||||
output_format: self
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
| ^^^^^^^^^^^^^^ 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)
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `Literal[1]`
|
||||
| ^^^^^^^^^^^^^^ `Literal[1]`
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -991,6 +1052,39 @@ fn concise_diagnostics() -> anyhow::Result<()> {
|
||||
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,
|
||||
|
||||
@@ -17,6 +17,7 @@ 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 }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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};
|
||||
@@ -16,40 +17,18 @@ pub fn goto_type_definition(
|
||||
let goto_target = find_goto_target(parsed, offset)?;
|
||||
|
||||
let model = SemanticModel::new(db.upcast(), file);
|
||||
|
||||
let ty = match goto_target {
|
||||
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,
|
||||
};
|
||||
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: ty.navigation_targets(db),
|
||||
value: navigation_targets,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,6 +128,37 @@ pub(crate) enum GotoTarget<'a> {
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -174,16 +184,18 @@ impl Ranged for GotoTarget<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Option<GotoTarget> {
|
||||
let token = parsed.tokens().at_offset(offset).find(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
let token = parsed
|
||||
.tokens()
|
||||
.at_offset(offset)
|
||||
.max_by_key(|token| match token.kind() {
|
||||
TokenKind::Name
|
||||
| TokenKind::String
|
||||
| TokenKind::Complex
|
||||
| TokenKind::Float
|
||||
| TokenKind::Int
|
||||
)
|
||||
})?;
|
||||
| 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()?;
|
||||
@@ -241,27 +253,18 @@ pub(crate) fn find_goto_target(parsed: &ParsedModule, offset: TextSize) -> Optio
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::tests::{cursor_test, CursorTest, IntoDiagnostic};
|
||||
use crate::{goto_type_definition, NavigationTarget};
|
||||
use insta::assert_snapshot;
|
||||
use insta::internals::SettingsBindDropGuard;
|
||||
use red_knot_python_semantic::{
|
||||
Program, ProgramSettings, PythonPath, PythonPlatform, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName,
|
||||
Severity, Span, SubDiagnostic,
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::{system_path_to_file, File, FileRange};
|
||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_class_type() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Test: ...
|
||||
|
||||
@@ -291,7 +294,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_function_type() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
@@ -323,7 +326,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_union_type() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
@@ -380,7 +383,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_module() {
|
||||
let mut test = goto_test(
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
@@ -390,12 +393,12 @@ mod tests {
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
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
|
||||
@@ -405,12 +408,12 @@ mod tests {
|
||||
4 | lib
|
||||
| ^^^
|
||||
|
|
||||
"###);
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_type() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "test"
|
||||
|
||||
@@ -441,7 +444,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_node() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "te<CURSOR>st"
|
||||
"#,
|
||||
@@ -469,7 +472,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_type() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
@@ -493,7 +496,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_param_spec() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
@@ -507,7 +510,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_tuple() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
@@ -521,7 +524,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
@@ -553,7 +556,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
@@ -588,7 +591,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_kwargs() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def f(name: str): ...
|
||||
|
||||
@@ -622,14 +625,13 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_builtin() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
// FIXME: This should go to `str`
|
||||
assert_snapshot!(test.goto_type_definition(), @r###"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/builtins.pyi:443:7
|
||||
@@ -653,7 +655,7 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
#[test]
|
||||
fn goto_type_definition_cursor_between_object_and_attribute() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class X:
|
||||
def foo(a, b): ...
|
||||
@@ -685,7 +687,7 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
#[test]
|
||||
fn goto_between_call_arguments() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
@@ -715,7 +717,7 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
#[test]
|
||||
fn goto_type_narrowing() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
@@ -747,7 +749,7 @@ f(**kwargs<CURSOR>)
|
||||
|
||||
#[test]
|
||||
fn goto_type_none() {
|
||||
let test = goto_test(
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
a<CURSOR>
|
||||
@@ -755,6 +757,23 @@ f(**kwargs<CURSOR>)
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/types.pyi:677:11
|
||||
|
|
||||
675 | if sys.version_info >= (3, 10):
|
||||
676 | @final
|
||||
677 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
678 | 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:443:7
|
||||
|
|
||||
@@ -769,88 +788,13 @@ f(**kwargs<CURSOR>)
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info: lint:goto-type-definition: Type definition
|
||||
--> stdlib/types.pyi:677:11
|
||||
|
|
||||
675 | if sys.version_info >= (3, 10):
|
||||
676 | @final
|
||||
677 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
678 | def __bool__(self) -> Literal[False]: ...
|
||||
|
|
||||
info: Source
|
||||
--> /main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
fn goto_test(source: &str) -> GotoTest {
|
||||
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");
|
||||
|
||||
let insta_settings_guard = insta_settings.bind_to_scope();
|
||||
|
||||
GotoTest {
|
||||
db,
|
||||
cursor_offset: TextSize::try_from(cursor_offset)
|
||||
.expect("source to be smaller than 4GB"),
|
||||
file,
|
||||
_insta_settings_guard: insta_settings_guard,
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoTest {
|
||||
db: TestDb,
|
||||
cursor_offset: TextSize,
|
||||
file: File,
|
||||
_insta_settings_guard: SettingsBindDropGuard,
|
||||
}
|
||||
|
||||
impl GotoTest {
|
||||
fn write_file(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: &str,
|
||||
) -> std::io::Result<()> {
|
||||
self.db.write_file(path, content)
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_type_definition(&self) -> String {
|
||||
let Some(targets) = goto_type_definition(&self.db, self.file, self.cursor_offset)
|
||||
else {
|
||||
@@ -861,19 +805,12 @@ f(**kwargs<CURSOR>)
|
||||
return "No type definitions found".to_string();
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
let source = targets.range;
|
||||
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.color(false)
|
||||
.format(DiagnosticFormat::Full);
|
||||
for target in &*targets {
|
||||
let diag = GotoTypeDefinitionDiagnostic::new(source, target).into_diagnostic();
|
||||
write!(buf, "{}", diag.display(&self.db, &config)).unwrap();
|
||||
}
|
||||
|
||||
buf
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,7 +826,9 @@ f(**kwargs<CURSOR>)
|
||||
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(
|
||||
|
||||
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"
|
||||
Literal[foo]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[foo]
|
||||
```
|
||||
---------------------------------------------
|
||||
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"
|
||||
Literal[foo, bar]
|
||||
---------------------------------------------
|
||||
```text
|
||||
Literal[foo, bar]
|
||||
```
|
||||
---------------------------------------------
|
||||
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
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
mod db;
|
||||
mod find_node;
|
||||
mod goto;
|
||||
|
||||
use std::ops::{Deref, DerefMut};
|
||||
mod hover;
|
||||
mod inlay_hints;
|
||||
mod markup;
|
||||
|
||||
pub use db::Db;
|
||||
pub use goto::goto_type_definition;
|
||||
use red_knot_python_semantic::types::{
|
||||
Class, ClassBase, ClassLiteralType, FunctionType, InstanceType, IntersectionType,
|
||||
KnownInstanceType, ModuleLiteralType, Type,
|
||||
};
|
||||
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_db::source::source_text;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
/// Information associated with a text range.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -54,7 +58,7 @@ where
|
||||
}
|
||||
|
||||
/// Target to which the editor can navigate to.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NavigationTarget {
|
||||
file: File,
|
||||
|
||||
@@ -95,6 +99,17 @@ impl NavigationTargets {
|
||||
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()
|
||||
}
|
||||
@@ -125,7 +140,7 @@ impl<'a> IntoIterator for &'a NavigationTargets {
|
||||
|
||||
impl FromIterator<NavigationTarget> for NavigationTargets {
|
||||
fn from_iter<T: IntoIterator<Item = NavigationTarget>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
Self::unique(iter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,128 +151,146 @@ pub trait HasNavigationTargets {
|
||||
impl HasNavigationTargets for Type<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
match self {
|
||||
Type::BoundMethod(method) => method.function(db).navigation_targets(db),
|
||||
Type::FunctionLiteral(function) => function.navigation_targets(db),
|
||||
Type::ModuleLiteral(module) => module.navigation_targets(db),
|
||||
Type::Union(union) => union
|
||||
.iter(db.upcast())
|
||||
.flat_map(|target| target.navigation_targets(db))
|
||||
.collect(),
|
||||
Type::ClassLiteral(class) => class.navigation_targets(db),
|
||||
Type::Instance(instance) => instance.navigation_targets(db),
|
||||
Type::KnownInstance(instance) => instance.navigation_targets(db),
|
||||
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
ClassBase::Class(class) => class.navigation_targets(db),
|
||||
ClassBase::Dynamic(_) => NavigationTargets::empty(),
|
||||
|
||||
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");
|
||||
|
||||
Type::StringLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::IntLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::MethodWrapper(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Tuple(_) => self.to_meta_type(db.upcast()).navigation_targets(db),
|
||||
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");
|
||||
|
||||
Type::Intersection(intersection) => intersection.navigation_targets(db),
|
||||
let insta_settings_guard = insta_settings.bind_to_scope();
|
||||
|
||||
Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::Callable(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy => NavigationTargets::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for FunctionType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let function_range = self.focus_range(db.upcast());
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: function_range.file(),
|
||||
focus_range: function_range.range(),
|
||||
full_range: self.full_range(db.upcast()).range(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for Class<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let class_range = self.focus_range(db.upcast());
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: class_range.file(),
|
||||
focus_range: class_range.range(),
|
||||
full_range: self.full_range(db.upcast()).range(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for ClassLiteralType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
self.class().navigation_targets(db)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for InstanceType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
self.class().navigation_targets(db)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for ModuleLiteralType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
let file = self.module(db).file();
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
CursorTest {
|
||||
db,
|
||||
cursor_offset: TextSize::try_from(cursor_offset)
|
||||
.expect("source to be smaller than 4GB"),
|
||||
file,
|
||||
focus_range: TextRange::default(),
|
||||
full_range: TextRange::up_to(source.text_len()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for KnownInstanceType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
match self {
|
||||
KnownInstanceType::TypeVar(var) => {
|
||||
let definition = var.definition(db);
|
||||
let full_range = definition.full_range(db.upcast());
|
||||
|
||||
NavigationTargets::single(NavigationTarget {
|
||||
file: full_range.file(),
|
||||
focus_range: definition.focus_range(db.upcast()).range(),
|
||||
full_range: full_range.range(),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Track the definition of `KnownInstance` and navigate to their definition.
|
||||
_ => NavigationTargets::empty(),
|
||||
_insta_settings_guard: insta_settings_guard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasNavigationTargets for IntersectionType<'_> {
|
||||
fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets {
|
||||
// Only consider the positive elements because the negative elements are mainly from narrowing constraints.
|
||||
let mut targets = self
|
||||
.iter_positive(db.upcast())
|
||||
.filter(|ty| !ty.is_unknown());
|
||||
pub(super) struct CursorTest {
|
||||
pub(super) db: TestDb,
|
||||
pub(super) cursor_offset: TextSize,
|
||||
pub(super) file: File,
|
||||
_insta_settings_guard: SettingsBindDropGuard,
|
||||
}
|
||||
|
||||
let Some(first) = targets.next() else {
|
||||
return NavigationTargets::empty();
|
||||
};
|
||||
impl CursorTest {
|
||||
pub(super) fn write_file(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: &str,
|
||||
) -> std::io::Result<()> {
|
||||
self.db.write_file(path, content)
|
||||
}
|
||||
|
||||
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()
|
||||
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();
|
||||
}
|
||||
None => first.navigation_targets(db),
|
||||
|
||||
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,6 +16,7 @@ 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"] }
|
||||
@@ -43,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
|
||||
|
||||
@@ -159,7 +159,7 @@ impl salsa::Database for ProjectDatabase {
|
||||
}
|
||||
|
||||
let event = event();
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation { .. }) {
|
||||
if matches!(event.kind, salsa::EventKind::WillCheckCancellation) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -174,6 +174,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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -54,20 +54,25 @@ impl Options {
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -224,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
|
||||
@@ -233,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>>,
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
References:
|
||||
|
||||
- <https://typing.readthedocs.io/en/latest/spec/callables.html#callable>
|
||||
- <https://typing.python.org/en/latest/spec/callables.html#callable>
|
||||
|
||||
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
|
||||
@@ -299,4 +299,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -147,4 +147,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
|
||||
|
||||
@@ -8,7 +8,11 @@ Currently, red-knot doesn't support `typing.NewType` in type annotations.
|
||||
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 _(
|
||||
|
||||
@@ -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
|
||||
@@ -69,3 +69,20 @@ from typing import Union
|
||||
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)
|
||||
```
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -409,8 +410,8 @@ reveal_type(c_instance.a) # revealed: Unknown
|
||||
|
||||
#### 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 +429,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 +544,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
|
||||
@@ -1046,13 +1129,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]
|
||||
|
||||
# Same here
|
||||
# error: [possibly-unbound-attribute]
|
||||
Foo().x = 2
|
||||
|
||||
reveal_type(Foo().y) # revealed: Unknown | Literal["a", "b"]
|
||||
Foo().y = "c"
|
||||
```
|
||||
|
||||
### Unions with all paths unbound
|
||||
@@ -1395,6 +1483,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
|
||||
@@ -1747,5 +1888,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
|
||||
|
||||
@@ -9,8 +9,8 @@ def _(c: Callable[[], int]):
|
||||
def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c(1, "a")) # revealed: int
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 1; expected type `int`"
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2; expected type `str`"
|
||||
# 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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -39,6 +43,24 @@ type("Foo", ())
|
||||
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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -72,7 +72,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
|
||||
```
|
||||
|
||||
@@ -82,7 +82,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
|
||||
```
|
||||
|
||||
@@ -92,7 +92,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
|
||||
```
|
||||
|
||||
@@ -102,7 +102,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
|
||||
```
|
||||
|
||||
@@ -112,7 +112,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
|
||||
```
|
||||
|
||||
@@ -122,7 +122,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
|
||||
```
|
||||
|
||||
@@ -132,8 +132,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,7 +56,7 @@ 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(sys, "dont_write_bytecode")) # revealed: bool
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static]
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
|
||||
@@ -114,7 +114,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"
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -207,7 +207,7 @@ first argument:
|
||||
def wrong_signature(f: int) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 1 (`f`) of function `wrong_signature`; expected type `int`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal[f]`"
|
||||
@wrong_signature
|
||||
def f(x): ...
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# `assert_never`
|
||||
|
||||
## Basic functionality
|
||||
|
||||
`assert_never` makes sure that the type of the argument is `Never`. If it is not, a
|
||||
`type-assertion-failure` diagnostic is emitted.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_never, Never, Any
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
|
||||
assert_never(never) # fine
|
||||
|
||||
assert_never(0) # error: [type-assertion-failure]
|
||||
assert_never("") # error: [type-assertion-failure]
|
||||
assert_never(None) # error: [type-assertion-failure]
|
||||
assert_never([]) # error: [type-assertion-failure]
|
||||
assert_never({}) # error: [type-assertion-failure]
|
||||
assert_never(()) # error: [type-assertion-failure]
|
||||
assert_never(1 if flag else never) # error: [type-assertion-failure]
|
||||
|
||||
assert_never(any_) # error: [type-assertion-failure]
|
||||
assert_never(unknown) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Use case: Type narrowing and exhaustiveness checking
|
||||
|
||||
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
|
||||
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_never, Literal
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def if_else_isinstance_success(obj: A | B):
|
||||
if isinstance(obj, A):
|
||||
pass
|
||||
elif isinstance(obj, B):
|
||||
pass
|
||||
elif isinstance(obj, C):
|
||||
pass
|
||||
else:
|
||||
assert_never(obj)
|
||||
|
||||
def if_else_isinstance_error(obj: A | B):
|
||||
if isinstance(obj, A):
|
||||
pass
|
||||
# B is missing
|
||||
elif isinstance(obj, C):
|
||||
pass
|
||||
else:
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `B & ~A & ~C` instead"
|
||||
assert_never(obj)
|
||||
|
||||
def if_else_singletons_success(obj: Literal[1, "a"] | None):
|
||||
if obj == 1:
|
||||
pass
|
||||
elif obj == "a":
|
||||
pass
|
||||
elif obj is None:
|
||||
pass
|
||||
else:
|
||||
assert_never(obj)
|
||||
|
||||
def if_else_singletons_error(obj: Literal[1, "a"] | None):
|
||||
if obj == 1:
|
||||
pass
|
||||
elif obj is "A": # "A" instead of "a"
|
||||
pass
|
||||
elif obj is None:
|
||||
pass
|
||||
else:
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `Literal["a"]` instead"
|
||||
assert_never(obj)
|
||||
|
||||
def match_singletons_success(obj: Literal[1, "a"] | None):
|
||||
match obj:
|
||||
case 1:
|
||||
pass
|
||||
case "a":
|
||||
pass
|
||||
case None:
|
||||
pass
|
||||
case _ as obj:
|
||||
# TODO: Ideally, we would not emit an error here
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo"
|
||||
assert_never(obj)
|
||||
|
||||
def match_singletons_error(obj: Literal[1, "a"] | None):
|
||||
match obj:
|
||||
case 1:
|
||||
pass
|
||||
case "A": # "A" instead of "a"
|
||||
pass
|
||||
case None:
|
||||
pass
|
||||
case _ as obj:
|
||||
# TODO: We should emit an error here, but the message should
|
||||
# show the type `Literal["a"]` instead of `@Todo(…)`.
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo"
|
||||
assert_never(obj)
|
||||
```
|
||||
@@ -49,3 +49,22 @@ def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]):
|
||||
a = cast(Callable[[list[bytes]], None], x)
|
||||
b = cast(tuple[list[bytes]], y)
|
||||
```
|
||||
|
||||
A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are
|
||||
understood as gradually equivalent types by red-knot, they are understood as different types by
|
||||
human readers of red-knot's output. For `Unknown` in particular, we may consider it differently in
|
||||
the context of some opt-in diagnostics, as it indicates that the gradual type has come about due to
|
||||
an invalid annotation, missing annotation or missing type argument somewhere.
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def f(x: Any, y: Unknown, z: Any | str | int):
|
||||
a = cast(dict[str, Any], x)
|
||||
reveal_type(a) # revealed: @Todo(generics)
|
||||
|
||||
b = cast(Any, y)
|
||||
reveal_type(b) # revealed: Any
|
||||
|
||||
c = cast(str | int | Any, z) # error: [redundant-cast]
|
||||
```
|
||||
|
||||
@@ -42,7 +42,7 @@ def f(w: Wrapper) -> None:
|
||||
v: int | None = w.value
|
||||
|
||||
# This function call is incorrect, because `w.value` could be `None`. We therefore emit the following
|
||||
# error: "`Unknown | None` cannot be assigned to parameter 1 (`i`) of function `accepts_int`; expected type `int`"
|
||||
# error: "Argument to this function is incorrect: Expected `int`, found `Unknown | None`"
|
||||
c = accepts_int(w.value)
|
||||
```
|
||||
|
||||
@@ -122,4 +122,4 @@ class Wrapper:
|
||||
reveal_type(Wrapper.value) # revealed: Unknown | None
|
||||
```
|
||||
|
||||
[gradual guarantee]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-gradual-guarantee
|
||||
[gradual guarantee]: https://typing.python.org/en/latest/spec/concepts.html#the-gradual-guarantee
|
||||
|
||||
@@ -73,3 +73,42 @@ from typing import Any
|
||||
def g(x: Any = "foo"):
|
||||
reveal_type(x) # revealed: Any | Literal["foo"]
|
||||
```
|
||||
|
||||
## Stub functions
|
||||
|
||||
### In Protocol
|
||||
|
||||
```py
|
||||
from typing import Protocol
|
||||
|
||||
class Foo(Protocol):
|
||||
def x(self, y: bool = ...): ...
|
||||
def y[T](self, y: T = ...) -> T: ...
|
||||
|
||||
class GenericFoo[T](Protocol):
|
||||
def x(self, y: bool = ...) -> T: ...
|
||||
```
|
||||
|
||||
### In abstract method
|
||||
|
||||
```py
|
||||
from abc import abstractmethod
|
||||
|
||||
class Bar:
|
||||
@abstractmethod
|
||||
def x(self, y: bool = ...): ...
|
||||
@abstractmethod
|
||||
def y[T](self, y: T = ...) -> T: ...
|
||||
```
|
||||
|
||||
### In function overload
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def x(y: None = ...) -> None: ...
|
||||
@overload
|
||||
def x(y: int) -> str: ...
|
||||
def x(y: int | None = None) -> str | None: ...
|
||||
```
|
||||
|
||||
@@ -310,7 +310,7 @@ def f(cond: bool) -> str:
|
||||
return "hello" if cond else NotImplemented
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
# error: [invalid-return-type] "Object of type `Literal["hello"]` is not assignable to return type `int`"
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `int`, found `Literal["hello"]`"
|
||||
return "hello" if cond else NotImplemented
|
||||
```
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ class C[T]: ...
|
||||
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:
|
||||
|
||||
```py
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class D[U](C[U]): ...
|
||||
```
|
||||
|
||||
@@ -22,8 +20,6 @@ A class that inherits from a generic class, but fills its type parameters with c
|
||||
_not_ generic:
|
||||
|
||||
```py
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class E(C[int]): ...
|
||||
```
|
||||
|
||||
@@ -57,7 +53,7 @@ class D(C[T]): ...
|
||||
|
||||
(Examples `E` and `F` from above do not have analogues in the legacy syntax.)
|
||||
|
||||
## Inferring generic class parameters
|
||||
## Specializing generic classes explicitly
|
||||
|
||||
The type parameter can be specified explicitly:
|
||||
|
||||
@@ -65,25 +61,77 @@ The type parameter can be specified explicitly:
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
# TODO: no error
|
||||
# TODO: revealed: C[int]
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(C[int]()) # revealed: C
|
||||
reveal_type(C[int]()) # revealed: C[int]
|
||||
```
|
||||
|
||||
The specialization must match the generic types:
|
||||
|
||||
```py
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
|
||||
reveal_type(C[int, int]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type variable has an upper bound, the specialized type must satisfy that bound:
|
||||
|
||||
```py
|
||||
class Bounded[T: int]: ...
|
||||
class BoundedByUnion[T: int | str]: ...
|
||||
class IntSubclass(int): ...
|
||||
|
||||
reveal_type(Bounded[int]()) # revealed: Bounded[int]
|
||||
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`"
|
||||
reveal_type(Bounded[str]()) # revealed: Unknown
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
|
||||
reveal_type(Bounded[int | str]()) # revealed: Unknown
|
||||
|
||||
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
|
||||
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
|
||||
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
|
||||
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
|
||||
```
|
||||
|
||||
If the type variable is constrained, the specialized type must satisfy those constraints:
|
||||
|
||||
```py
|
||||
class Constrained[T: (int, str)]: ...
|
||||
|
||||
reveal_type(Constrained[int]()) # revealed: Constrained[int]
|
||||
|
||||
# TODO: error: [invalid-argument-type]
|
||||
# TODO: revealed: Constrained[Unknown]
|
||||
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]
|
||||
|
||||
reveal_type(Constrained[str]()) # revealed: Constrained[str]
|
||||
|
||||
# TODO: error: [invalid-argument-type]
|
||||
# TODO: revealed: Unknown
|
||||
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`"
|
||||
reveal_type(Constrained[object]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring generic class parameters
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
c: C[int] = C()
|
||||
# TODO: revealed: C[int]
|
||||
reveal_type(c) # revealed: C
|
||||
reveal_type(c) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
The typevars of a fully specialized generic class should no longer be visible:
|
||||
|
||||
```py
|
||||
# TODO: revealed: int
|
||||
reveal_type(c.x) # revealed: T
|
||||
reveal_type(c.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
|
||||
@@ -92,15 +140,13 @@ specific type, we infer the typevar's default type:
|
||||
```py
|
||||
class D[T = int]: ...
|
||||
|
||||
# TODO: revealed: D[int]
|
||||
reveal_type(D()) # revealed: D
|
||||
reveal_type(D()) # revealed: D[int]
|
||||
```
|
||||
|
||||
If a typevar does not provide a default, we use `Unknown`:
|
||||
|
||||
```py
|
||||
# TODO: revealed: C[Unknown]
|
||||
reveal_type(C()) # revealed: C
|
||||
reveal_type(C()) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
If the type of a constructor parameter is a class typevar, we can use that to infer the type
|
||||
@@ -111,14 +157,14 @@ class E[T]:
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
# TODO: revealed: E[int] or E[Literal[1]]
|
||||
reveal_type(E(1)) # revealed: E
|
||||
reveal_type(E(1)) # revealed: E[Unknown]
|
||||
```
|
||||
|
||||
The types inferred from a type context and from a constructor parameter must be consistent with each
|
||||
other:
|
||||
|
||||
```py
|
||||
# TODO: error
|
||||
# TODO: error: [invalid-argument-type]
|
||||
wrong_innards: E[int] = E("five")
|
||||
```
|
||||
|
||||
@@ -131,17 +177,33 @@ propagate through:
|
||||
class Base[T]:
|
||||
x: T | None = None
|
||||
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class Sub[U](Base[U]): ...
|
||||
|
||||
reveal_type(Base[int].x) # revealed: int | None
|
||||
reveal_type(Sub[int].x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Generic methods
|
||||
|
||||
Generic classes can contain methods that are themselves generic. The generic methods can refer to
|
||||
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
|
||||
scope for the method.
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def method[U](self, u: U) -> U:
|
||||
return u
|
||||
# error: [unresolved-reference]
|
||||
def cannot_use_outside_of_method(self, u: U): ...
|
||||
|
||||
# TODO: error
|
||||
def cannot_shadow_class_typevar[T](self, t: T): ...
|
||||
|
||||
c: C[int] = C[int]()
|
||||
# TODO: no error
|
||||
# TODO: revealed: int | None
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(Base[int].x) # revealed: T | None
|
||||
# TODO: revealed: int | None
|
||||
# error: [non-subscriptable]
|
||||
reveal_type(Sub[int].x) # revealed: T | None
|
||||
# TODO: revealed: str or Literal["string"]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(c.method("string")) # revealed: U
|
||||
```
|
||||
|
||||
## Cyclic class definition
|
||||
@@ -155,8 +217,6 @@ Here, `Sub` is not a generic class, since it fills its superclass's type paramet
|
||||
|
||||
```pyi
|
||||
class Base[T]: ...
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class Sub(Base[Sub]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
@@ -168,9 +228,6 @@ A similar case can work in a non-stub file, if forward references are stringifie
|
||||
|
||||
```py
|
||||
class Base[T]: ...
|
||||
|
||||
# TODO: no error
|
||||
# error: [non-subscriptable]
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
@@ -183,8 +240,6 @@ In a non-stub file, without stringified forward references, this raises a `NameE
|
||||
```py
|
||||
class Base[T]: ...
|
||||
|
||||
# TODO: the unresolved-reference error is correct, the non-subscriptable is not
|
||||
# error: [non-subscriptable]
|
||||
# error: [unresolved-reference]
|
||||
class Sub(Base[Sub]): ...
|
||||
```
|
||||
|
||||
@@ -107,8 +107,7 @@ def good_return[T: int](x: T) -> T:
|
||||
return x
|
||||
|
||||
def bad_return[T: int](x: T) -> T:
|
||||
# TODO: error: int is not assignable to T
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `Literal[1]`"
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`"
|
||||
return x + 1
|
||||
```
|
||||
|
||||
@@ -121,7 +120,7 @@ def different_types[T, S](cond: bool, t: T, s: S) -> T:
|
||||
if cond:
|
||||
return t
|
||||
else:
|
||||
# error: [invalid-return-type] "Object of type `S` is not assignable to return type `T`"
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`"
|
||||
return s
|
||||
|
||||
def same_types[T](cond: bool, t1: T, t2: T) -> T:
|
||||
|
||||
@@ -69,4 +69,4 @@ from typing import TypeVar
|
||||
T = TypeVar("T", int)
|
||||
```
|
||||
|
||||
[generics]: https://typing.readthedocs.io/en/latest/spec/generics.html
|
||||
[generics]: https://typing.python.org/en/latest/spec/generics.html
|
||||
|
||||
@@ -48,4 +48,523 @@ class C[T]:
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
## Fully static typevars
|
||||
|
||||
We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This
|
||||
is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This
|
||||
is similar to how you can assign an expression whose type is not fully static to a target whose type
|
||||
is.)
|
||||
|
||||
```py
|
||||
from knot_extensions import is_fully_static, static_assert
|
||||
from typing import Any
|
||||
|
||||
def unbounded_unconstrained[T](t: list[T]) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded[T: int](t: list[T]) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded_by_gradual[T: Any](t: list[T]) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
def constrained[T: (int, str)](t: list[T]) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def constrained_by_gradual[T: (int, Any)](t: list[T]) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
```
|
||||
|
||||
## Subtyping and assignability
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static
|
||||
typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual
|
||||
typevars.)
|
||||
|
||||
We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be
|
||||
specialized to. Properties are true of the typevar only if they are true for every valid
|
||||
specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type
|
||||
(including other typevars).
|
||||
|
||||
```py
|
||||
from knot_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T, U](t: list[T], u: list[U]) -> None:
|
||||
static_assert(is_assignable_to(T, T))
|
||||
static_assert(is_assignable_to(T, object))
|
||||
static_assert(not is_assignable_to(T, Super))
|
||||
static_assert(is_assignable_to(U, U))
|
||||
static_assert(is_assignable_to(U, object))
|
||||
static_assert(not is_assignable_to(U, Super))
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(is_subtype_of(T, T))
|
||||
static_assert(is_subtype_of(T, object))
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(is_subtype_of(U, U))
|
||||
static_assert(is_subtype_of(U, object))
|
||||
static_assert(not is_subtype_of(U, Super))
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of
|
||||
its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does
|
||||
not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the
|
||||
typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound
|
||||
is a final class, since the typevar can still be specialized to `Never`.)
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import final
|
||||
|
||||
def bounded[T: Super](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(not is_assignable_to(Sub, T))
|
||||
|
||||
static_assert(is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Sub, T))
|
||||
|
||||
def bounded_by_gradual[T: Any](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, Any))
|
||||
static_assert(is_assignable_to(Any, T))
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(Sub, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Any))
|
||||
static_assert(not is_subtype_of(Any, T))
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(Sub, T))
|
||||
|
||||
@final
|
||||
class FinalClass: ...
|
||||
|
||||
def bounded_final[T: FinalClass](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, FinalClass))
|
||||
static_assert(not is_assignable_to(FinalClass, T))
|
||||
|
||||
static_assert(is_subtype_of(T, FinalClass))
|
||||
static_assert(not is_subtype_of(FinalClass, T))
|
||||
```
|
||||
|
||||
Two distinct fully static typevars are not subtypes of each other, even if they have the same
|
||||
bounds, since there is (still) no guarantee that they will be specialized to the same type. This is
|
||||
true even if both typevars are bounded by the same final class, since you can specialize the
|
||||
typevars to `Never` in addition to that final class.
|
||||
|
||||
```py
|
||||
def two_bounded[T: Super, U: Super](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
|
||||
def two_final_bounded[T: FinalClass, U: FinalClass](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
A constrained fully static typevar is assignable to the union of its constraints, but not to any of
|
||||
the constraints individually. None of the constraints are subtypes of the typevar, though the
|
||||
intersection of all of its constraints is a subtype of the typevar.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection
|
||||
|
||||
def constrained[T: (Base, Unrelated)](t: list[T]) -> None:
|
||||
static_assert(not is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(T, Base))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(T, Unrelated))
|
||||
static_assert(is_assignable_to(T, Super | Unrelated))
|
||||
static_assert(is_assignable_to(T, Base | Unrelated))
|
||||
static_assert(not is_assignable_to(T, Sub | Unrelated))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(not is_assignable_to(Unrelated, T))
|
||||
static_assert(not is_assignable_to(Super | Unrelated, T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Base))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(T, Unrelated))
|
||||
static_assert(is_subtype_of(T, Super | Unrelated))
|
||||
static_assert(is_subtype_of(T, Base | Unrelated))
|
||||
static_assert(not is_subtype_of(T, Sub | Unrelated))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Unrelated, T))
|
||||
static_assert(not is_subtype_of(Super | Unrelated, T))
|
||||
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
|
||||
|
||||
def constrained_by_gradual[T: (Base, Any)](t: list[T]) -> None:
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(is_assignable_to(T, Base))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(T, Unrelated))
|
||||
static_assert(is_assignable_to(T, Any))
|
||||
static_assert(is_assignable_to(T, Super | Any))
|
||||
static_assert(is_assignable_to(T, Super | Unrelated))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(is_assignable_to(Base, T))
|
||||
static_assert(not is_assignable_to(Unrelated, T))
|
||||
static_assert(is_assignable_to(Any, T))
|
||||
static_assert(not is_assignable_to(Super | Any, T))
|
||||
static_assert(is_assignable_to(Base | Any, T))
|
||||
static_assert(not is_assignable_to(Super | Unrelated, T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Any], T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Base))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(T, Unrelated))
|
||||
static_assert(not is_subtype_of(T, Any))
|
||||
static_assert(not is_subtype_of(T, Super | Any))
|
||||
static_assert(not is_subtype_of(T, Super | Unrelated))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Base, T))
|
||||
static_assert(not is_subtype_of(Unrelated, T))
|
||||
static_assert(not is_subtype_of(Any, T))
|
||||
static_assert(not is_subtype_of(Super | Any, T))
|
||||
static_assert(not is_subtype_of(Base | Any, T))
|
||||
static_assert(not is_subtype_of(Super | Unrelated, T))
|
||||
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
|
||||
static_assert(not is_subtype_of(Intersection[Base, Any], T))
|
||||
```
|
||||
|
||||
Two distinct fully static typevars are not subtypes of each other, even if they have the same
|
||||
constraints, and even if any of the constraints are final. There must always be at least two
|
||||
distinct constraints, meaning that there is (still) no guarantee that they will be specialized to
|
||||
the same type.
|
||||
|
||||
```py
|
||||
def two_constrained[T: (int, str), U: (int, str)](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
|
||||
@final
|
||||
class AnotherFinalClass: ...
|
||||
|
||||
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: list[T], u: list[U]) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
## Singletons and single-valued types
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the
|
||||
claims also apply to _single-valued_ types.)
|
||||
|
||||
An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a
|
||||
non-singleton type.
|
||||
|
||||
```py
|
||||
from knot_extensions import is_singleton, is_single_valued, static_assert
|
||||
|
||||
def unbounded_unconstrained[T](t: list[T]) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
```
|
||||
|
||||
A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be
|
||||
specialized to `Never`.
|
||||
|
||||
```py
|
||||
def bounded[T: None](t: list[T]) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
```
|
||||
|
||||
A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot
|
||||
specialize a constrained typevar to a subtype of a constraint.)
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
def constrained_non_singletons[T: (int, str)](t: list[T]) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
|
||||
def constrained_singletons[T: (Literal[True], Literal[False])](t: list[T]) -> None:
|
||||
static_assert(is_singleton(T))
|
||||
|
||||
def constrained_single_valued[T: (Literal[True], tuple[()])](t: list[T]) -> None:
|
||||
static_assert(is_single_valued(T))
|
||||
```
|
||||
|
||||
## Unions involving typevars
|
||||
|
||||
The union of an unbounded unconstrained typevar with any other type cannot be simplified, since
|
||||
there is no guarantee what type the typevar will be specialized to.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: T | Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: T | Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T | Sub
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be
|
||||
specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound
|
||||
cannot be simplified. (The typevar might be specialized to a different subtype of the bound.)
|
||||
|
||||
```py
|
||||
def bounded[T: Base](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T | Sub
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
The union of a constrained typevar with a type depends on how that type relates to the constraints.
|
||||
If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely,
|
||||
if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the
|
||||
union cannot be simplified.
|
||||
|
||||
```py
|
||||
def constrained[T: (Base, Sub)](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
## Intersections involving typevars
|
||||
|
||||
The intersection of an unbounded unconstrained typevar with any other type cannot be simplified,
|
||||
since there is no guarantee what type the typevar will be specialized to.
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection
|
||||
from typing import Any
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
def _(x: Intersection[T, Super]) -> None:
|
||||
reveal_type(x) # revealed: T & Super
|
||||
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
reveal_type(x) # revealed: T & Base
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: T & Sub
|
||||
|
||||
def _(x: Intersection[T, Unrelated]) -> None:
|
||||
reveal_type(x) # revealed: T & Unrelated
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar
|
||||
itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded
|
||||
typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a
|
||||
different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint
|
||||
from its bound is `Never`.
|
||||
|
||||
```py
|
||||
def bounded[T: Base](t: T) -> None:
|
||||
def _(x: Intersection[T, Super]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: T & Sub
|
||||
|
||||
def _(x: Intersection[T, None]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must
|
||||
be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints,
|
||||
since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The
|
||||
`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it
|
||||
would not represent a single set of runtime objects; it would instead represent a _set of_ sets of
|
||||
runtime objects. This is one reason we have not actually added this connector to our data model yet.
|
||||
Nevertheless, describing constrained typevars this way helps explain how we simplify intersections
|
||||
involving them.
|
||||
|
||||
This means that when intersecting a constrained typevar with a type `T`, constraints that are
|
||||
supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover,
|
||||
constraints that are disjoint from `T` are no longer valid specializations of the typevar, since
|
||||
`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we
|
||||
can simplify the intersection as a whole to that constraint.
|
||||
|
||||
```py
|
||||
def constrained[T: (Base, Sub, Unrelated)](t: T) -> None:
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
# With OneOf this would be OneOf[Base, Sub]
|
||||
reveal_type(x) # revealed: T & Base
|
||||
|
||||
def _(x: Intersection[T, Unrelated]) -> None:
|
||||
reveal_type(x) # revealed: Unrelated
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: Sub
|
||||
|
||||
def _(x: Intersection[T, None]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
We can simplify the intersection similarly when removing a type from a constrained typevar, since
|
||||
this is modeled internally as an intersection with a negation.
|
||||
|
||||
```py
|
||||
from knot_extensions import Not
|
||||
|
||||
def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
||||
def _(x: Intersection[T, Not[int]]) -> None:
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
|
||||
def _(x: Intersection[T, Not[str]]) -> None:
|
||||
# With OneOf this would be OneOf[int, bool]
|
||||
reveal_type(x) # revealed: T & ~str
|
||||
|
||||
def _(x: Intersection[T, Not[bool]]) -> None:
|
||||
reveal_type(x) # revealed: T & ~bool
|
||||
|
||||
def _(x: Intersection[T, Not[int], Not[str]]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Not[None]]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Not[Any]]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
|
||||
|
||||
```py
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
def f[T: (P, Q)](t: T) -> None:
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
else:
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
|
||||
if isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q
|
||||
q: Q = t
|
||||
else:
|
||||
reveal_type(t) # revealed: P & ~Q
|
||||
p: P = t
|
||||
|
||||
def g[T: (P, Q, R)](t: T) -> None:
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
elif isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
else:
|
||||
reveal_type(t) # revealed: R & ~P & ~Q
|
||||
r: R = t
|
||||
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
elif isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
elif isinstance(t, R):
|
||||
reveal_type(t) # revealed: R & ~P & ~Q
|
||||
r: R = t
|
||||
else:
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
If the constraints are disjoint, simplification does eliminate the redundant negative:
|
||||
|
||||
```py
|
||||
def h[T: (P, None)](t: T) -> None:
|
||||
if t is None:
|
||||
reveal_type(t) # revealed: None
|
||||
p: None = t
|
||||
else:
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
```
|
||||
|
||||
[pep 695]: https://peps.python.org/pep-0695/
|
||||
|
||||
@@ -82,18 +82,51 @@ class C[T]:
|
||||
def m2(self, x: T) -> T:
|
||||
return x
|
||||
|
||||
c: C[int] = C()
|
||||
# TODO: no error
|
||||
# error: [invalid-argument-type]
|
||||
c: C[int] = C[int]()
|
||||
c.m1(1)
|
||||
# TODO: no error
|
||||
# error: [invalid-argument-type]
|
||||
c.m2(1)
|
||||
# TODO: expected type `int`
|
||||
# error: [invalid-argument-type] "Object of type `Literal["string"]` cannot be assigned to parameter 2 (`x`) of bound method `m2`; expected type `T`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["string"]`"
|
||||
c.m2("string")
|
||||
```
|
||||
|
||||
## Functions on generic classes are descriptors
|
||||
|
||||
This repeats the tests in the [Functions as descriptors](./call/methods.md) test suite, but on a
|
||||
generic class. This ensures that we are carrying any specializations through the entirety of the
|
||||
descriptor protocol, which is how `self` parameters are bound to instance methods.
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
class C[T]:
|
||||
def f(self, x: T) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(getattr_static(C[int], "f")) # revealed: Literal[f[int]]
|
||||
reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper `__get__` of `f[int]`>
|
||||
reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: Literal[f[int]]
|
||||
# revealed: <bound method `f` of `C[int]`>
|
||||
reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int]))
|
||||
|
||||
reveal_type(C[int].f) # revealed: Literal[f[int]]
|
||||
reveal_type(C[int]().f) # revealed: <bound method `f` of `C[int]`>
|
||||
|
||||
bound_method = C[int]().f
|
||||
reveal_type(bound_method.__self__) # revealed: C[int]
|
||||
reveal_type(bound_method.__func__) # revealed: Literal[f[int]]
|
||||
|
||||
reveal_type(C[int]().f(1)) # revealed: str
|
||||
reveal_type(bound_method(1)) # revealed: str
|
||||
|
||||
C[int].f(1) # error: [missing-argument]
|
||||
reveal_type(C[int].f(C[int](), 1)) # revealed: str
|
||||
|
||||
class D[U](C[U]):
|
||||
pass
|
||||
|
||||
reveal_type(D[int]().f) # revealed: <bound method `f` of `D[int]`>
|
||||
```
|
||||
|
||||
## Methods can mention other typevars
|
||||
|
||||
> A type variable used in a method that does not match any of the variables that parameterize the
|
||||
@@ -127,7 +160,6 @@ c: C[int] = C()
|
||||
# TODO: no errors
|
||||
# TODO: revealed: str
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(c.m(1, "string")) # revealed: S
|
||||
```
|
||||
|
||||
@@ -267,4 +299,4 @@ class C[T]:
|
||||
ok2: Inner[T]
|
||||
```
|
||||
|
||||
[scoping]: https://typing.readthedocs.io/en/latest/spec/generics.html#scoping-rules-for-type-variables
|
||||
[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables
|
||||
|
||||
@@ -4,7 +4,7 @@ This document describes the conventions for importing symbols.
|
||||
|
||||
Reference:
|
||||
|
||||
- <https://typing.readthedocs.io/en/latest/spec/distributing.html#import-conventions>
|
||||
- <https://typing.python.org/en/latest/spec/distributing.html#import-conventions>
|
||||
|
||||
## Builtins scope
|
||||
|
||||
|
||||
@@ -236,3 +236,36 @@ X: int = 42
|
||||
```py
|
||||
from .parser import X # error: [unresolved-import]
|
||||
```
|
||||
|
||||
## Relative imports in `site-packages`
|
||||
|
||||
Relative imports in `site-packages` are correctly resolved even when the `site-packages` search path
|
||||
is a subdirectory of the first-party search path. Note that mdtest sets the first-party search path
|
||||
to `/src/`, which is why the virtual environment in this test is a subdirectory of `/src/`, even
|
||||
though this is not how a typical Python project would be structured:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python = "/src/.venv"
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
`/src/bar.py`:
|
||||
|
||||
```py
|
||||
from foo import A
|
||||
|
||||
reveal_type(A) # revealed: Literal[A]
|
||||
```
|
||||
|
||||
`/src/.venv/<path-to-site-packages>/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
from .a import A as A
|
||||
```
|
||||
|
||||
`/src/.venv/<path-to-site-packages>/foo/a.py`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
```
|
||||
|
||||
@@ -6,52 +6,52 @@ See the [Python language reference for import statements].
|
||||
|
||||
### A simple `*` import
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
print(Y) # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
### Overriding existing definition
|
||||
### Overriding an existing definition
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
X = 42
|
||||
reveal_type(X) # revealed: Literal[42]
|
||||
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
```
|
||||
|
||||
### Overridden by later definition
|
||||
### Overridden by a later definition
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
X = False
|
||||
@@ -78,7 +78,7 @@ from a import *
|
||||
from b import *
|
||||
```
|
||||
|
||||
`d.py`:
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from c import *
|
||||
@@ -88,6 +88,9 @@ reveal_type(X) # revealed: bool
|
||||
|
||||
### A wildcard import constitutes a re-export
|
||||
|
||||
This is specified
|
||||
[here](https://typing.python.org/en/latest/spec/distributing.html#import-conventions).
|
||||
|
||||
`a.pyi`:
|
||||
|
||||
```pyi
|
||||
@@ -107,7 +110,7 @@ from a import *
|
||||
from b import Y
|
||||
```
|
||||
|
||||
`d.py`:
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
# `X` is accessible because the `*` import in `c` re-exports it from `c`
|
||||
@@ -117,9 +120,15 @@ from c import X
|
||||
from c import Y # error: [unresolved-import]
|
||||
```
|
||||
|
||||
## Esoteric definitions and redefinintions
|
||||
|
||||
We understand all public symbols defined in an external module as being imported by a `*` import,
|
||||
not just those that are defined in `StmtAssign` nodes and `StmtAnnAssign` nodes. This section
|
||||
provides tests for definitions, and redefinitions, that use more esoteric AST nodes.
|
||||
|
||||
### Global-scope symbols defined using walrus expressions
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
X = (Y := 3) + 4
|
||||
@@ -128,7 +137,7 @@ X = (Y := 3) + 4
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: Unknown | Literal[7]
|
||||
reveal_type(Y) # revealed: Unknown | Literal[3]
|
||||
@@ -136,7 +145,7 @@ reveal_type(Y) # revealed: Unknown | Literal[3]
|
||||
|
||||
### Global-scope symbols defined in many other ways
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
@@ -158,8 +167,15 @@ def J(): ...
|
||||
|
||||
type K = int
|
||||
|
||||
with () as L: # error: [invalid-context-manager]
|
||||
...
|
||||
class ContextManagerThatMightNotRunToCompletion:
|
||||
def __enter__(self) -> "ContextManagerThatMightNotRunToCompletion":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> typing.Literal[True]:
|
||||
return True
|
||||
|
||||
with ContextManagerThatMightNotRunToCompletion() as L:
|
||||
U = ...
|
||||
|
||||
match 42:
|
||||
case {"something": M}:
|
||||
@@ -172,16 +188,35 @@ match 42:
|
||||
...
|
||||
case object(foo=R):
|
||||
...
|
||||
|
||||
match 56:
|
||||
case x if something_unresolvable: # error: [unresolved-reference]
|
||||
...
|
||||
|
||||
case object(S):
|
||||
...
|
||||
|
||||
match 12345:
|
||||
case x if something_unresolvable: # error: [unresolved-reference]
|
||||
...
|
||||
|
||||
case T:
|
||||
...
|
||||
|
||||
def boolean_condition() -> bool:
|
||||
return True
|
||||
|
||||
if boolean_condition():
|
||||
V = ...
|
||||
|
||||
while boolean_condition():
|
||||
W = ...
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
# fmt: off
|
||||
|
||||
@@ -192,31 +227,197 @@ print((
|
||||
D,
|
||||
E,
|
||||
F,
|
||||
G, # TODO: could emit diagnostic about being possibly unbound
|
||||
H,
|
||||
G, # error: [possibly-unresolved-reference]
|
||||
H, # error: [possibly-unresolved-reference]
|
||||
I,
|
||||
J,
|
||||
K,
|
||||
L,
|
||||
M, # TODO: could emit diagnostic about being possibly unbound
|
||||
N, # TODO: could emit diagnostic about being possibly unbound
|
||||
O, # TODO: could emit diagnostic about being possibly unbound
|
||||
P, # TODO: could emit diagnostic about being possibly unbound
|
||||
Q, # TODO: could emit diagnostic about being possibly unbound
|
||||
R, # TODO: could emit diagnostic about being possibly unbound
|
||||
S, # TODO: could emit diagnostic about being possibly unbound
|
||||
T, # TODO: could emit diagnostic about being possibly unbound
|
||||
M, # error: [possibly-unresolved-reference]
|
||||
N, # error: [possibly-unresolved-reference]
|
||||
O, # error: [possibly-unresolved-reference]
|
||||
P, # error: [possibly-unresolved-reference]
|
||||
Q, # error: [possibly-unresolved-reference]
|
||||
R, # error: [possibly-unresolved-reference]
|
||||
S, # error: [possibly-unresolved-reference]
|
||||
T, # error: [possibly-unresolved-reference]
|
||||
U, # TODO: could emit [possibly-unresolved-reference here] (https://github.com/astral-sh/ruff/issues/16996)
|
||||
V, # error: [possibly-unresolved-reference]
|
||||
W, # error: [possibly-unresolved-reference]
|
||||
typing,
|
||||
OrderedDict,
|
||||
Foo,
|
||||
))
|
||||
```
|
||||
|
||||
### Esoteric possible redefinitions following definitely bound prior definitions
|
||||
|
||||
There should be no complaint about the symbols being possibly unbound in `b.py` here: although the
|
||||
second definition might or might not take place, each symbol is definitely bound by a prior
|
||||
definition.
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
A = 1
|
||||
B = 2
|
||||
C = 3
|
||||
D = 4
|
||||
E = 5
|
||||
F = 6
|
||||
G = 7
|
||||
H = 8
|
||||
I = 9
|
||||
J = 10
|
||||
K = 11
|
||||
L = 12
|
||||
|
||||
for A in [1]:
|
||||
...
|
||||
|
||||
match 42:
|
||||
case {"something": B}:
|
||||
...
|
||||
case [*C]:
|
||||
...
|
||||
case [D]:
|
||||
...
|
||||
case E | F:
|
||||
...
|
||||
case object(foo=G):
|
||||
...
|
||||
case object(H):
|
||||
...
|
||||
case I:
|
||||
...
|
||||
|
||||
def boolean_condition() -> bool:
|
||||
return True
|
||||
|
||||
if boolean_condition():
|
||||
J = ...
|
||||
|
||||
while boolean_condition():
|
||||
K = ...
|
||||
|
||||
class ContextManagerThatMightNotRunToCompletion:
|
||||
def __enter__(self) -> "ContextManagerThatMightNotRunToCompletion":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> Literal[True]:
|
||||
return True
|
||||
|
||||
with ContextManagerThatMightNotRunToCompletion():
|
||||
L = ...
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from exporter import *
|
||||
|
||||
print(A)
|
||||
print(B)
|
||||
print(C)
|
||||
print(D)
|
||||
print(E)
|
||||
print(F)
|
||||
print(G)
|
||||
print(H)
|
||||
print(I)
|
||||
print(J)
|
||||
print(K)
|
||||
print(L)
|
||||
```
|
||||
|
||||
### Esoteric possible definitions prior to definitely bound prior redefinitions
|
||||
|
||||
The same principle applies here to the symbols in `b.py`. Although the first definition might or
|
||||
might not take place, each symbol is definitely bound by a later definition.
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
for A in [1]:
|
||||
...
|
||||
|
||||
match 42:
|
||||
case {"something": B}:
|
||||
...
|
||||
case [*C]:
|
||||
...
|
||||
case [D]:
|
||||
...
|
||||
case E | F:
|
||||
...
|
||||
case object(foo=G):
|
||||
...
|
||||
case object(H):
|
||||
...
|
||||
case I:
|
||||
...
|
||||
|
||||
def boolean_condition() -> bool:
|
||||
return True
|
||||
|
||||
if boolean_condition():
|
||||
J = ...
|
||||
|
||||
while boolean_condition():
|
||||
K = ...
|
||||
|
||||
class ContextManagerThatMightNotRunToCompletion:
|
||||
def __enter__(self) -> "ContextManagerThatMightNotRunToCompletion":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> Literal[True]:
|
||||
return True
|
||||
|
||||
with ContextManagerThatMightNotRunToCompletion():
|
||||
L = ...
|
||||
|
||||
A = 1
|
||||
B = 2
|
||||
C = 3
|
||||
D = 4
|
||||
E = 5
|
||||
F = 6
|
||||
G = 7
|
||||
H = 8
|
||||
I = 9
|
||||
J = 10
|
||||
K = 11
|
||||
L = 12
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from exporter import *
|
||||
|
||||
print(A)
|
||||
print(B)
|
||||
print(C)
|
||||
print(D)
|
||||
print(E)
|
||||
print(F)
|
||||
print(G)
|
||||
print(H)
|
||||
print(I)
|
||||
print(J)
|
||||
print(K)
|
||||
print(L)
|
||||
```
|
||||
|
||||
### Definitions in function-like scopes are not global definitions
|
||||
|
||||
Except for some cases involving walrus expressions inside comprehension scopes.
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
@@ -248,10 +449,10 @@ list(((o := p * 2) for p in Iterable()))
|
||||
[(lambda s=s: (t := 42))() for s in Iterable()]
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(a) # revealed: Unknown
|
||||
@@ -278,15 +479,21 @@ reveal_type(s) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(t) # revealed: Unknown
|
||||
|
||||
# TODO: these should all reveal `Unknown | int`.
|
||||
# TODO: these should all reveal `Unknown | int` and should not emit errors.
|
||||
# (We don't generally model elsewhere in red-knot that bindings from walruses
|
||||
# "leak" from comprehension scopes into outer scopes, but we should.)
|
||||
# See https://github.com/astral-sh/ruff/issues/16954
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(g) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(i) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(k) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(m) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(o) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(q) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -315,12 +522,19 @@ reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Which symbols are exported
|
||||
|
||||
Not all symbols in the global namespace are considered "public". As a result, not all symbols bound
|
||||
in the global namespace of an `exporter.py` module will be imported by a `from exporter import *`
|
||||
statement in an `importer.py` module. The tests in this section elaborate on these semantics.
|
||||
|
||||
### Global-scope names starting with underscores
|
||||
|
||||
Global-scope names starting with underscores are not imported from a `*` import (unless the module
|
||||
has `__all__` and they are included in `__all__`):
|
||||
Global-scope names starting with underscores are not imported from a `*` import (unless the
|
||||
exporting module has an `__all__` symbol in its global scope, and the underscore-prefixed symbols
|
||||
are included in `__all__`):
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
_private: bool = False
|
||||
@@ -331,10 +545,10 @@ ___thunder___: bool = False
|
||||
Y: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(_private) # revealed: Unknown
|
||||
@@ -354,8 +568,11 @@ For `.py` files, we should consider all public symbols in the global namespace e
|
||||
module when considering which symbols are made available by a `*` import. Here, `b.py` does not use
|
||||
the explicit `from a import X as X` syntax to explicitly mark it as publicly re-exported, and `X` is
|
||||
not included in `__all__`; whether it should be considered a "public name" in module `b` is
|
||||
ambiguous. We could consider an opt-in rule to warn the user when they use `X` in `c.py` that it was
|
||||
not included in `__all__` and was not marked as an explicit re-export.
|
||||
ambiguous.
|
||||
|
||||
We should consider `X` bound in `c.py`. However, we could consider adding an opt-in rule to warn the
|
||||
user when they use `X` in `c.py` that it was neither included in `b.__all__` nor marked as an
|
||||
explicit re-export from `b` through the "redundant alias" convention.
|
||||
|
||||
`a.py`:
|
||||
|
||||
@@ -380,7 +597,7 @@ reveal_type(X) # revealed: bool
|
||||
|
||||
### Only explicit re-exports are considered re-exported from `.pyi` files
|
||||
|
||||
For `.pyi` files, we should consider all imports private to the stub unless they are included in
|
||||
For `.pyi` files, we should consider all imports "private to the stub" unless they are included in
|
||||
`__all__` or use the explicit `from foo import X as X` syntax.
|
||||
|
||||
`a.pyi`:
|
||||
@@ -409,14 +626,32 @@ reveal_type(X) # revealed: Unknown
|
||||
reveal_type(Y) # revealed: bool
|
||||
```
|
||||
|
||||
### Symbols in statically known branches
|
||||
## Visibility constraints
|
||||
|
||||
If an `importer` module contains a `from exporter import *` statement in its global namespace, the
|
||||
statement will *not* necessarily import *all* symbols that have definitions in `exporter.py`'s
|
||||
global scope. For any given symbol in `exporter.py`'s global scope, that symbol will *only* be
|
||||
imported by the `*` import if at least one definition for that symbol is visible from the *end* of
|
||||
`exporter.py`'s global scope.
|
||||
|
||||
For example, say that `exporter.py` contains a symbol `X` in its global scope, and the definition
|
||||
for `X` in `exporter.py` has visibility constraints <code>vis<sub>1</sub></code>. The
|
||||
`from exporter import *` statement in `importer.py` creates a definition for `X` in `importer`, and
|
||||
there are visibility constraints <code>vis<sub>2</sub></code> on the import statement in
|
||||
`importer.py`. This means that the overall visibility constraints on the `X` definnition created by
|
||||
the import statement in `importer.py` will be <code>vis<sub>1</sub> AND vis<sub>2</sub></code>.
|
||||
|
||||
A visibility constraint in the external module must be understood and evaluated whether or not its
|
||||
truthiness can be statically determined.
|
||||
|
||||
### Statically known branches in the external module
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
@@ -428,27 +663,158 @@ else:
|
||||
Z: int = 42
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
Z: bool = True
|
||||
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
# TODO: should emit error: [unresolved-reference]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
|
||||
# TODO: The `*` import should not be considered a redefinition
|
||||
# of the global variable in this module, as the symbol in
|
||||
# The `*` import is not considered a redefinition
|
||||
# of the global variable `Z` in this module, as the symbol in
|
||||
# the `a` module is in a branch that is statically known
|
||||
# to be dead code given the `python-version` configuration.
|
||||
# Thus this should reveal `Literal[True]`.
|
||||
reveal_type(Z) # revealed: Unknown
|
||||
# Thus this still reveals `Literal[True]`.
|
||||
reveal_type(Z) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Relative `*` imports
|
||||
### Multiple `*` imports with always-false visibility constraints
|
||||
|
||||
Our understanding of visibility constraints in an external module remains accurate, even if there
|
||||
are multiple `*` imports from that module.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
Z: str = "foo"
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
Z = True
|
||||
|
||||
from exporter import *
|
||||
from exporter import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(Z) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Ambiguous visibility constraints
|
||||
|
||||
Some constraints in the external module may resolve to an "ambiguous truthiness". For these, we
|
||||
should emit `possibly-unresolved-reference` diagnostics when they are used in the module in which
|
||||
the `*` import occurs.
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
A = 1
|
||||
B = 2
|
||||
else:
|
||||
B = 3
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from exporter import *
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(A) # revealed: Unknown | Literal[1]
|
||||
|
||||
reveal_type(B) # revealed: Unknown | Literal[2, 3]
|
||||
```
|
||||
|
||||
### Visibility constraints in the importing module
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
A = 1
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
from exporter import *
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(A) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
### Visibility constraints in the exporting module *and* the importing module
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
A: bool = True
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
B: bool = True
|
||||
```
|
||||
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from exporter import *
|
||||
|
||||
# it's correct to have no diagnostics here as this branch is unreachable
|
||||
reveal_type(A) # revealed: Unknown
|
||||
reveal_type(B) # revealed: bool
|
||||
else:
|
||||
from exporter import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(A) # revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(B) # revealed: bool
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(A) # revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(B) # revealed: bool
|
||||
```
|
||||
|
||||
## Relative `*` imports
|
||||
|
||||
Relative `*` imports are also supported by Python:
|
||||
|
||||
@@ -478,7 +844,7 @@ If a module `x` contains `__all__`, all symbols included in `x.__all__` are impo
|
||||
|
||||
### Simple tuple `__all__`
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ("X", "_private", "__protected", "__dunder__", "___thunder___")
|
||||
@@ -492,10 +858,10 @@ ___thunder___: bool = True
|
||||
Y: bool = False
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
@@ -515,7 +881,7 @@ reveal_type(Y) # revealed: bool
|
||||
|
||||
### Simple list `__all__`
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["X"]
|
||||
@@ -524,10 +890,10 @@ X: bool = True
|
||||
Y: bool = False
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
|
||||
@@ -537,7 +903,9 @@ reveal_type(Y) # revealed: bool
|
||||
|
||||
### `__all__` with additions later on in the global scope
|
||||
|
||||
The [typing spec] lists certain modifications to `__all__` that must be understood by type checkers.
|
||||
The
|
||||
[typing spec](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
|
||||
lists certain modifications to `__all__` that must be understood by type checkers.
|
||||
|
||||
`a.py`:
|
||||
|
||||
@@ -589,7 +957,7 @@ reveal_type(F) # revealed: bool
|
||||
Whereas there are many ways of adding to `__all__` that type checkers must support, there is only
|
||||
one way of subtracting from `__all__` that type checkers are required to support:
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["A", "B"]
|
||||
@@ -599,10 +967,10 @@ A: bool = True
|
||||
B: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(A) # revealed: bool
|
||||
|
||||
@@ -618,11 +986,11 @@ a wildcard import from module `a` will fail at runtime.
|
||||
TODO: Should we:
|
||||
|
||||
1. Emit a diagnostic at the invalid definition of `__all__` (which will not fail at runtime)?
|
||||
1. Emit a diagnostic at the star-import from the module with the invalid `__all__` (which _will_
|
||||
1. Emit a diagnostic at the star-import from the module with the invalid `__all__` (which *will*
|
||||
fail at runtime)?
|
||||
1. Emit a diagnostic on both?
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
__all__ = ["a", "b"]
|
||||
@@ -630,11 +998,11 @@ __all__ = ["a", "b"]
|
||||
a = 42
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
# TODO we should consider emitting a diagnostic here (see prose description above)
|
||||
from a import * # fails with `AttributeError: module 'foo' has no attribute 'b'` at runtime
|
||||
from exporter import * # fails with `AttributeError: module 'foo' has no attribute 'b'` at runtime
|
||||
```
|
||||
|
||||
### Dynamic `__all__`
|
||||
@@ -645,7 +1013,7 @@ treat the module as though it has no `__all__` at all: all global-scope members
|
||||
be considered imported by the import statement. We should probably also emit a warning telling the
|
||||
user that we cannot statically determine the elements of `__all__`.
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
def f() -> str:
|
||||
@@ -658,10 +1026,10 @@ def g() -> int:
|
||||
__all__ = [f()]
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
# At runtime, `f` is imported but `g` is not; to avoid false positives, however,
|
||||
# we treat `a` as though it does not have `__all__` at all,
|
||||
@@ -677,7 +1045,7 @@ reveal_type(g) # revealed: Literal[g]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
@@ -692,15 +1060,15 @@ else:
|
||||
Z: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: bool
|
||||
|
||||
# TODO: should error with [unresolved-reference]
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Z) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -711,7 +1079,7 @@ reveal_type(Z) # revealed: Unknown
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
import sys
|
||||
@@ -727,15 +1095,15 @@ else:
|
||||
Z: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
reveal_type(Y) # revealed: bool
|
||||
|
||||
# TODO should have an [unresolved-reference] diagnostic
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Z) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -776,16 +1144,16 @@ reveal_type(Y) # revealed: bool
|
||||
If a name is included in `__all__` in a stub file, it is considered re-exported even if it was only
|
||||
defined using an import without the explicit `from foo import X as X` syntax:
|
||||
|
||||
`a.py`:
|
||||
`a.pyi`:
|
||||
|
||||
```py
|
||||
```pyi
|
||||
X: bool = True
|
||||
Y: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`b.pyi`:
|
||||
|
||||
```py
|
||||
```pyi
|
||||
from a import X, Y
|
||||
|
||||
__all__ = ["X"]
|
||||
@@ -796,10 +1164,17 @@ __all__ = ["X"]
|
||||
```py
|
||||
from b import *
|
||||
|
||||
reveal_type(X) # revealed: bool
|
||||
# TODO: should not error, should reveal `bool`
|
||||
# (`X` is re-exported from `b.pyi` due to presence in `__all__`)
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
|
||||
# TODO this should have an [unresolved-reference] diagnostic and reveal `Unknown`
|
||||
reveal_type(Y) # revealed: bool
|
||||
# This diagnostic is accurate: `Y` does not use the "redundant alias" convention in `b.pyi`,
|
||||
# nor is it included in `b.__all__`, so it is not exported from `b.pyi`
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(Y) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `global` statements in non-global scopes
|
||||
@@ -825,7 +1200,12 @@ from a import *
|
||||
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
|
||||
# TODO: false positive, should be `bool` with no diagnostic
|
||||
# TODO: we're undecided about whether we should consider this a false positive or not.
|
||||
# Mutating the global scope to add a symbol from an inner scope will not *necessarily* result
|
||||
# in the symbol being bound from the perspective of other modules (the function that creates
|
||||
# the inner scope, and adds the symbol to the global scope, might never be called!)
|
||||
# See discussion in https://github.com/astral-sh/ruff/pull/16959
|
||||
#
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(g) # revealed: Unknown
|
||||
|
||||
@@ -836,7 +1216,7 @@ reveal_type(h) # revealed: Unknown
|
||||
|
||||
## Cyclic star imports
|
||||
|
||||
Believe it or not, this code does _not_ raise an exception at runtime!
|
||||
Believe it or not, this code does *not* raise an exception at runtime!
|
||||
|
||||
`a.py`:
|
||||
|
||||
@@ -869,11 +1249,14 @@ The `collections.abc` standard-library module provides a good integration test,
|
||||
are present due to `*` imports.
|
||||
|
||||
```py
|
||||
import typing
|
||||
import collections.abc
|
||||
|
||||
reveal_type(collections.abc.Sequence) # revealed: Literal[Sequence]
|
||||
reveal_type(collections.abc.Callable) # revealed: typing.Callable
|
||||
|
||||
# TODO: false positive as it's only re-exported from `_collections.abc` due to presence in `__all__`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(collections.abc.Set) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Invalid `*` imports
|
||||
@@ -892,18 +1275,18 @@ from foo import * # error: [unresolved-import] "Cannot resolve import `foo`"
|
||||
A `*` import in a nested scope are always a syntax error. Red-knot does not infer any bindings from
|
||||
them:
|
||||
|
||||
`a.py`:
|
||||
`exporter.py`:
|
||||
|
||||
```py
|
||||
X: bool = True
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
`importer.py`:
|
||||
|
||||
```py
|
||||
def f():
|
||||
# TODO: we should emit a syntax errror here (tracked by https://github.com/astral-sh/ruff/issues/11934)
|
||||
from a import *
|
||||
from exporter import *
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(X) # revealed: Unknown
|
||||
@@ -948,4 +1331,3 @@ from a import *, *, _Y # error: [invalid-syntax]
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
[python language reference for import statements]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
||||
[typing spec]: https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
# Stub packages
|
||||
|
||||
Stub packages are packages named `<package>-stubs` that provide typing stubs for `<package>`. See
|
||||
[specification](https://typing.python.org/en/latest/spec/distributing.html#stub-only-packages).
|
||||
|
||||
## Simple stub
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo:
|
||||
name: str
|
||||
age: int
|
||||
```
|
||||
|
||||
`/packages/foo/__init__.py`:
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo import Foo
|
||||
|
||||
reveal_type(Foo().name) # revealed: str
|
||||
```
|
||||
|
||||
## Stubs only
|
||||
|
||||
The regular package isn't required for type checking.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo:
|
||||
name: str
|
||||
age: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo import Foo
|
||||
|
||||
reveal_type(Foo().name) # revealed: str
|
||||
```
|
||||
|
||||
## `-stubs` named module
|
||||
|
||||
A module named `<module>-stubs` isn't a stub package.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/foo-stubs.pyi`:
|
||||
|
||||
```pyi
|
||||
class Foo:
|
||||
name: str
|
||||
age: int
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from foo import Foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(Foo().name) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Namespace package in different search paths
|
||||
|
||||
A namespace package with multiple stub packages spread over multiple search paths.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/stubs1", "/stubs2", "/packages"]
|
||||
```
|
||||
|
||||
`/stubs1/shapes-stubs/polygons/pentagon.pyi`:
|
||||
|
||||
```pyi
|
||||
class Pentagon:
|
||||
sides: int
|
||||
area: float
|
||||
```
|
||||
|
||||
`/stubs2/shapes-stubs/polygons/hexagon.pyi`:
|
||||
|
||||
```pyi
|
||||
class Hexagon:
|
||||
sides: int
|
||||
area: float
|
||||
```
|
||||
|
||||
`/packages/shapes/polygons/pentagon.py`:
|
||||
|
||||
```py
|
||||
class Pentagon: ...
|
||||
```
|
||||
|
||||
`/packages/shapes/polygons/hexagon.py`:
|
||||
|
||||
```py
|
||||
class Hexagon: ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from shapes.polygons.hexagon import Hexagon
|
||||
from shapes.polygons.pentagon import Pentagon
|
||||
|
||||
reveal_type(Pentagon().sides) # revealed: int
|
||||
reveal_type(Hexagon().area) # revealed: int | float
|
||||
```
|
||||
|
||||
## Inconsistent stub packages
|
||||
|
||||
Stub packages where one is a namespace package and the other is a regular package. Module resolution
|
||||
should stop after the first non-namespace stub package. This matches Pyright's behavior.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/stubs1", "/stubs2", "/packages"]
|
||||
```
|
||||
|
||||
`/stubs1/shapes-stubs/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/stubs1/shapes-stubs/polygons/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
`/stubs1/shapes-stubs/polygons/pentagon.pyi`:
|
||||
|
||||
```pyi
|
||||
class Pentagon:
|
||||
sides: int
|
||||
area: float
|
||||
```
|
||||
|
||||
`/stubs2/shapes-stubs/polygons/hexagon.pyi`:
|
||||
|
||||
```pyi
|
||||
class Hexagon:
|
||||
sides: int
|
||||
area: float
|
||||
```
|
||||
|
||||
`/packages/shapes/polygons/pentagon.py`:
|
||||
|
||||
```py
|
||||
class Pentagon: ...
|
||||
```
|
||||
|
||||
`/packages/shapes/polygons/hexagon.py`:
|
||||
|
||||
```py
|
||||
class Hexagon: ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from shapes.polygons.pentagon import Pentagon
|
||||
from shapes.polygons.hexagon import Hexagon # error: [unresolved-import]
|
||||
|
||||
reveal_type(Pentagon().sides) # revealed: int
|
||||
reveal_type(Hexagon().area) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Namespace stubs for non-namespace package
|
||||
|
||||
The runtime package is a regular package but the stubs are namespace packages. Pyright skips the
|
||||
stub package if the "regular" package isn't a namespace package. I'm not aware that the behavior
|
||||
here is specified, and using the stubs without probing the runtime package first requires slightly
|
||||
fewer lookups.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/shapes-stubs/polygons/pentagon.pyi`:
|
||||
|
||||
```pyi
|
||||
class Pentagon:
|
||||
sides: int
|
||||
area: float
|
||||
```
|
||||
|
||||
`/packages/shapes-stubs/polygons/hexagon.pyi`:
|
||||
|
||||
```pyi
|
||||
class Hexagon:
|
||||
sides: int
|
||||
area: float
|
||||
```
|
||||
|
||||
`/packages/shapes/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/shapes/polygons/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`/packages/shapes/polygons/pentagon.py`:
|
||||
|
||||
```py
|
||||
class Pentagon: ...
|
||||
```
|
||||
|
||||
`/packages/shapes/polygons/hexagon.py`:
|
||||
|
||||
```py
|
||||
class Hexagon: ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from shapes.polygons.pentagon import Pentagon
|
||||
from shapes.polygons.hexagon import Hexagon
|
||||
|
||||
reveal_type(Pentagon().sides) # revealed: int
|
||||
reveal_type(Hexagon().area) # revealed: int | float
|
||||
```
|
||||
|
||||
## Stub package using `__init__.py` over `.pyi`
|
||||
|
||||
It's recommended that stub packages use `__init__.pyi` files over `__init__.py` but it doesn't seem
|
||||
to be an enforced convention. At least, Pyright is fine with the following.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["/packages"]
|
||||
```
|
||||
|
||||
`/packages/shapes-stubs/__init__.py`:
|
||||
|
||||
```py
|
||||
class Pentagon:
|
||||
sides: int
|
||||
area: float
|
||||
|
||||
class Hexagon:
|
||||
sides: int
|
||||
area: float
|
||||
```
|
||||
|
||||
`/packages/shapes/__init__.py`:
|
||||
|
||||
```py
|
||||
class Pentagon: ...
|
||||
class Hexagon: ...
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from shapes import Hexagon, Pentagon
|
||||
|
||||
reveal_type(Pentagon().sides) # revealed: int
|
||||
reveal_type(Hexagon().area) # revealed: int | float
|
||||
```
|
||||
@@ -76,6 +76,9 @@ No narrowing should occur if `type` is used to dynamically create a class:
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
# The following diagnostic is valid, since the three-argument form of `type`
|
||||
# can only be called with `str` as the first argument.
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
@@ -106,6 +109,24 @@ def _(x: A | B):
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## Narrowing for generic classes
|
||||
|
||||
Note that `type` returns the runtime class of an object, which does _not_ include specializations in
|
||||
the case of a generic class. (The typevars are erased.) That means we cannot narrow the type to the
|
||||
specialization that we compare with; we must narrow to an unknown specialization of the generic
|
||||
class.
|
||||
|
||||
```py
|
||||
class A[T = int]: ...
|
||||
class B: ...
|
||||
|
||||
def _[T](x: A | B):
|
||||
if type(x) is A[str]:
|
||||
reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown]
|
||||
else:
|
||||
reveal_type(x) # revealed: A[int] | B
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
```py
|
||||
|
||||
@@ -146,7 +146,7 @@ class C:
|
||||
@property
|
||||
def attr(self) -> int:
|
||||
return 1
|
||||
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 2 (`fset`) of bound method `setter`; expected type `(Any, Any, /) -> None`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `(Any, Any, /) -> None`, found `Literal[attr]`"
|
||||
@attr.setter
|
||||
def attr(self) -> None:
|
||||
pass
|
||||
@@ -156,7 +156,7 @@ class C:
|
||||
|
||||
```py
|
||||
class C:
|
||||
# error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 1 (`fget`) of class `property`; expected type `((Any, /) -> Any) | None`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `((Any, /) -> Any) | None`, found `Literal[attr]`"
|
||||
@property
|
||||
def attr(self, x: int) -> int:
|
||||
return 1
|
||||
|
||||
@@ -40,13 +40,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:11:5
|
||||
|
|
||||
9 | # error: [not-iterable]
|
||||
10 | for x in Iterable():
|
||||
11 | reveal_type(x) # revealed: int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -37,13 +37,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:8:5
|
||||
|
|
||||
6 | # error: [not-iterable]
|
||||
7 | for x in Bad():
|
||||
8 | reveal_type(x) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `Unknown`
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -59,13 +59,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:24:9
|
||||
|
|
||||
22 | for x in Iterable1():
|
||||
23 | # TODO... `int` might be ideal here?
|
||||
24 | reveal_type(x) # revealed: int | Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
25 |
|
||||
26 | # error: [not-iterable]
|
||||
|
|
||||
@@ -86,13 +86,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:29:9
|
||||
|
|
||||
27 | for y in Iterable2():
|
||||
28 | # TODO... `int` might be ideal here?
|
||||
29 | reveal_type(y) # revealed: int | Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -56,13 +56,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:22:9
|
||||
|
|
||||
20 | for x in Iterable1():
|
||||
21 | # TODO: `str` might be better
|
||||
22 | reveal_type(x) # revealed: str | Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `str | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `str | Unknown`
|
||||
23 |
|
||||
24 | # error: [not-iterable]
|
||||
|
|
||||
@@ -82,13 +82,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:26:9
|
||||
|
|
||||
24 | # error: [not-iterable]
|
||||
25 | for y in Iterable2():
|
||||
26 | reveal_type(y) # revealed: str | int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `str | int`
|
||||
| ^^^^^^^^^^^^^^ `str | int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -59,13 +59,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:18:9
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable1():
|
||||
18 | reveal_type(x) # revealed: int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
19 |
|
||||
20 | class Iterable2:
|
||||
|
|
||||
@@ -86,13 +86,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:30:9
|
||||
|
|
||||
28 | for x in Iterable2():
|
||||
29 | # TODO: `int` would probably be better here:
|
||||
30 | reveal_type(x) # revealed: int | Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -63,13 +63,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:29:9
|
||||
|
|
||||
27 | # error: [not-iterable]
|
||||
28 | for x in Iterable1():
|
||||
29 | reveal_type(x) # revealed: int | str
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int | str`
|
||||
| ^^^^^^^^^^^^^^ `int | str`
|
||||
30 |
|
||||
31 | # error: [not-iterable]
|
||||
|
|
||||
@@ -90,13 +90,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:34:9
|
||||
|
|
||||
32 | for y in Iterable2():
|
||||
33 | # TODO: `int` would probably be better here:
|
||||
34 | reveal_type(y) # revealed: int | Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `int | Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -48,13 +48,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:19:9
|
||||
|
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Iterable():
|
||||
19 | reveal_type(x) # revealed: int | bytes
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int | bytes`
|
||||
| ^^^^^^^^^^^^^^ `int | bytes`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -67,13 +67,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:33:9
|
||||
|
|
||||
31 | for x in Iterable1():
|
||||
32 | # TODO: `bytes | str` might be better
|
||||
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `bytes | str | Unknown`
|
||||
| ^^^^^^^^^^^^^^ `bytes | str | Unknown`
|
||||
34 |
|
||||
35 | # error: [not-iterable]
|
||||
|
|
||||
@@ -93,13 +93,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:37:9
|
||||
|
|
||||
35 | # error: [not-iterable]
|
||||
36 | for y in Iterable2():
|
||||
37 | reveal_type(y) # revealed: bytes | str | int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `bytes | str | int`
|
||||
| ^^^^^^^^^^^^^^ `bytes | str | int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -47,13 +47,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:18:9
|
||||
|
|
||||
16 | # error: [not-iterable]
|
||||
17 | for x in Iterable():
|
||||
18 | reveal_type(x) # revealed: int | bytes
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int | bytes`
|
||||
| ^^^^^^^^^^^^^^ `int | bytes`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -49,13 +49,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:19:9
|
||||
|
|
||||
17 | # error: [not-iterable]
|
||||
18 | for x in Test() if flag else Test2():
|
||||
19 | reveal_type(x) # revealed: int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -44,13 +44,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:14:9
|
||||
|
|
||||
12 | # error: [not-iterable]
|
||||
13 | for x in Test() if flag else 42:
|
||||
14 | reveal_type(x) # revealed: int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -57,13 +57,13 @@ warning: lint:possibly-unresolved-reference
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:16:5
|
||||
|
|
||||
14 | # revealed: Unknown
|
||||
15 | # error: [possibly-unresolved-reference]
|
||||
16 | reveal_type(x)
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `Unknown`
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -38,13 +38,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:9:5
|
||||
|
|
||||
7 | # error: [not-iterable]
|
||||
8 | for x in Bad():
|
||||
9 | reveal_type(x) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `Unknown`
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -42,13 +42,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:13:5
|
||||
|
|
||||
11 | # error: [not-iterable]
|
||||
12 | for x in Iterable():
|
||||
13 | reveal_type(x) # revealed: int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -53,13 +53,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:20:5
|
||||
|
|
||||
18 | # error: [not-iterable]
|
||||
19 | for x in Iterable1():
|
||||
20 | reveal_type(x) # revealed: int
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `int`
|
||||
| ^^^^^^^^^^^^^^ `int`
|
||||
21 |
|
||||
22 | # error: [not-iterable]
|
||||
|
|
||||
@@ -79,13 +79,13 @@ error: lint:not-iterable
|
||||
```
|
||||
|
||||
```
|
||||
info: revealed-type
|
||||
info: revealed-type: Revealed type
|
||||
--> /src/mdtest_snippet.py:24:5
|
||||
|
|
||||
22 | # error: [not-iterable]
|
||||
23 | for y in Iterable2():
|
||||
24 | reveal_type(y) # revealed: Unknown
|
||||
| ^^^^^^^^^^^^^^ Revealed type is `Unknown`
|
||||
| ^^^^^^^^^^^^^^ `Unknown`
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:5
|
||||
|
|
||||
2 | return x * x
|
||||
3 |
|
||||
4 | foo("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:9
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
2 | return x * x
|
||||
|
|
||||
|
||||
|
||||
@@ -23,19 +23,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:6:10
|
||||
|
|
||||
5 | c = C()
|
||||
6 | c.square("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`x`) of bound method `square`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:2:22
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | class C:
|
||||
2 | def square(self, x: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^^^^ ------ Parameter declared here
|
||||
3 | return x * x
|
||||
|
|
||||
|
||||
|
||||
@@ -27,19 +27,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:3:13
|
||||
|
|
||||
1 | import package
|
||||
2 |
|
||||
3 | package.foo("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/package.py:1:9
|
||||
info: Function defined here
|
||||
--> /src/package.py:1:5
|
||||
|
|
||||
1 | def foo(x: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
2 | return x * x
|
||||
|
|
||||
|
||||
|
||||
@@ -22,22 +22,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | def bar():
|
||||
2 | foo("hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
3 |
|
||||
4 | def foo(x: int) -> int:
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:4:9
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:4:5
|
||||
|
|
||||
2 | foo("hello") # error: [invalid-argument-type]
|
||||
3 |
|
||||
4 | def foo(x: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
5 | return x * x
|
||||
|
|
||||
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:8
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:17
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
|
||||
@@ -25,21 +25,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:8:8
|
||||
|
|
||||
6 | return x * y * z
|
||||
7 |
|
||||
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:3:5
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(
|
||||
| ^^^
|
||||
2 | x: int,
|
||||
3 | y: int,
|
||||
| ------ parameter declared in function definition here
|
||||
| ------ Parameter declared here
|
||||
4 | z: int,
|
||||
5 | ) -> int:
|
||||
|
|
||||
|
||||
@@ -24,57 +24,57 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:7:5
|
||||
|
|
||||
5 | # error: [invalid-argument-type]
|
||||
6 | # error: [invalid-argument-type]
|
||||
7 | foo("a", "b", "c")
|
||||
| ^^^ Object of type `Literal["a"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
||||
| ^^^ Expected `int`, found `Literal["a"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:9
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:7:10
|
||||
|
|
||||
5 | # error: [invalid-argument-type]
|
||||
6 | # error: [invalid-argument-type]
|
||||
7 | foo("a", "b", "c")
|
||||
| ^^^ Object of type `Literal["b"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
| ^^^ Expected `int`, found `Literal["b"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:17
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:7:15
|
||||
|
|
||||
5 | # error: [invalid-argument-type]
|
||||
6 | # error: [invalid-argument-type]
|
||||
7 | foo("a", "b", "c")
|
||||
| ^^^ Object of type `Literal["c"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
||||
| ^^^ Expected `int`, found `Literal["c"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:25
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
|
||||
@@ -20,21 +20,23 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:3:12
|
||||
|
|
||||
1 | import json
|
||||
2 |
|
||||
3 | json.loads(5) # error: [invalid-argument-type]
|
||||
| ^ Object of type `Literal[5]` cannot be assigned to parameter 1 (`s`) of function `loads`; expected type `str | bytes | bytearray`
|
||||
| ^ Expected `str | bytes | bytearray`, found `Literal[5]`
|
||||
|
|
||||
info
|
||||
--> stdlib/json/__init__.pyi:40:5
|
||||
info: Function defined here
|
||||
--> stdlib/json/__init__.pyi:39:5
|
||||
|
|
||||
37 | **kwds: Any,
|
||||
38 | ) -> None: ...
|
||||
39 | def loads(
|
||||
| ^^^^^
|
||||
40 | s: str | bytes | bytearray,
|
||||
| -------------------------- parameter declared in function definition here
|
||||
| -------------------------- Parameter declared here
|
||||
41 | *,
|
||||
42 | cls: type[JSONDecoder] | None = None,
|
||||
|
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:11
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
||||
| ^^^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:28
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
|
||||
| ---------- parameter declared in function definition here
|
||||
| ^^^ ---------- Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:11
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
||||
| ^^^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:31
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
|
||||
| ---------- parameter declared in function definition here
|
||||
| ^^^ ---------- Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:11
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:25
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int = 0) -> int:
|
||||
| ---------- parameter declared in function definition here
|
||||
| ^^^ ---------- Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:8
|
||||
|
|
||||
2 | return x * y * z
|
||||
3 |
|
||||
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:17
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(x: int, y: int, z: int, /) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^ ------ Parameter declared here
|
||||
2 | return x * y * z
|
||||
|
|
||||
|
||||
|
||||
@@ -23,19 +23,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:6:3
|
||||
|
|
||||
5 | c = C()
|
||||
6 | c("wrong") # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["wrong"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:2:24
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | class C:
|
||||
2 | def __call__(self, x: int) -> int:
|
||||
| ------ parameter declared in function definition here
|
||||
| ^^^^^^^^ ------ Parameter declared here
|
||||
3 | return 1
|
||||
|
|
||||
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:14
|
||||
|
|
||||
2 | return len(numbers)
|
||||
3 |
|
||||
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
|
||||
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `*numbers` of function `foo`; expected type `int`
|
||||
| ^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:9
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(*numbers: int) -> int:
|
||||
| ------------- parameter declared in function definition here
|
||||
| ^^^ ------------- Parameter declared here
|
||||
2 | return len(numbers)
|
||||
|
|
||||
|
||||
|
||||
@@ -21,19 +21,19 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invali
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-argument-type
|
||||
error: lint:invalid-argument-type: Argument to this function is incorrect
|
||||
--> /src/mdtest_snippet.py:4:20
|
||||
|
|
||||
2 | return len(numbers)
|
||||
3 |
|
||||
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
|
||||
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `**numbers` of function `foo`; expected type `int`
|
||||
| ^^^^^^^^^ Expected `int`, found `Literal["hello"]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:1:9
|
||||
info: Function defined here
|
||||
--> /src/mdtest_snippet.py:1:5
|
||||
|
|
||||
1 | def foo(**numbers: int) -> int:
|
||||
| -------------- parameter declared in function definition here
|
||||
| ^^^ -------------- Parameter declared here
|
||||
2 | return len(numbers)
|
||||
|
|
||||
|
||||
|
||||
@@ -31,67 +31,56 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type
|
||||
--> /src/mdtest_snippet.py:6:16
|
||||
|
|
||||
4 | else:
|
||||
5 | # error: [invalid-return-type]
|
||||
6 | return 1
|
||||
| ^ Object of type `Literal[1]` is not assignable to return type `str`
|
||||
7 |
|
||||
8 | def f(cond: bool) -> str:
|
||||
|
|
||||
info
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> /src/mdtest_snippet.py:1:22
|
||||
|
|
||||
1 | def f(cond: bool) -> str:
|
||||
| --- Return type is declared here as `str`
|
||||
| --- Expected `str` because of return type
|
||||
2 | if cond:
|
||||
3 | return "a"
|
||||
4 | else:
|
||||
5 | # error: [invalid-return-type]
|
||||
6 | return 1
|
||||
| ^ Expected `str`, found `Literal[1]`
|
||||
7 |
|
||||
8 | def f(cond: bool) -> str:
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type
|
||||
--> /src/mdtest_snippet.py:11:16
|
||||
|
|
||||
9 | if cond:
|
||||
10 | # error: [invalid-return-type]
|
||||
11 | return 1
|
||||
| ^ Object of type `Literal[1]` is not assignable to return type `str`
|
||||
12 | else:
|
||||
13 | # error: [invalid-return-type]
|
||||
|
|
||||
info
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> /src/mdtest_snippet.py:8:22
|
||||
|
|
||||
6 | return 1
|
||||
7 |
|
||||
8 | def f(cond: bool) -> str:
|
||||
| --- Return type is declared here as `str`
|
||||
| --- Expected `str` because of return type
|
||||
9 | if cond:
|
||||
10 | # error: [invalid-return-type]
|
||||
11 | return 1
|
||||
| ^ Expected `str`, found `Literal[1]`
|
||||
12 | else:
|
||||
13 | # error: [invalid-return-type]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> /src/mdtest_snippet.py:14:16
|
||||
|
|
||||
12 | else:
|
||||
13 | # error: [invalid-return-type]
|
||||
14 | return 2
|
||||
| ^ Object of type `Literal[2]` is not assignable to return type `str`
|
||||
| ^ Expected `str`, found `Literal[2]`
|
||||
|
|
||||
info
|
||||
--> /src/mdtest_snippet.py:8:22
|
||||
::: /src/mdtest_snippet.py:8:22
|
||||
|
|
||||
6 | return 1
|
||||
7 |
|
||||
8 | def f(cond: bool) -> str:
|
||||
| --- Return type is declared here as `str`
|
||||
| --- Expected `str` because of return type
|
||||
9 | if cond:
|
||||
10 | # error: [invalid-return-type]
|
||||
|
|
||||
|
||||
@@ -40,23 +40,17 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type
|
||||
--> /src/mdtest_snippet.py:4:16
|
||||
|
|
||||
2 | if False:
|
||||
3 | # error: [invalid-return-type]
|
||||
4 | return 1
|
||||
| ^ Object of type `Literal[1]` is not assignable to return type `None`
|
||||
5 |
|
||||
6 | # error: [invalid-return-type]
|
||||
|
|
||||
info
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> /src/mdtest_snippet.py:1:12
|
||||
|
|
||||
1 | def f() -> None:
|
||||
| ---- Return type is declared here as `None`
|
||||
| ---- Expected `None` because of return type
|
||||
2 | if False:
|
||||
3 | # error: [invalid-return-type]
|
||||
4 | return 1
|
||||
| ^ Expected `None`, found `Literal[1]`
|
||||
5 |
|
||||
6 | # error: [invalid-return-type]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -47,49 +47,35 @@ error: lint:invalid-return-type
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type
|
||||
--> /src/mdtest_snippet.py:7:12
|
||||
|
|
||||
5 | def f() -> str:
|
||||
6 | # error: [invalid-return-type]
|
||||
7 | return 1
|
||||
| ^ Object of type `Literal[1]` is not assignable to return type `str`
|
||||
8 |
|
||||
9 | def f() -> int:
|
||||
|
|
||||
info
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> /src/mdtest_snippet.py:5:12
|
||||
|
|
||||
3 | 1
|
||||
4 |
|
||||
5 | def f() -> str:
|
||||
| --- Return type is declared here as `str`
|
||||
| --- Expected `str` because of return type
|
||||
6 | # error: [invalid-return-type]
|
||||
7 | return 1
|
||||
| ^ Expected `str`, found `Literal[1]`
|
||||
8 |
|
||||
9 | def f() -> int:
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type
|
||||
--> /src/mdtest_snippet.py:11:5
|
||||
|
|
||||
9 | def f() -> int:
|
||||
10 | # error: [invalid-return-type]
|
||||
11 | return
|
||||
| ^^^^^^ Object of type `None` is not assignable to return type `int`
|
||||
12 |
|
||||
13 | from typing import TypeVar
|
||||
|
|
||||
info
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> /src/mdtest_snippet.py:9:12
|
||||
|
|
||||
7 | return 1
|
||||
8 |
|
||||
9 | def f() -> int:
|
||||
| --- Return type is declared here as `int`
|
||||
| --- Expected `int` because of return type
|
||||
10 | # error: [invalid-return-type]
|
||||
11 | return
|
||||
| ^^^^^^ Expected `int`, found `None`
|
||||
12 |
|
||||
13 | from typing import TypeVar
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -30,23 +30,16 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_ty
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-return-type
|
||||
--> /src/mdtest_snippet.pyi:3:12
|
||||
|
|
||||
1 | def f() -> int:
|
||||
2 | # error: [invalid-return-type]
|
||||
3 | return ...
|
||||
| ^^^ Object of type `ellipsis` is not assignable to return type `int`
|
||||
4 |
|
||||
5 | # error: [invalid-return-type]
|
||||
|
|
||||
info
|
||||
error: lint:invalid-return-type: Return type does not match returned value
|
||||
--> /src/mdtest_snippet.pyi:1:12
|
||||
|
|
||||
1 | def f() -> int:
|
||||
| --- Return type is declared here as `int`
|
||||
| --- Expected `int` because of return type
|
||||
2 | # error: [invalid-return-type]
|
||||
3 | return ...
|
||||
| ^^^ Expected `int`, found `ellipsis`
|
||||
4 |
|
||||
5 | # error: [invalid-return-type]
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
@@ -1502,13 +1502,14 @@ if True:
|
||||
from module import symbol
|
||||
```
|
||||
|
||||
## Unsupported features
|
||||
## Unreachable code
|
||||
|
||||
We do not support full unreachable code analysis yet. We also raise diagnostics from
|
||||
statically-known to be false branches:
|
||||
A closely related feature is the ability to detect unreachable code. For example, we do not emit a
|
||||
diagnostic here:
|
||||
|
||||
```py
|
||||
if False:
|
||||
# error: [unresolved-reference]
|
||||
x
|
||||
```
|
||||
|
||||
See [unreachable.md](unreachable.md) for more tests on this topic.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user