Compare commits

..

10 Commits

Author SHA1 Message Date
Dhruv Manilawala
f3c483a545 Fix is_airflow_* function, add docs 2024-12-30 21:15:23 +05:30
Wei Lee
c59cdd25de refactor(AIR303): refactor utility functions with is_airflow_builtin_or_provider 2024-12-30 23:10:08 +09:00
Wei Lee
8026d7712c feat(AIR302): airflow.hooks.base_hook.BaseHook → airflow.hooks.base.BaseHook 2024-12-30 23:10:08 +09:00
Wei Lee
7ca2d283c1 feat(AIR302): argument appbuilder is now removed in BaseAuthManager and its subclasses 2024-12-30 23:10:08 +09:00
Wei Lee
316126cf38 feat(AIR302): extension "executors", "operators", "sensors", "hooks" have been removed in AirflowPlugin 2024-12-30 23:10:08 +09:00
Wei Lee
39d545b738 feat(AIR302): argument "filename_template" is removed in task handlers 2024-12-30 23:10:08 +09:00
Wei Lee
aa049d5071 feat(AIR302): argument sla is removed and argument task_concurrency is renamed as max_active_tis_per_dag in all airflow operators 2024-12-30 23:10:08 +09:00
Wei Lee
1743f029f1 refactor(AIR302): refactor regex usage 2024-12-30 23:10:08 +09:00
Wei Lee
1483804487 feat(AIR302): add function removed_class_attribute and the following rules
* `airflow.providers_manager.ProvidersManager.dataset_factories` → `airflow.providers_manager.ProvidersManager.asset_factories`
* `airflow.providers_manager.ProvidersManager.dataset_uri_handlers` → `airflow.providers_manager.ProvidersManager.asset_uri_handlers`
* `airflow.providers_manager.ProvidersManager.dataset_to_openlineage_converters` → `airflow.providers_manager.ProvidersManager.asset_to_openlineage_converters`
* `airflow.lineage.hook.DatasetLineageInfo.dataset`  → `airflow.lineage.hook.AssetLineageInfo.asset`
2024-12-30 23:10:08 +09:00
Wei Lee
e292b7b277 feat(AIR302): extend the following rules
Any class in Airflow that inherits these class should not have these methods

* `airflow.secrets.base_secrets.BaseSecretsBackend.get_conn_uri` → `airflow.secrets.base_secrets.BaseSecretsBackend.get_conn_value`
* `airflow.secrets.base_secrets.BaseSecretsBackend.get_connections` → `airflow.secrets.base_secrets.BaseSecretsBackend.get_connection`
* `airflow.hooks.base.BaseHook.get_connections` → use `get_connection`
* `airflow.datasets.BaseDataset.iter_datasets` → `airflow.sdk.definitions.asset.BaseAsset.iter_assets`
* `airflow.datasets.BaseDataset.iter_dataset_aliases` → `airflow.sdk.definitions.asset.BaseAsset.iter_asset_aliases`
2024-12-30 23:10:08 +09:00
548 changed files with 11827 additions and 30742 deletions

View File

@@ -45,7 +45,7 @@
groupName: "Artifact GitHub Actions dependencies",
matchManagers: ["github-actions"],
matchDatasources: ["gitea-tags", "github-tags"],
matchPackageNames: ["actions/.*-artifact"],
matchPackagePatterns: ["actions/.*-artifact"],
description: "Weekly update of artifact-related GitHub Actions dependencies",
},
{
@@ -61,7 +61,7 @@
{
// Disable updates of `zip-rs`; intentionally pinned for now due to ownership change
// See: https://github.com/astral-sh/uv/issues/3642
matchPackageNames: ["zip"],
matchPackagePatterns: ["zip"],
matchManagers: ["cargo"],
enabled: false,
},
@@ -70,7 +70,7 @@
// with `mkdocs-material-insider`.
// See: https://squidfunk.github.io/mkdocs-material/insiders/upgrade/
matchManagers: ["pip_requirements"],
matchPackageNames: ["mkdocs-material"],
matchPackagePatterns: ["mkdocs-material"],
enabled: false,
},
{
@@ -87,13 +87,13 @@
{
groupName: "Monaco",
matchManagers: ["npm"],
matchPackageNames: ["monaco"],
matchPackagePatterns: ["monaco"],
description: "Weekly update of the Monaco editor",
},
{
groupName: "strum",
matchManagers: ["cargo"],
matchPackageNames: ["strum"],
matchPackagePatterns: ["strum"],
description: "Weekly update of strum dependencies",
},
{

View File

@@ -48,13 +48,11 @@ jobs:
- name: Check tag consistency
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
env:
TAG: ${{ inputs.plan != '' && fromJson(inputs.plan).announcement_tag || 'dry-run' }}
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${TAG}" != "${version}" ]; then
if [ "${{ fromJson(inputs.plan).announcement_tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${TAG}" >&2
echo "${{ fromJson(inputs.plan).announcement_tag }}" >&2
echo "${version}" >&2
exit 1
else
@@ -177,8 +175,6 @@ jobs:
- name: Generate Dynamic Dockerfile Tags
shell: bash
env:
TAG_VALUE: ${{ fromJson(inputs.plan).announcement_tag }}
run: |
set -euo pipefail
@@ -199,8 +195,8 @@ jobs:
# Loop through all base tags and append its docker metadata pattern to the list
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
IFS=','; for TAG in ${BASE_TAGS}; do
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${TAG_VALUE}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${TAG_VALUE}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ version }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=pep440,pattern={{ major }}.{{ minor }},suffix=-${TAG},value=${{ fromJson(inputs.plan).announcement_tag }}\n"
TAG_PATTERNS="${TAG_PATTERNS}type=raw,value=${TAG}\n"
done

View File

@@ -386,7 +386,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes
- run: ./scripts/add_rule.py --name DoTheThing --prefix PL --code C0999 --linter pylint
- run: cargo check
- run: cargo fmt --all --check
- run: |

View File

@@ -73,6 +73,6 @@ jobs:
owner: "astral-sh",
repo: "ruff",
title: `Daily parser fuzz failed on ${new Date().toDateString()}`,
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
body: "Runs listed here: https://github.com/astral-sh/ruff/actions/workflows/daily_fuzz.yml",
labels: ["bug", "parser", "fuzzer"],
})

View File

@@ -1,71 +0,0 @@
name: Daily property test run
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
pull_request:
paths:
- ".github/workflows/daily_property_tests.yaml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
FORCE_COLOR: 1
jobs:
property_tests:
name: Property tests
runs-on: ubuntu-latest
timeout-minutes: 20
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@v2
- 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.
run: cargo build --locked --release --package red_knot_python_semantic --tests
- name: Run property tests
shell: bash
run: |
export QUICKCHECK_TESTS=100000
for _ in {1..5}; do
cargo test --locked --release --package red_knot_python_semantic -- --ignored types::property_tests::stable
done
create-issue-on-failure:
name: Create an issue if the daily property test run surfaced any bugs
runs-on: ubuntu-latest
needs: property_tests
if: ${{ github.repository == 'astral-sh/ruff' && always() && github.event_name == 'schedule' && needs.property_tests.result == 'failure' }}
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.create({
owner: "astral-sh",
repo: "ruff",
title: `Daily property test run failed on ${new Date().toDateString()}`,
body: "Run listed here: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
labels: ["bug", "red-knot", "testing"],
})

View File

@@ -33,9 +33,8 @@ jobs:
python-version: 3.12
- name: "Set docs version"
env:
version: ${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}
run: |
version="${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}"
# if version is missing, use 'latest'
if [ -z "$version" ]; then
echo "Using 'latest' as version"

6
.github/zizmor.yml vendored
View File

@@ -1,12 +1,6 @@
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
# https://woodruffw.github.io/zizmor/configuration/
#
# TODO: can we remove the ignores here so that our workflows are more secure?
rules:
dangerous-triggers:
ignore:
- pr-comment.yaml
cache-poisoning:
ignore:
- build-docker.yml
- publish-playground.yml

View File

@@ -59,7 +59,7 @@ repos:
- black==24.10.0
- repo: https://github.com/crate-ci/typos
rev: v1.29.4
rev: v1.28.4
hooks:
- id: typos
@@ -73,7 +73,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6
rev: v0.8.4
hooks:
- id: ruff-format
- id: ruff
@@ -91,7 +91,7 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.0.0
rev: v0.10.0
hooks:
- id: zizmor
@@ -103,7 +103,7 @@ repos:
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
- repo: https://github.com/rhysd/actionlint
rev: v1.7.6
rev: v1.7.5
hooks:
- id: actionlint
stages:

View File

@@ -1,9 +1,5 @@
# Breaking Changes
## 0.9.0
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the [changelog](./CHANGELOG.md#090) for a detailed list of changes.
## 0.8.0
- **Default to Python 3.9**

View File

@@ -1,180 +1,5 @@
# Changelog
## 0.9.1
### Preview features
- \[`pycodestyle`\] Run `too-many-newlines-at-end-of-file` on each cell in notebooks (`W391`) ([#15308](https://github.com/astral-sh/ruff/pull/15308))
- \[`ruff`\] Omit diagnostic for shadowed private function parameters in `used-dummy-variable` (`RUF052`) ([#15376](https://github.com/astral-sh/ruff/pull/15376))
### Rule changes
- \[`flake8-bugbear`\] Improve `assert-raises-exception` message (`B017`) ([#15389](https://github.com/astral-sh/ruff/pull/15389))
### Formatter
- Preserve trailing end-of line comments for the last string literal in implicitly concatenated strings ([#15378](https://github.com/astral-sh/ruff/pull/15378))
### Server
- Fix a bug where the server and client notebooks were out of sync after reordering cells ([#15398](https://github.com/astral-sh/ruff/pull/15398))
### Bug fixes
- \[`flake8-pie`\] Correctly remove wrapping parentheses (`PIE800`) ([#15394](https://github.com/astral-sh/ruff/pull/15394))
- \[`pyupgrade`\] Handle comments and multiline expressions correctly (`UP037`) ([#15337](https://github.com/astral-sh/ruff/pull/15337))
## 0.9.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.9.0) for a migration guide and overview of the changes!
### Breaking changes
Ruff now formats your code according to the 2025 style guide. As a result, your code might now get formatted differently. See the formatter section for a detailed list of changes.
This release doesnt remove or remap any existing stable rules.
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) (`A005`).
This rule has also been renamed: previously, it was called `builtin-module-shadowing`.
- [`builtin-lambda-argument-shadowing`](https://docs.astral.sh/ruff/rules/builtin-lambda-argument-shadowing/) (`A006`)
- [`slice-to-remove-prefix-or-suffix`](https://docs.astral.sh/ruff/rules/slice-to-remove-prefix-or-suffix/) (`FURB188`)
- [`boolean-chained-comparison`](https://docs.astral.sh/ruff/rules/boolean-chained-comparison/) (`PLR1716`)
- [`decimal-from-float-literal`](https://docs.astral.sh/ruff/rules/decimal-from-float-literal/) (`RUF032`)
- [`post-init-default`](https://docs.astral.sh/ruff/rules/post-init-default/) (`RUF033`)
- [`useless-if-else`](https://docs.astral.sh/ruff/rules/useless-if-else/) (`RUF034`)
The following behaviors have been stabilized:
- [`pytest-parametrize-names-wrong-type`](https://docs.astral.sh/ruff/rules/pytest-parametrize-names-wrong-type/) (`PT006`): Detect [`pytest.parametrize`](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize) calls outside decorators and calls with keyword arguments.
- [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/) (`E402`): Ignore [`pytest.importorskip`](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-importorskip) calls between import statements.
- [`mutable-dataclass-default`](https://docs.astral.sh/ruff/rules/mutable-dataclass-default/) (`RUF008`) and [`function-call-in-dataclass-default-argument`](https://docs.astral.sh/ruff/rules/function-call-in-dataclass-default-argument/) (`RUF009`): Add support for [`attrs`](https://www.attrs.org/en/stable/).
- [`bad-version-info-comparison`](https://docs.astral.sh/ruff/rules/bad-version-info-comparison/) (`PYI006`): Extend the rule to check non-stub files.
The following fixes or improvements to fixes have been stabilized:
- [`redundant-numeric-union`](https://docs.astral.sh/ruff/rules/redundant-numeric-union/) (`PYI041`)
- [`duplicate-union-members`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`)
### Formatter
This release introduces the new 2025 stable style ([#13371](https://github.com/astral-sh/ruff/issues/13371)), stabilizing the following changes:
- Format expressions in f-string elements ([#7594](https://github.com/astral-sh/ruff/issues/7594))
- Alternate quotes for strings inside f-strings ([#13860](https://github.com/astral-sh/ruff/pull/13860))
- Preserve the casing of hex codes in f-string debug expressions ([#14766](https://github.com/astral-sh/ruff/issues/14766))
- Choose the quote style for each string literal in an implicitly concatenated f-string rather than for the entire string ([#13539](https://github.com/astral-sh/ruff/pull/13539))
- Automatically join an implicitly concatenated string into a single string literal if it fits on a single line ([#9457](https://github.com/astral-sh/ruff/issues/9457))
- Remove the [`ISC001`](https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/) incompatibility warning ([#15123](https://github.com/astral-sh/ruff/pull/15123))
- Prefer parenthesizing the `assert` message over breaking the assertion expression ([#9457](https://github.com/astral-sh/ruff/issues/9457))
- Automatically parenthesize over-long `if` guards in `match` `case` clauses ([#13513](https://github.com/astral-sh/ruff/pull/13513))
- More consistent formatting for `match` `case` patterns ([#6933](https://github.com/astral-sh/ruff/issues/6933))
- Avoid unnecessary parentheses around return type annotations ([#13381](https://github.com/astral-sh/ruff/pull/13381))
- Keep the opening parentheses on the same line as the `if` keyword for comprehensions where the condition has a leading comment ([#12282](https://github.com/astral-sh/ruff/pull/12282))
- More consistent formatting for `with` statements with a single context manager for Python 3.8 or older ([#10276](https://github.com/astral-sh/ruff/pull/10276))
- Correctly calculate the line-width for code blocks in docstrings when using `max-doc-code-line-length = "dynamic"` ([#13523](https://github.com/astral-sh/ruff/pull/13523))
### Preview features
- \[`flake8-bugbear`\] Implement `class-as-data-structure` (`B903`) ([#9601](https://github.com/astral-sh/ruff/pull/9601))
- \[`flake8-type-checking`\] Apply `quoted-type-alias` more eagerly in `TYPE_CHECKING` blocks and ignore it in stubs (`TC008`) ([#15180](https://github.com/astral-sh/ruff/pull/15180))
- \[`pylint`\] Ignore `eq-without-hash` in stub files (`PLW1641`) ([#15310](https://github.com/astral-sh/ruff/pull/15310))
- \[`pyupgrade`\] Split `UP007` into two individual rules: `UP007` for `Union` and `UP045` for `Optional` (`UP007`, `UP045`) ([#15313](https://github.com/astral-sh/ruff/pull/15313))
- \[`ruff`\] New rule that detects classes that are both an enum and a `dataclass` (`RUF049`) ([#15299](https://github.com/astral-sh/ruff/pull/15299))
- \[`ruff`\] Recode `RUF025` to `RUF037` (`RUF037`) ([#15258](https://github.com/astral-sh/ruff/pull/15258))
### Rule changes
- \[`flake8-builtins`\] Ignore [`stdlib-module-shadowing`](https://docs.astral.sh/ruff/rules/stdlib-module-shadowing/) in stub files(`A005`) ([#15350](https://github.com/astral-sh/ruff/pull/15350))
- \[`flake8-return`\] Add support for functions returning `typing.Never` (`RET503`) ([#15298](https://github.com/astral-sh/ruff/pull/15298))
### Server
- Improve the observability by removing the need for the ["trace" value](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#traceValue) to turn on or off logging. The server logging is solely controlled using the [`logLevel` server setting](https://docs.astral.sh/ruff/editors/settings/#loglevel)
which defaults to `info`. This addresses the issue where users were notified about an error and told to consult the log, but it didnt contain any messages. ([#15232](https://github.com/astral-sh/ruff/pull/15232))
- Ignore diagnostics from other sources for code action requests ([#15373](https://github.com/astral-sh/ruff/pull/15373))
### CLI
- Improve the error message for `--config key=value` when the `key` is for a table and its a simple `value`
### Bug fixes
- \[`eradicate`\] Ignore metadata blocks directly followed by normal blocks (`ERA001`) ([#15330](https://github.com/astral-sh/ruff/pull/15330))
- \[`flake8-django`\] Recognize other magic methods (`DJ012`) ([#15365](https://github.com/astral-sh/ruff/pull/15365))
- \[`pycodestyle`\] Avoid false positives related to type aliases (`E252`) ([#15356](https://github.com/astral-sh/ruff/pull/15356))
- \[`pydocstyle`\] Avoid treating newline-separated sections as sub-sections (`D405`) ([#15311](https://github.com/astral-sh/ruff/pull/15311))
- \[`pyflakes`\] Remove call when removing final argument from `format` (`F523`) ([#15309](https://github.com/astral-sh/ruff/pull/15309))
- \[`refurb`\] Mark fix as unsafe when the right-hand side is a string (`FURB171`) ([#15273](https://github.com/astral-sh/ruff/pull/15273))
- \[`ruff`\] Treat `)` as a regex metacharacter (`RUF043`, `RUF055`) ([#15318](https://github.com/astral-sh/ruff/pull/15318))
- \[`ruff`\] Parenthesize the `int`-call argument when removing the `int` call would change semantics (`RUF046`) ([#15277](https://github.com/astral-sh/ruff/pull/15277))
## 0.8.6
### Preview features
- \[`format`\]: Preserve multiline implicit concatenated strings in docstring positions ([#15126](https://github.com/astral-sh/ruff/pull/15126))
- \[`ruff`\] Add rule to detect empty literal in deque call (`RUF025`) ([#15104](https://github.com/astral-sh/ruff/pull/15104))
- \[`ruff`\] Avoid reporting when `ndigits` is possibly negative (`RUF057`) ([#15234](https://github.com/astral-sh/ruff/pull/15234))
### Rule changes
- \[`flake8-todos`\] remove issue code length restriction (`TD003`) ([#15175](https://github.com/astral-sh/ruff/pull/15175))
- \[`pyflakes`\] Ignore errors in `@no_type_check` string annotations (`F722`, `F821`) ([#15215](https://github.com/astral-sh/ruff/pull/15215))
### CLI
- Show errors for attempted fixes only when passed `--verbose` ([#15237](https://github.com/astral-sh/ruff/pull/15237))
### Bug fixes
- \[`ruff`\] Avoid syntax error when removing int over multiple lines (`RUF046`) ([#15230](https://github.com/astral-sh/ruff/pull/15230))
- \[`pyupgrade`\] Revert "Add all PEP-585 names to `UP006` rule" ([#15250](https://github.com/astral-sh/ruff/pull/15250))
## 0.8.5
### Preview features
- \[`airflow`\] Extend names moved from core to provider (`AIR303`) ([#15145](https://github.com/astral-sh/ruff/pull/15145), [#15159](https://github.com/astral-sh/ruff/pull/15159), [#15196](https://github.com/astral-sh/ruff/pull/15196), [#15216](https://github.com/astral-sh/ruff/pull/15216))
- \[`airflow`\] Extend rule to check class attributes, methods, arguments (`AIR302`) ([#15054](https://github.com/astral-sh/ruff/pull/15054), [#15083](https://github.com/astral-sh/ruff/pull/15083))
- \[`fastapi`\] Update `FAST002` to check keyword-only arguments ([#15119](https://github.com/astral-sh/ruff/pull/15119))
- \[`flake8-type-checking`\] Disable `TC006` and `TC007` in stub files ([#15179](https://github.com/astral-sh/ruff/pull/15179))
- \[`pylint`\] Detect nested methods correctly (`PLW1641`) ([#15032](https://github.com/astral-sh/ruff/pull/15032))
- \[`ruff`\] Detect more strict-integer expressions (`RUF046`) ([#14833](https://github.com/astral-sh/ruff/pull/14833))
- \[`ruff`\] Implement `falsy-dict-get-fallback` (`RUF056`) ([#15160](https://github.com/astral-sh/ruff/pull/15160))
- \[`ruff`\] Implement `unnecessary-round` (`RUF057`) ([#14828](https://github.com/astral-sh/ruff/pull/14828))
### Rule changes
- Visit PEP 764 inline `TypedDict` keys as non-type-expressions ([#15073](https://github.com/astral-sh/ruff/pull/15073))
- \[`flake8-comprehensions`\] Skip `C416` if comprehension contains unpacking ([#14909](https://github.com/astral-sh/ruff/pull/14909))
- \[`flake8-pie`\] Allow `cast(SomeType, ...)` (`PIE796`) ([#15141](https://github.com/astral-sh/ruff/pull/15141))
- \[`flake8-simplify`\] More precise inference for dictionaries (`SIM300`) ([#15164](https://github.com/astral-sh/ruff/pull/15164))
- \[`flake8-use-pathlib`\] Catch redundant joins in `PTH201` and avoid syntax errors ([#15177](https://github.com/astral-sh/ruff/pull/15177))
- \[`pycodestyle`\] Preserve original value format (`E731`) ([#15097](https://github.com/astral-sh/ruff/pull/15097))
- \[`pydocstyle`\] Split on first whitespace character (`D403`) ([#15082](https://github.com/astral-sh/ruff/pull/15082))
- \[`pyupgrade`\] Add all PEP-585 names to `UP006` rule ([#5454](https://github.com/astral-sh/ruff/pull/5454))
### Configuration
- \[`flake8-type-checking`\] Improve flexibility of `runtime-evaluated-decorators` ([#15204](https://github.com/astral-sh/ruff/pull/15204))
- \[`pydocstyle`\] Add setting to ignore missing documentation for `*args` and `**kwargs` parameters (`D417`) ([#15210](https://github.com/astral-sh/ruff/pull/15210))
- \[`ruff`\] Add an allowlist for `unsafe-markup-use` (`RUF035`) ([#15076](https://github.com/astral-sh/ruff/pull/15076))
### Bug fixes
- Fix type subscript on older python versions ([#15090](https://github.com/astral-sh/ruff/pull/15090))
- Use `TypeChecker` for detecting `fastapi` routes ([#15093](https://github.com/astral-sh/ruff/pull/15093))
- \[`pycodestyle`\] Avoid false positives and negatives related to type parameter default syntax (`E225`, `E251`) ([#15214](https://github.com/astral-sh/ruff/pull/15214))
### Documentation
- Fix incorrect doc in `shebang-not-executable` (`EXE001`) and add git+windows solution to executable bit ([#15208](https://github.com/astral-sh/ruff/pull/15208))
- Rename rules currently not conforming to naming convention ([#15102](https://github.com/astral-sh/ruff/pull/15102))
## 0.8.4
### Preview features

203
Cargo.lock generated
View File

@@ -220,9 +220,9 @@ checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
[[package]]
name = "bstr"
version = "1.11.3"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
dependencies = [
"memchr",
"regex-automata 0.4.8",
@@ -291,6 +291,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@@ -407,7 +413,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -418,15 +424,15 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clearscreen"
version = "4.0.1"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c41dc435a7b98e4608224bbf65282309f5403719df9113621b30f8b6f74e2f4"
checksum = "2f8c93eb5f77c9050c7750e14f13ef1033a40a0aac70c6371535b6763a01438c"
dependencies = [
"nix",
"nix 0.28.0",
"terminfo",
"thiserror 2.0.9",
"thiserror 1.0.67",
"which",
"windows-sys 0.59.0",
"winapi",
]
[[package]]
@@ -464,7 +470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -662,7 +668,7 @@ version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
dependencies = [
"nix",
"nix 0.29.0",
"windows-sys 0.59.0",
]
@@ -687,7 +693,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -698,7 +704,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -768,7 +774,16 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys 0.3.7",
]
[[package]]
@@ -777,7 +792,18 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
@@ -800,7 +826,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -849,12 +875,6 @@ dependencies = [
"regex",
]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
version = "0.11.6"
@@ -1226,7 +1246,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -1345,14 +1365,14 @@ dependencies = [
[[package]]
name = "insta"
version = "1.42.0"
version = "1.41.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513e4067e16e69ed1db5ab56048ed65db32d10ba5fc1217f5393f8f17d8b5a5"
checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8"
dependencies = [
"console",
"globset",
"lazy_static",
"linked-hash-map",
"once_cell",
"pest",
"pest_derive",
"regex",
@@ -1400,7 +1420,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -1442,15 +1462,6 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
@@ -1536,7 +1547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
dependencies = [
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -1636,9 +1647,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.8.6"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f926ade0c4e170215ae43342bf13b9310a437609c81f29f86c5df6657582ef9"
checksum = "bd0aa4b8ca861b08d68afc8702af3250776898c1508b278e1da9d01e01d4b45c"
[[package]]
name = "memchr"
@@ -1708,6 +1719,18 @@ dependencies = [
"uuid",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
@@ -1716,7 +1739,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"cfg_aliases 0.2.1",
"libc",
]
@@ -1992,7 +2015,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -2270,7 +2293,7 @@ dependencies = [
"hashbrown 0.15.2",
"indexmap",
"insta",
"itertools 0.14.0",
"itertools 0.13.0",
"memchr",
"ordermap",
"quickcheck",
@@ -2497,7 +2520,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.1"
version = "0.8.4"
dependencies = [
"anyhow",
"argfile",
@@ -2517,7 +2540,7 @@ dependencies = [
"insta",
"insta-cmd",
"is-macro",
"itertools 0.14.0",
"itertools 0.13.0",
"log",
"mimalloc",
"notify",
@@ -2583,7 +2606,7 @@ dependencies = [
"filetime",
"glob",
"globset",
"itertools 0.14.0",
"itertools 0.13.0",
"regex",
"ruff_macros",
"seahash",
@@ -2632,7 +2655,7 @@ dependencies = [
"imara-diff",
"indicatif",
"indoc",
"itertools 0.14.0",
"itertools 0.13.0",
"libcst",
"pretty_assertions",
"rayon",
@@ -2716,7 +2739,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.9.1"
version = "0.8.4"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2732,7 +2755,7 @@ dependencies = [
"insta",
"is-macro",
"is-wsl",
"itertools 0.14.0",
"itertools 0.13.0",
"libcst",
"log",
"memchr",
@@ -2745,7 +2768,6 @@ dependencies = [
"regex",
"ruff_cache",
"ruff_diagnostics",
"ruff_index",
"ruff_macros",
"ruff_notebook",
"ruff_python_ast",
@@ -2780,11 +2802,11 @@ dependencies = [
name = "ruff_macros"
version = "0.0.0"
dependencies = [
"itertools 0.14.0",
"itertools 0.13.0",
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -2792,7 +2814,7 @@ name = "ruff_notebook"
version = "0.0.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
"itertools 0.13.0",
"rand",
"ruff_diagnostics",
"ruff_source_file",
@@ -2813,7 +2835,7 @@ dependencies = [
"bitflags 2.6.0",
"compact_str",
"is-macro",
"itertools 0.14.0",
"itertools 0.13.0",
"memchr",
"ruff_cache",
"ruff_macros",
@@ -2855,7 +2877,7 @@ dependencies = [
"clap",
"countme",
"insta",
"itertools 0.14.0",
"itertools 0.13.0",
"memchr",
"regex",
"ruff_cache",
@@ -2893,7 +2915,7 @@ name = "ruff_python_literal"
version = "0.0.0"
dependencies = [
"bitflags 2.6.0",
"itertools 0.14.0",
"itertools 0.13.0",
"ruff_python_ast",
"unic-ucd-category",
]
@@ -2947,7 +2969,6 @@ dependencies = [
"rustc-hash 2.1.0",
"schemars",
"serde",
"smallvec",
]
[[package]]
@@ -2962,7 +2983,7 @@ dependencies = [
name = "ruff_python_trivia"
version = "0.0.0"
dependencies = [
"itertools 0.14.0",
"itertools 0.13.0",
"ruff_source_file",
"ruff_text_size",
"unicode-ident",
@@ -3033,7 +3054,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.1"
version = "0.8.4"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3067,7 +3088,7 @@ dependencies = [
"globset",
"ignore",
"is-macro",
"itertools 0.14.0",
"itertools 0.13.0",
"log",
"matchit",
"path-absolutize",
@@ -3175,7 +3196,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
dependencies = [
"append-only-vec",
"arc-swap",
@@ -3183,6 +3204,7 @@ dependencies = [
"dashmap 6.1.0",
"hashlink",
"indexmap",
"lazy_static",
"parking_lot",
"rayon",
"rustc-hash 2.1.0",
@@ -3195,17 +3217,17 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3c7f1694c9efba751dbeeacfbc93b227586e316a#3c7f1694c9efba751dbeeacfbc93b227586e316a"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
"synstructure",
]
@@ -3239,7 +3261,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3288,7 +3310,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3299,7 +3321,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3322,7 +3344,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3363,7 +3385,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3392,7 +3414,7 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
dependencies = [
"dirs",
"dirs 5.0.1",
]
[[package]]
@@ -3477,7 +3499,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3499,9 +3521,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.95"
version = "2.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
dependencies = [
"proc-macro2",
"quote",
@@ -3516,7 +3538,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3544,10 +3566,11 @@ dependencies = [
[[package]]
name = "terminfo"
version = "0.9.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f"
dependencies = [
"dirs 4.0.0",
"fnv",
"nom",
"phf",
@@ -3578,7 +3601,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3589,7 +3612,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
"test-case-core",
]
@@ -3619,7 +3642,7 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3630,7 +3653,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -3752,7 +3775,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -4022,7 +4045,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -4117,7 +4140,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
"wasm-bindgen-shared",
]
@@ -4152,7 +4175,7 @@ checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4186,7 +4209,7 @@ checksum = "222ebde6ea87fbfa6bdd2e9f1fd8a91d60aee5db68792632176c4e16a74fc7d8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -4220,12 +4243,12 @@ dependencies = [
[[package]]
name = "which"
version = "7.0.1"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
dependencies = [
"either",
"env_home",
"home",
"rustix",
"winsafe",
]
@@ -4489,7 +4512,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
"synstructure",
]
@@ -4510,7 +4533,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]
@@ -4530,7 +4553,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
"synstructure",
]
@@ -4559,7 +4582,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
"syn 2.0.93",
]
[[package]]

View File

@@ -55,7 +55,7 @@ camino = { version = "1.1.7" }
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
clap = { version = "4.5.3", features = ["derive"] }
clap_complete_command = { version = "0.6.0" }
clearscreen = { version = "4.0.0" }
clearscreen = { version = "3.0.0" }
codspeed-criterion-compat = { version = "2.6.0", default-features = false }
colored = { version = "2.1.0" }
console_error_panic_hook = { version = "0.1.7" }
@@ -89,7 +89,7 @@ insta = { version = "1.35.1" }
insta-cmd = { version = "0.6.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
itertools = { version = "0.14.0" }
itertools = { version = "0.13.0" }
js-sys = { version = "0.3.69" }
jod-thread = { version = "0.1.2" }
libc = { version = "0.2.153" }
@@ -119,7 +119,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 = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3c7f1694c9efba751dbeeacfbc93b227586e316a" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -116,21 +116,12 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
### Installation
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI.
Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/):
```shell
uvx ruff check # Lint all files in the current directory.
uvx ruff format # Format all files in the current directory.
```
Or install Ruff with `uv` (recommended), `pip`, or `pipx`:
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
```shell
# With uv.
uv tool install ruff@latest # Install Ruff globally.
uv add --dev ruff # Or add Ruff to your project.
uv add --dev ruff # to add ruff to your project
uv tool install ruff # to install ruff globally
# With pip.
pip install ruff
@@ -149,8 +140,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.9.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.8.4/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.8.4/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 +174,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.9.1
rev: v0.8.4
hooks:
# Run the linter.
- id: ruff
@@ -205,7 +196,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
- uses: astral-sh/ruff-action@v1
```
### Configuration<a id="configuration"></a>

View File

@@ -9,6 +9,8 @@ from typing import Literal
from enum import Enum
mode: Literal["w", "r"]
mode2: Literal["w"] | Literal["r"]
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
a1: Literal[26]
a2: Literal[0x1A]
a3: Literal[-4]
@@ -17,6 +19,7 @@ a5: Literal[b"hello world"]
a6: Literal[True]
a7: Literal[None]
a8: Literal[Literal[1]]
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
class Color(Enum):
RED = 0
@@ -27,6 +30,9 @@ b1: Literal[Color.RED]
def f():
reveal_type(mode) # revealed: Literal["w", "r"]
reveal_type(mode2) # revealed: Literal["w", "r"]
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
reveal_type(a1) # revealed: Literal[26]
reveal_type(a2) # revealed: Literal[26]
reveal_type(a3) # revealed: Literal[-4]
@@ -35,6 +41,7 @@ def f():
reveal_type(a6) # revealed: Literal[True]
reveal_type(a7) # revealed: None
reveal_type(a8) # revealed: Literal[1]
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
# TODO: This should be Color.RED
reveal_type(b1) # revealed: Literal[0]
@@ -54,63 +61,6 @@ invalid4: Literal[
]
```
## Shortening unions of literals
When a Literal is parameterized with more than one value, its treated as exactly to equivalent to
the union of those types.
```py
from typing import Literal
def x(
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
a2: Literal["w"] | Literal["r"],
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
):
reveal_type(a1) # revealed: Literal[1, 2, 3, "foo", 5] | None
reveal_type(a2) # revealed: Literal["w", "r"]
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
```
## Display of heterogeneous unions of literals
```py
from typing import Literal, Union
def foo(x: int) -> int:
return x + 1
def bar(s: str) -> str:
return s
class A: ...
class B: ...
def union_example(
x: Union[
# unknown type
# error: [unresolved-reference]
y,
Literal[-1],
Literal["A"],
Literal[b"A"],
Literal[b"\x00"],
Literal[b"\x07"],
Literal[0],
Literal[1],
Literal["B"],
Literal["foo"],
Literal["bar"],
Literal["B"],
Literal[True],
None,
]
):
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
```
## Detecting Literal outside typing and typing_extensions
Only Literal that is defined in typing and typing_extension modules is detected as the special

View File

@@ -107,7 +107,7 @@ def _(flag: bool):
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
baz_3 = "foo" if flag else 1
reveal_type(baz_3) # revealed: Literal["foo", 1]
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
```

View File

@@ -105,7 +105,7 @@ def f1(
from typing import Literal
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
reveal_type(v) # revealed: Literal["a", "b", b"c", "de", "f", "g", "h"]
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
```
## Class variables

View File

@@ -122,10 +122,3 @@ class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
```
## Annotated assignments in stub files are inferred correctly
```pyi path=main.pyi
x: int = 1
reveal_type(x) # revealed: Literal[1]
```

View File

@@ -40,9 +40,9 @@ class C:
return 42
x = C()
# error: [invalid-argument-type]
x -= 1
# TODO: should error, once operand type check is implemented
reveal_type(x) # revealed: int
```

View File

@@ -46,50 +46,3 @@ reveal_type(a | b) # revealed: Literal[True]
reveal_type(b | a) # revealed: Literal[True]
reveal_type(b | b) # revealed: Literal[False]
```
## Arithmetic with a variable
```py
a = True
b = False
def lhs_is_int(x: int):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_int(x: int):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def lhs_is_bool(x: bool):
reveal_type(x + a) # revealed: int
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_bool(x: bool):
reveal_type(a + x) # revealed: int
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def both_are_bool(x: bool, y: bool):
reveal_type(x + y) # revealed: int
reveal_type(x - y) # revealed: int
reveal_type(x * y) # revealed: int
reveal_type(x // y) # revealed: int
reveal_type(x / y) # revealed: float
reveal_type(x % y) # revealed: int
```

View File

@@ -1,27 +0,0 @@
# Binary operations on classes
## Union of two classes
Unioning two classes via the `|` operator is only available in Python 3.10 and later.
```toml
[environment]
python-version = "3.10"
```
```py
class A: ...
class B: ...
reveal_type(A | B) # revealed: UnionType
```
## Union of two classes (prior to 3.10)
```py
class A: ...
class B: ...
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
reveal_type(A | B) # revealed: Unknown
```

View File

@@ -1,371 +0,0 @@
# Custom binary operations
## Class instances
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
def __sub__(self, other) -> Literal["-"]:
return "-"
def __mul__(self, other) -> Literal["*"]:
return "*"
def __matmul__(self, other) -> Literal["@"]:
return "@"
def __truediv__(self, other) -> Literal["/"]:
return "/"
def __mod__(self, other) -> Literal["%"]:
return "%"
def __pow__(self, other) -> Literal["**"]:
return "**"
def __lshift__(self, other) -> Literal["<<"]:
return "<<"
def __rshift__(self, other) -> Literal[">>"]:
return ">>"
def __or__(self, other) -> Literal["|"]:
return "|"
def __xor__(self, other) -> Literal["^"]:
return "^"
def __and__(self, other) -> Literal["&"]:
return "&"
def __floordiv__(self, other) -> Literal["//"]:
return "//"
class Sub(Yes): ...
class No: ...
# Yes implements all of the dunder methods.
reveal_type(Yes() + Yes()) # revealed: Literal["+"]
reveal_type(Yes() - Yes()) # revealed: Literal["-"]
reveal_type(Yes() * Yes()) # revealed: Literal["*"]
reveal_type(Yes() @ Yes()) # revealed: Literal["@"]
reveal_type(Yes() / Yes()) # revealed: Literal["/"]
reveal_type(Yes() % Yes()) # revealed: Literal["%"]
reveal_type(Yes() ** Yes()) # revealed: Literal["**"]
reveal_type(Yes() << Yes()) # revealed: Literal["<<"]
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"]
reveal_type(Yes() | Yes()) # revealed: Literal["|"]
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"]
reveal_type(Yes() & Yes()) # revealed: Literal["&"]
reveal_type(Yes() // Yes()) # revealed: Literal["//"]
# Sub inherits Yes's implementation of the dunder methods.
reveal_type(Sub() + Sub()) # revealed: Literal["+"]
reveal_type(Sub() - Sub()) # revealed: Literal["-"]
reveal_type(Sub() * Sub()) # revealed: Literal["*"]
reveal_type(Sub() @ Sub()) # revealed: Literal["@"]
reveal_type(Sub() / Sub()) # revealed: Literal["/"]
reveal_type(Sub() % Sub()) # revealed: Literal["%"]
reveal_type(Sub() ** Sub()) # revealed: Literal["**"]
reveal_type(Sub() << Sub()) # revealed: Literal["<<"]
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"]
reveal_type(Sub() | Sub()) # revealed: Literal["|"]
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"]
reveal_type(Sub() & Sub()) # revealed: Literal["&"]
reveal_type(Sub() // Sub()) # revealed: Literal["//"]
# No does not implement any of the dunder methods.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`"
reveal_type(No() + No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`"
reveal_type(No() - No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`"
reveal_type(No() * No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`"
reveal_type(No() @ No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`"
reveal_type(No() / No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`"
reveal_type(No() % No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`"
reveal_type(No() ** No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`"
reveal_type(No() << No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`"
reveal_type(No() >> No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`"
reveal_type(No() | No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`"
reveal_type(No() ^ No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`"
reveal_type(No() & No()) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`"
reveal_type(No() // No()) # revealed: Unknown
# Yes does not implement any of the reflected dunder methods.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() + Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() - Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() * Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() @ Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() / Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() % Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() ** Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() << Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() >> Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() | Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() ^ Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() & Yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`"
reveal_type(No() // Yes()) # revealed: Unknown
```
## Subclass reflections override superclass dunders
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
def __sub__(self, other) -> Literal["-"]:
return "-"
def __mul__(self, other) -> Literal["*"]:
return "*"
def __matmul__(self, other) -> Literal["@"]:
return "@"
def __truediv__(self, other) -> Literal["/"]:
return "/"
def __mod__(self, other) -> Literal["%"]:
return "%"
def __pow__(self, other) -> Literal["**"]:
return "**"
def __lshift__(self, other) -> Literal["<<"]:
return "<<"
def __rshift__(self, other) -> Literal[">>"]:
return ">>"
def __or__(self, other) -> Literal["|"]:
return "|"
def __xor__(self, other) -> Literal["^"]:
return "^"
def __and__(self, other) -> Literal["&"]:
return "&"
def __floordiv__(self, other) -> Literal["//"]:
return "//"
class Sub(Yes):
def __radd__(self, other) -> Literal["r+"]:
return "r+"
def __rsub__(self, other) -> Literal["r-"]:
return "r-"
def __rmul__(self, other) -> Literal["r*"]:
return "r*"
def __rmatmul__(self, other) -> Literal["r@"]:
return "r@"
def __rtruediv__(self, other) -> Literal["r/"]:
return "r/"
def __rmod__(self, other) -> Literal["r%"]:
return "r%"
def __rpow__(self, other) -> Literal["r**"]:
return "r**"
def __rlshift__(self, other) -> Literal["r<<"]:
return "r<<"
def __rrshift__(self, other) -> Literal["r>>"]:
return "r>>"
def __ror__(self, other) -> Literal["r|"]:
return "r|"
def __rxor__(self, other) -> Literal["r^"]:
return "r^"
def __rand__(self, other) -> Literal["r&"]:
return "r&"
def __rfloordiv__(self, other) -> Literal["r//"]:
return "r//"
class No:
def __radd__(self, other) -> Literal["r+"]:
return "r+"
def __rsub__(self, other) -> Literal["r-"]:
return "r-"
def __rmul__(self, other) -> Literal["r*"]:
return "r*"
def __rmatmul__(self, other) -> Literal["r@"]:
return "r@"
def __rtruediv__(self, other) -> Literal["r/"]:
return "r/"
def __rmod__(self, other) -> Literal["r%"]:
return "r%"
def __rpow__(self, other) -> Literal["r**"]:
return "r**"
def __rlshift__(self, other) -> Literal["r<<"]:
return "r<<"
def __rrshift__(self, other) -> Literal["r>>"]:
return "r>>"
def __ror__(self, other) -> Literal["r|"]:
return "r|"
def __rxor__(self, other) -> Literal["r^"]:
return "r^"
def __rand__(self, other) -> Literal["r&"]:
return "r&"
def __rfloordiv__(self, other) -> Literal["r//"]:
return "r//"
# Subclass reflected dunder methods take precedence over the superclass's regular dunders.
reveal_type(Yes() + Sub()) # revealed: Literal["r+"]
reveal_type(Yes() - Sub()) # revealed: Literal["r-"]
reveal_type(Yes() * Sub()) # revealed: Literal["r*"]
reveal_type(Yes() @ Sub()) # revealed: Literal["r@"]
reveal_type(Yes() / Sub()) # revealed: Literal["r/"]
reveal_type(Yes() % Sub()) # revealed: Literal["r%"]
reveal_type(Yes() ** Sub()) # revealed: Literal["r**"]
reveal_type(Yes() << Sub()) # revealed: Literal["r<<"]
reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"]
reveal_type(Yes() | Sub()) # revealed: Literal["r|"]
reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"]
reveal_type(Yes() & Sub()) # revealed: Literal["r&"]
reveal_type(Yes() // Sub()) # revealed: Literal["r//"]
# But for an unrelated class, the superclass regular dunders are used.
reveal_type(Yes() + No()) # revealed: Literal["+"]
reveal_type(Yes() - No()) # revealed: Literal["-"]
reveal_type(Yes() * No()) # revealed: Literal["*"]
reveal_type(Yes() @ No()) # revealed: Literal["@"]
reveal_type(Yes() / No()) # revealed: Literal["/"]
reveal_type(Yes() % No()) # revealed: Literal["%"]
reveal_type(Yes() ** No()) # revealed: Literal["**"]
reveal_type(Yes() << No()) # revealed: Literal["<<"]
reveal_type(Yes() >> No()) # revealed: Literal[">>"]
reveal_type(Yes() | No()) # revealed: Literal["|"]
reveal_type(Yes() ^ No()) # revealed: Literal["^"]
reveal_type(Yes() & No()) # revealed: Literal["&"]
reveal_type(Yes() // No()) # revealed: Literal["//"]
```
## Classes
Dunder methods defined in a class are available to instances of that class, but not to the class
itself. (For these operators to work on the class itself, they would have to be defined on the
class's type, i.e. `type`.)
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
class Sub(Yes): ...
class No: ...
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
reveal_type(Yes + Yes) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
reveal_type(Sub + Sub) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
reveal_type(No + No) # revealed: Unknown
```
## Subclass
```py
class Yes:
def __add__(self, other) -> Literal["+"]:
return "+"
class Sub(Yes): ...
class No: ...
def yes() -> type[Yes]:
return Yes
def sub() -> type[Sub]:
return Sub
def no() -> type[No]:
return No
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`"
reveal_type(yes() + yes()) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`"
reveal_type(sub() + sub()) # revealed: Unknown
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`"
reveal_type(no() + no()) # revealed: Unknown
```
## Function literals
```py
def f():
pass
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f + f) # revealed: Unknown
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f - f) # revealed: Unknown
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f * f) # revealed: Unknown
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f @ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f / f) # revealed: Unknown
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f % f) # revealed: Unknown
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f**f) # revealed: Unknown
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f << f) # revealed: Unknown
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f >> f) # revealed: Unknown
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f | f) # revealed: Unknown
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f ^ f) # revealed: Unknown
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f & f) # revealed: Unknown
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
reveal_type(f // f) # revealed: Unknown
```

View File

@@ -9,34 +9,6 @@ reveal_type(3 * -1) # revealed: Literal[-3]
reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
# formal parameter type.
reveal_type(2 + "f") # revealed: int
def lhs(x: int):
reveal_type(x + 1) # revealed: int
reveal_type(x - 4) # revealed: int
reveal_type(x * -1) # revealed: int
reveal_type(x // 3) # revealed: int
reveal_type(x / 3) # revealed: float
reveal_type(x % 3) # revealed: int
def rhs(x: int):
reveal_type(2 + x) # revealed: int
reveal_type(3 - x) # revealed: int
reveal_type(3 * x) # revealed: int
reveal_type(-3 // x) # revealed: int
reveal_type(-3 / x) # revealed: float
reveal_type(5 % x) # revealed: int
def both(x: int):
reveal_type(x + x) # revealed: int
reveal_type(x - x) # revealed: int
reveal_type(x * x) # revealed: int
reveal_type(x // x) # revealed: int
reveal_type(x / x) # revealed: float
reveal_type(x % x) # revealed: int
```
## Power
@@ -49,11 +21,6 @@ largest_u32 = 4_294_967_295
reveal_type(2**2) # revealed: Literal[4]
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
reveal_type(2**largest_u32) # revealed: int
def variable(x: int):
reveal_type(x**2) # revealed: @Todo(return type)
reveal_type(2**x) # revealed: @Todo(return type)
reveal_type(x**x) # revealed: @Todo(return type)
```
## Division by Zero

View File

@@ -70,32 +70,3 @@ def _(flag: bool):
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
```
## Call binding errors
### Wrong argument type
```py
class C:
def __call__(self, x: int) -> int:
return 1
c = C()
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`"
reveal_type(c("foo")) # revealed: int
```
### Wrong argument type on `self`
```py
class C:
# TODO this definition should also be an error; `C` must be assignable to type of `self`
def __call__(self: int) -> int:
return 1
c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
reveal_type(c()) # revealed: int
```

View File

@@ -64,260 +64,3 @@ def _(flag: bool):
# error: [possibly-unresolved-reference]
reveal_type(foo()) # revealed: int
```
## Wrong argument type
### Positional argument, positional-or-keyword parameter
```py
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`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, positional-only parameter
```py
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`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, variadic parameter
```py
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`"
reveal_type(f("foo")) # revealed: int
```
### Keyword argument, positional-or-keyword parameter
```py
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`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keyword-only parameter
```py
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`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keywords parameter
```py
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`"
reveal_type(f(x="foo")) # revealed: int
```
### Correctly match keyword out-of-order
```py
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`"
reveal_type(f(y=2, x="bar")) # revealed: int
```
## Too many positional arguments
### One too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
reveal_type(f("foo")) # revealed: int
```
### Two too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
reveal_type(f("foo", "bar")) # revealed: int
```
### No too-many-positional if variadic is taken
```py
def f(*args: int) -> int:
return 1
reveal_type(f(1, 2, 3)) # revealed: int
```
## Missing arguments
### No defaults or variadic
```py
def f(x: int) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### With default
```py
def f(x: int, y: str = "foo") -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Defaulted argument is not required
```py
def f(x: int = 1) -> int:
return 1
reveal_type(f()) # revealed: int
```
### With variadic
```py
def f(x: int, *y: str) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Variadic argument is not required
```py
def f(*args: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Keywords argument is not required
```py
def f(**kwargs: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Multiple
```py
def f(x: int, y: int) -> int:
return 1
# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
reveal_type(f()) # revealed: int
```
## Unknown argument
```py
def f(x: int) -> int:
return 1
# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
reveal_type(f(x=1, y=2)) # revealed: int
```
## Parameter already assigned
```py
def f(x: int) -> int:
return 1
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2)) # revealed: int
```
## Special functions
Some functions require special handling in type inference. Here, we make sure that we still emit
proper diagnostics in case of missing or superfluous arguments.
### `reveal_type`
```py
from typing_extensions import reveal_type
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type() # revealed: Unknown
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2) # revealed: Literal[1]
```
### `static_assert`
```py
from knot_extensions import static_assert
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
# error: [static-assert-error]
static_assert()
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
static_assert(True, 2, 3)
```
### `len`
```py
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
len()
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
len([], 1)
```
### Type API predicates
```py
from knot_extensions import is_subtype_of, is_fully_static
# error: [missing-argument]
is_subtype_of()
# error: [missing-argument]
is_subtype_of(int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int, int)
# error: [missing-argument]
is_fully_static()
# error: [too-many-positional-arguments]
is_fully_static(int, int)
```

View File

@@ -1,44 +0,0 @@
# Invalid signatures
## Multiple arguments with the same name
We always map a keyword argument to the first parameter of that name.
```py
# error: [invalid-syntax] "Duplicate parameter "x""
def f(x: int, x: str) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2)) # revealed: int
```
## Positional after non-positional
When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them
to the valid order:
```py
# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter"
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`"
reveal_type(f(1)) # revealed: int
```
## Non-defaulted after defaulted
We emit a syntax diagnostic for this, but it doesn't cause any problems for binding.
```py
# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default"
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: [missing-argument] "No argument provided for required parameter `y` of function `f`"
reveal_type(f("foo")) # revealed: int
```

View File

@@ -56,7 +56,7 @@ def _(flag: bool, flag2: bool):
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
reveal_type(f())
```
@@ -72,6 +72,6 @@ def _(flag: bool):
else:
f = "foo"
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```

View File

@@ -22,7 +22,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
reveal_type(d) # revealed: bool
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool

View File

@@ -115,35 +115,3 @@ def _(flag: bool, flag2: bool):
reveal_type(y) # revealed: Literal[2, 3, 4]
```
## if-elif with assignment expressions in tests
```py
def check(x: int) -> bool:
return bool(x)
if check(x := 1):
x = 2
elif check(x := 3):
x = 4
reveal_type(x) # revealed: Literal[2, 3, 4]
```
## constraints apply to later test expressions
```py
def check(x) -> bool:
return bool(x)
def _(flag: bool):
x = 1 if flag else None
y = 0
if x is None:
pass
elif check(y := x):
pass
reveal_type(y) # revealed: Literal[0, 1]
```

View File

@@ -1,145 +0,0 @@
# `assert_type`
## Basic
```py
from typing_extensions import assert_type
def _(x: int):
assert_type(x, int) # fine
assert_type(x, str) # error: [type-assertion-failure]
```
## Narrowing
The asserted type is checked against the inferred type, not the declared type.
```toml
[environment]
python-version = "3.10"
```
```py
from typing_extensions import assert_type
def _(x: int | str):
if isinstance(x, int):
reveal_type(x) # revealed: int
assert_type(x, int) # fine
```
## Equivalence
The actual type must match the asserted type precisely.
```py
from typing import Any, Type, Union
from typing_extensions import assert_type
# Subtype does not count
def _(x: bool):
assert_type(x, int) # error: [type-assertion-failure]
def _(a: type[int], b: type[Any]):
assert_type(a, type[Any]) # error: [type-assertion-failure]
assert_type(b, type[int]) # error: [type-assertion-failure]
# The expression constructing the type is not taken into account
def _(a: type[int]):
assert_type(a, Type[int]) # fine
```
## Gradual types
```py
from typing import Any
from typing_extensions import Literal, assert_type
from knot_extensions import Unknown
# Any and Unknown are considered equivalent
def _(a: Unknown, b: Any):
reveal_type(a) # revealed: Unknown
assert_type(a, Any) # fine
reveal_type(b) # revealed: Any
assert_type(b, Unknown) # fine
def _(a: type[Unknown], b: type[Any]):
# TODO: Should be `type[Unknown]`
reveal_type(a) # revealed: @Todo(unsupported type[X] special form)
# TODO: Should be fine
assert_type(a, type[Any]) # error: [type-assertion-failure]
reveal_type(b) # revealed: type[Any]
# TODO: Should be fine
assert_type(b, type[Unknown]) # error: [type-assertion-failure]
```
## Tuples
Tuple types with the same elements are the same.
```py
from typing_extensions import assert_type
from knot_extensions import Unknown
def _(a: tuple[int, str, bytes]):
assert_type(a, tuple[int, str, bytes]) # fine
assert_type(a, tuple[int, str]) # error: [type-assertion-failure]
assert_type(a, tuple[int, str, bytes, None]) # error: [type-assertion-failure]
assert_type(a, tuple[int, bytes, str]) # error: [type-assertion-failure]
def _(a: tuple[Any, ...], b: tuple[Unknown, ...]):
assert_type(a, tuple[Any, ...]) # fine
assert_type(a, tuple[Unknown, ...]) # fine
assert_type(b, tuple[Unknown, ...]) # fine
assert_type(b, tuple[Any, ...]) # fine
```
## Unions
Unions with the same elements are the same, regardless of order.
```toml
[environment]
python-version = "3.10"
```
```py
from typing_extensions import assert_type
def _(a: str | int):
assert_type(a, str | int) # fine
# TODO: Order-independent union handling in type equivalence
assert_type(a, int | str) # error: [type-assertion-failure]
```
## Intersections
Intersections are the same when their positive and negative parts are respectively the same,
regardless of order.
```py
from typing_extensions import assert_type
from knot_extensions import Intersection, Not
class A: ...
class B: ...
class C: ...
class D: ...
def _(a: A):
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
reveal_type(a) # revealed: A & B & ~C & ~D
assert_type(a, Intersection[A, B, Not[C], Not[D]]) # fine
# TODO: Order-independent intersection handling in type equivalence
assert_type(a, Intersection[B, A, Not[D], Not[C]]) # error: [type-assertion-failure]
```

View File

@@ -17,7 +17,7 @@ def _(flag: bool):
reveal_type(A.always_bound) # revealed: Literal[1]
reveal_type(A.union) # revealed: Literal[1, "abc"]
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]

View File

@@ -31,9 +31,9 @@ The test inside an if expression should not affect code outside of the expressio
def _(flag: bool):
x: Literal[42, "hello"] = 42 if flag else "hello"
reveal_type(x) # revealed: Literal[42, "hello"]
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
_ = ... if isinstance(x, str) else ...
reveal_type(x) # revealed: Literal[42, "hello"]
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
```

View File

@@ -119,7 +119,7 @@ class ZeroOrStr:
reveal_type(len(Zero())) # revealed: Literal[0]
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0]
reveal_type(len(OneOrFalse())) # revealed: Literal[0, 1]
# TODO: Emit a diagnostic
reveal_type(len(OneOrFoo())) # revealed: int

View File

@@ -1,747 +0,0 @@
# Intersection types
## Introduction
This test suite covers certain properties of intersection types and makes sure that we can apply
various simplification strategies. We use `Intersection` (`&`) and `Not` (`~`) to construct
intersection types (note that we display negative contributions at the end; the order does not
matter):
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
def _(
i1: Intersection[P, Q],
i2: Intersection[P, Not[Q]],
i3: Intersection[Not[P], Q],
i4: Intersection[Not[P], Not[Q]],
) -> None:
reveal_type(i1) # revealed: P & Q
reveal_type(i2) # revealed: P & ~Q
reveal_type(i3) # revealed: Q & ~P
reveal_type(i4) # revealed: ~P & ~Q
```
## Notation
Throughout this document, we use the following types as representatives for certain equivalence
classes.
### Non-disjoint types
We use `P`, `Q`, `R`, … to denote types that are non-disjoint:
```py
from knot_extensions import static_assert, is_disjoint_from
class P: ...
class Q: ...
class R: ...
static_assert(not is_disjoint_from(P, Q))
static_assert(not is_disjoint_from(P, R))
static_assert(not is_disjoint_from(Q, R))
```
Although `P` is not a subtype of `Q` and `Q` is not a subtype of `P`, the two types are not disjoint
because it would be possible to create a class `S` that inherits from both `P` and `Q` using
multiple inheritance. An instance of `S` would be a member of the `P` type _and_ the `Q` type.
### Disjoint types
We use `Literal[1]`, `Literal[2]`, … as examples of pairwise-disjoint types, and `int` as a joint
supertype of these:
```py
from knot_extensions import static_assert, is_disjoint_from, is_subtype_of
from typing import Literal
static_assert(is_disjoint_from(Literal[1], Literal[2]))
static_assert(is_disjoint_from(Literal[1], Literal[3]))
static_assert(is_disjoint_from(Literal[2], Literal[3]))
static_assert(is_subtype_of(Literal[1], int))
static_assert(is_subtype_of(Literal[2], int))
static_assert(is_subtype_of(Literal[3], int))
```
### Subtypes
Finally, we use `A <: B <: C` and `A <: B1`, `A <: B2` to denote hierarchies of (proper) subtypes:
```py
from knot_extensions import static_assert, is_subtype_of, is_disjoint_from
class A: ...
class B(A): ...
class C(B): ...
static_assert(is_subtype_of(B, A))
static_assert(is_subtype_of(C, B))
static_assert(is_subtype_of(C, A))
static_assert(not is_subtype_of(A, B))
static_assert(not is_subtype_of(B, C))
static_assert(not is_subtype_of(A, C))
class B1(A): ...
class B2(A): ...
static_assert(is_subtype_of(B1, A))
static_assert(is_subtype_of(B2, A))
static_assert(not is_subtype_of(A, B1))
static_assert(not is_subtype_of(A, B2))
static_assert(not is_subtype_of(B1, B2))
static_assert(not is_subtype_of(B2, B1))
```
## Structural properties
This section covers structural properties of intersection types and documents some decisions on how
to represent mixtures of intersections and unions.
### Single-element unions
If we have a union of a single element, we can simplify to that element. Similarly, we show an
intersection with a single negative contribution as just the negation of that element.
```py
from knot_extensions import Intersection, Not
class P: ...
def _(
i1: Intersection[P],
i2: Intersection[Not[P]],
) -> None:
reveal_type(i1) # revealed: P
reveal_type(i2) # revealed: ~P
```
### Flattening of nested intersections
We eagerly flatten nested intersections types.
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
class S: ...
def positive_contributions(
i1: Intersection[P, Intersection[Q, R]],
i2: Intersection[Intersection[P, Q], R],
) -> None:
reveal_type(i1) # revealed: P & Q & R
reveal_type(i2) # revealed: P & Q & R
def negative_contributions(
i1: Intersection[Not[P], Intersection[Not[Q], Not[R]]],
i2: Intersection[Intersection[Not[P], Not[Q]], Not[R]],
) -> None:
reveal_type(i1) # revealed: ~P & ~Q & ~R
reveal_type(i2) # revealed: ~P & ~Q & ~R
def mixed(
i1: Intersection[P, Intersection[Not[Q], R]],
i2: Intersection[Intersection[P, Not[Q]], R],
i3: Intersection[Not[P], Intersection[Q, Not[R]]],
i4: Intersection[Intersection[Q, Not[R]], Not[P]],
) -> None:
reveal_type(i1) # revealed: P & R & ~Q
reveal_type(i2) # revealed: P & R & ~Q
reveal_type(i3) # revealed: Q & ~P & ~R
reveal_type(i4) # revealed: Q & ~R & ~P
def multiple(
i1: Intersection[Intersection[P, Q], Intersection[R, S]],
):
reveal_type(i1) # revealed: P & Q & R & S
def nested(
i1: Intersection[Intersection[Intersection[P, Q], R], S],
i2: Intersection[P, Intersection[Q, Intersection[R, S]]],
):
reveal_type(i1) # revealed: P & Q & R & S
reveal_type(i2) # revealed: P & Q & R & S
```
### Union of intersections
We always normalize our representation to a _union of intersections_, so when we add a _union to an
intersection_, we distribute the union over the respective elements:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
class S: ...
def _(
i1: Intersection[P, Q | R | S],
i2: Intersection[P | Q | R, S],
i3: Intersection[P | Q, R | S],
) -> None:
reveal_type(i1) # revealed: P & Q | P & R | P & S
reveal_type(i2) # revealed: P & S | Q & S | R & S
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
def simplifications_for_same_elements(
i1: Intersection[P, Q | P],
i2: Intersection[Q, P | Q],
i3: Intersection[P | Q, Q | R],
i4: Intersection[P | Q, P | Q],
i5: Intersection[P | Q, Q | P],
) -> None:
# P & (Q | P)
# = P & Q | P & P
# = P & Q | P
# = P
# (because P is a supertype of P & Q)
reveal_type(i1) # revealed: P
# similar here:
reveal_type(i2) # revealed: Q
# (P | Q) & (Q | R)
# = P & Q | P & R | Q & Q | Q & R
# = P & Q | P & R | Q | Q & R
# = Q | P & R
# (again, because Q is a supertype of P & Q and of Q & R)
reveal_type(i3) # revealed: Q | P & R
# (P | Q) & (P | Q)
# = P & P | P & Q | Q & P | Q & Q
# = P | P & Q | Q
# = P | Q
reveal_type(i4) # revealed: P | Q
```
### Negation distributes over union
Distribution also applies to a negation operation. This is a manifestation of one of
[De Morgan's laws], namely `~(P | Q) = ~P & ~Q`:
```py
from knot_extensions import Not
from typing import Literal
class P: ...
class Q: ...
class R: ...
def _(i1: Not[P | Q], i2: Not[P | Q | R]) -> None:
reveal_type(i1) # revealed: ~P & ~Q
reveal_type(i2) # revealed: ~P & ~Q & ~R
def example_literals(i: Not[Literal[1, 2]]) -> None:
reveal_type(i) # revealed: ~Literal[1] & ~Literal[2]
```
### Negation of intersections
The other of [De Morgan's laws], `~(P & Q) = ~P | ~Q`, also holds:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
class R: ...
def _(
i1: Not[Intersection[P, Q]],
i2: Not[Intersection[P, Q, R]],
) -> None:
reveal_type(i1) # revealed: ~P | ~Q
reveal_type(i2) # revealed: ~P | ~Q | ~R
```
### `Never` is dual to `object`
`Never` represents the empty set of values, while `object` represents the set of all values, so
`~Never` is equivalent to `object`, and `~object` is equivalent to `Never`. This is a manifestation
of the [complement laws] of set theory.
```py
from knot_extensions import Intersection, Not
from typing_extensions import Never
def _(
not_never: Not[Never],
not_object: Not[object],
) -> None:
reveal_type(not_never) # revealed: object
reveal_type(not_object) # revealed: Never
```
### Intersection of a type and its negation
Continuing with more [complement laws], if we see both `P` and `~P` in an intersection, we can
simplify to `Never`, even in the presence of other types:
```py
from knot_extensions import Intersection, Not
from typing import Any
class P: ...
class Q: ...
def _(
i1: Intersection[P, Not[P]],
i2: Intersection[Not[P], P],
i3: Intersection[P, Q, Not[P]],
i4: Intersection[Not[P], Q, P],
i5: Intersection[P, Any, Not[P]],
i6: Intersection[Not[P], Any, P],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
```
### Union of a type and its negation
Similarly, if we have both `P` and `~P` in a _union_, we could simplify that to `object`. However,
this is a rather costly operation which would require us to build the negation of each type that we
add to a union, so this is not implemented at the moment.
```py
from knot_extensions import Intersection, Not
class P: ...
def _(
i1: P | Not[P],
i2: Not[P] | P,
) -> None:
# These could be simplified to `object`
reveal_type(i1) # revealed: P | ~P
reveal_type(i2) # revealed: ~P | P
```
### Negation is an involution
The final of the [complement laws] states that negating twice is equivalent to not negating at all:
```py
from knot_extensions import Not
class P: ...
def _(
i1: Not[P],
i2: Not[Not[P]],
i3: Not[Not[Not[P]]],
i4: Not[Not[Not[Not[P]]]],
) -> None:
reveal_type(i1) # revealed: ~P
reveal_type(i2) # revealed: P
reveal_type(i3) # revealed: ~P
reveal_type(i4) # revealed: P
```
## Simplification strategies
In this section, we present various simplification strategies that go beyond the structure of the
representation.
### `Never` in intersections
If we intersect with `Never`, we can simplify the whole intersection to `Never`, even if there are
dynamic types involved:
```py
from knot_extensions import Intersection, Not
from typing_extensions import Never, Any
class P: ...
class Q: ...
def _(
i1: Intersection[P, Never],
i2: Intersection[Never, P],
i3: Intersection[Any, Never],
i4: Intersection[Never, Not[Any]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
```
### Simplifications using disjointness
#### Positive contributions
If we intersect disjoint types, we can simplify to `Never`, even in the presence of other types:
```py
from knot_extensions import Intersection, Not
from typing import Literal, Any
class P: ...
def _(
i01: Intersection[Literal[1], Literal[2]],
i02: Intersection[Literal[2], Literal[1]],
i03: Intersection[Literal[1], Literal[2], P],
i04: Intersection[Literal[1], P, Literal[2]],
i05: Intersection[P, Literal[1], Literal[2]],
i06: Intersection[Literal[1], Literal[2], Any],
i07: Intersection[Literal[1], Any, Literal[2]],
i08: Intersection[Any, Literal[1], Literal[2]],
) -> None:
reveal_type(i01) # revealed: Never
reveal_type(i02) # revealed: Never
reveal_type(i03) # revealed: Never
reveal_type(i04) # revealed: Never
reveal_type(i05) # revealed: Never
reveal_type(i06) # revealed: Never
reveal_type(i07) # revealed: Never
reveal_type(i08) # revealed: Never
# `bool` is final and can not be subclassed, so `type[bool]` is equivalent to `Literal[bool]`, which
# is disjoint from `type[str]`:
def example_type_bool_type_str(
i: Intersection[type[bool], type[str]],
) -> None:
reveal_type(i) # revealed: Never
```
#### Positive and negative contributions
If we intersect a type `X` with the negation of a disjoint type `Y`, we can remove the negative
contribution `~Y`, as it necessarily overlaps with the positive contribution `X`:
```py
from knot_extensions import Intersection, Not
from typing import Literal
def _(
i1: Intersection[Literal[1], Not[Literal[2]]],
i2: Intersection[Not[Literal[2]], Literal[1]],
i3: Intersection[Literal[1], Not[Literal[2]], int],
i4: Intersection[Literal[1], int, Not[Literal[2]]],
i5: Intersection[int, Literal[1], Not[Literal[2]]],
) -> None:
reveal_type(i1) # revealed: Literal[1]
reveal_type(i2) # revealed: Literal[1]
reveal_type(i3) # revealed: Literal[1]
reveal_type(i4) # revealed: Literal[1]
reveal_type(i5) # revealed: Literal[1]
# None is disjoint from int, so this simplification applies here
def example_none(
i1: Intersection[int, Not[None]],
i2: Intersection[Not[None], int],
) -> None:
reveal_type(i1) # revealed: int
reveal_type(i2) # revealed: int
```
### Simplifications using subtype relationships
#### Positive type and positive subtype
Subtypes are contained within their supertypes, so we can simplify intersections by removing
superfluous supertypes:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i01: Intersection[A, B],
i02: Intersection[B, A],
i03: Intersection[A, C],
i04: Intersection[C, A],
i05: Intersection[B, C],
i06: Intersection[C, B],
i07: Intersection[A, B, C],
i08: Intersection[C, B, A],
i09: Intersection[B, C, A],
i10: Intersection[A, B, Unrelated],
i11: Intersection[B, A, Unrelated],
i12: Intersection[B, Unrelated, A],
i13: Intersection[A, Unrelated, B],
i14: Intersection[Unrelated, A, B],
i15: Intersection[Unrelated, B, A],
i16: Intersection[A, B, Any],
i17: Intersection[B, A, Any],
i18: Intersection[B, Any, A],
i19: Intersection[A, Any, B],
i20: Intersection[Any, A, B],
i21: Intersection[Any, B, A],
) -> None:
reveal_type(i01) # revealed: B
reveal_type(i02) # revealed: B
reveal_type(i03) # revealed: C
reveal_type(i04) # revealed: C
reveal_type(i05) # revealed: C
reveal_type(i06) # revealed: C
reveal_type(i07) # revealed: C
reveal_type(i08) # revealed: C
reveal_type(i09) # revealed: C
reveal_type(i10) # revealed: B & Unrelated
reveal_type(i11) # revealed: B & Unrelated
reveal_type(i12) # revealed: B & Unrelated
reveal_type(i13) # revealed: Unrelated & B
reveal_type(i14) # revealed: Unrelated & B
reveal_type(i15) # revealed: Unrelated & B
reveal_type(i16) # revealed: B & Any
reveal_type(i17) # revealed: B & Any
reveal_type(i18) # revealed: B & Any
reveal_type(i19) # revealed: Any & B
reveal_type(i20) # revealed: Any & B
reveal_type(i21) # revealed: Any & B
```
#### Negative type and negative subtype
For negative contributions, this property is reversed. Here we can get remove superfluous
_subtypes_:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i01: Intersection[Not[B], Not[A]],
i02: Intersection[Not[A], Not[B]],
i03: Intersection[Not[A], Not[C]],
i04: Intersection[Not[C], Not[A]],
i05: Intersection[Not[B], Not[C]],
i06: Intersection[Not[C], Not[B]],
i07: Intersection[Not[A], Not[B], Not[C]],
i08: Intersection[Not[C], Not[B], Not[A]],
i09: Intersection[Not[B], Not[C], Not[A]],
i10: Intersection[Not[B], Not[A], Unrelated],
i11: Intersection[Not[A], Not[B], Unrelated],
i12: Intersection[Not[A], Unrelated, Not[B]],
i13: Intersection[Not[B], Unrelated, Not[A]],
i14: Intersection[Unrelated, Not[A], Not[B]],
i15: Intersection[Unrelated, Not[B], Not[A]],
i16: Intersection[Not[B], Not[A], Any],
i17: Intersection[Not[A], Not[B], Any],
i18: Intersection[Not[A], Any, Not[B]],
i19: Intersection[Not[B], Any, Not[A]],
i20: Intersection[Any, Not[A], Not[B]],
i21: Intersection[Any, Not[B], Not[A]],
) -> None:
reveal_type(i01) # revealed: ~A
reveal_type(i02) # revealed: ~A
reveal_type(i03) # revealed: ~A
reveal_type(i04) # revealed: ~A
reveal_type(i05) # revealed: ~B
reveal_type(i06) # revealed: ~B
reveal_type(i07) # revealed: ~A
reveal_type(i08) # revealed: ~A
reveal_type(i09) # revealed: ~A
reveal_type(i10) # revealed: Unrelated & ~A
reveal_type(i11) # revealed: Unrelated & ~A
reveal_type(i12) # revealed: Unrelated & ~A
reveal_type(i13) # revealed: Unrelated & ~A
reveal_type(i14) # revealed: Unrelated & ~A
reveal_type(i15) # revealed: Unrelated & ~A
reveal_type(i16) # revealed: Any & ~A
reveal_type(i17) # revealed: Any & ~A
reveal_type(i18) # revealed: Any & ~A
reveal_type(i19) # revealed: Any & ~A
reveal_type(i20) # revealed: Any & ~A
reveal_type(i21) # revealed: Any & ~A
```
#### Negative type and multiple negative subtypes
If there are multiple negative subtypes, all of them can be removed:
```py
from knot_extensions import Intersection, Not
class A: ...
class B1(A): ...
class B2(A): ...
def _(
i1: Intersection[Not[A], Not[B1], Not[B2]],
i2: Intersection[Not[A], Not[B2], Not[B1]],
i3: Intersection[Not[B1], Not[A], Not[B2]],
i4: Intersection[Not[B1], Not[B2], Not[A]],
i5: Intersection[Not[B2], Not[A], Not[B1]],
i6: Intersection[Not[B2], Not[B1], Not[A]],
) -> None:
reveal_type(i1) # revealed: ~A
reveal_type(i2) # revealed: ~A
reveal_type(i3) # revealed: ~A
reveal_type(i4) # revealed: ~A
reveal_type(i5) # revealed: ~A
reveal_type(i6) # revealed: ~A
```
#### Negative type and positive subtype
When `A` is a supertype of `B`, its negation `~A` is disjoint from `B`, so we can simplify the
intersection to `Never`:
```py
from knot_extensions import Intersection, Not
from typing import Any
class A: ...
class B(A): ...
class C(B): ...
class Unrelated: ...
def _(
i1: Intersection[Not[A], B],
i2: Intersection[B, Not[A]],
i3: Intersection[Not[A], C],
i4: Intersection[C, Not[A]],
i5: Intersection[Unrelated, Not[A], B],
i6: Intersection[B, Not[A], Not[Unrelated]],
i7: Intersection[Any, Not[A], B],
i8: Intersection[B, Not[A], Not[Any]],
) -> None:
reveal_type(i1) # revealed: Never
reveal_type(i2) # revealed: Never
reveal_type(i3) # revealed: Never
reveal_type(i4) # revealed: Never
reveal_type(i5) # revealed: Never
reveal_type(i6) # revealed: Never
reveal_type(i7) # revealed: Never
reveal_type(i8) # revealed: Never
```
## Non fully-static types
### Negation of dynamic types
`Any` represents the dynamic type, an unknown set of runtime values. The negation of that, `~Any`,
is still an unknown set of runtime values, so `~Any` is equivalent to `Any`. We therefore eagerly
simplify `~Any` to `Any` in intersections. The same applies to `Unknown`.
```py
from knot_extensions import Intersection, Not, Unknown
from typing_extensions import Any, Never
class P: ...
def any(
i1: Not[Any],
i2: Intersection[P, Not[Any]],
i3: Intersection[Never, Not[Any]],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: P & Any
reveal_type(i3) # revealed: Never
def unknown(
i1: Not[Unknown],
i2: Intersection[P, Not[Unknown]],
i3: Intersection[Never, Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: P & Unknown
reveal_type(i3) # revealed: Never
```
### Collapsing of multiple `Any`/`Unknown` contributions
The intersection of an unknown set of runtime values with (another) unknown set of runtime values is
still an unknown set of runtime values:
```py
from knot_extensions import Intersection, Not, Unknown
from typing_extensions import Any
class P: ...
def any(
i1: Intersection[Any, Any],
i2: Intersection[P, Any, Any],
i3: Intersection[Any, P, Any],
i4: Intersection[Any, Any, P],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: P & Any
reveal_type(i3) # revealed: Any & P
reveal_type(i4) # revealed: Any & P
def unknown(
i1: Intersection[Unknown, Unknown],
i2: Intersection[P, Unknown, Unknown],
i3: Intersection[Unknown, P, Unknown],
i4: Intersection[Unknown, Unknown, P],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: P & Unknown
reveal_type(i3) # revealed: Unknown & P
reveal_type(i4) # revealed: Unknown & P
```
### No self-cancellation
Dynamic types do not cancel each other out. Intersecting an unknown set of values with the negation
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
```py
from knot_extensions import Intersection, Not, Unknown
def any(
i1: Intersection[Any, Not[Any]],
i2: Intersection[Not[Any], Any],
) -> None:
reveal_type(i1) # revealed: Any
reveal_type(i2) # revealed: Any
def unknown(
i1: Intersection[Unknown, Not[Unknown]],
i2: Intersection[Not[Unknown], Unknown],
) -> None:
reveal_type(i1) # revealed: Unknown
reveal_type(i2) # revealed: Unknown
```
### Mixed dynamic types
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
```py
from knot_extensions import Intersection, Not, Unknown
def mixed(
i1: Intersection[Any, Unknown],
i2: Intersection[Any, Not[Unknown]],
i3: Intersection[Not[Any], Unknown],
i4: Intersection[Not[Any], Not[Unknown]],
) -> None:
reveal_type(i1) # revealed: Any & Unknown
reveal_type(i2) # revealed: Any & Unknown
reveal_type(i3) # revealed: Any & Unknown
reveal_type(i4) # revealed: Any & Unknown
```
[complement laws]: https://en.wikipedia.org/wiki/Complement_(set_theory)
[de morgan's laws]: https://en.wikipedia.org/wiki/De_Morgan%27s_laws

View File

@@ -98,7 +98,7 @@ reveal_type(x)
for x in (1, "a", b"foo"):
pass
# revealed: Literal[1, "a", b"foo"]
# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
```

View File

@@ -41,7 +41,7 @@ def _(flag: bool, flag2: bool):
x = 3
reveal_type(x) # revealed: Literal[2, 3]
reveal_type(y) # revealed: Literal[4, 1, 2]
reveal_type(y) # revealed: Literal[1, 2, 4]
```
## Nested `while` loops

View File

@@ -170,35 +170,8 @@ def f(*args, **kwargs) -> int: ...
class A(metaclass=f): ...
# TODO: Should be `int`
reveal_type(A) # revealed: Literal[A]
reveal_type(A.__class__) # revealed: type[int]
def _(n: int):
# error: [invalid-metaclass]
class B(metaclass=n): ...
# TODO: Should be `Unknown`
reveal_type(B) # revealed: Literal[B]
reveal_type(B.__class__) # revealed: type[Unknown]
def _(flag: bool):
m = f if flag else 42
# error: [invalid-metaclass]
class C(metaclass=m): ...
# TODO: Should be `int | Unknown`
reveal_type(C) # revealed: Literal[C]
reveal_type(C.__class__) # revealed: type[Unknown]
class SignatureMismatch: ...
# TODO: Emit a diagnostic
class D(metaclass=SignatureMismatch): ...
# TODO: Should be `Unknown`
reveal_type(D) # revealed: Literal[D]
# TODO: Should be `type[Unknown]`
reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
# TODO should be `type[int]`
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
```
## Cyclic

View File

@@ -56,7 +56,7 @@ def _(x_flag: bool, y_flag: bool):
def _(flag1: bool, flag2: bool):
x = None if flag1 else (1 if flag2 else True)
reveal_type(x) # revealed: None | Literal[1, True]
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
if x is None:
reveal_type(x) # revealed: None
elif x is True:

View File

@@ -17,7 +17,7 @@ def _(flag: bool):
reveal_type(x) # revealed: Never
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1, "a"]
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## `classinfo` is a tuple of types
@@ -30,7 +30,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
x = 1 if flag else "a"
if isinstance(x, (int, str)):
reveal_type(x) # revealed: Literal[1, "a"]
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
@@ -43,19 +43,19 @@ def _(flag: bool, flag1: bool, flag2: bool):
# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
reveal_type(x) # revealed: Literal[1, "a"]
reveal_type(x) # revealed: Literal[1] | Literal["a"]
else:
reveal_type(x) # revealed: Never
y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
reveal_type(y) # revealed: Literal[1, "a"]
reveal_type(y) # revealed: Literal[1] | Literal["a"]
if isinstance(y, (int, bytes)):
reveal_type(y) # revealed: Literal[1, b"b"]
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
if isinstance(y, (str, bytes)):
reveal_type(y) # revealed: Literal["a", b"b"]
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
```
## `classinfo` is a nested tuple of types
@@ -107,7 +107,7 @@ def _(flag: bool):
x = 1 if flag else "foo"
if isinstance(x, t):
reveal_type(x) # revealed: Literal[1, "foo"]
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
```
## Do not use custom `isinstance` for narrowing
@@ -119,7 +119,7 @@ def _(flag: bool):
x = 1 if flag else "a"
if isinstance(x, int):
reveal_type(x) # revealed: Literal[1, "a"]
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## Do support narrowing if `isinstance` is aliased
@@ -155,12 +155,12 @@ def _(flag: bool):
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
reveal_type(x) # revealed: Literal[1, "a"]
reveal_type(x) # revealed: Literal[1] | Literal["a"]
# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
reveal_type(x) # revealed: Literal[1, "a"]
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```
## Do not narrow if there are keyword arguments
@@ -169,15 +169,8 @@ def _(flag: bool):
def _(flag: bool):
x = 1 if flag else "a"
# error: [unknown-argument]
# TODO: this should cause us to emit a diagnostic
# (`isinstance` has no `foo` parameter)
if isinstance(x, int, foo="bar"):
reveal_type(x) # revealed: Literal[1, "a"]
```
## `type[]` types are narrowed as well as class-literal types
```py
def _(x: object, y: type[int]):
if isinstance(x, y):
reveal_type(x) # revealed: int
reveal_type(x) # revealed: Literal[1] | Literal["a"]
```

View File

@@ -90,7 +90,7 @@ def _(t: type[object]):
if issubclass(t, B):
reveal_type(t) # revealed: type[A] & type[B]
else:
reveal_type(t) # revealed: type & ~type[A]
reveal_type(t) # revealed: type[object] & ~type[A]
```
### Handling of `None`
@@ -146,7 +146,7 @@ class A: ...
t = object()
# error: [invalid-argument-type]
# TODO: we should emit a diagnostic here
if issubclass(t, A):
reveal_type(t) # revealed: type[A]
```
@@ -160,7 +160,7 @@ branch:
```py
t = 1
# error: [invalid-argument-type]
# TODO: we should emit a diagnostic here
if issubclass(t, int):
reveal_type(t) # revealed: Never
```
@@ -234,15 +234,8 @@ def flag() -> bool: ...
t = int if flag() else str
# error: [unknown-argument]
# TODO: this should cause us to emit a diagnostic
# (`issubclass` has no `foo` parameter)
if issubclass(t, int, foo="bar"):
reveal_type(t) # revealed: Literal[int, str]
```
### `type[]` types are narrowed as well as class-literal types
```py
def _(x: type, y: type[int]):
if issubclass(x, y):
reveal_type(x) # revealed: type[int]
```

View File

@@ -16,48 +16,3 @@ def _(flag: bool):
reveal_type(y) # revealed: Literal[0] | None
```
## Class patterns
```py
def get_object() -> object: ...
class A: ...
class B: ...
x = get_object()
reveal_type(x) # revealed: object
match x:
case A():
reveal_type(x) # revealed: A
case B():
# TODO could be `B & ~A`
reveal_type(x) # revealed: B
reveal_type(x) # revealed: object
```
## Class pattern with guard
```py
def get_object() -> object: ...
class A:
def y() -> int: ...
class B: ...
x = get_object()
reveal_type(x) # revealed: object
match x:
case A() if reveal_type(x): # revealed: A
pass
case B() if reveal_type(x): # revealed: B
pass
reveal_type(x) # revealed: object
```

View File

@@ -9,39 +9,39 @@ def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[(
x = foo()
if x:
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
else:
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
if not x:
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
reveal_type(x) # revealed: Literal[0] | Literal[False] | Literal[""] | Literal[b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"] | Literal[b"bar"]
if x and not x:
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
if not (x and not x):
reveal_type(x) # revealed: Literal[0, "", b"", -1, "foo", b"bar"] | bool | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["", "foo"] | Literal[b"", b"bar"] | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if x or not x:
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Never
if not (x or not x):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, "foo", b"bar", 0, "", b""] | bool | None | tuple[()]
reveal_type(x) # revealed: Literal[-1, 0] | bool | Literal["foo", ""] | Literal[b"bar", b""] | None | tuple[()]
if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1, True, "foo"]
reveal_type(x) # revealed: Literal[-1] | Literal[True] | Literal["foo"]
else:
reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()]
reveal_type(x) # revealed: Literal[b"", b"bar"] | None | tuple[()] | Literal[0] | Literal[False] | Literal[""]
```
## Function Literals
@@ -166,16 +166,16 @@ y = literals()
if isinstance(x, str) and not isinstance(x, B):
reveal_type(x) # revealed: A & str & ~B
reveal_type(y) # revealed: Literal[0, 42, "", "hello"]
reveal_type(y) # revealed: Literal[0, 42] | Literal["", "hello"]
z = x if flag() else y
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42] | Literal["", "hello"]
if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42] | Literal["hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0] | Literal[""]
```
## Narrowing Multiple Variables

View File

@@ -37,7 +37,7 @@ class C:
# error: [possibly-unresolved-reference]
y = x
reveal_type(C.y) # revealed: Literal[1, "abc"]
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
```
## Unbound function local

View File

@@ -167,7 +167,7 @@ class A:
__slots__ = ()
__slots__ += ("a", "b")
reveal_type(A.__slots__) # revealed: @Todo(return type)
reveal_type(A.__slots__) # revealed: @Todo(Support for more binary expressions)
class B:
__slots__ = ("c", "d")

View File

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

View File

@@ -1,352 +0,0 @@
# Type API (`knot_extensions`)
This document describes the internal `knot_extensions` API for creating and manipulating types as
well as testing various type system properties.
## Type extensions
The Python language itself allows us to perform a variety of operations on types. For example, we
can build a union of types like `int | None`, or we can use type constructors such as `list[int]`
and `type[int]` to create new types. But some type-level operations that we rely on in Red Knot,
like intersections, cannot yet be expressed in Python. The `knot_extensions` module provides the
`Intersection` and `Not` type constructors (special forms) which allow us to construct these types
directly.
### Negation
```py
from knot_extensions import Not, static_assert
def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
reveal_type(n1) # revealed: ~int
reveal_type(n2) # revealed: int
reveal_type(n3) # revealed: ~int
def static_truthiness(not_one: Not[Literal[1]]) -> None:
static_assert(not_one != 1)
static_assert(not (not_one == 1))
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
n: Not[int, str]
```
### Intersection
```py
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
from typing_extensions import Never
class S: ...
class T: ...
def x(x1: Intersection[S, T], x2: Intersection[S, Not[T]]) -> None:
reveal_type(x1) # revealed: S & T
reveal_type(x2) # revealed: S & ~T
def y(y1: Intersection[int, object], y2: Intersection[int, bool], y3: Intersection[int, Never]) -> None:
reveal_type(y1) # revealed: int
reveal_type(y2) # revealed: bool
reveal_type(y3) # revealed: Never
def z(z1: Intersection[int, Not[Literal[1]], Not[Literal[2]]]) -> None:
reveal_type(z1) # revealed: int & ~Literal[1] & ~Literal[2]
class A: ...
class B: ...
class C: ...
type ABC = Intersection[A, B, C]
static_assert(is_subtype_of(ABC, A))
static_assert(is_subtype_of(ABC, B))
static_assert(is_subtype_of(ABC, C))
class D: ...
static_assert(not is_subtype_of(ABC, D))
```
### Unknown type
The `Unknown` type is a special type that we use to represent actually unknown types (no
annotation), as opposed to `Any` which represents an explicitly unknown type.
```py
from knot_extensions import Unknown, static_assert, is_assignable_to, is_fully_static
static_assert(is_assignable_to(Unknown, int))
static_assert(is_assignable_to(int, Unknown))
static_assert(not is_fully_static(Unknown))
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: tuple[str, Unknown]
reveal_type(z) # revealed: Unknown | Literal[1]
# Unknown can be subclassed, just like Any
class C(Unknown): ...
# revealed: tuple[Literal[C], Unknown, Literal[object]]
reveal_type(C.__mro__)
# error: "Special form `knot_extensions.Unknown` expected no type parameter"
u: Unknown[str]
```
## Static assertions
### Basics
The `knot_extensions` module provides a `static_assert` function that can be used to enforce
properties at type-check time. The function takes an arbitrary expression and raises a type error if
the expression is not of statically known truthiness.
```py
from knot_extensions import static_assert
from typing import TYPE_CHECKING
import sys
static_assert(True)
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
static_assert(False or True)
static_assert(True and True)
static_assert(False or False) # error: "Static assertion error: argument evaluates to `False`"
static_assert(False and True) # error: "Static assertion error: argument evaluates to `False`"
static_assert(1 + 1 == 2)
static_assert(1 + 1 == 3) # error: "Static assertion error: argument evaluates to `False`"
static_assert("a" in "abc")
static_assert("d" in "abc") # error: "Static assertion error: argument evaluates to `False`"
n = None
static_assert(n is None)
static_assert(TYPE_CHECKING)
static_assert(sys.version_info >= (3, 6))
```
### Narrowing constraints
Static assertions can be used to enforce narrowing constraints:
```py
from knot_extensions import static_assert
def f(x: int) -> None:
if x != 0:
static_assert(x != 0)
else:
# `int` can be subclassed, so we cannot assert that `x == 0` here:
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
static_assert(x == 0)
```
### Truthy expressions
See also: <https://docs.python.org/3/library/stdtypes.html#truth-value-testing>
```py
from knot_extensions import static_assert
static_assert(True)
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
static_assert(None) # error: "Static assertion error: argument of type `None` is statically known to be falsy"
static_assert(1)
static_assert(0) # error: "Static assertion error: argument of type `Literal[0]` is statically known to be falsy"
static_assert((0,))
static_assert(()) # error: "Static assertion error: argument of type `tuple[()]` is statically known to be falsy"
static_assert("a")
static_assert("") # error: "Static assertion error: argument of type `Literal[""]` is statically known to be falsy"
static_assert(b"a")
static_assert(b"") # error: "Static assertion error: argument of type `Literal[b""]` is statically known to be falsy"
```
### Error messages
We provide various tailored error messages for wrong argument types to `static_assert`:
```py
from knot_extensions import static_assert
static_assert(2 * 3 == 6)
# error: "Static assertion error: argument evaluates to `False`"
static_assert(2 * 3 == 7)
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
static_assert(int(2.0 * 3.0) == 6)
class InvalidBoolDunder:
def __bool__(self) -> int:
return 1
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
static_assert(InvalidBoolDunder())
```
### Custom error messages
Alternatively, users can provide custom error messages:
```py
from knot_extensions import static_assert
# error: "Static assertion error: I really want this to be true"
static_assert(1 + 1 == 3, "I really want this to be true")
error_message = "A custom message "
error_message += "constructed from multiple string literals"
# error: "Static assertion error: A custom message constructed from multiple string literals"
static_assert(False, error_message)
# There are limitations to what we can still infer as a string literal. In those cases,
# we simply fall back to the default message.
shouted_message = "A custom message".upper()
# error: "Static assertion error: argument evaluates to `False`"
static_assert(False, shouted_message)
```
## Type predicates
The `knot_extensions` module also provides predicates to test various properties of types. These are
implemented as functions that return `Literal[True]` or `Literal[False]` depending on the result of
the test.
### Equivalence
```py
from knot_extensions import is_equivalent_to, static_assert
from typing_extensions import Never, Union
static_assert(is_equivalent_to(type, type[object]))
static_assert(is_equivalent_to(tuple[int, Never], Never))
static_assert(is_equivalent_to(int | str, Union[int, str]))
static_assert(not is_equivalent_to(int, str))
static_assert(not is_equivalent_to(int | str, int | str | bytes))
```
### Subtyping
```py
from knot_extensions import is_subtype_of, static_assert
static_assert(is_subtype_of(bool, int))
static_assert(not is_subtype_of(str, int))
static_assert(is_subtype_of(bool, int | str))
static_assert(is_subtype_of(str, int | str))
static_assert(not is_subtype_of(bytes, int | str))
class Base: ...
class Derived(Base): ...
class Unrelated: ...
static_assert(is_subtype_of(Derived, Base))
static_assert(not is_subtype_of(Base, Derived))
static_assert(is_subtype_of(Base, Base))
static_assert(not is_subtype_of(Unrelated, Base))
static_assert(not is_subtype_of(Base, Unrelated))
```
### Assignability
```py
from knot_extensions import is_assignable_to, static_assert
from typing import Any
static_assert(is_assignable_to(int, Any))
static_assert(is_assignable_to(Any, str))
static_assert(not is_assignable_to(int, str))
```
### Disjointness
```py
from knot_extensions import is_disjoint_from, static_assert
static_assert(is_disjoint_from(None, int))
static_assert(not is_disjoint_from(Literal[2] | str, int))
```
### Fully static types
```py
from knot_extensions import is_fully_static, static_assert
from typing import Any
static_assert(is_fully_static(int | str))
static_assert(is_fully_static(type[int]))
static_assert(not is_fully_static(int | Any))
static_assert(not is_fully_static(type[Any]))
```
### Singleton types
```py
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(None))
static_assert(is_singleton(Literal[True]))
static_assert(not is_singleton(int))
static_assert(not is_singleton(Literal["a"]))
```
### Single-valued types
```py
from knot_extensions import is_single_valued, static_assert
static_assert(is_single_valued(None))
static_assert(is_single_valued(Literal[True]))
static_assert(is_single_valued(Literal["a"]))
static_assert(not is_single_valued(int))
static_assert(not is_single_valued(Literal["a"] | Literal["b"]))
```
## `TypeOf`
We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to
it in a type expression. For example, if we want to make sure that the class literal type `str` is a
subtype of `type[str]`, we can not use `is_subtype_of(str, type[str])`, as that would test if the
type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` to get the type of
the expression `str`:
```py
from knot_extensions import TypeOf, is_subtype_of, static_assert
# This is incorrect and therefore fails with ...
# error: "Static assertion error: argument evaluates to `False`"
static_assert(is_subtype_of(str, type[str]))
# Correct, returns True:
static_assert(is_subtype_of(TypeOf[str], type[str]))
class Base: ...
class Derived(Base): ...
# `TypeOf` can be used in annotations:
def type_of_annotation() -> None:
t1: TypeOf[Base] = Base
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
# Note how this is different from `type[…]` which includes subclasses:
s1: type[Base] = Base
s2: type[Base] = Derived # no error here
# error: "Special form `knot_extensions.TypeOf` expected exactly one type parameter"
t: TypeOf[int, str, bytes]
```

View File

@@ -142,25 +142,3 @@ class Foo(type[int]): ...
# TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]]
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
```
## `@final` classes
`type[]` types are eagerly converted to class-literal types if a class decorated with `@final` is
used as the type argument. This applies to standard-library classes and user-defined classes:
```toml
[environment]
python-version = "3.10"
```
```py
from types import EllipsisType
from typing import final
@final
class Foo: ...
def _(x: type[Foo], y: type[EllipsisType]):
reveal_type(x) # revealed: Literal[Foo]
reveal_type(y) # revealed: Literal[EllipsisType]
```

View File

@@ -47,8 +47,9 @@ x: type = A() # error: [invalid-assignment]
```py
def f(x: type[object]):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
reveal_type(x) # revealed: type[object]
# TODO: bound method types
reveal_type(x.__repr__) # revealed: Literal[__repr__]
class A: ...

View File

@@ -1,352 +0,0 @@
# Assignable-to relation
The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target).
This allows us to check if a type `S` can be used in a context where a type `T` is expected
(function arguments, variable assignments). See the [typing documentation] for a precise definition
of this concept.
## Basic types
### Fully static
Fully static types participate in subtyping. If a type `S` is a subtype of `T`, `S` will also be
assignable to `T`. Two equivalent types are subtypes of each other:
```py
from knot_extensions import static_assert, is_assignable_to
class Parent: ...
class Child1(Parent): ...
class Child2(Parent): ...
class Grandchild(Child1, Child2): ...
class Unrelated: ...
static_assert(is_assignable_to(int, int))
static_assert(is_assignable_to(Parent, Parent))
static_assert(is_assignable_to(Child1, Parent))
static_assert(is_assignable_to(Grandchild, Parent))
static_assert(is_assignable_to(Unrelated, Unrelated))
static_assert(not is_assignable_to(str, int))
static_assert(not is_assignable_to(object, int))
static_assert(not is_assignable_to(Parent, Child1))
static_assert(not is_assignable_to(Unrelated, Parent))
static_assert(not is_assignable_to(Child1, Child2))
```
### Gradual types
Gradual types do not participate in subtyping, but can still be assignable to other types (and
static types can be assignable to gradual types):
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Any
static_assert(is_assignable_to(Unknown, Literal[1]))
static_assert(is_assignable_to(Any, Literal[1]))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(Literal[1], Any))
```
## Literal types
### Boolean literals
`Literal[True]` and `Literal[False]` are both subtypes of (and therefore assignable to) `bool`,
which is in turn a subtype of `int`:
```py
from knot_extensions import static_assert, is_assignable_to
from typing import Literal
static_assert(is_assignable_to(Literal[True], Literal[True]))
static_assert(is_assignable_to(Literal[True], bool))
static_assert(is_assignable_to(Literal[True], int))
static_assert(not is_assignable_to(Literal[True], Literal[False]))
static_assert(not is_assignable_to(bool, Literal[True]))
```
### Integer literals
```py
from knot_extensions import static_assert, is_assignable_to
from typing import Literal
static_assert(is_assignable_to(Literal[1], Literal[1]))
static_assert(is_assignable_to(Literal[1], int))
static_assert(not is_assignable_to(Literal[1], Literal[2]))
static_assert(not is_assignable_to(int, Literal[1]))
static_assert(not is_assignable_to(Literal[1], str))
```
### String literals and `LiteralString`
All string-literal types are subtypes of (and therefore assignable to) `LiteralString`, which is in
turn a subtype of `str`:
```py
from knot_extensions import static_assert, is_assignable_to
from typing_extensions import Literal, LiteralString
static_assert(is_assignable_to(Literal["foo"], Literal["foo"]))
static_assert(is_assignable_to(Literal["foo"], LiteralString))
static_assert(is_assignable_to(Literal["foo"], str))
static_assert(is_assignable_to(LiteralString, str))
static_assert(not is_assignable_to(Literal["foo"], Literal["bar"]))
static_assert(not is_assignable_to(str, Literal["foo"]))
static_assert(not is_assignable_to(str, LiteralString))
```
### Byte literals
```py
from knot_extensions import static_assert, is_assignable_to
from typing_extensions import Literal, LiteralString
static_assert(is_assignable_to(Literal[b"foo"], bytes))
static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"]))
static_assert(not is_assignable_to(Literal[b"foo"], str))
static_assert(not is_assignable_to(Literal[b"foo"], LiteralString))
static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"]))
static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))
```
## `type[…]` and class literals
In the following tests, `TypeOf[str]` is a singleton type with a single inhabitant, the class `str`.
This contrasts with `type[str]`, which represents "all possible subclasses of `str`".
Both `TypeOf[str]` and `type[str]` are subtypes of `type` and `type[object]`, which both represent
"all possible instances of `type`"; therefore both `type[str]` and `TypeOf[str]` are assignable to
`type`. `type[Any]`, on the other hand, represents a type of unknown size or inhabitants, but which
is known to be no larger than the set of possible objects represented by `type`.
```py
from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf
from typing import Any
static_assert(is_assignable_to(type, type))
static_assert(is_assignable_to(type[object], type[object]))
static_assert(is_assignable_to(type, type[object]))
static_assert(is_assignable_to(type[object], type))
static_assert(is_assignable_to(type[str], type[object]))
static_assert(is_assignable_to(TypeOf[str], type[object]))
static_assert(is_assignable_to(type[str], type))
static_assert(is_assignable_to(TypeOf[str], type))
static_assert(is_assignable_to(type[str], type[str]))
static_assert(is_assignable_to(TypeOf[str], type[str]))
static_assert(not is_assignable_to(TypeOf[int], type[str]))
static_assert(not is_assignable_to(type, type[str]))
static_assert(not is_assignable_to(type[object], type[str]))
static_assert(is_assignable_to(type[Any], type[Any]))
static_assert(is_assignable_to(type[Any], type[object]))
static_assert(is_assignable_to(type[object], type[Any]))
static_assert(is_assignable_to(type, type[Any]))
static_assert(is_assignable_to(type[Any], type[str]))
static_assert(is_assignable_to(type[str], type[Any]))
static_assert(is_assignable_to(TypeOf[str], type[Any]))
static_assert(is_assignable_to(type[Unknown], type[Unknown]))
static_assert(is_assignable_to(type[Unknown], type[object]))
static_assert(is_assignable_to(type[object], type[Unknown]))
static_assert(is_assignable_to(type, type[Unknown]))
static_assert(is_assignable_to(type[Unknown], type[str]))
static_assert(is_assignable_to(type[str], type[Unknown]))
static_assert(is_assignable_to(TypeOf[str], type[Unknown]))
static_assert(is_assignable_to(type[Unknown], type[Any]))
static_assert(is_assignable_to(type[Any], type[Unknown]))
static_assert(not is_assignable_to(object, type[Any]))
static_assert(not is_assignable_to(str, type[Any]))
class Meta(type): ...
static_assert(is_assignable_to(type[Any], Meta))
static_assert(is_assignable_to(type[Unknown], Meta))
static_assert(is_assignable_to(Meta, type[Any]))
static_assert(is_assignable_to(Meta, type[Unknown]))
```
## Tuple types
```py
from knot_extensions import static_assert, is_assignable_to
from typing import Literal, Any
static_assert(is_assignable_to(tuple[()], tuple[()]))
static_assert(is_assignable_to(tuple[int], tuple[int]))
static_assert(is_assignable_to(tuple[int], tuple[Any]))
static_assert(is_assignable_to(tuple[Any], tuple[int]))
static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
static_assert(is_assignable_to(tuple[Literal[1], Literal[2]], tuple[int, int]))
static_assert(is_assignable_to(tuple[Any, Literal[2]], tuple[int, int]))
static_assert(is_assignable_to(tuple[Literal[1], Any], tuple[int, int]))
static_assert(not is_assignable_to(tuple[()], tuple[int]))
static_assert(not is_assignable_to(tuple[int], tuple[str]))
static_assert(not is_assignable_to(tuple[int], tuple[int, str]))
static_assert(not is_assignable_to(tuple[int, str], tuple[int]))
static_assert(not is_assignable_to(tuple[int, int], tuple[Literal[1], int]))
static_assert(not is_assignable_to(tuple[Any, Literal[2]], tuple[int, str]))
```
## Union types
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Literal, Any
static_assert(is_assignable_to(int, int | str))
static_assert(is_assignable_to(str, int | str))
static_assert(is_assignable_to(int | str, int | str))
static_assert(is_assignable_to(str | int, int | str))
static_assert(is_assignable_to(Literal[1], int | str))
static_assert(is_assignable_to(Literal[1], Unknown | str))
static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2]))
static_assert(is_assignable_to(Literal[1] | Literal[2], int))
static_assert(is_assignable_to(Literal[1] | None, int | None))
static_assert(is_assignable_to(Any, int | str))
static_assert(is_assignable_to(Any | int, int))
static_assert(is_assignable_to(str, int | Any))
static_assert(not is_assignable_to(int | None, int))
static_assert(not is_assignable_to(int | None, str | None))
static_assert(not is_assignable_to(Literal[1] | None, int))
static_assert(not is_assignable_to(Literal[1] | None, str | None))
static_assert(not is_assignable_to(Any | int | str, int))
```
## Intersection types
```py
from knot_extensions import static_assert, is_assignable_to, Intersection, Not
from typing_extensions import Any, Literal
class Parent: ...
class Child1(Parent): ...
class Child2(Parent): ...
class Grandchild(Child1, Child2): ...
class Unrelated: ...
static_assert(is_assignable_to(Intersection[Child1, Child2], Child1))
static_assert(is_assignable_to(Intersection[Child1, Child2], Child2))
static_assert(is_assignable_to(Intersection[Child1, Child2], Parent))
static_assert(is_assignable_to(Intersection[Child1, Parent], Parent))
static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent))
static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1))
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1))
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent))
static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent))
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2]))
static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1]))
static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2]))
static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated]))
static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]]))
static_assert(not is_assignable_to(int, Not[int]))
static_assert(not is_assignable_to(int, Not[Literal[1]]))
static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated))
# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899)
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Any, int], int))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
```
## General properties
See also: our property tests in `property_tests.rs`.
### Everything is assignable to `object`
`object` is Python's top type; the set of all possible objects at runtime:
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Literal, Any
static_assert(is_assignable_to(str, object))
static_assert(is_assignable_to(Literal[1], object))
static_assert(is_assignable_to(object, object))
static_assert(is_assignable_to(type, object))
static_assert(is_assignable_to(Any, object))
static_assert(is_assignable_to(Unknown, object))
static_assert(is_assignable_to(type[object], object))
static_assert(is_assignable_to(type[str], object))
static_assert(is_assignable_to(type[Any], object))
```
### Every type is assignable to `Any` / `Unknown`
`Any` and `Unknown` are gradual types. They could materialize to any given type at runtime, and so
any type is assignable to them:
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing import Literal, Any
static_assert(is_assignable_to(str, Any))
static_assert(is_assignable_to(Literal[1], Any))
static_assert(is_assignable_to(object, Any))
static_assert(is_assignable_to(type, Any))
static_assert(is_assignable_to(Any, Any))
static_assert(is_assignable_to(Unknown, Any))
static_assert(is_assignable_to(type[object], Any))
static_assert(is_assignable_to(type[str], Any))
static_assert(is_assignable_to(type[Any], Any))
static_assert(is_assignable_to(str, Unknown))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(object, Unknown))
static_assert(is_assignable_to(type, Unknown))
static_assert(is_assignable_to(Any, Unknown))
static_assert(is_assignable_to(Unknown, Unknown))
static_assert(is_assignable_to(type[object], Unknown))
static_assert(is_assignable_to(type[str], Unknown))
static_assert(is_assignable_to(type[Any], Unknown))
```
### `Never` is assignable to every type
`Never` is Python's bottom type: the empty set, a type with no inhabitants. It is therefore
assignable to any arbitrary type.
```py
from knot_extensions import static_assert, is_assignable_to, Unknown
from typing_extensions import Never, Any
static_assert(is_assignable_to(Never, str))
static_assert(is_assignable_to(Never, Literal[1]))
static_assert(is_assignable_to(Never, object))
static_assert(is_assignable_to(Never, type))
static_assert(is_assignable_to(Never, Any))
static_assert(is_assignable_to(Never, Unknown))
static_assert(is_assignable_to(Never, type[object]))
static_assert(is_assignable_to(Never, type[str]))
static_assert(is_assignable_to(Never, type[Any]))
```
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

View File

@@ -1,33 +0,0 @@
# Tuples containing `Never`
A heterogeneous `tuple[…]` type that contains `Never` as a type argument simplifies to `Never`. One
way to think about this is the following: in order to construct a tuple, you need to have an object
of every element type. But since there is no object of type `Never`, you cannot construct the tuple.
Such a tuple type is therefore uninhabited and equivalent to `Never`.
In the language of algebraic data types, a tuple type is a product type and `Never` acts like the
zero element in multiplication, similar to how a Cartesian product with the empty set is the empty
set.
```py
from knot_extensions import static_assert, is_equivalent_to
from typing_extensions import Never, NoReturn
static_assert(is_equivalent_to(Never, tuple[Never]))
static_assert(is_equivalent_to(Never, tuple[Never, int]))
static_assert(is_equivalent_to(Never, tuple[int, Never]))
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
# The empty tuple is *not* equivalent to Never!
static_assert(not is_equivalent_to(Never, tuple[()]))
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn, str]))
static_assert(is_equivalent_to(NoReturn, tuple[int, tuple[str, NoReturn]]))
static_assert(is_equivalent_to(NoReturn, tuple[tuple[str, NoReturn], int]))
```

View File

@@ -34,10 +34,6 @@ reveal_type(~No()) # revealed: Unknown
## Classes
Dunder methods defined in a class are available to instances of that class, but not to the class
itself. (For these operators to work on the class itself, they would have to be defined on the
class's type, i.e. `type`.)
```py
class Yes:
def __pos__(self) -> bool:

View File

@@ -1,143 +0,0 @@
# Union types
This test suite covers certain basic properties and simplification strategies for union types.
## Basic unions
```py
from typing import Literal
def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None:
reveal_type(u1) # revealed: int | str
reveal_type(u2) # revealed: Literal[0, 1]
```
## Duplicate elements are collapsed
```py
def _(u1: int | int | str, u2: int | str | int) -> None:
reveal_type(u1) # revealed: int | str
reveal_type(u2) # revealed: int | str
```
## `Never` is removed
`Never` is an empty set, a type with no inhabitants. Its presence in a union is always redundant,
and so we eagerly simplify it away. `NoReturn` is equivalent to `Never`.
```py
from typing_extensions import Never, NoReturn
def never(u1: int | Never, u2: int | Never | str) -> None:
reveal_type(u1) # revealed: int
reveal_type(u2) # revealed: int | str
def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None:
reveal_type(u1) # revealed: int
reveal_type(u2) # revealed: int | str
```
## Flattening of nested unions
```py
from typing import Literal
def _(
u1: (int | str) | bytes,
u2: int | (str | bytes),
u3: int | (str | (bytes | complex)),
) -> None:
reveal_type(u1) # revealed: int | str | bytes
reveal_type(u2) # revealed: int | str | bytes
reveal_type(u3) # revealed: int | str | bytes | complex
```
## Simplification using subtyping
The type `S | T` can be simplified to `T` if `S` is a subtype of `T`:
```py
from typing_extensions import Literal, LiteralString
def _(
u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString
) -> None:
reveal_type(u1) # revealed: str
reveal_type(u2) # revealed: str
reveal_type(u3) # revealed: str
reveal_type(u4) # revealed: str | bytes
```
## Boolean literals
The union `Literal[True] | Literal[False]` is exactly equivalent to `bool`:
```py
from typing import Literal
def _(
u1: Literal[True, False],
u2: bool | Literal[True],
u3: Literal[True] | bool,
u4: Literal[True] | Literal[True, 17],
u5: Literal[True, False, True, 17],
) -> None:
reveal_type(u1) # revealed: bool
reveal_type(u2) # revealed: bool
reveal_type(u3) # revealed: bool
reveal_type(u4) # revealed: Literal[True, 17]
reveal_type(u5) # revealed: bool | Literal[17]
```
## Do not erase `Unknown`
```py
from knot_extensions import Unknown
def _(u1: Unknown | str, u2: str | Unknown) -> None:
reveal_type(u1) # revealed: Unknown | str
reveal_type(u2) # revealed: str | Unknown
```
## Collapse multiple `Unknown`s
Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple `Unknown`s in a
union are still redundant:
```py
from knot_extensions import Unknown
def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None:
reveal_type(u1) # revealed: Unknown | str
reveal_type(u2) # revealed: Unknown | str
reveal_type(u3) # revealed: str | Unknown
```
## Subsume multiple elements
Simplifications still apply when `Unknown` is present.
```py
from knot_extensions import Unknown
def _(u1: str | Unknown | int | object):
reveal_type(u1) # revealed: Unknown | object
```
## Union of intersections
We can simplify unions of intersections:
```py
from knot_extensions import Intersection, Not
class P: ...
class Q: ...
def _(
i1: Intersection[P, Q] | Intersection[P, Q],
i2: Intersection[P, Q] | Intersection[Q, P],
) -> None:
reveal_type(i1) # revealed: P & Q
reveal_type(i2) # revealed: P & Q
```

View File

@@ -426,8 +426,8 @@ def _(flag: bool):
value = ("a", "b")
a, b = value
reveal_type(a) # revealed: Literal[1, "a"]
reveal_type(b) # revealed: Literal[2, "b"]
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
```
### Typing literal
@@ -528,8 +528,8 @@ for a, b in ((1, 2), (3, 4)):
```py
for a, b in ((1, 2), ("a", "b")):
reveal_type(a) # revealed: Literal[1, "a"]
reveal_type(b) # revealed: Literal[2, "b"]
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
```
### Mixed literals values (2)

View File

@@ -109,7 +109,6 @@ pub enum KnownModule {
#[allow(dead_code)]
Abc, // currently only used in tests
Collections,
KnotExtensions,
}
impl KnownModule {
@@ -123,7 +122,6 @@ impl KnownModule {
Self::Sys => "sys",
Self::Abc => "abc",
Self::Collections => "collections",
Self::KnotExtensions => "knot_extensions",
}
}
@@ -149,7 +147,6 @@ impl KnownModule {
"sys" => Some(Self::Sys),
"abc" => Some(Self::Abc),
"collections" => Some(Self::Collections),
"knot_extensions" => Some(Self::KnotExtensions),
_ => None,
}
}
@@ -157,8 +154,4 @@ impl KnownModule {
pub const fn is_typing(self) -> bool {
matches!(self, Self::Typing)
}
pub const fn is_knot_extensions(self) -> bool {
matches!(self, Self::KnotExtensions)
}
}

View File

@@ -404,17 +404,6 @@ impl<'db> SemanticIndexBuilder<'db> {
pattern: &ast::Pattern,
guard: Option<&ast::Expr>,
) -> Constraint<'db> {
// This is called for the top-level pattern of each match arm. We need to create a
// standalone expression for each arm of a match statement, since they can introduce
// constraints on the match subject. (Or more accurately, for the match arm's pattern,
// since its the pattern that introduces any constraints, not the body.) Ideally, that
// standalone expression would wrap the match arm's pattern as a whole. But a standalone
// expression can currently only wrap an ast::Expr, which patterns are not. So, we need to
// choose an Expr that can “stand in” for the pattern, which we can wrap in a standalone
// expression.
//
// See the comment in TypeInferenceBuilder::infer_match_pattern for more details.
let guard = guard.map(|guard| self.add_standalone_expression(guard));
let kind = match pattern {
@@ -425,10 +414,6 @@ impl<'db> SemanticIndexBuilder<'db> {
ast::Pattern::MatchSingleton(singleton) => {
PatternConstraintKind::Singleton(singleton.value, guard)
}
ast::Pattern::MatchClass(pattern) => {
let cls = self.add_standalone_expression(&pattern.cls);
PatternConstraintKind::Class(cls, guard)
}
_ => PatternConstraintKind::Unsupported,
};
@@ -893,11 +878,12 @@ where
}
ast::Stmt::If(node) => {
self.visit_expr(&node.test);
let mut no_branch_taken = self.flow_snapshot();
let mut last_constraint = self.record_expression_constraint(&node.test);
let pre_if = self.flow_snapshot();
let constraint = self.record_expression_constraint(&node.test);
let mut constraints = vec![constraint];
self.visit_body(&node.body);
let visibility_constraint_id = self.record_visibility_constraint(last_constraint);
let visibility_constraint_id = self.record_visibility_constraint(constraint);
let mut vis_constraints = vec![visibility_constraint_id];
let mut post_clauses: Vec<FlowSnapshot> = vec![];
@@ -921,27 +907,26 @@ where
// the state that we merge the other snapshots into
post_clauses.push(self.flow_snapshot());
// we can only take an elif/else branch if none of the previous ones were
// taken
self.flow_restore(no_branch_taken.clone());
self.record_negated_constraint(last_constraint);
// taken, so the block entry state is always `pre_if`
self.flow_restore(pre_if.clone());
for constraint in &constraints {
self.record_negated_constraint(*constraint);
}
let elif_constraint = if let Some(elif_test) = clause_test {
self.visit_expr(elif_test);
// A test expression is evaluated whether the branch is taken or not
no_branch_taken = self.flow_snapshot();
let constraint = self.record_expression_constraint(elif_test);
constraints.push(constraint);
Some(constraint)
} else {
None
};
self.visit_body(clause_body);
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
}
if let Some(elif_constraint) = elif_constraint {
last_constraint = elif_constraint;
let id = self.record_visibility_constraint(elif_constraint);
vis_constraints.push(id);
}
@@ -951,7 +936,7 @@ where
self.flow_merge(post_clause_state);
}
self.simplify_visibility_constraints(no_branch_taken);
self.simplify_visibility_constraints(pre_if);
}
ast::Stmt::While(ast::StmtWhile {
test,
@@ -1104,35 +1089,37 @@ where
cases,
range: _,
}) => {
debug_assert_eq!(self.current_match_case, None);
let subject_expr = self.add_standalone_expression(subject);
self.visit_expr(subject);
if cases.is_empty() {
let after_subject = self.flow_snapshot();
let Some((first, remaining)) = cases.split_first() else {
return;
};
let after_subject = self.flow_snapshot();
let mut vis_constraints = vec![];
let mut post_case_snapshots = vec![];
for (i, case) in cases.iter().enumerate() {
if i != 0 {
post_case_snapshots.push(self.flow_snapshot());
self.flow_restore(after_subject.clone());
}
let first_constraint_id = self.add_pattern_constraint(
subject_expr,
&first.pattern,
first.guard.as_deref(),
);
self.current_match_case = Some(CurrentMatchCase::new(&case.pattern));
self.visit_pattern(&case.pattern);
self.current_match_case = None;
self.visit_match_case(first);
let first_vis_constraint_id =
self.record_visibility_constraint(first_constraint_id);
let mut vis_constraints = vec![first_vis_constraint_id];
let mut post_case_snapshots = vec![];
for case in remaining {
post_case_snapshots.push(self.flow_snapshot());
self.flow_restore(after_subject.clone());
let constraint_id = self.add_pattern_constraint(
subject_expr,
&case.pattern,
case.guard.as_deref(),
);
if let Some(expr) = &case.guard {
self.visit_expr(expr);
}
self.visit_body(&case.body);
self.visit_match_case(case);
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
}
@@ -1551,6 +1538,18 @@ where
}
}
fn visit_match_case(&mut self, match_case: &'ast ast::MatchCase) {
debug_assert!(self.current_match_case.is_none());
self.current_match_case = Some(CurrentMatchCase::new(&match_case.pattern));
self.visit_pattern(&match_case.pattern);
self.current_match_case = None;
if let Some(expr) = &match_case.guard {
self.visit_expr(expr);
}
self.visit_body(&match_case.body);
}
fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) {
if let ast::Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
@@ -1637,7 +1636,6 @@ impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
}
}
#[derive(Debug, PartialEq)]
struct CurrentMatchCase<'a> {
/// The pattern that's part of the current match case.
pattern: &'a ast::Pattern,

View File

@@ -22,7 +22,6 @@ pub(crate) enum ConstraintNode<'db> {
pub(crate) enum PatternConstraintKind<'db> {
Singleton(Singleton, Option<Expression<'db>>),
Value(Expression<'db>, Option<Expression<'db>>),
Class(Expression<'db>, Option<Expression<'db>>),
Unsupported,
}

View File

@@ -74,11 +74,6 @@ impl<'db> Definition<'db> {
Some(KnownModule::Typing | KnownModule::TypingExtensions)
)
}
pub(crate) fn is_knot_extensions_definition(self, db: &'db dyn Db) -> bool {
file_to_module(db, self.file(db))
.is_some_and(|module| module.is_known(KnownModule::KnotExtensions))
}
}
#[derive(Copy, Clone, Debug)]

File diff suppressed because it is too large Load Diff

View File

@@ -321,15 +321,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
self.add_positive(db, *neg);
}
}
Type::Never => {
// Adding ~Never to an intersection is a no-op.
}
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
// Adding ~object to an intersection results in Never.
*self = Self::default();
self.positive.insert(Type::Never);
}
ty @ Type::Dynamic(_) => {
ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => {
// Adding any of these types to the negative side of an intersection
// is equivalent to adding it to the positive side. We do this to
// simplify the representation.
@@ -394,34 +386,18 @@ impl<'db> InnerIntersectionBuilder<'db> {
#[cfg(test)]
mod tests {
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
use crate::db::tests::setup_db;
use crate::types::{KnownClass, Truthiness};
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, todo_type, KnownClass, Truthiness, UnionBuilder};
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
use test_case::test_case;
#[test]
fn build_union_no_elements() {
fn build_union() {
let db = setup_db();
let empty_union = UnionBuilder::new(&db).build();
assert_eq!(empty_union, Type::Never);
}
#[test]
fn build_union_single_element() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let union = UnionType::from_elements(&db, [t0]);
assert_eq!(union, t0);
}
#[test]
fn build_union_two_elements() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
@@ -429,12 +405,605 @@ mod tests {
assert_eq!(union.elements(&db), &[t0, t1]);
}
#[test]
fn build_union_single() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ty = UnionType::from_elements(&db, [t0]);
assert_eq!(ty, t0);
}
#[test]
fn build_union_empty() {
let db = setup_db();
let ty = UnionBuilder::new(&db).build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_union_never() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ty = UnionType::from_elements(&db, [t0, Type::Never]);
assert_eq!(ty, t0);
}
#[test]
fn build_union_bool() {
let db = setup_db();
let bool_instance_ty = KnownClass::Bool.to_instance(&db);
let t0 = Type::BooleanLiteral(true);
let t1 = Type::BooleanLiteral(true);
let t2 = Type::BooleanLiteral(false);
let t3 = Type::IntLiteral(17);
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
assert_eq!(union.elements(&db), &[t0, t3]);
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
let result_ty = UnionType::from_elements(&db, [bool_instance_ty, t0]);
assert_eq!(result_ty, bool_instance_ty);
let result_ty = UnionType::from_elements(&db, [t0, bool_instance_ty]);
assert_eq!(result_ty, bool_instance_ty);
}
#[test]
fn build_union_flatten() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let t2 = Type::IntLiteral(2);
let u1 = UnionType::from_elements(&db, [t0, t1]);
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
assert_eq!(union.elements(&db), &[t0, t1, t2]);
}
#[test]
fn build_union_simplify_subtype() {
let db = setup_db();
let t0 = KnownClass::Str.to_instance(&db);
let t1 = Type::LiteralString;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let u1 = UnionType::from_elements(&db, [t1, t0]);
assert_eq!(u0, t0);
assert_eq!(u1, t0);
}
#[test]
fn build_union_no_simplify_unknown() {
let db = setup_db();
let t0 = KnownClass::Str.to_instance(&db);
let t1 = Type::Unknown;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let u1 = UnionType::from_elements(&db, [t1, t0]);
assert_eq!(u0.expect_union().elements(&db), &[t0, t1]);
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
}
#[test]
fn build_union_simplify_multiple_unknown() {
let db = setup_db();
let t0 = KnownClass::Str.to_instance(&db);
let t1 = Type::Unknown;
let u = UnionType::from_elements(&db, [t0, t1, t1]);
assert_eq!(u.expect_union().elements(&db), &[t0, t1]);
}
#[test]
fn build_union_subsume_multiple() {
let db = setup_db();
let str_ty = KnownClass::Str.to_instance(&db);
let int_ty = KnownClass::Int.to_instance(&db);
let object_ty = KnownClass::Object.to_instance(&db);
let unknown_ty = Type::Unknown;
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]);
}
impl<'db> IntersectionType<'db> {
fn pos_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
self.positive(db).into_iter().copied().collect()
}
fn neg_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
self.negative(db).into_iter().copied().collect()
}
}
#[test]
fn build_intersection() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let ta = Type::Any;
let intersection = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t0)
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[ta]);
assert_eq!(intersection.neg_vec(&db), &[t0]);
}
#[test]
fn build_intersection_empty_intersection_equals_object() {
let db = setup_db();
let intersection = IntersectionBuilder::new(&db).build();
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
let ty = IntersectionBuilder::new(&db).build();
assert_eq!(ty, KnownClass::Object.to_instance(&db));
}
#[test]
fn build_intersection_flatten_positive() {
let db = setup_db();
let ta = Type::Any;
let t1 = Type::IntLiteral(1);
let t2 = Type::IntLiteral(2);
let i0 = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t1)
.build();
let intersection = IntersectionBuilder::new(&db)
.add_positive(t2)
.add_positive(i0)
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
assert_eq!(intersection.neg_vec(&db), &[]);
}
#[test]
fn build_intersection_flatten_negative() {
let db = setup_db();
let ta = Type::Any;
let t1 = Type::IntLiteral(1);
let t2 = KnownClass::Int.to_instance(&db);
// i0 = Any & ~Literal[1]
let i0 = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_negative(t1)
.build();
// ta_not_i0 = int & ~(Any & ~Literal[1])
// -> int & (~Any | Literal[1])
// (~Any is equivalent to Any)
// -> (int & Any) | (int & Literal[1])
// -> (int & Any) | Literal[1]
let ta_not_i0 = IntersectionBuilder::new(&db)
.add_positive(t2)
.add_negative(i0)
.build();
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
}
#[test]
fn build_intersection_simplify_negative_any() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_negative(Type::Any)
.build();
assert_eq!(ty, Type::Any);
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Never)
.add_negative(Type::Any)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_multiple_unknown() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_positive(Type::Unknown)
.build();
assert_eq!(ty, Type::Unknown);
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_negative(Type::Unknown)
.build();
assert_eq!(ty, Type::Unknown);
let ty = IntersectionBuilder::new(&db)
.add_negative(Type::Unknown)
.add_negative(Type::Unknown)
.build();
assert_eq!(ty, Type::Unknown);
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_positive(Type::IntLiteral(0))
.add_negative(Type::Unknown)
.build();
assert_eq!(
ty,
IntersectionBuilder::new(&db)
.add_positive(Type::Unknown)
.add_positive(Type::IntLiteral(0))
.build()
);
}
#[test]
fn intersection_distributes_over_union() {
let db = setup_db();
let t0 = Type::IntLiteral(0);
let t1 = Type::IntLiteral(1);
let ta = Type::Any;
let u0 = UnionType::from_elements(&db, [t0, t1]);
let union = IntersectionBuilder::new(&db)
.add_positive(ta)
.add_positive(u0)
.build()
.expect_union();
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
panic!("expected a union of two intersections");
};
assert_eq!(i0.pos_vec(&db), &[ta, t0]);
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
}
#[test]
fn intersection_negation_distributes_over_union() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
r#"
class A: ...
class B: ...
"#,
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
let a = global_symbol(&db, module, "A")
.expect_type()
.to_instance(&db);
let b = global_symbol(&db, module, "B")
.expect_type()
.to_instance(&db);
// intersection: A & B
let intersection = IntersectionBuilder::new(&db)
.add_positive(a)
.add_positive(b)
.build()
.expect_intersection();
assert_eq!(intersection.pos_vec(&db), &[a, b]);
assert_eq!(intersection.neg_vec(&db), &[]);
// ~intersection => ~A | ~B
let negated_intersection = IntersectionBuilder::new(&db)
.add_negative(Type::Intersection(intersection))
.build()
.expect_union();
// should have as elements ~A and ~B
let not_a = a.negate(&db);
let not_b = b.negate(&db);
assert_eq!(negated_intersection.elements(&db), &[not_a, not_b]);
}
#[test]
fn mixed_intersection_negation_distributes_over_union() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
r#"
class A: ...
class B: ...
"#,
)
.unwrap();
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
let a = global_symbol(&db, module, "A")
.expect_type()
.to_instance(&db);
let b = global_symbol(&db, module, "B")
.expect_type()
.to_instance(&db);
let int = KnownClass::Int.to_instance(&db);
// a_not_b: A & ~B
let a_not_b = IntersectionBuilder::new(&db)
.add_positive(a)
.add_negative(b)
.build()
.expect_intersection();
assert_eq!(a_not_b.pos_vec(&db), &[a]);
assert_eq!(a_not_b.neg_vec(&db), &[b]);
// let's build
// int & ~(A & ~B)
// = int & ~(A & ~B)
// = int & (~A | B)
// = (int & ~A) | (int & B)
let t = IntersectionBuilder::new(&db)
.add_positive(int)
.add_negative(Type::Intersection(a_not_b))
.build();
assert_eq!(t.display(&db).to_string(), "int & ~A | int & B");
}
#[test]
fn build_intersection_self_negation() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::none(&db))
.add_negative(Type::none(&db))
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_negative_never() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::none(&db))
.add_negative(Type::Never)
.build();
assert_eq!(ty, Type::none(&db));
}
#[test]
fn build_intersection_simplify_positive_never() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::none(&db))
.add_positive(Type::Never)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_negative_none() {
let db = setup_db();
let ty = IntersectionBuilder::new(&db)
.add_negative(Type::none(&db))
.add_positive(Type::IntLiteral(1))
.build();
assert_eq!(ty, Type::IntLiteral(1));
let ty = IntersectionBuilder::new(&db)
.add_positive(Type::IntLiteral(1))
.add_negative(Type::none(&db))
.build();
assert_eq!(ty, Type::IntLiteral(1));
}
#[test]
fn build_negative_union_de_morgan() {
let db = setup_db();
let union = UnionBuilder::new(&db)
.add(Type::IntLiteral(1))
.add(Type::IntLiteral(2))
.build();
assert_eq!(union.display(&db).to_string(), "Literal[1, 2]");
let ty = IntersectionBuilder::new(&db).add_negative(union).build();
let expected = IntersectionBuilder::new(&db)
.add_negative(Type::IntLiteral(1))
.add_negative(Type::IntLiteral(2))
.build();
assert_eq!(ty.display(&db).to_string(), "~Literal[1] & ~Literal[2]");
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_positive_type_and_positive_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let ty = IntersectionBuilder::new(&db)
.add_positive(t)
.add_positive(s)
.build();
assert_eq!(ty, s);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_positive(t)
.build();
assert_eq!(ty, s);
let literal = Type::string_literal(&db, "a");
let expected = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(literal)
.build();
let ty = IntersectionBuilder::new(&db)
.add_positive(t)
.add_negative(literal)
.add_positive(s)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(literal)
.add_positive(t)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_negative_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_negative(s)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_negative(s)
.add_negative(t)
.build();
assert_eq!(ty, expected);
let object = KnownClass::Object.to_instance(&db);
let expected = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(object)
.build();
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(object)
.add_negative(s)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_multiple_negative_subtypes() {
let db = setup_db();
let s1 = Type::IntLiteral(1);
let s2 = Type::IntLiteral(2);
let t = KnownClass::Int.to_instance(&db);
let expected = IntersectionBuilder::new(&db).add_negative(t).build();
let ty = IntersectionBuilder::new(&db)
.add_negative(s1)
.add_negative(s2)
.add_negative(t)
.build();
assert_eq!(ty, expected);
}
#[test]
fn build_intersection_simplify_negative_type_and_positive_subtype() {
let db = setup_db();
let t = KnownClass::Str.to_instance(&db);
let s = Type::LiteralString;
let ty = IntersectionBuilder::new(&db)
.add_negative(t)
.add_positive(s)
.build();
assert_eq!(ty, Type::Never);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(t)
.build();
assert_eq!(ty, Type::Never);
// This should also work in the presence of additional contributions:
let ty = IntersectionBuilder::new(&db)
.add_positive(KnownClass::Object.to_instance(&db))
.add_negative(t)
.add_positive(s)
.build();
assert_eq!(ty, Type::Never);
let ty = IntersectionBuilder::new(&db)
.add_positive(s)
.add_negative(Type::string_literal(&db, "a"))
.add_negative(t)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_disjoint_positive_types() {
let db = setup_db();
let t1 = Type::IntLiteral(1);
let t2 = Type::none(&db);
let ty = IntersectionBuilder::new(&db)
.add_positive(t1)
.add_positive(t2)
.build();
assert_eq!(ty, Type::Never);
// If there are any negative contributions, they should
// be removed too.
let ty = IntersectionBuilder::new(&db)
.add_positive(KnownClass::Str.to_instance(&db))
.add_negative(Type::LiteralString)
.add_positive(t2)
.build();
assert_eq!(ty, Type::Never);
}
#[test]
fn build_intersection_simplify_disjoint_positive_and_negative_types() {
let db = setup_db();
let t_p = KnownClass::Int.to_instance(&db);
let t_n = Type::string_literal(&db, "t_n");
let ty = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(t_n)
.build();
assert_eq!(ty, t_p);
let ty = IntersectionBuilder::new(&db)
.add_negative(t_n)
.add_positive(t_p)
.build();
assert_eq!(ty, t_p);
let int_literal = Type::IntLiteral(1);
let expected = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(int_literal)
.build();
let ty = IntersectionBuilder::new(&db)
.add_positive(t_p)
.add_negative(int_literal)
.add_negative(t_n)
.build();
assert_eq!(ty, expected);
let ty = IntersectionBuilder::new(&db)
.add_negative(t_n)
.add_negative(int_literal)
.add_positive(t_p)
.build();
assert_eq!(ty, expected);
}
#[test_case(Type::BooleanLiteral(true))]
@@ -479,4 +1048,85 @@ mod tests {
.build();
assert_eq!(ty, Type::BooleanLiteral(!bool_value));
}
#[test_case(Type::Any)]
#[test_case(Type::Unknown)]
#[test_case(todo_type!())]
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
let db = setup_db();
let result = IntersectionBuilder::new(&db)
.add_positive(ty)
.add_negative(ty)
.build();
assert_eq!(result, ty);
let result = IntersectionBuilder::new(&db)
.add_negative(ty)
.add_positive(ty)
.build();
assert_eq!(result, ty);
}
#[test]
fn build_intersection_of_two_unions_simplify() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
"
class A: ...
class B: ...
a = A()
b = B()
",
)
.unwrap();
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
let a = global_symbol(&db, file, "a").expect_type();
let b = global_symbol(&db, file, "b").expect_type();
let union = UnionBuilder::new(&db).add(a).add(b).build();
assert_eq!(union.display(&db).to_string(), "A | B");
let reversed_union = UnionBuilder::new(&db).add(b).add(a).build();
assert_eq!(reversed_union.display(&db).to_string(), "B | A");
let intersection = IntersectionBuilder::new(&db)
.add_positive(union)
.add_positive(reversed_union)
.build();
assert_eq!(intersection.display(&db).to_string(), "B | A");
}
#[test]
fn build_union_of_two_intersections_simplify() {
let mut db = setup_db();
db.write_dedented(
"/src/module.py",
"
class A: ...
class B: ...
a = A()
b = B()
",
)
.unwrap();
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
let a = global_symbol(&db, file, "a").expect_type();
let b = global_symbol(&db, file, "b").expect_type();
let intersection = IntersectionBuilder::new(&db)
.add_positive(a)
.add_positive(b)
.build();
let reversed_intersection = IntersectionBuilder::new(&db)
.add_positive(b)
.add_positive(a)
.build();
let union = UnionBuilder::new(&db)
.add(intersection)
.add(reversed_intersection)
.build();
assert_eq!(union.display(&db).to_string(), "A & B");
}
}

View File

@@ -1,32 +1,17 @@
use super::context::InferContext;
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
use super::diagnostic::CALL_NON_CALLABLE;
use super::{Severity, Type, TypeArrayDisplay, UnionBuilder};
use crate::Db;
use ruff_db::diagnostic::DiagnosticId;
use ruff_python_ast as ast;
mod arguments;
mod bind;
pub(super) use arguments::{Argument, CallArguments};
pub(super) use bind::{bind_call, CallBinding};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum StaticAssertionErrorKind<'db> {
ArgumentIsFalse,
ArgumentIsFalsy(Type<'db>),
ArgumentTruthinessIsAmbiguous(Type<'db>),
CustomError(&'db str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallOutcome<'db> {
Callable {
binding: CallBinding<'db>,
return_ty: Type<'db>,
},
RevealType {
binding: CallBinding<'db>,
return_ty: Type<'db>,
revealed_ty: Type<'db>,
},
NotCallable {
@@ -40,20 +25,12 @@ pub(super) enum CallOutcome<'db> {
called_ty: Type<'db>,
call_outcome: Box<CallOutcome<'db>>,
},
StaticAssertionError {
binding: CallBinding<'db>,
error_kind: StaticAssertionErrorKind<'db>,
},
AssertType {
binding: CallBinding<'db>,
asserted_ty: Type<'db>,
},
}
impl<'db> CallOutcome<'db> {
/// Create a new `CallOutcome::Callable` with given return type.
pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> {
CallOutcome::Callable { binding }
pub(super) fn callable(return_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::Callable { return_ty }
}
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
@@ -62,9 +39,9 @@ impl<'db> CallOutcome<'db> {
}
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
pub(super) fn revealed(return_ty: Type<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::RevealType {
binding,
return_ty,
revealed_ty,
}
}
@@ -80,22 +57,14 @@ impl<'db> CallOutcome<'db> {
}
}
/// Create a new `CallOutcome::AssertType` with given revealed and return types.
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::AssertType {
binding,
asserted_ty,
}
}
/// Get the return type of the call, or `None` if not callable.
pub(super) fn return_ty(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Callable { binding } => Some(binding.return_ty()),
Self::Callable { return_ty } => Some(*return_ty),
Self::RevealType {
binding,
return_ty,
revealed_ty: _,
} => Some(binding.return_ty()),
} => Some(*return_ty),
Self::NotCallable { not_callable_ty: _ } => None,
Self::Union {
outcomes,
@@ -109,16 +78,11 @@ impl<'db> CallOutcome<'db> {
match (acc, ty) {
(None, None) => None,
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::unknown()))),
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::Unknown))),
}
})
.map(UnionBuilder::build),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
Self::StaticAssertionError { .. } => Some(Type::none(db)),
Self::AssertType {
binding,
asserted_ty: _,
} => Some(binding.return_ty()),
}
}
@@ -199,30 +163,23 @@ impl<'db> CallOutcome<'db> {
context: &InferContext<'db>,
node: ast::AnyNodeRef,
) -> Result<Type<'db>, NotCallableError<'db>> {
// TODO should this method emit diagnostics directly, or just return results that allow the
// caller to decide about emitting diagnostics? Currently it emits binding diagnostics, but
// only non-callable diagnostics in the union case, which is inconsistent.
match self {
Self::Callable { binding } => {
binding.report_diagnostics(context, node);
Ok(binding.return_ty())
}
Self::Callable { return_ty } => Ok(*return_ty),
Self::RevealType {
binding,
return_ty,
revealed_ty,
} => {
binding.report_diagnostics(context, node);
context.report_diagnostic(
node,
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
);
Ok(binding.return_ty())
Ok(*return_ty)
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
not_callable_ty: *not_callable_ty,
return_ty: Type::unknown(),
return_ty: Type::Unknown,
}),
Self::PossiblyUnboundDunderCall {
called_ty,
@@ -231,7 +188,7 @@ impl<'db> CallOutcome<'db> {
callable_ty: *called_ty,
return_ty: call_outcome
.return_ty(context.db())
.unwrap_or(Type::unknown()),
.unwrap_or(Type::Unknown),
}),
Self::Union {
outcomes,
@@ -244,14 +201,14 @@ impl<'db> CallOutcome<'db> {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
Type::unknown()
Type::Unknown
}
Self::RevealType {
binding,
return_ty,
revealed_ty: _,
} => {
if revealed {
binding.return_ty()
*return_ty
} else {
revealed = true;
outcome.unwrap_with_diagnostic(context, node)
@@ -280,73 +237,6 @@ impl<'db> CallOutcome<'db> {
}),
}
}
CallOutcome::StaticAssertionError {
binding,
error_kind,
} => {
binding.report_diagnostics(context, node);
match error_kind {
StaticAssertionErrorKind::ArgumentIsFalse => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: argument evaluates to `False`"),
);
}
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::CustomError(message) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: {message}"),
);
}
}
Ok(Type::unknown())
}
CallOutcome::AssertType {
binding,
asserted_ty,
} => {
let [actual_ty, _asserted] = binding.parameter_tys() else {
return Ok(binding.return_ty());
};
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
context.report_lint(
&TYPE_ASSERTION_FAILURE,
node,
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(context.db()),
asserted_ty.display(context.db()),
),
);
}
Ok(binding.return_ty())
}
}
}
}

View File

@@ -1,73 +0,0 @@
use super::Type;
/// Typed arguments for a single call, in source order.
#[derive(Clone, Debug, Default)]
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
impl<'a, 'db> CallArguments<'a, 'db> {
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
positional_tys
.into_iter()
.map(Argument::Positional)
.collect()
}
/// Prepend an extra positional argument.
pub(crate) fn with_self(&self, self_ty: Type<'db>) -> Self {
let mut arguments = Vec::with_capacity(self.0.len() + 1);
arguments.push(Argument::Synthetic(self_ty));
arguments.extend_from_slice(&self.0);
Self(arguments)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &Argument<'a, 'db>> {
self.0.iter()
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
self.0.first().map(Argument::ty)
}
}
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
type Item = &'b Argument<'a, 'db>;
type IntoIter = std::slice::Iter<'b, Argument<'a, 'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a, 'db> FromIterator<Argument<'a, 'db>> for CallArguments<'a, 'db> {
fn from_iter<T: IntoIterator<Item = Argument<'a, 'db>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Clone, Debug)]
pub(crate) enum Argument<'a, 'db> {
/// The synthetic `self` or `cls` argument, which doesn't appear explicitly at the call site.
Synthetic(Type<'db>),
/// A positional argument.
Positional(Type<'db>),
/// A starred positional argument (e.g. `*args`).
Variadic(Type<'db>),
/// A keyword argument (e.g. `a=1`).
Keyword { name: &'a str, ty: Type<'db> },
/// The double-starred keywords argument (e.g. `**kwargs`).
Keywords(Type<'db>),
}
impl<'db> Argument<'_, 'db> {
fn ty(&self) -> Type<'db> {
match self {
Self::Synthetic(ty) => *ty,
Self::Positional(ty) => *ty,
Self::Variadic(ty) => *ty,
Self::Keyword { name: _, ty } => *ty,
Self::Keywords(ty) => *ty,
}
}
}

View File

@@ -1,411 +0,0 @@
use super::{Argument, CallArguments, InferContext, Signature, Type};
use crate::db::Db;
use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED,
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
};
use crate::types::signatures::Parameter;
use crate::types::UnionType;
use ruff_python_ast as ast;
/// Bind a [`CallArguments`] against a callable [`Signature`].
///
/// The returned [`CallBinding`] provides the return type of the call, the bound types for all
/// parameters, and any errors resulting from binding the call.
pub(crate) fn bind_call<'db>(
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
signature: &Signature<'db>,
callable_ty: Option<Type<'db>>,
) -> CallBinding<'db> {
let parameters = signature.parameters();
// The type assigned to each parameter at this call site.
let mut parameter_tys = vec![None; parameters.len()];
let mut errors = vec![];
let mut next_positional = 0;
let mut first_excess_positional = None;
let mut num_synthetic_args = 0;
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
if argument_index >= num_synthetic_args {
// Adjust the argument index to skip synthetic args, which don't appear at the call
// site and thus won't be in the Call node arguments list.
Some(argument_index - num_synthetic_args)
} else {
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
// entire Call node, since there's no argument node for this argument at the call site
None
}
};
for (argument_index, argument) in arguments.iter().enumerate() {
let (index, parameter, argument_ty, positional) = match argument {
Argument::Positional(ty) | Argument::Synthetic(ty) => {
if matches!(argument, Argument::Synthetic(_)) {
num_synthetic_args += 1;
}
let Some((index, parameter)) = parameters
.get_positional(next_positional)
.map(|param| (next_positional, param))
.or_else(|| parameters.variadic())
else {
first_excess_positional.get_or_insert(argument_index);
next_positional += 1;
continue;
};
next_positional += 1;
(index, parameter, ty, !parameter.is_variadic())
}
Argument::Keyword { name, ty } => {
let Some((index, parameter)) = parameters
.keyword_by_name(name)
.or_else(|| parameters.keyword_variadic())
else {
errors.push(CallBindingError::UnknownArgument {
argument_name: ast::name::Name::new(name),
argument_index: get_argument_index(argument_index, num_synthetic_args),
});
continue;
};
(index, parameter, ty, false)
}
Argument::Variadic(_) | Argument::Keywords(_) => {
// TODO
continue;
}
};
if let Some(expected_ty) = parameter.annotated_ty() {
if !argument_ty.is_assignable_to(db, expected_ty) {
errors.push(CallBindingError::InvalidArgumentType {
parameter: ParameterContext::new(parameter, index, positional),
argument_index: get_argument_index(argument_index, num_synthetic_args),
expected_ty,
provided_ty: *argument_ty,
});
}
}
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
if parameter.is_variadic() {
let union = UnionType::from_elements(db, [existing, *argument_ty]);
parameter_tys[index].replace(union);
} else {
errors.push(CallBindingError::ParameterAlreadyAssigned {
argument_index: get_argument_index(argument_index, num_synthetic_args),
parameter: ParameterContext::new(parameter, index, positional),
});
}
}
}
if let Some(first_excess_argument_index) = first_excess_positional {
errors.push(CallBindingError::TooManyPositionalArguments {
first_excess_argument_index: get_argument_index(
first_excess_argument_index,
num_synthetic_args,
),
expected_positional_count: parameters.positional().count(),
provided_positional_count: next_positional,
});
}
let mut missing = vec![];
for (index, bound_ty) in parameter_tys.iter().enumerate() {
if bound_ty.is_none() {
let param = &parameters[index];
if param.is_variadic() || param.is_keyword_variadic() || param.default_ty().is_some() {
// variadic/keywords and defaulted arguments are not required
continue;
}
missing.push(ParameterContext::new(param, index, false));
}
}
if !missing.is_empty() {
errors.push(CallBindingError::MissingArguments {
parameters: ParameterContexts(missing),
});
}
CallBinding {
callable_ty,
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
parameter_tys: parameter_tys
.into_iter()
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
.collect(),
errors,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CallBinding<'db> {
/// Type of the callable object (function, class...)
callable_ty: Option<Type<'db>>,
/// Return type of the call.
return_ty: Type<'db>,
/// Bound types for parameters, in parameter source order.
parameter_tys: Box<[Type<'db>]>,
/// Call binding errors, if any.
errors: Vec<CallBindingError<'db>>,
}
impl<'db> CallBinding<'db> {
// TODO remove this constructor and construct always from `bind_call`
pub(crate) fn from_return_ty(return_ty: Type<'db>) -> Self {
Self {
callable_ty: None,
return_ty,
parameter_tys: Box::default(),
errors: vec![],
}
}
pub(crate) fn set_return_ty(&mut self, return_ty: Type<'db>) {
self.return_ty = return_ty;
}
pub(crate) fn return_ty(&self) -> Type<'db> {
self.return_ty
}
pub(crate) fn parameter_tys(&self) -> &[Type<'db>] {
&self.parameter_tys
}
pub(crate) fn one_parameter_ty(&self) -> Option<Type<'db>> {
match self.parameter_tys() {
[ty] => Some(*ty),
_ => None,
}
}
pub(crate) fn two_parameter_tys(&self) -> Option<(Type<'db>, Type<'db>)> {
match self.parameter_tys() {
[first, second] => Some((*first, *second)),
_ => None,
}
}
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
match self.callable_ty {
Some(Type::FunctionLiteral(function)) => Some(function.name(db)),
Some(Type::ClassLiteral(class_type)) => Some(class_type.class.name(db)),
_ => None,
}
}
pub(super) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
let callable_name = self.callable_name(context.db());
for error in &self.errors {
error.report_diagnostic(context, node, callable_name);
}
}
}
/// Information needed to emit a diagnostic regarding a parameter.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ParameterContext {
name: Option<ast::name::Name>,
index: usize,
/// Was the argument for this parameter passed positionally, and matched to a non-variadic
/// positional parameter? (If so, we will provide the index in the diagnostic, not just the
/// name.)
positional: bool,
}
impl ParameterContext {
fn new(parameter: &Parameter, index: usize, positional: bool) -> Self {
Self {
name: parameter.display_name(),
index,
positional,
}
}
}
impl std::fmt::Display for ParameterContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(name) = &self.name {
if self.positional {
write!(f, "{} (`{name}`)", self.index + 1)
} else {
write!(f, "`{name}`")
}
} else {
write!(f, "{}", self.index + 1)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ParameterContexts(Vec<ParameterContext>);
impl std::fmt::Display for ParameterContexts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut iter = self.0.iter();
if let Some(first) = iter.next() {
write!(f, "{first}")?;
for param in iter {
f.write_str(", ")?;
write!(f, "{param}")?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum CallBindingError<'db> {
/// The type of an argument is not assignable to the annotated type of its corresponding
/// parameter.
InvalidArgumentType {
parameter: ParameterContext,
argument_index: Option<usize>,
expected_ty: Type<'db>,
provided_ty: Type<'db>,
},
/// One or more required parameters (that is, with no default) is not supplied by any argument.
MissingArguments { parameters: ParameterContexts },
/// A call argument can't be matched to any parameter.
UnknownArgument {
argument_name: ast::name::Name,
argument_index: Option<usize>,
},
/// More positional arguments are provided in the call than can be handled by the signature.
TooManyPositionalArguments {
first_excess_argument_index: Option<usize>,
expected_positional_count: usize,
provided_positional_count: usize,
},
/// Multiple arguments were provided for a single parameter.
ParameterAlreadyAssigned {
argument_index: Option<usize>,
parameter: ParameterContext,
},
}
impl<'db> CallBindingError<'db> {
pub(super) fn report_diagnostic(
&self,
context: &InferContext<'db>,
node: ast::AnyNodeRef,
callable_name: Option<&str>,
) {
match self {
Self::InvalidArgumentType {
parameter,
argument_index,
expected_ty,
provided_ty,
} => {
let provided_ty_display = provided_ty.display(context.db());
let expected_ty_display = expected_ty.display(context.db());
context.report_lint(
&INVALID_ARGUMENT_TYPE,
Self::get_node(node, *argument_index),
format_args!(
"Object of type `{provided_ty_display}` cannot be assigned to \
parameter {parameter}{}; expected type `{expected_ty_display}`",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::TooManyPositionalArguments {
first_excess_argument_index,
expected_positional_count,
provided_positional_count,
} => {
context.report_lint(
&TOO_MANY_POSITIONAL_ARGUMENTS,
Self::get_node(node, *first_excess_argument_index),
format_args!(
"Too many positional arguments{}: expected \
{expected_positional_count}, got {provided_positional_count}",
if let Some(callable_name) = callable_name {
format!(" to function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::MissingArguments { parameters } => {
let s = if parameters.0.len() == 1 { "" } else { "s" };
context.report_lint(
&MISSING_ARGUMENT,
node,
format_args!(
"No argument{s} provided for required parameter{s} {parameters}{}",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::UnknownArgument {
argument_name,
argument_index,
} => {
context.report_lint(
&UNKNOWN_ARGUMENT,
Self::get_node(node, *argument_index),
format_args!(
"Argument `{argument_name}` does not match any known parameter{}",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
Self::ParameterAlreadyAssigned {
argument_index,
parameter,
} => {
context.report_lint(
&PARAMETER_ALREADY_ASSIGNED,
Self::get_node(node, *argument_index),
format_args!(
"Multiple values provided for parameter {parameter}{}",
if let Some(callable_name) = callable_name {
format!(" of function `{callable_name}`")
} else {
String::new()
}
),
);
}
}
}
fn get_node(node: ast::AnyNodeRef, argument_index: Option<usize>) -> ast::AnyNodeRef {
// If we have a Call node and an argument index, report the diagnostic on the correct
// argument node; otherwise, report it on the entire provided node.
match (node, argument_index) {
(ast::AnyNodeRef::ExprCall(call_node), Some(argument_index)) => {
match call_node
.arguments
.arguments_source_order()
.nth(argument_index)
.expect("argument index should not be out of range")
{
ast::ArgOrKeyword::Arg(expr) => expr.into(),
ast::ArgOrKeyword::Keyword(keyword) => keyword.into(),
}
}
_ => node,
}
}
}

View File

@@ -1,5 +1,5 @@
use crate::types::{
todo_type, Class, ClassLiteralType, DynamicType, KnownClass, KnownInstanceType, Type,
todo_type, Class, ClassLiteralType, KnownClass, KnownInstanceType, TodoType, Type,
};
use crate::Db;
use itertools::Either;
@@ -8,29 +8,16 @@ use itertools::Either;
///
/// This is much more limited than the [`Type`] enum:
/// all types that would be invalid to have as a class base are
/// transformed into [`ClassBase::unknown`]
/// transformed into [`ClassBase::Unknown`]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub enum ClassBase<'db> {
Dynamic(DynamicType),
Any,
Unknown,
Todo(TodoType),
Class(Class<'db>),
}
impl<'db> ClassBase<'db> {
pub const fn any() -> Self {
Self::Dynamic(DynamicType::Any)
}
pub const fn unknown() -> Self {
Self::Dynamic(DynamicType::Unknown)
}
pub const fn is_dynamic(self) -> bool {
match self {
ClassBase::Dynamic(_) => true,
ClassBase::Class(_) => false,
}
}
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct Display<'db> {
base: ClassBase<'db>,
@@ -40,7 +27,9 @@ impl<'db> ClassBase<'db> {
impl std::fmt::Display for Display<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.base {
ClassBase::Dynamic(dynamic) => dynamic.fmt(f),
ClassBase::Any => f.write_str("Any"),
ClassBase::Todo(todo) => todo.fmt(f),
ClassBase::Unknown => f.write_str("Unknown"),
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
}
}
@@ -54,7 +43,7 @@ impl<'db> ClassBase<'db> {
KnownClass::Object
.to_class_literal(db)
.into_class_literal()
.map_or(Self::unknown(), |ClassLiteralType { class }| {
.map_or(Self::Unknown, |ClassLiteralType { class }| {
Self::Class(class)
})
}
@@ -64,7 +53,9 @@ impl<'db> ClassBase<'db> {
/// Return `None` if `ty` is not an acceptable type for a class base.
pub(super) fn try_from_ty(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
match ty {
Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),
Type::Any => Some(Self::Any),
Type::Unknown => Some(Self::Unknown),
Type::Todo(todo) => Some(Self::Todo(todo)),
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
Type::Intersection(_) => None, // TODO -- probably incorrect?
@@ -102,12 +93,8 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::Required
| KnownInstanceType::TypeAlias
| KnownInstanceType::ReadOnly
| KnownInstanceType::Optional
| KnownInstanceType::Not
| KnownInstanceType::Intersection
| KnownInstanceType::TypeOf => None,
KnownInstanceType::Unknown => Some(Self::unknown()),
KnownInstanceType::Any => Some(Self::any()),
| KnownInstanceType::Optional => None,
KnownInstanceType::Any => Some(Self::Any),
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
KnownInstanceType::Dict => {
Self::try_from_ty(db, KnownClass::Dict.to_class_literal(db))
@@ -152,7 +139,7 @@ impl<'db> ClassBase<'db> {
pub(super) fn into_class(self) -> Option<Class<'db>> {
match self {
Self::Class(class) => Some(class),
Self::Dynamic(_) => None,
_ => None,
}
}
@@ -162,7 +149,13 @@ impl<'db> ClassBase<'db> {
db: &'db dyn Db,
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
match self {
ClassBase::Dynamic(_) => Either::Left([self, ClassBase::object(db)].into_iter()),
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
ClassBase::Unknown => {
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
}
ClassBase::Todo(todo) => {
Either::Left([ClassBase::Todo(todo), ClassBase::object(db)].into_iter())
}
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
}
}
@@ -177,7 +170,9 @@ impl<'db> From<Class<'db>> for ClassBase<'db> {
impl<'db> From<ClassBase<'db>> for Type<'db> {
fn from(value: ClassBase<'db>) -> Self {
match value {
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
ClassBase::Any => Type::Any,
ClassBase::Todo(todo) => Type::Todo(todo),
ClassBase::Unknown => Type::Unknown,
ClassBase::Class(class) => Type::class_literal(class),
}
}

View File

@@ -162,11 +162,6 @@ impl<'db> InferContext<'db> {
}
}
/// Are we currently inferring types in a stub file?
pub(crate) fn in_stub(&self) -> bool {
self.file.is_stub(self.db().upcast())
}
#[must_use]
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
self.bomb.defuse();

View File

@@ -30,35 +30,27 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INCOMPATIBLE_SLOTS);
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
registry.register_lint(&INVALID_ARGUMENT_TYPE);
registry.register_lint(&INVALID_ASSIGNMENT);
registry.register_lint(&INVALID_BASE);
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_RAISE);
registry.register_lint(&INVALID_TYPE_FORM);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NON_SUBSCRIPTABLE);
registry.register_lint(&NOT_ITERABLE);
registry.register_lint(&PARAMETER_ALREADY_ASSIGNED);
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
registry.register_lint(&POSSIBLY_UNRESOLVED_REFERENCE);
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
registry.register_lint(&TYPE_ASSERTION_FAILURE);
registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS);
registry.register_lint(&UNDEFINED_REVEAL);
registry.register_lint(&UNKNOWN_ARGUMENT);
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
registry.register_lint(&UNRESOLVED_IMPORT);
registry.register_lint(&UNRESOLVED_REFERENCE);
registry.register_lint(&UNSUPPORTED_OPERATOR);
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&STATIC_ASSERT_ERROR);
// String annotations
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
@@ -234,27 +226,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects call arguments whose type is not assignable to the corresponding typed parameter.
///
/// ## Why is this bad?
/// Passing an argument of a type the function (or callable object) does not accept violates
/// the expectations of the function author and may cause unexpected runtime errors within the
/// body of the function.
///
/// ## Examples
/// ```python
/// def func(x: int): ...
/// func("foo") # error: [invalid-argument-type]
/// ```
pub(crate) static INVALID_ARGUMENT_TYPE = {
summary: "detects call arguments whose type is not assignable to the corresponding typed parameter",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
pub(crate) static INVALID_ASSIGNMENT = {
@@ -292,7 +263,6 @@ declare_lint! {
}
declare_lint! {
/// ## What it does
/// Checks for exception handlers that catch non-exception classes.
///
/// ## Why is this bad?
@@ -327,33 +297,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for arguments to `metaclass=` that are invalid.
///
/// ## Why is this bad?
/// Python allows arbitrary expressions to be used as the argument to `metaclass=`.
/// These expressions, however, need to be callable and accept the same arguments
/// as `type.__new__`.
///
/// ## Example
///
/// ```python
/// def f(): ...
///
/// # TypeError: f() takes 0 positional arguments but 3 were given
/// class B(metaclass=f): ...
/// ```
///
/// ## References
/// - [Python documentation: Metaclasses](https://docs.python.org/3/reference/datamodel.html#metaclasses)
pub(crate) static INVALID_METACLASS = {
summary: "detects invalid `metaclass=` arguments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for default values that can't be assigned to the parameter's annotated type.
@@ -432,25 +375,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for missing required arguments in a call.
///
/// ## Why is this bad?
/// Failing to provide a required argument will raise a `TypeError` at runtime.
///
/// ## Examples
/// ```python
/// def func(x: int): ...
/// func() # TypeError: func() missing 1 required positional argument: 'x'
/// ```
pub(crate) static MISSING_ARGUMENT = {
summary: "detects missing required arguments in a call",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for subscripting objects that do not support subscripting.
@@ -489,27 +413,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for calls which provide more than one argument for a single parameter.
///
/// ## Why is this bad?
/// Providing multiple values for a single parameter will raise a `TypeError` at runtime.
///
/// ## Examples
///
/// ```python
/// def f(x: int) -> int: ...
///
/// f(1, x=2) # Error raised here
/// ```
pub(crate) static PARAMETER_ALREADY_ASSIGNED = {
summary: "detects multiple arguments for the same parameter",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for possibly unbound attributes.
@@ -576,49 +479,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for `assert_type()` calls where the actual type
/// is not the same as the asserted type.
///
/// ## Why is this bad?
/// `assert_type()` allows confirming the inferred type of a certain value.
///
/// ## Example
///
/// ```python
/// def _(x: int):
/// assert_type(x, int) # fine
/// assert_type(x, str) # error: Actual type does not match asserted type
/// ```
pub(crate) static TYPE_ASSERTION_FAILURE = {
summary: "detects failed type assertions",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls that pass more positional arguments than the callable can accept.
///
/// ## Why is this bad?
/// Passing too many positional arguments will raise `TypeError` at runtime.
///
/// ## Example
///
/// ```python
/// def f(): ...
///
/// f("foo") # Error raised here
/// ```
pub(crate) static TOO_MANY_POSITIONAL_ARGUMENTS = {
summary: "detects calls passing too many positional arguments",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.
@@ -635,27 +495,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for keyword arguments in calls that don't match any parameter of the callable.
///
/// ## Why is this bad?
/// Providing an unknown keyword argument will raise `TypeError` at runtime.
///
/// ## Example
///
/// ```python
/// def f(x: int) -> int: ...
///
/// f(x=1, y=2) # Error raised here
/// ```
pub(crate) static UNKNOWN_ARGUMENT = {
summary: "detects unknown keyword arguments in calls",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for unresolved attributes.
@@ -731,25 +570,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Makes sure that the argument of `static_assert` is statically known to be true.
///
/// ## Examples
/// ```python
/// from knot_extensions import static_assert
///
/// static_assert(1 + 1 == 3) # error: evaluates to `False`
///
/// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness
/// ```
pub(crate) static STATIC_ASSERT_ERROR = {
summary: "Failed static assertion",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
pub(crate) id: DiagnosticId,

View File

@@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape;
use crate::types::class_base::ClassBase;
use crate::types::{
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type,
UnionType,
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
SubclassOfType, Type, UnionType,
};
use crate::Db;
use rustc_hash::FxHashMap;
@@ -65,8 +65,9 @@ struct DisplayRepresentation<'db> {
impl Display for DisplayRepresentation<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.ty {
Type::Dynamic(dynamic) => dynamic.fmt(f),
Type::Any => f.write_str("Any"),
Type::Never => f.write_str("Never"),
Type::Unknown => f.write_str("Unknown"),
Type::Instance(InstanceType { class }) => {
let representation = match class.known(self.db) {
Some(KnownClass::NoneType) => "None",
@@ -75,17 +76,24 @@ impl Display for DisplayRepresentation<'_> {
};
f.write_str(representation)
}
// `[Type::Todo]`'s display should be explicit that is not a valid display of
// any other type
Type::Todo(todo) => write!(f, "@Todo{todo}"),
Type::ModuleLiteral(module) => {
write!(f, "<module '{}'>", module.module(self.db).name())
}
// TODO functions and classes should display using a fully qualified name
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => {
// Only show the bare class name here; ClassBase::display would render this as
// type[<class 'Foo'>] instead of type[Foo].
ClassBase::Class(class) => write!(f, "type[{}]", class.name(self.db)),
ClassBase::Dynamic(dynamic) => write!(f, "type[{dynamic}]"),
},
write!(f, "type[{}]", class.name(self.db))
}
Type::SubclassOf(SubclassOfType { base }) => {
write!(f, "type[{}]", base.display(self.db))
}
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f),
@@ -169,9 +177,12 @@ impl Display for DisplayUnionType<'_> {
for element in elements {
if let Ok(kind) = CondensedDisplayTypeKind::try_from(*element) {
let Some(condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
let Some(mut condensed_kind) = grouped_condensed_kinds.remove(&kind) else {
continue;
};
if kind == CondensedDisplayTypeKind::Int {
condensed_kind.sort_unstable_by_key(|ty| ty.expect_int_literal());
}
join.entry(&DisplayLiteralGroup {
literals: condensed_kind,
db: self.db,
@@ -212,12 +223,17 @@ impl Display for DisplayLiteralGroup<'_> {
/// Enumeration of literal types that are displayed in a "condensed way" inside `Literal` slices.
///
/// For example, `Literal[1] | Literal[2] | Literal["s"]` is displayed as `"Literal[1, 2, "s"]"`.
/// For example, `Literal[1] | Literal[2]` is displayed as `"Literal[1, 2]"`.
/// Not all `Literal` types are displayed using `Literal` slices
/// (e.g. it would be inappropriate to display `LiteralString`
/// as `Literal[LiteralString]`).
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum CondensedDisplayTypeKind {
Class,
Function,
LiteralExpression,
Int,
String,
Bytes,
}
impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
@@ -227,10 +243,9 @@ impl TryFrom<Type<'_>> for CondensedDisplayTypeKind {
match value {
Type::ClassLiteral(_) => Ok(Self::Class),
Type::FunctionLiteral(_) => Ok(Self::Function),
Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::BytesLiteral(_)
| Type::BooleanLiteral(_) => Ok(Self::LiteralExpression),
Type::IntLiteral(_) => Ok(Self::Int),
Type::StringLiteral(_) => Ok(Self::String),
Type::BytesLiteral(_) => Ok(Self::Bytes),
_ => Err(()),
}
}
@@ -357,8 +372,64 @@ impl Display for DisplayStringLiteralType<'_> {
#[cfg(test)]
mod tests {
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
use crate::db::tests::setup_db;
use crate::types::{SliceLiteralType, StringLiteralType, Type};
use crate::types::{global_symbol, SliceLiteralType, StringLiteralType, Type, UnionType};
#[test]
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
let mut db = setup_db();
db.write_dedented(
"src/main.py",
"
def foo(x: int) -> int:
return x + 1
def bar(s: str) -> str:
return s
class A: ...
class B: ...
",
)?;
let mod_file = system_path_to_file(&db, "src/main.py").expect("file to exist");
let union_elements = &[
Type::Unknown,
Type::IntLiteral(-1),
global_symbol(&db, mod_file, "A").expect_type(),
Type::string_literal(&db, "A"),
Type::bytes_literal(&db, &[0u8]),
Type::bytes_literal(&db, &[7u8]),
Type::IntLiteral(0),
Type::IntLiteral(1),
Type::string_literal(&db, "B"),
global_symbol(&db, mod_file, "foo").expect_type(),
global_symbol(&db, mod_file, "bar").expect_type(),
global_symbol(&db, mod_file, "B").expect_type(),
Type::BooleanLiteral(true),
Type::none(&db),
];
let union = UnionType::from_elements(&db, union_elements).expect_union();
let display = format!("{}", union.display(&db));
assert_eq!(
display,
concat!(
"Unknown | ",
"Literal[-1, 0, 1] | ",
"Literal[A, B] | ",
"Literal[\"A\", \"B\"] | ",
"Literal[b\"\\x00\", b\"\\x07\"] | ",
"Literal[foo, bar] | ",
"Literal[True] | ",
"None"
)
);
Ok(())
}
#[test]
fn test_slice_literal_display() {

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ impl<'db> Mro<'db> {
pub(super) fn from_error(db: &'db dyn Db, class: Class<'db>) -> Self {
Self::from([
ClassBase::Class(class),
ClassBase::unknown(),
ClassBase::Unknown,
ClassBase::object(db),
])
}

View File

@@ -7,8 +7,8 @@ use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
use crate::semantic_index::symbol_table;
use crate::types::{
infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass, KnownFunction,
SubclassOfType, Truthiness, Type, UnionBuilder,
infer_expression_types, ClassLiteralType, IntersectionBuilder, KnownClass,
KnownConstraintFunction, KnownFunction, Truthiness, Type, UnionBuilder,
};
use crate::Db;
use itertools::Itertools;
@@ -83,39 +83,28 @@ fn all_negative_narrowing_constraints_for_expression<'db>(
NarrowingConstraintsBuilder::new(db, ConstraintNode::Expression(expression), false).finish()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KnownConstraintFunction {
/// `builtins.isinstance`
IsInstance,
/// `builtins.issubclass`
IsSubclass,
}
impl KnownConstraintFunction {
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
///
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
let constraint_fn = |class| match self {
KnownConstraintFunction::IsInstance => Type::instance(class),
KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
};
match classinfo {
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.elements(db) {
builder = builder.add(self.generate_constraint(db, *element)?);
}
Some(builder.build())
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
///
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
fn generate_classinfo_constraint<'db, F>(
db: &'db dyn Db,
classinfo: &Type<'db>,
to_constraint: F,
) -> Option<Type<'db>>
where
F: Fn(ClassLiteralType<'db>) -> Type<'db> + Copy,
{
match classinfo {
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.elements(db) {
builder = builder.add(generate_classinfo_constraint(db, element, to_constraint)?);
}
Type::ClassLiteral(ClassLiteralType { class }) => Some(constraint_fn(class)),
Type::SubclassOf(subclass_of_ty) => {
subclass_of_ty.subclass_of().into_class().map(constraint_fn)
}
_ => None,
Some(builder.build())
}
Type::ClassLiteral(class_literal_type) => Some(to_constraint(*class_literal_type)),
_ => None,
}
}
@@ -233,9 +222,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
PatternConstraintKind::Singleton(singleton, _guard) => {
self.evaluate_match_pattern_singleton(*subject, *singleton)
}
PatternConstraintKind::Class(cls, _guard) => {
self.evaluate_match_pattern_class(*subject, *cls)
}
// TODO: support more pattern kinds
PatternConstraintKind::Value(..) | PatternConstraintKind::Unsupported => None,
}
@@ -443,13 +429,24 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
let class_info_ty =
inference.expression_ty(class_info.scoped_expression_id(self.db, scope));
function
.generate_constraint(self.db, class_info_ty)
.map(|constraint| {
let to_constraint = match function {
KnownConstraintFunction::IsInstance => {
|class_literal: ClassLiteralType<'db>| Type::instance(class_literal.class)
}
KnownConstraintFunction::IsSubclass => {
|class_literal: ClassLiteralType<'db>| {
Type::subclass_of(class_literal.class)
}
}
};
generate_classinfo_constraint(self.db, &class_info_ty, to_constraint).map(
|constraint| {
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, constraint.negate_if(self.db, !is_positive));
constraints
})
},
)
}
// for the expression `bool(E)`, we further narrow the type based on `E`
Type::ClassLiteral(class_type)
@@ -489,27 +486,6 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
}
}
fn evaluate_match_pattern_class(
&mut self,
subject: Expression<'db>,
cls: Expression<'db>,
) -> Option<NarrowingConstraints<'db>> {
if let Some(ast::ExprName { id, .. }) = subject.node_ref(self.db).as_name_expr() {
// SAFETY: we should always have a symbol for every Name node.
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let scope = self.scope();
let inference = infer_expression_types(self.db, cls);
let ty = inference
.expression_ty(cls.node_ref(self.db).scoped_expression_id(self.db, scope))
.to_instance(self.db);
let mut constraints = NarrowingConstraints::default();
constraints.insert(symbol, ty);
Some(constraints)
} else {
None
}
}
fn evaluate_bool_op(
&mut self,
expr_bool_op: &ExprBoolOp,

View File

@@ -123,61 +123,14 @@ impl Arbitrary for Ty {
}
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
// This is incredibly naive. We can do much better here by
// trying various subsets of the elements in unions, tuples,
// and intersections. For now, we only try to shrink by
// reducing unions/tuples/intersections to a single element.
match self.clone() {
Ty::Union(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() {
0 => None,
1 => Some(elts.into_iter().next().unwrap()),
_ => Some(Ty::Union(elts)),
})),
Ty::Tuple(types) => Box::new(types.shrink().filter_map(|elts| match elts.len() {
0 => None,
1 => Some(elts.into_iter().next().unwrap()),
_ => Some(Ty::Tuple(elts)),
})),
Ty::Intersection { pos, neg } => {
// Shrinking on intersections is not exhaustive!
//
// We try to shrink the positive side or the negative side,
// but we aren't shrinking both at the same time.
//
// This should remove positive or negative constraints but
// won't shrink (A & B & ~C & ~D) to (A & ~C) in one shrink
// iteration.
//
// Instead, it hopes that (A & B & ~C) or (A & ~C & ~D) fails
// so that shrinking can happen there.
let pos_orig = pos.clone();
let neg_orig = neg.clone();
Box::new(
// we shrink negative constraints first, as
// intersections with only negative constraints are
// more confusing
neg.shrink()
.map(move |shrunk_neg| Ty::Intersection {
pos: pos_orig.clone(),
neg: shrunk_neg,
})
.chain(pos.shrink().map(move |shrunk_pos| Ty::Intersection {
pos: shrunk_pos,
neg: neg_orig.clone(),
}))
.filter_map(|ty| {
if let Ty::Intersection { pos, neg } = &ty {
match (pos.len(), neg.len()) {
// an empty intersection does not mean
// anything
(0, 0) => None,
// a single positive element should be
// unwrapped
(1, 0) => Some(pos[0].clone()),
_ => Some(ty),
}
} else {
unreachable!()
}
}),
)
}
Ty::Union(types) => Box::new(types.into_iter()),
Ty::Tuple(types) => Box::new(types.into_iter()),
Ty::Intersection { pos, neg } => Box::new(pos.into_iter().chain(neg)),
_ => Box::new(std::iter::empty()),
}
}
@@ -220,8 +173,6 @@ macro_rules! type_property_test {
}
mod stable {
use crate::types::{KnownClass, Type};
// `T` is equivalent to itself.
type_property_test!(
equivalent_to_is_reflexive, db,
@@ -258,6 +209,12 @@ mod stable {
forall types s, t. s.is_subtype_of(db, t) => !s.is_disjoint_from(db, t) || s.is_never()
);
// `T` can be assigned to itself.
type_property_test!(
assignable_to_is_reflexive, db,
forall types t. t.is_assignable_to(db, t)
);
// `S <: T` implies that `S` can be assigned to `T`.
type_property_test!(
subtype_of_implies_assignable_to, db,
@@ -281,30 +238,6 @@ mod stable {
non_fully_static_types_do_not_participate_in_subtyping, db,
forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s)
);
// All types should be assignable to `object`
type_property_test!(
all_types_assignable_to_object, db,
forall types t. t.is_assignable_to(db, KnownClass::Object.to_instance(db))
);
// And for fully static types, they should also be subtypes of `object`
type_property_test!(
all_fully_static_types_subtype_of_object, db,
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, KnownClass::Object.to_instance(db))
);
// Never should be assignable to every type
type_property_test!(
never_assignable_to_every_type, db,
forall types t. Type::Never.is_assignable_to(db, t)
);
// And it should be a subtype of all fully static types
type_property_test!(
never_subtype_of_every_fully_static_type, db,
forall types t. t.is_fully_static(db) => Type::Never.is_subtype_of(db, t)
);
}
/// This module contains property tests that currently lead to many false positives.
@@ -315,32 +248,6 @@ mod stable {
/// tests to the `stable` section. In the meantime, it can still be useful to run these
/// tests (using [`types::property_tests::flaky`]), to see if there are any new obvious bugs.
mod flaky {
use crate::{
db::tests::TestDb,
types::{IntersectionBuilder, Type},
};
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
// `T` can be assigned to itself.
type_property_test!(
assignable_to_is_reflexive, db,
forall types t. t.is_assignable_to(db, t)
);
// Currently fails due to https://github.com/astral-sh/ruff/issues/14899
// An intersection of two types should be assignable to both of them
fn intersection<'db>(db: &'db TestDb, s: Type<'db>, t: Type<'db>) -> Type<'db> {
IntersectionBuilder::new(db)
.add_positive(s)
.add_positive(t)
.build()
}
type_property_test!(
intersection_assignable_to_both, db,
forall types s, t. intersection(db, s, t).is_assignable_to(db, s) && intersection(db, s, t).is_assignable_to(db, t)
);
// `S <: T` and `T <: S` implies that `S` is equivalent to `T`.
type_property_test!(
subtype_of_is_antisymmetric, db,

View File

@@ -1,3 +1,4 @@
#![allow(dead_code)]
use super::{definition_expression_ty, Type};
use crate::Db;
use crate::{semantic_index::definition::Definition, types::todo_type};
@@ -6,18 +7,10 @@ use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Signature<'db> {
/// Parameters, in source order.
///
/// The ordering of parameters in a valid signature must be: first positional-only parameters,
/// then positional-or-keyword, then optionally the variadic parameter, then keyword-only
/// parameters, and last, optionally the variadic keywords parameter. Parameters with defaults
/// must come after parameters without defaults.
///
/// We may get invalid signatures, though, and need to handle them without panicking.
parameters: Parameters<'db>,
/// Annotated return type, if any.
pub(crate) return_ty: Option<Type<'db>>,
/// Annotated return type (Unknown if no annotation.)
pub(crate) return_ty: Type<'db>,
}
impl<'db> Signature<'db> {
@@ -25,7 +18,7 @@ impl<'db> Signature<'db> {
pub(crate) fn todo() -> Self {
Self {
parameters: Parameters::todo(),
return_ty: Some(todo_type!("return type")),
return_ty: todo_type!("return type"),
}
}
@@ -35,13 +28,17 @@ impl<'db> Signature<'db> {
definition: Definition<'db>,
function_node: &'db ast::StmtFunctionDef,
) -> Self {
let return_ty = function_node.returns.as_ref().map(|returns| {
if function_node.is_async {
todo_type!("generic types.CoroutineType")
} else {
definition_expression_ty(db, definition, returns.as_ref())
}
});
let return_ty = function_node
.returns
.as_ref()
.map(|returns| {
if function_node.is_async {
todo_type!("generic types.CoroutineType")
} else {
definition_expression_ty(db, definition, returns.as_ref())
}
})
.unwrap_or(Type::Unknown);
Self {
parameters: Parameters::from_parameters(
@@ -52,32 +49,45 @@ impl<'db> Signature<'db> {
return_ty,
}
}
/// Return the parameters in this signature.
pub(crate) fn parameters(&self) -> &Parameters<'db> {
&self.parameters
}
}
// TODO: use SmallVec here once invariance bug is fixed
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
/// The parameters portion of a typed signature.
///
/// The ordering of parameters is always as given in this struct: first positional-only parameters,
/// then positional-or-keyword, then optionally the variadic parameter, then keyword-only
/// parameters, and last, optionally the variadic keywords parameter.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct Parameters<'db> {
/// Parameters which may only be filled by positional arguments.
positional_only: Box<[ParameterWithDefault<'db>]>,
/// Parameters which may be filled by positional or keyword arguments.
positional_or_keyword: Box<[ParameterWithDefault<'db>]>,
/// The `*args` variadic parameter, if any.
variadic: Option<Parameter<'db>>,
/// Parameters which may only be filled by keyword arguments.
keyword_only: Box<[ParameterWithDefault<'db>]>,
/// The `**kwargs` variadic keywords parameter, if any.
keywords: Option<Parameter<'db>>,
}
impl<'db> Parameters<'db> {
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
fn todo() -> Self {
Self(vec![
Parameter {
Self {
variadic: Some(Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Some(todo_type!("todo signature *args")),
kind: ParameterKind::Variadic,
},
Parameter {
annotated_ty: todo_type!(),
}),
keywords: Some(Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: Some(todo_type!("todo signature **kwargs")),
kind: ParameterKind::KeywordVariadic,
},
])
annotated_ty: todo_type!(),
}),
..Default::default()
}
}
fn from_parameters(
@@ -93,238 +103,94 @@ impl<'db> Parameters<'db> {
kwarg,
range: _,
} = parameters;
let default_ty = |parameter_with_default: &ast::ParameterWithDefault| {
parameter_with_default
.default
.as_deref()
.map(|default| definition_expression_ty(db, definition, default))
};
let positional_only = posonlyargs.iter().map(|arg| {
Parameter::from_node_and_kind(
db,
definition,
&arg.parameter,
ParameterKind::PositionalOnly {
default_ty: default_ty(arg),
},
)
});
let positional_or_keyword = args.iter().map(|arg| {
Parameter::from_node_and_kind(
db,
definition,
&arg.parameter,
ParameterKind::PositionalOrKeyword {
default_ty: default_ty(arg),
},
)
});
let positional_only = posonlyargs
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let positional_or_keyword = args
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let variadic = vararg
.as_ref()
.map(|arg| Parameter::from_node_and_kind(db, definition, arg, ParameterKind::Variadic));
let keyword_only = kwonlyargs.iter().map(|arg| {
Parameter::from_node_and_kind(
db,
definition,
&arg.parameter,
ParameterKind::KeywordOnly {
default_ty: default_ty(arg),
},
)
});
let keywords = kwarg.as_ref().map(|arg| {
Parameter::from_node_and_kind(db, definition, arg, ParameterKind::KeywordVariadic)
});
Self(
positional_only
.chain(positional_or_keyword)
.chain(variadic)
.chain(keyword_only)
.chain(keywords)
.collect(),
)
}
pub(crate) fn len(&self) -> usize {
self.0.len()
}
pub(crate) fn iter(&self) -> std::slice::Iter<Parameter<'db>> {
self.0.iter()
}
/// Iterate initial positional parameters, not including variadic parameter, if any.
///
/// For a valid signature, this will be all positional parameters. In an invalid signature,
/// there could be non-initial positional parameters; effectively, we just won't consider those
/// to be positional, which is fine.
pub(crate) fn positional(&self) -> impl Iterator<Item = &Parameter<'db>> {
self.iter().take_while(|param| param.is_positional())
}
/// Return parameter at given index, or `None` if index is out-of-range.
pub(crate) fn get(&self, index: usize) -> Option<&Parameter<'db>> {
self.0.get(index)
}
/// Return positional parameter at given index, or `None` if `index` is out of range.
///
/// Does not return variadic parameter.
pub(crate) fn get_positional(&self, index: usize) -> Option<&Parameter<'db>> {
self.get(index)
.and_then(|parameter| parameter.is_positional().then_some(parameter))
}
/// Return the variadic parameter (`*args`), if any, and its index, or `None`.
pub(crate) fn variadic(&self) -> Option<(usize, &Parameter<'db>)> {
self.iter()
.enumerate()
.find(|(_, parameter)| parameter.is_variadic())
}
/// Return parameter (with index) for given name, or `None` if no such parameter.
///
/// Does not return keywords (`**kwargs`) parameter.
///
/// In an invalid signature, there could be multiple parameters with the same name; we will
/// just return the first that matches.
pub(crate) fn keyword_by_name(&self, name: &str) -> Option<(usize, &Parameter<'db>)> {
self.iter()
.enumerate()
.find(|(_, parameter)| parameter.callable_by_name(name))
}
/// Return the keywords parameter (`**kwargs`), if any, and its index, or `None`.
pub(crate) fn keyword_variadic(&self) -> Option<(usize, &Parameter<'db>)> {
self.iter()
.enumerate()
.rfind(|(_, parameter)| parameter.is_keyword_variadic())
}
}
impl<'db, 'a> IntoIterator for &'a Parameters<'db> {
type Item = &'a Parameter<'db>;
type IntoIter = std::slice::Iter<'a, Parameter<'db>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'db> std::ops::Index<usize> for Parameters<'db> {
type Output = Parameter<'db>;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
.map(|arg| Parameter::from_node(db, definition, arg));
let keyword_only = kwonlyargs
.iter()
.map(|arg| ParameterWithDefault::from_node(db, definition, arg))
.collect();
let keywords = kwarg
.as_ref()
.map(|arg| Parameter::from_node(db, definition, arg));
Self {
positional_only,
positional_or_keyword,
variadic,
keyword_only,
keywords,
}
}
}
/// A single parameter of a typed signature, with optional default value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Parameter<'db> {
pub(super) struct ParameterWithDefault<'db> {
parameter: Parameter<'db>,
/// Type of the default value, if any.
default_ty: Option<Type<'db>>,
}
impl<'db> ParameterWithDefault<'db> {
fn from_node(
db: &'db dyn Db,
definition: Definition<'db>,
parameter_with_default: &'db ast::ParameterWithDefault,
) -> Self {
Self {
default_ty: parameter_with_default
.default
.as_deref()
.map(|default| definition_expression_ty(db, definition, default)),
parameter: Parameter::from_node(db, definition, &parameter_with_default.parameter),
}
}
}
/// A single parameter of a typed signature.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Parameter<'db> {
/// Parameter name.
///
/// It is possible for signatures to be defined in ways that leave positional-only parameters
/// nameless (e.g. via `Callable` annotations).
name: Option<Name>,
/// Annotated type of the parameter.
annotated_ty: Option<Type<'db>>,
kind: ParameterKind<'db>,
/// Annotated type of the parameter (Unknown if no annotation.)
annotated_ty: Type<'db>,
}
impl<'db> Parameter<'db> {
fn from_node_and_kind(
fn from_node(
db: &'db dyn Db,
definition: Definition<'db>,
parameter: &'db ast::Parameter,
kind: ParameterKind<'db>,
) -> Self {
Self {
Parameter {
name: Some(parameter.name.id.clone()),
annotated_ty: parameter
.annotation
.as_deref()
.map(|annotation| definition_expression_ty(db, definition, annotation)),
kind,
.map(|annotation| definition_expression_ty(db, definition, annotation))
.unwrap_or(Type::Unknown),
}
}
pub(crate) fn is_variadic(&self) -> bool {
matches!(self.kind, ParameterKind::Variadic)
}
pub(crate) fn is_keyword_variadic(&self) -> bool {
matches!(self.kind, ParameterKind::KeywordVariadic)
}
pub(crate) fn is_positional(&self) -> bool {
matches!(
self.kind,
ParameterKind::PositionalOnly { .. } | ParameterKind::PositionalOrKeyword { .. }
)
}
pub(crate) fn callable_by_name(&self, name: &str) -> bool {
match self.kind {
ParameterKind::PositionalOrKeyword { .. } | ParameterKind::KeywordOnly { .. } => self
.name
.as_ref()
.is_some_and(|param_name| param_name == name),
_ => false,
}
}
/// Annotated type of the parameter, if annotated.
pub(crate) fn annotated_ty(&self) -> Option<Type<'db>> {
self.annotated_ty
}
/// Name of the parameter (if it has one).
pub(crate) fn name(&self) -> Option<&ast::name::Name> {
self.name.as_ref()
}
/// Display name of the parameter, if it has one.
pub(crate) fn display_name(&self) -> Option<ast::name::Name> {
self.name().map(|name| match self.kind {
ParameterKind::Variadic => ast::name::Name::new(format!("*{name}")),
ParameterKind::KeywordVariadic => ast::name::Name::new(format!("**{name}")),
_ => name.clone(),
})
}
/// Default-value type of the parameter, if any.
pub(crate) fn default_ty(&self) -> Option<Type<'db>> {
match self.kind {
ParameterKind::PositionalOnly { default_ty } => default_ty,
ParameterKind::PositionalOrKeyword { default_ty } => default_ty,
ParameterKind::Variadic => None,
ParameterKind::KeywordOnly { default_ty } => default_ty,
ParameterKind::KeywordVariadic => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ParameterKind<'db> {
/// Positional-only parameter, e.g. `def f(x, /): ...`
PositionalOnly { default_ty: Option<Type<'db>> },
/// Positional-or-keyword parameter, e.g. `def f(x): ...`
PositionalOrKeyword { default_ty: Option<Type<'db>> },
/// Variadic parameter, e.g. `def f(*args): ...`
Variadic,
/// Keyword-only parameter, e.g. `def f(*, x): ...`
KeywordOnly { default_ty: Option<Type<'db>> },
/// Variadic keywords parameter, e.g. `def f(**kwargs): ...`
KeywordVariadic,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDb};
use crate::types::{global_symbol, FunctionType, KnownClass};
use crate::types::{global_symbol, FunctionType};
use ruff_db::system::DbWithTestSystem;
#[track_caller]
@@ -336,8 +202,39 @@ mod tests {
}
#[track_caller]
fn assert_params<'db>(signature: &Signature<'db>, expected: &[Parameter<'db>]) {
assert_eq!(signature.parameters.0.as_slice(), expected);
fn assert_param_with_default<'db>(
db: &'db TestDb,
param_with_default: &ParameterWithDefault<'db>,
expected_name: &'static str,
expected_annotation_ty_display: &'static str,
expected_default_ty_display: Option<&'static str>,
) {
assert_eq!(
param_with_default
.default_ty
.map(|ty| ty.display(db).to_string()),
expected_default_ty_display.map(ToString::to_string)
);
assert_param(
db,
&param_with_default.parameter,
expected_name,
expected_annotation_ty_display,
);
}
#[track_caller]
fn assert_param<'db>(
db: &'db TestDb,
param: &Parameter<'db>,
expected_name: &'static str,
expected_annotation_ty_display: &'static str,
) {
assert_eq!(param.name.as_ref().unwrap(), expected_name);
assert_eq!(
param.annotated_ty.display(db).to_string(),
expected_annotation_ty_display
);
}
#[test]
@@ -348,8 +245,13 @@ mod tests {
let sig = func.internal_signature(&db);
assert!(sig.return_ty.is_none());
assert_params(&sig, &[]);
assert_eq!(sig.return_ty.display(&db).to_string(), "Unknown");
let params = sig.parameters;
assert!(params.positional_only.is_empty());
assert!(params.positional_or_keyword.is_empty());
assert!(params.variadic.is_none());
assert!(params.keyword_only.is_empty());
assert!(params.keywords.is_none());
}
#[test]
@@ -369,74 +271,34 @@ mod tests {
let sig = func.internal_signature(&db);
assert_eq!(sig.return_ty.unwrap().display(&db).to_string(), "bytes");
assert_params(
&sig,
&[
Parameter {
name: Some(Name::new_static("a")),
annotated_ty: None,
kind: ParameterKind::PositionalOnly { default_ty: None },
},
Parameter {
name: Some(Name::new_static("b")),
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
kind: ParameterKind::PositionalOnly { default_ty: None },
},
Parameter {
name: Some(Name::new_static("c")),
annotated_ty: None,
kind: ParameterKind::PositionalOnly {
default_ty: Some(Type::IntLiteral(1)),
},
},
Parameter {
name: Some(Name::new_static("d")),
annotated_ty: Some(KnownClass::Int.to_instance(&db)),
kind: ParameterKind::PositionalOnly {
default_ty: Some(Type::IntLiteral(2)),
},
},
Parameter {
name: Some(Name::new_static("e")),
annotated_ty: None,
kind: ParameterKind::PositionalOrKeyword {
default_ty: Some(Type::IntLiteral(3)),
},
},
Parameter {
name: Some(Name::new_static("f")),
annotated_ty: Some(Type::IntLiteral(4)),
kind: ParameterKind::PositionalOrKeyword {
default_ty: Some(Type::IntLiteral(4)),
},
},
Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Some(KnownClass::Object.to_instance(&db)),
kind: ParameterKind::Variadic,
},
Parameter {
name: Some(Name::new_static("g")),
annotated_ty: None,
kind: ParameterKind::KeywordOnly {
default_ty: Some(Type::IntLiteral(5)),
},
},
Parameter {
name: Some(Name::new_static("h")),
annotated_ty: Some(Type::IntLiteral(6)),
kind: ParameterKind::KeywordOnly {
default_ty: Some(Type::IntLiteral(6)),
},
},
Parameter {
name: Some(Name::new_static("kwargs")),
annotated_ty: Some(KnownClass::Str.to_instance(&db)),
kind: ParameterKind::KeywordVariadic,
},
],
);
assert_eq!(sig.return_ty.display(&db).to_string(), "bytes");
let params = sig.parameters;
let [a, b, c, d] = &params.positional_only[..] else {
panic!("expected four positional-only parameters");
};
let [e, f] = &params.positional_or_keyword[..] else {
panic!("expected two positional-or-keyword parameters");
};
let Some(args) = params.variadic else {
panic!("expected a variadic parameter");
};
let [g, h] = &params.keyword_only[..] else {
panic!("expected two keyword-only parameters");
};
let Some(kwargs) = params.keywords else {
panic!("expected a kwargs parameter");
};
assert_param_with_default(&db, a, "a", "Unknown", None);
assert_param_with_default(&db, b, "b", "int", None);
assert_param_with_default(&db, c, "c", "Unknown", Some("Literal[1]"));
assert_param_with_default(&db, d, "d", "int", Some("Literal[2]"));
assert_param_with_default(&db, e, "e", "Unknown", Some("Literal[3]"));
assert_param_with_default(&db, f, "f", "Literal[4]", Some("Literal[4]"));
assert_param_with_default(&db, g, "g", "Unknown", Some("Literal[5]"));
assert_param_with_default(&db, h, "h", "Literal[6]", Some("Literal[6]"));
assert_param(&db, &args, "args", "object");
assert_param(&db, &kwargs, "kwargs", "str");
}
#[test]
@@ -460,17 +322,11 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
name: Some(name),
annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
let [a] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected one positional-or-keyword parameter");
};
assert_eq!(name, "a");
// Parameter resolution not deferred; we should see A not B
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "A");
assert_param_with_default(&db, a, "a", "A", None);
}
#[test]
@@ -494,17 +350,11 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
name: Some(name),
annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
let [a] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected one positional-or-keyword parameter");
};
assert_eq!(name, "a");
// Parameter resolution deferred; we should see B
assert_eq!(annotated_ty.unwrap().display(&db).to_string(), "B");
assert_param_with_default(&db, a, "a", "B", None);
}
#[test]
@@ -528,23 +378,12 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
name: Some(a_name),
annotated_ty: a_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}, Parameter {
name: Some(b_name),
annotated_ty: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected two positional-or-keyword parameters");
};
assert_eq!(a_name, "a");
assert_eq!(b_name, "b");
// TODO resolution should not be deferred; we should see A not B
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
assert_param_with_default(&db, a, "a", "B", None);
assert_param_with_default(&db, b, "b", "T", None);
}
#[test]
@@ -568,23 +407,12 @@ mod tests {
let sig = func.internal_signature(&db);
let [Parameter {
name: Some(a_name),
annotated_ty: a_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}, Parameter {
name: Some(b_name),
annotated_ty: b_annotated_ty,
kind: ParameterKind::PositionalOrKeyword { .. },
}] = &sig.parameters.0[..]
else {
let [a, b] = &sig.parameters.positional_or_keyword[..] else {
panic!("expected two positional-or-keyword parameters");
};
assert_eq!(a_name, "a");
assert_eq!(b_name, "b");
// Parameter resolution deferred; we should see B
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
assert_param_with_default(&db, a, "a", "B", None);
assert_param_with_default(&db, b, "b", "T", None);
}
#[test]

View File

@@ -1,89 +0,0 @@
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Symbol, Type};
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub struct SubclassOfType<'db> {
// Keep this field private, so that the only way of constructing the struct is through the `from` method.
subclass_of: ClassBase<'db>,
}
impl<'db> SubclassOfType<'db> {
/// Construct a new [`Type`] instance representing a given class object (or a given dynamic type)
/// and all possible subclasses of that class object/dynamic type.
///
/// This method does not always return a [`Type::SubclassOf`] variant.
/// If the class object is known to be a final class,
/// this method will return a [`Type::ClassLiteral`] variant; this is a more precise type.
/// If the class object is `builtins.object`, `Type::Instance(<builtins.type>)` will be returned;
/// this is no more precise, but it is exactly equivalent to `type[object]`.
///
/// The eager normalization here means that we do not need to worry elsewhere about distinguishing
/// between `@final` classes and other classes when dealing with [`Type::SubclassOf`] variants.
pub(crate) fn from(db: &'db dyn Db, subclass_of: impl Into<ClassBase<'db>>) -> Type<'db> {
let subclass_of = subclass_of.into();
match subclass_of {
ClassBase::Dynamic(_) => Type::SubclassOf(Self { subclass_of }),
ClassBase::Class(class) => {
if class.is_final(db) {
Type::ClassLiteral(ClassLiteralType { class })
} else if class.is_known(db, KnownClass::Object) {
KnownClass::Type.to_instance(db)
} else {
Type::SubclassOf(Self { subclass_of })
}
}
}
}
/// Return a [`Type`] instance representing the type `type[Unknown]`.
pub(crate) const fn subclass_of_unknown() -> Type<'db> {
Type::SubclassOf(SubclassOfType {
subclass_of: ClassBase::unknown(),
})
}
/// Return a [`Type`] instance representing the type `type[Any]`.
pub(crate) const fn subclass_of_any() -> Type<'db> {
Type::SubclassOf(SubclassOfType {
subclass_of: ClassBase::any(),
})
}
/// Return the inner [`ClassBase`] value wrapped by this `SubclassOfType`.
pub(crate) const fn subclass_of(self) -> ClassBase<'db> {
self.subclass_of
}
pub const fn is_dynamic(self) -> bool {
// Unpack `self` so that we're forced to update this method if any more fields are added in the future.
let Self { subclass_of } = self;
subclass_of.is_dynamic()
}
pub const fn is_fully_static(self) -> bool {
!self.is_dynamic()
}
pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
Type::from(self.subclass_of).member(db, name)
}
/// Return `true` if `self` is a subtype of `other`.
///
/// This can only return `true` if `self.subclass_of` is a [`ClassBase::Class`] variant;
/// only fully static types participate in subtyping.
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: SubclassOfType<'db>) -> bool {
match (self.subclass_of, other.subclass_of) {
// Non-fully-static types do not participate in subtyping
(ClassBase::Dynamic(_), _) | (_, ClassBase::Dynamic(_)) => false,
// For example, `type[bool]` describes all possible runtime subclasses of the class `bool`,
// and `type[int]` describes all possible runtime subclasses of the class `int`.
// The first set is a subset of the second set, because `bool` is itself a subclass of `int`.
(ClassBase::Class(self_class), ClassBase::Class(other_class)) => {
// N.B. The subclass relation is fully static
self_class.is_subclass_of(db, other_class)
}
}
}
}

View File

@@ -45,15 +45,6 @@ impl<'db> Unpacker<'db> {
let mut value_ty = infer_expression_types(self.db(), value.expression())
.expression_ty(value.scoped_expression_id(self.db(), self.scope));
if value.is_assign()
&& self.context.in_stub()
&& value
.expression()
.node_ref(self.db())
.is_ellipsis_literal_expr()
{
value_ty = Type::unknown();
}
if value.is_iterable() {
// If the value is an iterable, then the type that needs to be unpacked is the iterator
// type.
@@ -105,7 +96,7 @@ impl<'db> Unpacker<'db> {
// with each individual character, instead of just an array of
// `LiteralString`, but there would be a cost and it's not clear that
// it's worth it.
TupleType::from_elements(
Type::tuple(
self.db(),
std::iter::repeat(Type::LiteralString)
.take(string_literal_ty.python_len(self.db())),
@@ -164,7 +155,7 @@ impl<'db> Unpacker<'db> {
for (index, element) in elts.iter().enumerate() {
// SAFETY: `target_types` is initialized with the same length as `elts`.
let element_ty = match target_types[index].as_slice() {
[] => Type::unknown(),
[] => Type::Unknown,
types => UnionType::from_elements(self.db(), types),
};
self.unpack_inner(element, element_ty);
@@ -241,7 +232,7 @@ impl<'db> Unpacker<'db> {
// Subtract 1 to insert the starred expression type at the correct
// index.
element_types.resize(targets.len() - 1, Type::unknown());
element_types.resize(targets.len() - 1, Type::Unknown);
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, todo_type!("starred unpacking"));

View File

@@ -76,11 +76,6 @@ impl<'db> UnpackValue<'db> {
matches!(self, UnpackValue::Iterable(_))
}
/// Returns `true` if the value is being assigned to a target.
pub(crate) const fn is_assign(self) -> bool {
matches!(self, UnpackValue::Assign(_))
}
/// Returns the underlying [`Expression`] that is being unpacked.
pub(crate) const fn expression(self) -> Expression<'db> {
match self {

View File

@@ -329,9 +329,9 @@ impl<'db> VisibilityConstraints<'db> {
Truthiness::Ambiguous
}
}
PatternConstraintKind::Singleton(..)
| PatternConstraintKind::Class(..)
| PatternConstraintKind::Unsupported => Truthiness::Ambiguous,
PatternConstraintKind::Singleton(..) | PatternConstraintKind::Unsupported => {
Truthiness::Ambiguous
}
},
}
}

View File

@@ -11,10 +11,10 @@ use crate::server::Server;
mod message;
mod edit;
mod logging;
mod server;
mod session;
mod system;
mod trace;
pub(crate) const SERVER_NAME: &str = "red-knot";
pub(crate) const DIAGNOSTIC_NAME: &str = "Red Knot";

View File

@@ -1,114 +0,0 @@
//! The logging system for `red_knot server`.
//!
//! Log messages are controlled by the `logLevel` setting which defaults to `"info"`. Log messages
//! are written to `stderr` by default, which should appear in the logs for most LSP clients. A
//! `logFile` path can also be specified in the settings, and output will be directed there
//! instead.
use core::str;
use serde::Deserialize;
use std::{path::PathBuf, str::FromStr, sync::Arc};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
fmt::{time::Uptime, writer::BoxMakeWriter},
layer::SubscriberExt,
Layer,
};
pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Path>) {
let log_file = log_file
.map(|path| {
// this expands `logFile` so that tildes and environment variables
// are replaced with their values, if possible.
if let Some(expanded) = shellexpand::full(&path.to_string_lossy())
.ok()
.and_then(|path| PathBuf::from_str(&path).ok())
{
expanded
} else {
path.to_path_buf()
}
})
.and_then(|path| {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|err| {
#[allow(clippy::print_stderr)]
{
eprintln!(
"Failed to open file at {} for logging: {err}",
path.display()
);
}
})
.ok()
});
let logger = match log_file {
Some(file) => BoxMakeWriter::new(Arc::new(file)),
None => BoxMakeWriter::new(std::io::stderr),
};
let subscriber = tracing_subscriber::Registry::default().with(
tracing_subscriber::fmt::layer()
.with_timer(Uptime::default())
.with_thread_names(true)
.with_ansi(false)
.with_writer(logger)
.with_filter(LogLevelFilter { filter: log_level }),
);
tracing::subscriber::set_global_default(subscriber)
.expect("should be able to set global default subscriber");
}
/// The log level for the server as provided by the client during initialization.
///
/// The default log level is `info`.
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogLevel {
Error,
Warn,
#[default]
Info,
Debug,
Trace,
}
impl LogLevel {
fn trace_level(self) -> tracing::Level {
match self {
Self::Error => tracing::Level::ERROR,
Self::Warn => tracing::Level::WARN,
Self::Info => tracing::Level::INFO,
Self::Debug => tracing::Level::DEBUG,
Self::Trace => tracing::Level::TRACE,
}
}
}
/// Filters out traces which have a log level lower than the `logLevel` set by the client.
struct LogLevelFilter {
filter: LogLevel,
}
impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
fn enabled(
&self,
meta: &tracing::Metadata<'_>,
_: &tracing_subscriber::layer::Context<'_, S>,
) -> bool {
let filter = if meta.target().starts_with("red_knot") {
self.filter.trace_level()
} else {
tracing::Level::INFO
};
meta.level() <= &filter
}
fn max_level_hint(&self) -> Option<tracing::level_filters::LevelFilter> {
Some(LevelFilter::from_level(self.filter.trace_level()))
}
}

View File

@@ -51,6 +51,10 @@ impl Server {
crate::version(),
)?;
if let Some(trace) = init_params.trace {
crate::trace::set_trace_value(trace);
}
crate::message::init_messenger(connection.make_sender());
let AllSettings {
@@ -62,9 +66,14 @@ impl Server {
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
);
crate::logging::init_logging(
global_settings.tracing.log_level.unwrap_or_default(),
crate::trace::init_tracing(
connection.make_sender(),
global_settings
.tracing
.log_level
.unwrap_or(crate::trace::LogLevel::Info),
global_settings.tracing.log_file.as_deref(),
init_params.client_info.as_ref(),
);
let mut workspace_for_url = |url: Url| {

View File

@@ -52,6 +52,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
notification::DidCloseNotebookHandler::METHOD => {
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
}
notification::SetTraceHandler::METHOD => {
local_notification_task::<notification::SetTraceHandler>(notif)
}
method => {
tracing::warn!("Received notification {method} which does not have a handler.");
return Task::nothing();

View File

@@ -3,9 +3,11 @@ mod did_close;
mod did_close_notebook;
mod did_open;
mod did_open_notebook;
mod set_trace;
pub(super) use did_change::DidChangeTextDocumentHandler;
pub(super) use did_close::DidCloseTextDocumentHandler;
pub(super) use did_close_notebook::DidCloseNotebookHandler;
pub(super) use did_open::DidOpenTextDocumentHandler;
pub(super) use did_open_notebook::DidOpenNotebookHandler;
pub(super) use set_trace::SetTraceHandler;

View File

@@ -0,0 +1,25 @@
use lsp_types::notification::SetTrace;
use lsp_types::SetTraceParams;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::server::client::{Notifier, Requester};
use crate::server::Result;
use crate::session::Session;
pub(crate) struct SetTraceHandler;
impl NotificationHandler for SetTraceHandler {
type NotificationType = SetTrace;
}
impl SyncNotificationHandler for SetTraceHandler {
fn run(
_session: &mut Session,
_notifier: Notifier,
_requester: &mut Requester,
params: SetTraceParams,
) -> Result<()> {
crate::trace::set_trace_value(params.value);
Ok(())
}
}

View File

@@ -24,7 +24,7 @@ pub struct ClientSettings {
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct TracingSettings {
pub(crate) log_level: Option<crate::logging::LogLevel>,
pub(crate) log_level: Option<crate::trace::LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
pub(crate) log_file: Option<PathBuf>,
}

View File

@@ -0,0 +1,221 @@
//! The tracing system for `ruff server`.
//!
//! Traces are controlled by the `logLevel` setting, along with the
//! trace level set through the LSP. On VS Code, the trace level can
//! also be set with `ruff.trace.server`. A trace level of `messages` or
//! `verbose` will enable tracing - otherwise, no traces will be shown.
//!
//! `logLevel` can be used to configure the level of tracing that is shown.
//! By default, `logLevel` is set to `"info"`.
//!
//! The server also supports the `RUFF_TRACE` environment variable, which will
//! override the trace value provided by the LSP client. Use this if there's no good way
//! to set the trace value through your editor's configuration.
//!
//! Tracing will write to `stderr` by default, which should appear in the logs for most LSP clients.
//! A `logFile` path can also be specified in the settings, and output will be directed there instead.
use core::str;
use lsp_server::{Message, Notification};
use lsp_types::{
notification::{LogTrace, Notification as _},
ClientInfo, TraceValue,
};
use serde::Deserialize;
use std::{
io::{Error as IoError, ErrorKind, Write},
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex, OnceLock},
};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
fmt::{time::Uptime, writer::BoxMakeWriter, MakeWriter},
layer::SubscriberExt,
Layer,
};
use crate::server::ClientSender;
const TRACE_ENV_KEY: &str = "RUFF_TRACE";
static LOGGING_SENDER: OnceLock<ClientSender> = OnceLock::new();
static TRACE_VALUE: Mutex<lsp_types::TraceValue> = Mutex::new(lsp_types::TraceValue::Off);
pub(crate) fn set_trace_value(trace_value: TraceValue) {
let mut global_trace_value = TRACE_VALUE
.lock()
.expect("trace value mutex should be available");
*global_trace_value = trace_value;
}
// A tracing writer that uses LSPs logTrace method.
struct TraceLogWriter;
impl Write for TraceLogWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let message = str::from_utf8(buf).map_err(|e| IoError::new(ErrorKind::InvalidData, e))?;
LOGGING_SENDER
.get()
.expect("logging sender should be initialized at this point")
.send(Message::Notification(Notification {
method: LogTrace::METHOD.to_owned(),
params: serde_json::json!({
"message": message
}),
}))
.map_err(|e| IoError::new(ErrorKind::Other, e))?;
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for TraceLogWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
Self
}
}
pub(crate) fn init_tracing(
sender: ClientSender,
log_level: LogLevel,
log_file: Option<&std::path::Path>,
client: Option<&ClientInfo>,
) {
LOGGING_SENDER
.set(sender)
.expect("logging sender should only be initialized once");
let log_file = log_file
.map(|path| {
// this expands `logFile` so that tildes and environment variables
// are replaced with their values, if possible.
if let Some(expanded) = shellexpand::full(&path.to_string_lossy())
.ok()
.and_then(|path| PathBuf::from_str(&path).ok())
{
expanded
} else {
path.to_path_buf()
}
})
.and_then(|path| {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|err| {
#[allow(clippy::print_stderr)]
{
eprintln!(
"Failed to open file at {} for logging: {err}",
path.display()
);
}
})
.ok()
});
let logger = match log_file {
Some(file) => BoxMakeWriter::new(Arc::new(file)),
None => {
if client.is_some_and(|client| {
client.name.starts_with("Zed") || client.name.starts_with("Visual Studio Code")
}) {
BoxMakeWriter::new(TraceLogWriter)
} else {
BoxMakeWriter::new(std::io::stderr)
}
}
};
let subscriber = tracing_subscriber::Registry::default().with(
tracing_subscriber::fmt::layer()
.with_timer(Uptime::default())
.with_thread_names(true)
.with_ansi(false)
.with_writer(logger)
.with_filter(TraceLevelFilter)
.with_filter(LogLevelFilter { filter: log_level }),
);
tracing::subscriber::set_global_default(subscriber)
.expect("should be able to set global default subscriber");
}
#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogLevel {
#[default]
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
fn trace_level(self) -> tracing::Level {
match self {
Self::Error => tracing::Level::ERROR,
Self::Warn => tracing::Level::WARN,
Self::Info => tracing::Level::INFO,
Self::Debug => tracing::Level::DEBUG,
Self::Trace => tracing::Level::TRACE,
}
}
}
/// Filters out traces which have a log level lower than the `logLevel` set by the client.
struct LogLevelFilter {
filter: LogLevel,
}
/// Filters out traces if the trace value set by the client is `off`.
struct TraceLevelFilter;
impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
fn enabled(
&self,
meta: &tracing::Metadata<'_>,
_: &tracing_subscriber::layer::Context<'_, S>,
) -> bool {
let filter = if meta.target().starts_with("ruff") {
self.filter.trace_level()
} else {
tracing::Level::INFO
};
meta.level() <= &filter
}
fn max_level_hint(&self) -> Option<tracing::level_filters::LevelFilter> {
Some(LevelFilter::from_level(self.filter.trace_level()))
}
}
impl<S> tracing_subscriber::layer::Filter<S> for TraceLevelFilter {
fn enabled(
&self,
_: &tracing::Metadata<'_>,
_: &tracing_subscriber::layer::Context<'_, S>,
) -> bool {
trace_value() != lsp_types::TraceValue::Off
}
}
#[inline]
fn trace_value() -> lsp_types::TraceValue {
std::env::var(TRACE_ENV_KEY)
.ok()
.and_then(|trace| serde_json::from_value(serde_json::Value::String(trace)).ok())
.unwrap_or_else(|| {
*TRACE_VALUE
.lock()
.expect("trace value mutex should be available")
})
}

View File

@@ -6,11 +6,10 @@ use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::Program;
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::panic::catch_unwind;
use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_source_file::LineIndex;
use ruff_text_size::TextSize;
use salsa::Setter;
@@ -137,33 +136,7 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
})
.collect();
let type_diagnostics = match catch_unwind(|| check_types(db, test_file.file)) {
Ok(type_diagnostics) => type_diagnostics,
Err(info) => {
let mut by_line = matcher::FailuresByLine::default();
let mut messages = vec![];
match info.location {
Some(location) => messages.push(format!("panicked at {location}")),
None => messages.push("panicked at unknown location".to_string()),
};
match info.payload {
Some(payload) => messages.push(payload),
// Mimic the default panic hook's rendering of the panic payload if it's
// not a string.
None => messages.push("Box<dyn Any>".to_string()),
};
if let Some(backtrace) = info.backtrace {
if std::env::var("RUST_BACKTRACE").is_ok() {
messages.extend(backtrace.to_string().split('\n').map(String::from));
}
}
by_line.push(OneIndexed::from_zero_indexed(0), messages);
return Some(FileFailures {
backtick_offset: test_file.backtick_offset,
by_line,
});
}
};
let type_diagnostics = check_types(db, test_file.file);
diagnostics.extend(type_diagnostics.into_iter().map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new((*diagnostic).clone());
diagnostic

View File

@@ -27,7 +27,7 @@ impl FailuresByLine {
})
}
pub(super) fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
let start = self.failures.len();
self.failures.extend(messages);
self.lines.push(LineFailures {

View File

@@ -6,7 +6,6 @@
//! in `crates/red_knot_vendored/vendor/typeshed` change.
use std::fs::File;
use std::io::Write;
use std::path::Path;
use path_slash::PathExt;
@@ -15,15 +14,13 @@ use zip::write::{FileOptions, ZipWriter};
use zip::CompressionMethod;
const TYPESHED_SOURCE_DIR: &str = "vendor/typeshed";
const KNOT_EXTENSIONS_STUBS: &str = "knot_extensions/knot_extensions.pyi";
const TYPESHED_ZIP_LOCATION: &str = "/zipped_typeshed.zip";
/// Recursively zip the contents of the entire typeshed directory and patch typeshed
/// on the fly to include the `knot_extensions` module.
/// Recursively zip the contents of an entire directory.
///
/// This routine is adapted from a recipe at
/// <https://github.com/zip-rs/zip-old/blob/5d0f198124946b7be4e5969719a7f29f363118cd/examples/write_dir.rs>
fn write_zipped_typeshed_to(writer: File) -> ZipResult<File> {
fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
let mut zip = ZipWriter::new(writer);
// Use deflated compression for WASM builds because compiling `zstd-sys` requires clang
@@ -45,11 +42,11 @@ fn write_zipped_typeshed_to(writer: File) -> ZipResult<File> {
.compression_method(method)
.unix_permissions(0o644);
for entry in walkdir::WalkDir::new(TYPESHED_SOURCE_DIR) {
for entry in walkdir::WalkDir::new(directory_path) {
let dir_entry = entry.unwrap();
let absolute_path = dir_entry.path();
let normalized_relative_path = absolute_path
.strip_prefix(Path::new(TYPESHED_SOURCE_DIR))
.strip_prefix(Path::new(directory_path))
.unwrap()
.to_slash()
.expect("Unexpected non-utf8 typeshed path!");
@@ -58,14 +55,9 @@ fn write_zipped_typeshed_to(writer: File) -> ZipResult<File> {
// Some unzip tools unzip files with directory paths correctly, some do not!
if absolute_path.is_file() {
println!("adding file {absolute_path:?} as {normalized_relative_path:?} ...");
zip.start_file(&*normalized_relative_path, options)?;
zip.start_file(normalized_relative_path, options)?;
let mut f = File::open(absolute_path)?;
std::io::copy(&mut f, &mut zip).unwrap();
// Patch the VERSIONS file to make `knot_extensions` available
if normalized_relative_path == "stdlib/VERSIONS" {
writeln!(&mut zip, "knot_extensions: 3.0-")?;
}
} else if !normalized_relative_path.is_empty() {
// Only if not root! Avoids path spec / warning
// and mapname conversion failed error on unzip
@@ -73,17 +65,11 @@ fn write_zipped_typeshed_to(writer: File) -> ZipResult<File> {
zip.add_directory(normalized_relative_path, options)?;
}
}
// Patch typeshed and add the stubs for the `knot_extensions` module
println!("adding file {KNOT_EXTENSIONS_STUBS} as stdlib/knot_extensions.pyi ...");
zip.start_file("stdlib/knot_extensions.pyi", options)?;
let mut f = File::open(KNOT_EXTENSIONS_STUBS)?;
std::io::copy(&mut f, &mut zip).unwrap();
zip.finish()
}
fn main() {
println!("cargo::rerun-if-changed={TYPESHED_SOURCE_DIR}");
assert!(
Path::new(TYPESHED_SOURCE_DIR).is_dir(),
"Where is typeshed?"
@@ -98,6 +84,6 @@ fn main() {
// which can't be done at compile time.)
let zipped_typeshed_location = format!("{out_dir}{TYPESHED_ZIP_LOCATION}");
let zipped_typeshed_file = File::create(zipped_typeshed_location).unwrap();
write_zipped_typeshed_to(zipped_typeshed_file).unwrap();
let zipped_typeshed = File::create(zipped_typeshed_location).unwrap();
zip_dir(TYPESHED_SOURCE_DIR, zipped_typeshed).unwrap();
}

View File

@@ -1,3 +0,0 @@
The `knot_extensions.pyi` file in this directory will be symlinked into
the `vendor/typeshed/stdlib` directory every time we sync our `typeshed`
stubs (see `.github/workflows/sync_typeshed.yaml`).

View File

@@ -1,24 +0,0 @@
from typing import _SpecialForm, Any, LiteralString
# Special operations
def static_assert(condition: object, msg: LiteralString | None = None) -> None: ...
# Types
Unknown = object()
# Special forms
Not: _SpecialForm
Intersection: _SpecialForm
TypeOf: _SpecialForm
# Predicates on types
#
# Ideally, these would be annotated using `TypeForm`, but that has not been
# standardized yet (https://peps.python.org/pep-0747).
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
def is_subtype_of(type_derived: Any, typ_ebase: Any) -> bool: ...
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ...
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
def is_fully_static(type: Any) -> bool: ...
def is_singleton(type: Any) -> bool: ...
def is_single_valued(type: Any) -> bool: ...

View File

@@ -1 +1 @@
2047b820730fdd65d37e6e8efcf29ca9af7ec3e7
fc11e835108394728930059c8db5b436209bc957

View File

@@ -65,7 +65,7 @@ class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportIn
self,
coro: _TaskCompatibleCoro[_T_co],
*,
loop: AbstractEventLoop | None = None,
loop: AbstractEventLoop = ...,
name: str | None = ...,
context: Context | None = None,
eager_start: bool = False,
@@ -75,13 +75,13 @@ class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportIn
self,
coro: _TaskCompatibleCoro[_T_co],
*,
loop: AbstractEventLoop | None = None,
loop: AbstractEventLoop = ...,
name: str | None = ...,
context: Context | None = None,
) -> None: ...
else:
def __init__(
self, coro: _TaskCompatibleCoro[_T_co], *, loop: AbstractEventLoop | None = None, name: str | None = ...
self, coro: _TaskCompatibleCoro[_T_co], *, loop: AbstractEventLoop = ..., name: str | None = ...
) -> None: ...
if sys.version_info >= (3, 12):

View File

@@ -22,8 +22,8 @@ class blake2b:
digest_size: int
name: str
if sys.version_info >= (3, 9):
def __new__(
cls,
def __init__(
self,
data: ReadableBuffer = b"",
/,
*,
@@ -39,10 +39,10 @@ class blake2b:
inner_size: int = 0,
last_node: bool = False,
usedforsecurity: bool = True,
) -> Self: ...
) -> None: ...
else:
def __new__(
cls,
def __init__(
self,
data: ReadableBuffer = b"",
/,
*,
@@ -57,7 +57,7 @@ class blake2b:
node_depth: int = 0,
inner_size: int = 0,
last_node: bool = False,
) -> Self: ...
) -> None: ...
def copy(self) -> Self: ...
def digest(self) -> bytes: ...
@@ -74,8 +74,8 @@ class blake2s:
digest_size: int
name: str
if sys.version_info >= (3, 9):
def __new__(
cls,
def __init__(
self,
data: ReadableBuffer = b"",
/,
*,
@@ -91,10 +91,10 @@ class blake2s:
inner_size: int = 0,
last_node: bool = False,
usedforsecurity: bool = True,
) -> Self: ...
) -> None: ...
else:
def __new__(
cls,
def __init__(
self,
data: ReadableBuffer = b"",
/,
*,
@@ -109,7 +109,7 @@ class blake2s:
node_depth: int = 0,
inner_size: int = 0,
last_node: bool = False,
) -> Self: ...
) -> None: ...
def copy(self) -> Self: ...
def digest(self) -> bytes: ...

View File

@@ -1,15 +1,9 @@
import sys
from _typeshed import ReadableBuffer
from typing import final
from typing_extensions import Self
@final
class BZ2Compressor:
if sys.version_info >= (3, 12):
def __new__(cls, compresslevel: int = 9, /) -> Self: ...
else:
def __init__(self, compresslevel: int = 9, /) -> None: ...
def __init__(self, compresslevel: int = 9) -> None: ...
def compress(self, data: ReadableBuffer, /) -> bytes: ...
def flush(self) -> bytes: ...

View File

@@ -8,7 +8,6 @@ from typing import ( # noqa: Y022,Y038
AsyncIterator as AsyncIterator,
Awaitable as Awaitable,
Callable as Callable,
ClassVar,
Collection as Collection,
Container as Container,
Coroutine as Coroutine,
@@ -75,7 +74,6 @@ _VT_co = TypeVar("_VT_co", covariant=True) # Value type covariant containers.
class dict_keys(KeysView[_KT_co], Generic[_KT_co, _VT_co]): # undocumented
def __eq__(self, value: object, /) -> bool: ...
def __reversed__(self) -> Iterator[_KT_co]: ...
__hash__: ClassVar[None] # type: ignore[assignment]
if sys.version_info >= (3, 13):
def isdisjoint(self, other: Iterable[_KT_co], /) -> bool: ...
if sys.version_info >= (3, 10):
@@ -93,7 +91,6 @@ class dict_values(ValuesView[_VT_co], Generic[_KT_co, _VT_co]): # undocumented
class dict_items(ItemsView[_KT_co, _VT_co]): # undocumented
def __eq__(self, value: object, /) -> bool: ...
def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...
__hash__: ClassVar[None] # type: ignore[assignment]
if sys.version_info >= (3, 13):
def isdisjoint(self, other: Iterable[tuple[_KT_co, _VT_co]], /) -> bool: ...
if sys.version_info >= (3, 10):

View File

@@ -1,7 +1,7 @@
import sys
from collections.abc import Callable, Iterator, Mapping
from typing import Any, ClassVar, Generic, TypeVar, final, overload
from typing_extensions import ParamSpec, Self
from typing_extensions import ParamSpec
if sys.version_info >= (3, 9):
from types import GenericAlias
@@ -13,9 +13,9 @@ _P = ParamSpec("_P")
@final
class ContextVar(Generic[_T]):
@overload
def __new__(cls, name: str) -> Self: ...
def __init__(self, name: str) -> None: ...
@overload
def __new__(cls, name: str, *, default: _T) -> Self: ...
def __init__(self, name: str, *, default: _T) -> None: ...
def __hash__(self) -> int: ...
@property
def name(self) -> str: ...
@@ -37,7 +37,6 @@ class Token(Generic[_T]):
@property
def old_value(self) -> Any: ... # returns either _T or MISSING, but that's hard to express
MISSING: ClassVar[object]
__hash__: ClassVar[None] # type: ignore[assignment]
if sys.version_info >= (3, 9):
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
@@ -56,7 +55,6 @@ class Context(Mapping[ContextVar[Any], Any]):
def get(self, key: ContextVar[_T], default: _D, /) -> _T | _D: ...
def run(self, callable: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> _T: ...
def copy(self) -> Context: ...
__hash__: ClassVar[None] # type: ignore[assignment]
def __getitem__(self, key: ContextVar[_T], /) -> _T: ...
def __iter__(self) -> Iterator[ContextVar[Any]]: ...
def __len__(self) -> int: ...

View File

@@ -32,8 +32,8 @@ class Dialect:
lineterminator: str
quoting: _QuotingType
strict: bool
def __new__(
cls,
def __init__(
self,
dialect: _DialectLike | None = ...,
delimiter: str = ",",
doublequote: bool = True,
@@ -43,7 +43,7 @@ class Dialect:
quoting: _QuotingType = 0,
skipinitialspace: bool = False,
strict: bool = False,
) -> Self: ...
) -> None: ...
if sys.version_info >= (3, 10):
# This class calls itself _csv.reader.
@@ -115,7 +115,7 @@ def reader(
) -> _reader: ...
def register_dialect(
name: str,
dialect: type[Dialect | csv.Dialect] = ...,
dialect: type[Dialect] = ...,
*,
delimiter: str = ",",
quotechar: str | None = '"',

View File

@@ -169,18 +169,18 @@ class CFuncPtr(_PointerLike, _CData, metaclass=_PyCFuncPtrType):
# Abstract attribute that must be defined on subclasses
_flags_: ClassVar[int]
@overload
def __new__(cls) -> Self: ...
def __init__(self) -> None: ...
@overload
def __new__(cls, address: int, /) -> Self: ...
def __init__(self, address: int, /) -> None: ...
@overload
def __new__(cls, callable: Callable[..., Any], /) -> Self: ...
def __init__(self, callable: Callable[..., Any], /) -> None: ...
@overload
def __new__(cls, func_spec: tuple[str | int, CDLL], paramflags: tuple[_PF, ...] | None = ..., /) -> Self: ...
def __init__(self, func_spec: tuple[str | int, CDLL], paramflags: tuple[_PF, ...] | None = ..., /) -> None: ...
if sys.platform == "win32":
@overload
def __new__(
cls, vtbl_index: int, name: str, paramflags: tuple[_PF, ...] | None = ..., iid: _CData | _CDataType | None = ..., /
) -> Self: ...
def __init__(
self, vtbl_index: int, name: str, paramflags: tuple[_PF, ...] | None = ..., iid: _CData | _CDataType | None = ..., /
) -> None: ...
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...

View File

@@ -1,7 +1,7 @@
import sys
from _typeshed import ReadOnlyBuffer, SupportsRead, SupportsWrite
from _typeshed import ReadOnlyBuffer, SupportsRead
from curses import _ncurses_version
from typing import Any, final, overload
from typing import IO, Any, final, overload
from typing_extensions import TypeAlias
# NOTE: This module is ordinarily only available on Unix, but the windows-curses
@@ -517,7 +517,7 @@ class window: # undocumented
def overwrite(
self, destwin: window, sminrow: int, smincol: int, dminrow: int, dmincol: int, dmaxrow: int, dmaxcol: int
) -> None: ...
def putwin(self, file: SupportsWrite[bytes], /) -> None: ...
def putwin(self, file: IO[Any], /) -> None: ...
def redrawln(self, beg: int, num: int, /) -> None: ...
def redrawwin(self) -> None: ...
@overload

View File

@@ -5,7 +5,7 @@ import types
from _typeshed.importlib import LoaderProtocol
from collections.abc import Mapping, Sequence
from types import ModuleType
from typing import Any, ClassVar
from typing import Any
# Signature of `builtins.__import__` should be kept identical to `importlib.__import__`
def __import__(
@@ -43,7 +43,6 @@ class ModuleSpec:
def parent(self) -> str | None: ...
has_location: bool
def __eq__(self, other: object) -> bool: ...
__hash__: ClassVar[None] # type: ignore[assignment]
class BuiltinImporter(importlib.abc.MetaPathFinder, importlib.abc.InspectLoader):
# MetaPathFinder

View File

@@ -112,7 +112,7 @@ class BufferedRandom(BufferedIOBase, _BufferedIOBase, BinaryIO): # type: ignore
def truncate(self, pos: int | None = None, /) -> int: ...
class BufferedRWPair(BufferedIOBase, _BufferedIOBase):
def __init__(self, reader: RawIOBase, writer: RawIOBase, buffer_size: int = 8192, /) -> None: ...
def __init__(self, reader: RawIOBase, writer: RawIOBase, buffer_size: int = 8192) -> None: ...
def peek(self, size: int = 0, /) -> bytes: ...
class _TextIOBase(_IOBase):

View File

@@ -1,6 +1,5 @@
from collections.abc import Callable
from typing import Any, final
from typing_extensions import Self
@final
class make_encoder:
@@ -20,8 +19,8 @@ class make_encoder:
def encoder(self) -> Callable[[str], str]: ...
@property
def item_separator(self) -> str: ...
def __new__(
cls,
def __init__(
self,
markers: dict[int, Any] | None,
default: Callable[[Any], Any],
encoder: Callable[[str], str],
@@ -31,7 +30,7 @@ class make_encoder:
sort_keys: bool,
skipkeys: bool,
allow_nan: bool,
) -> Self: ...
) -> None: ...
def __call__(self, obj: object, _current_indent_level: int) -> Any: ...
@final
@@ -43,7 +42,7 @@ class make_scanner:
parse_float: Any
strict: bool
# TODO: 'context' needs the attrs above (ducktype), but not __call__.
def __new__(cls, context: make_scanner) -> Self: ...
def __init__(self, context: make_scanner) -> None: ...
def __call__(self, string: str, index: int) -> tuple[Any, int]: ...
def encode_basestring(s: str, /) -> str: ...

View File

@@ -1,8 +1,7 @@
import sys
from _typeshed import ReadableBuffer
from collections.abc import Mapping, Sequence
from typing import Any, Final, final
from typing_extensions import Self, TypeAlias
from typing_extensions import TypeAlias
_FilterChain: TypeAlias = Sequence[Mapping[str, Any]]
@@ -37,11 +36,7 @@ PRESET_EXTREME: int # v big number
@final
class LZMADecompressor:
if sys.version_info >= (3, 12):
def __new__(cls, format: int | None = ..., memlimit: int | None = ..., filters: _FilterChain | None = ...) -> Self: ...
else:
def __init__(self, format: int | None = ..., memlimit: int | None = ..., filters: _FilterChain | None = ...) -> None: ...
def __init__(self, format: int | None = ..., memlimit: int | None = ..., filters: _FilterChain | None = ...) -> None: ...
def decompress(self, data: ReadableBuffer, max_length: int = -1) -> bytes: ...
@property
def check(self) -> int: ...
@@ -54,15 +49,9 @@ class LZMADecompressor:
@final
class LZMACompressor:
if sys.version_info >= (3, 12):
def __new__(
cls, format: int | None = ..., check: int = ..., preset: int | None = ..., filters: _FilterChain | None = ...
) -> Self: ...
else:
def __init__(
self, format: int | None = ..., check: int = ..., preset: int | None = ..., filters: _FilterChain | None = ...
) -> None: ...
def __init__(
self, format: int | None = ..., check: int = ..., preset: int | None = ..., filters: _FilterChain | None = ...
) -> None: ...
def compress(self, data: ReadableBuffer, /) -> bytes: ...
def flush(self) -> bytes: ...

Some files were not shown because too many files have changed in this diff Show More