Compare commits
3 Commits
micha/perf
...
micha/erro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6b2544993 | ||
|
|
fe78d50560 | ||
|
|
b39def2915 |
20
.github/workflows/build-binaries.yml
vendored
20
.github/workflows/build-binaries.yml
vendored
@@ -53,9 +53,9 @@ jobs:
|
||||
args: --out dist
|
||||
- name: "Test sdist"
|
||||
run: |
|
||||
pip install dist/${PACKAGE_NAME}-*.tar.gz --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
|
||||
${{ env.MODULE_NAME }} --help
|
||||
python -m ${{ env.MODULE_NAME }} --help
|
||||
- name: "Upload sdist"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel - aarch64"
|
||||
run: |
|
||||
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
@@ -186,9 +186,9 @@ jobs:
|
||||
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
${{ env.MODULE_NAME }} --help
|
||||
python -m ${{ env.MODULE_NAME }} --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -236,9 +236,9 @@ jobs:
|
||||
- name: "Test wheel"
|
||||
if: ${{ startsWith(matrix.target, 'x86_64') }}
|
||||
run: |
|
||||
pip install dist/${PACKAGE_NAME}-*.whl --force-reinstall
|
||||
"${MODULE_NAME}" --help
|
||||
python -m "${MODULE_NAME}" --help
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
${{ env.MODULE_NAME }} --help
|
||||
python -m ${{ env.MODULE_NAME }} --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
15
.github/workflows/build-docker.yml
vendored
15
.github/workflows/build-docker.yml
vendored
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Normalize Platform Pair (replace / with -)
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_TUPLE=${platform//\//-}" >> "$GITHUB_ENV"
|
||||
echo "PLATFORM_TUPLE=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
# Adapted from https://docs.docker.com/build/ci/github-actions/multi-platform/
|
||||
- name: Build and push by digest
|
||||
@@ -87,10 +87,9 @@ jobs:
|
||||
outputs: type=image,name=${{ env.RUFF_BASE_IMG }},push-by-digest=true,name-canonical=true,push=${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
|
||||
- name: Export digests
|
||||
env:
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digests
|
||||
@@ -144,7 +143,7 @@ jobs:
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)
|
||||
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
|
||||
|
||||
docker-publish-extra:
|
||||
name: Publish additional Docker image based on ${{ matrix.image-mapping }}
|
||||
@@ -183,7 +182,7 @@ jobs:
|
||||
# Generate Dockerfile content
|
||||
cat <<EOF > Dockerfile
|
||||
FROM ${BASE_IMAGE}
|
||||
COPY --from=${RUFF_BASE_IMG}:latest /ruff /usr/local/bin/ruff
|
||||
COPY --from=${{ env.RUFF_BASE_IMG }}:latest /ruff /usr/local/bin/ruff
|
||||
ENTRYPOINT []
|
||||
CMD ["/usr/local/bin/ruff"]
|
||||
EOF
|
||||
@@ -203,14 +202,14 @@ jobs:
|
||||
TAG_PATTERNS="${TAG_PATTERNS%\\n}"
|
||||
|
||||
# Export image cache name
|
||||
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> "$GITHUB_ENV"
|
||||
echo "IMAGE_REF=${BASE_IMAGE//:/-}" >> $GITHUB_ENV
|
||||
|
||||
# Export tag patterns using the multiline env var syntax
|
||||
{
|
||||
echo "TAG_PATTERNS<<EOF"
|
||||
echo -e "${TAG_PATTERNS}"
|
||||
echo EOF
|
||||
} >> "$GITHUB_ENV"
|
||||
} >> $GITHUB_ENV
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
@@ -289,4 +288,4 @@ jobs:
|
||||
docker buildx imagetools create \
|
||||
"${annotations[@]}" \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf "${RUFF_BASE_IMG}@sha256:%s " *)
|
||||
$(printf '${{ env.RUFF_BASE_IMG }}@sha256:%s ' *)
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -312,7 +312,7 @@ jobs:
|
||||
name: "cargo fuzz build"
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine_changes
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
28
.github/workflows/publish-docs.yml
vendored
28
.github/workflows/publish-docs.yml
vendored
@@ -49,11 +49,13 @@ jobs:
|
||||
|
||||
- name: "Set branch name"
|
||||
run: |
|
||||
version="${{ env.version }}"
|
||||
display_name="${{ env.display_name }}"
|
||||
timestamp="$(date +%s)"
|
||||
|
||||
# create branch_display_name from display_name by replacing all
|
||||
# characters disallowed in git branch names with hyphens
|
||||
branch_display_name="$(echo "${display_name}" | tr -c '[:alnum:]._' '-' | tr -s '-')"
|
||||
branch_display_name="$(echo "$display_name" | tr -c '[:alnum:]._' '-' | tr -s '-')"
|
||||
|
||||
echo "branch_name=update-docs-$branch_display_name-$timestamp" >> $GITHUB_ENV
|
||||
echo "timestamp=$timestamp" >> $GITHUB_ENV
|
||||
@@ -91,7 +93,9 @@ jobs:
|
||||
run: mkdocs build --strict -f mkdocs.public.yml
|
||||
|
||||
- name: "Clone docs repo"
|
||||
run: git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
|
||||
run: |
|
||||
version="${{ env.version }}"
|
||||
git clone https://${{ secrets.ASTRAL_DOCS_PAT }}@github.com/astral-sh/docs.git astral-docs
|
||||
|
||||
- name: "Copy docs"
|
||||
run: rm -rf astral-docs/site/ruff && mkdir -p astral-docs/site && cp -r site/ruff astral-docs/site/
|
||||
@@ -99,10 +103,12 @@ jobs:
|
||||
- name: "Commit docs"
|
||||
working-directory: astral-docs
|
||||
run: |
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
git config user.name "astral-docs-bot"
|
||||
git config user.email "176161322+astral-docs-bot@users.noreply.github.com"
|
||||
|
||||
git checkout -b "${branch_name}"
|
||||
git checkout -b $branch_name
|
||||
git add site/ruff
|
||||
git commit -m "Update ruff documentation for $version"
|
||||
|
||||
@@ -111,8 +117,12 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
|
||||
run: |
|
||||
version="${{ env.version }}"
|
||||
display_name="${{ env.display_name }}"
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
# set the PR title
|
||||
pull_request_title="Update ruff documentation for "${display_name}""
|
||||
pull_request_title="Update ruff documentation for $display_name"
|
||||
|
||||
# Delete any existing pull requests that are open for this version
|
||||
# by checking against pull_request_title because the new PR will
|
||||
@@ -121,12 +131,12 @@ jobs:
|
||||
xargs -I {} gh pr close {}
|
||||
|
||||
# push the branch to GitHub
|
||||
git push origin "${branch_name}"
|
||||
git push origin $branch_name
|
||||
|
||||
# create the PR
|
||||
gh pr create --base main --head "${branch_name}" \
|
||||
gh pr create --base main --head $branch_name \
|
||||
--title "$pull_request_title" \
|
||||
--body "Automated documentation update for "${display_name}"" \
|
||||
--body "Automated documentation update for $display_name" \
|
||||
--label "documentation"
|
||||
|
||||
- name: "Merge Pull Request"
|
||||
@@ -135,7 +145,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
|
||||
run: |
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
# auto-merge the PR if the build was triggered by a release. Manual builds should be reviewed by a human.
|
||||
# give the PR a few seconds to be created before trying to auto-merge it
|
||||
sleep 10
|
||||
gh pr merge --squash "${branch_name}"
|
||||
gh pr merge --squash $branch_name
|
||||
|
||||
2
.github/workflows/sync_typeshed.yaml
vendored
2
.github/workflows/sync_typeshed.yaml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
repository: python/typeshed
|
||||
path: typeshed
|
||||
persist-credentials: false
|
||||
persist-credentials: true
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
@@ -95,6 +95,8 @@ repos:
|
||||
# `release.yml` is autogenerated by `dist`; security issues need to be fixed there
|
||||
# (https://opensource.axo.dev/cargo-dist/)
|
||||
exclude: .github/workflows/release.yml
|
||||
# We could consider enabling the low-severity warnings, but they're noisy
|
||||
args: [--min-severity=medium]
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.30.0
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,40 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- Fix fstring formatting removing overlong implicit concatenated string in expression part ([#14811](https://github.com/astral-sh/ruff/pull/14811))
|
||||
- \[`airflow`\] Add fix to remove deprecated keyword arguments (`AIR302`) ([#14887](https://github.com/astral-sh/ruff/pull/14887))
|
||||
- \[`airflow`\]: Extend rule to include deprecated names for Airflow 3.0 (`AIR302`) ([#14765](https://github.com/astral-sh/ruff/pull/14765) and [#14804](https://github.com/astral-sh/ruff/pull/14804))
|
||||
- \[`flake8-bugbear`\] Improve error messages for `except*` (`B025`, `B029`, `B030`, `B904`) ([#14815](https://github.com/astral-sh/ruff/pull/14815))
|
||||
- \[`flake8-bugbear`\] `itertools.batched()` without explicit `strict` (`B911`) ([#14408](https://github.com/astral-sh/ruff/pull/14408))
|
||||
- \[`flake8-use-pathlib`\] Dotless suffix passed to `Path.with_suffix()` (`PTH210`) ([#14779](https://github.com/astral-sh/ruff/pull/14779))
|
||||
- \[`pylint`\] Include parentheses and multiple comparators in check for `boolean-chained-comparison` (`PLR1716`) ([#14781](https://github.com/astral-sh/ruff/pull/14781))
|
||||
- \[`ruff`\] Do not simplify `round()` calls (`RUF046`) ([#14832](https://github.com/astral-sh/ruff/pull/14832))
|
||||
- \[`ruff`\] Don't emit `used-dummy-variable` on function parameters (`RUF052`) ([#14818](https://github.com/astral-sh/ruff/pull/14818))
|
||||
- \[`ruff`\] Implement `if-key-in-dict-del` (`RUF051`) ([#14553](https://github.com/astral-sh/ruff/pull/14553))
|
||||
- \[`ruff`\] Mark autofix for `RUF052` as always unsafe ([#14824](https://github.com/astral-sh/ruff/pull/14824))
|
||||
- \[`ruff`\] Teach autofix for `used-dummy-variable` about TypeVars etc. (`RUF052`) ([#14819](https://github.com/astral-sh/ruff/pull/14819))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bugbear`\] Offer unsafe autofix for `no-explicit-stacklevel` (`B028`) ([#14829](https://github.com/astral-sh/ruff/pull/14829))
|
||||
- \[`flake8-pyi`\] Skip all type definitions in `string-or-bytes-too-long` (`PYI053`) ([#14797](https://github.com/astral-sh/ruff/pull/14797))
|
||||
- \[`pyupgrade`\] Do not report when a UTF-8 comment is followed by a non-UTF-8 one (`UP009`) ([#14728](https://github.com/astral-sh/ruff/pull/14728))
|
||||
- \[`pyupgrade`\] Mark fixes for `convert-typed-dict-functional-to-class` and `convert-named-tuple-functional-to-class` as unsafe if they will remove comments (`UP013`, `UP014`) ([#14842](https://github.com/astral-sh/ruff/pull/14842))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Raise syntax error for mixing `except` and `except*` ([#14895](https://github.com/astral-sh/ruff/pull/14895))
|
||||
- \[`flake8-bugbear`\] Fix `B028` to allow `stacklevel` to be explicitly assigned as a positional argument ([#14868](https://github.com/astral-sh/ruff/pull/14868))
|
||||
- \[`flake8-bugbear`\] Skip `B028` if `warnings.warn` is called with `*args` or `**kwargs` ([#14870](https://github.com/astral-sh/ruff/pull/14870))
|
||||
- \[`flake8-comprehensions`\] Skip iterables with named expressions in `unnecessary-map` (`C417`) ([#14827](https://github.com/astral-sh/ruff/pull/14827))
|
||||
- \[`flake8-pyi`\] Also remove `self` and `cls`'s annotation (`PYI034`) ([#14801](https://github.com/astral-sh/ruff/pull/14801))
|
||||
- \[`flake8-pytest-style`\] Fix `pytest-parametrize-names-wrong-type` (`PT006`) to edit both `argnames` and `argvalues` if both of them are single-element tuples/lists ([#14699](https://github.com/astral-sh/ruff/pull/14699))
|
||||
- \[`perflint`\] Improve autofix for `PERF401` ([#14369](https://github.com/astral-sh/ruff/pull/14369))
|
||||
- \[`pylint`\] Fix `PLW1508` false positive for default string created via a mult operation ([#14841](https://github.com/astral-sh/ruff/pull/14841))
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Preview features
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2517,7 +2517,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.3"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2736,7 +2736,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.8.3"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -3051,7 +3051,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.8.3"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -140,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.8.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.3/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.8.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.2/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -174,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.8.3
|
||||
rev: v0.8.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -5,7 +5,6 @@ use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use python_version::PythonVersion;
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
@@ -16,11 +15,12 @@ use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use target_version::TargetVersion;
|
||||
|
||||
use crate::logging::{setup_tracing, Verbosity};
|
||||
|
||||
mod logging;
|
||||
mod python_version;
|
||||
mod target_version;
|
||||
mod verbosity;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -34,39 +34,54 @@ struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Option<Command>,
|
||||
|
||||
/// Run the command within the given project directory.
|
||||
///
|
||||
/// All `pyproject.toml` files will be discovered by walking up the directory tree from the given project directory,
|
||||
/// as will the project's virtual environment (`.venv`) unless the `venv-path` option is set.
|
||||
///
|
||||
/// Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.
|
||||
#[arg(long, value_name = "PROJECT")]
|
||||
project: Option<SystemPathBuf>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Changes the current working directory.",
|
||||
long_help = "Changes the current working directory before any specified operations. This affects the workspace and configuration discovery.",
|
||||
value_name = "PATH"
|
||||
)]
|
||||
current_directory: Option<SystemPathBuf>,
|
||||
|
||||
/// Path to the virtual environment the project uses.
|
||||
///
|
||||
/// If provided, red-knot will use the `site-packages` directory of this virtual environment
|
||||
/// to resolve type information for the project's third-party dependencies.
|
||||
#[arg(long, value_name = "PATH")]
|
||||
#[arg(
|
||||
long,
|
||||
help = "Path to the virtual environment the project uses",
|
||||
long_help = "\
|
||||
Path to the virtual environment the project uses. \
|
||||
If provided, red-knot will use the `site-packages` directory of this virtual environment \
|
||||
to resolve type information for the project's third-party dependencies.",
|
||||
value_name = "PATH"
|
||||
)]
|
||||
venv_path: Option<SystemPathBuf>,
|
||||
|
||||
/// Custom directory to use for stdlib typeshed stubs.
|
||||
#[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")]
|
||||
typeshed: Option<SystemPathBuf>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIRECTORY",
|
||||
help = "Custom directory to use for stdlib typeshed stubs"
|
||||
)]
|
||||
custom_typeshed_dir: Option<SystemPathBuf>,
|
||||
|
||||
/// Additional path to use as a module-resolution source (can be passed multiple times).
|
||||
#[arg(long, value_name = "PATH")]
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PATH",
|
||||
help = "Additional path to use as a module-resolution source (can be passed multiple times)"
|
||||
)]
|
||||
extra_search_path: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
/// Python version to assume when resolving types.
|
||||
#[arg(long, value_name = "VERSION", alias = "target-version")]
|
||||
python_version: Option<PythonVersion>,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Python version to assume when resolving types",
|
||||
value_name = "VERSION"
|
||||
)]
|
||||
target_version: Option<TargetVersion>,
|
||||
|
||||
#[clap(flatten)]
|
||||
verbosity: Verbosity,
|
||||
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[arg(long, short = 'W')]
|
||||
#[arg(
|
||||
long,
|
||||
help = "Run in watch mode by re-running whenever files change",
|
||||
short = 'W'
|
||||
)]
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
@@ -74,8 +89,8 @@ impl Args {
|
||||
fn to_configuration(&self, cli_cwd: &SystemPath) -> Configuration {
|
||||
let mut configuration = Configuration::default();
|
||||
|
||||
if let Some(python_version) = self.python_version {
|
||||
configuration.python_version = Some(python_version.into());
|
||||
if let Some(target_version) = self.target_version {
|
||||
configuration.target_version = Some(target_version.into());
|
||||
}
|
||||
|
||||
if let Some(venv_path) = &self.venv_path {
|
||||
@@ -84,8 +99,9 @@ impl Args {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(typeshed) = &self.typeshed {
|
||||
configuration.search_paths.typeshed = Some(SystemPath::absolute(typeshed, cli_cwd));
|
||||
if let Some(custom_typeshed_dir) = &self.custom_typeshed_dir {
|
||||
configuration.search_paths.custom_typeshed =
|
||||
Some(SystemPath::absolute(custom_typeshed_dir, cli_cwd));
|
||||
}
|
||||
|
||||
if let Some(extra_search_paths) = &self.extra_search_path {
|
||||
@@ -151,13 +167,15 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
};
|
||||
|
||||
let cwd = args
|
||||
.project
|
||||
.current_directory
|
||||
.as_ref()
|
||||
.map(|cwd| {
|
||||
if cwd.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(cwd, &cli_base_path))
|
||||
} else {
|
||||
Err(anyhow!("Provided project path `{cwd}` is not a directory"))
|
||||
Err(anyhow!(
|
||||
"Provided current-directory path `{cwd}` is not a directory"
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum PythonVersion {
|
||||
#[value(name = "3.7")]
|
||||
Py37,
|
||||
#[value(name = "3.8")]
|
||||
Py38,
|
||||
#[default]
|
||||
#[value(name = "3.9")]
|
||||
Py39,
|
||||
#[value(name = "3.10")]
|
||||
Py310,
|
||||
#[value(name = "3.11")]
|
||||
Py311,
|
||||
#[value(name = "3.12")]
|
||||
Py312,
|
||||
#[value(name = "3.13")]
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "3.7",
|
||||
Self::Py38 => "3.8",
|
||||
Self::Py39 => "3.9",
|
||||
Self::Py310 => "3.10",
|
||||
Self::Py311 => "3.11",
|
||||
Self::Py312 => "3.12",
|
||||
Self::Py313 => "3.13",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PythonVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: PythonVersion) -> Self {
|
||||
match value {
|
||||
PythonVersion::Py37 => Self::PY37,
|
||||
PythonVersion::Py38 => Self::PY38,
|
||||
PythonVersion::Py39 => Self::PY39,
|
||||
PythonVersion::Py310 => Self::PY310,
|
||||
PythonVersion::Py311 => Self::PY311,
|
||||
PythonVersion::Py312 => Self::PY312,
|
||||
PythonVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
|
||||
red_knot_python_semantic::PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
62
crates/red_knot/src/target_version.rs
Normal file
62
crates/red_knot/src/target_version.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
Py38,
|
||||
#[default]
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl TargetVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "py37",
|
||||
Self::Py38 => "py38",
|
||||
Self::Py39 => "py39",
|
||||
Self::Py310 => "py310",
|
||||
Self::Py311 => "py311",
|
||||
Self::Py312 => "py312",
|
||||
Self::Py313 => "py313",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::PY37,
|
||||
TargetVersion::Py38 => Self::PY38,
|
||||
TargetVersion::Py39 => Self::PY39,
|
||||
TargetVersion::Py310 => Self::PY310,
|
||||
TargetVersion::Py311 => Self::PY311,
|
||||
TargetVersion::Py312 => Self::PY312,
|
||||
TargetVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::target_version::TargetVersion;
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
|
||||
#[test]
|
||||
fn same_default_as_python_version() {
|
||||
assert_eq!(
|
||||
PythonVersion::from(TargetVersion::default()),
|
||||
PythonVersion::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -282,7 +282,7 @@ where
|
||||
.extra_paths
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(search_paths.typeshed.iter())
|
||||
.chain(search_paths.custom_typeshed.iter())
|
||||
.chain(search_paths.site_packages.iter().flat_map(|site_packages| {
|
||||
if let SitePackages::Known(path) = site_packages {
|
||||
path.as_slice()
|
||||
@@ -296,7 +296,7 @@ where
|
||||
}
|
||||
|
||||
let configuration = Configuration {
|
||||
python_version: Some(PythonVersion::PY312),
|
||||
target_version: Some(PythonVersion::PY312),
|
||||
search_paths,
|
||||
};
|
||||
|
||||
@@ -888,7 +888,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
},
|
||||
|root_path, _workspace_path| SearchPathConfiguration {
|
||||
typeshed: Some(root_path.join("typeshed")),
|
||||
custom_typeshed: Some(root_path.join("typeshed")),
|
||||
..SearchPathConfiguration::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -53,9 +53,5 @@ tempfile = { workspace = true }
|
||||
quickcheck = { version = "1.0.3", default-features = false }
|
||||
quickcheck_macros = { version = "1.0.0" }
|
||||
|
||||
[features]
|
||||
serde = ["ruff_db/serde", "dep:serde"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# `Annotated`
|
||||
|
||||
`Annotated` attaches arbitrary metadata to a given type.
|
||||
|
||||
## Usages
|
||||
|
||||
`Annotated[T, ...]` is equivalent to `T`: All metadata arguments are simply ignored.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
def _(x: Annotated[int, "foo"]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[int, lambda: 0 + 1 * 2 // 3, _(4)]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[int, "arbitrary", "metadata", "elements", "are", "fine"]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[tuple[str, int], bytes]):
|
||||
reveal_type(x) # revealed: tuple[str, int]
|
||||
```
|
||||
|
||||
## Parameterization
|
||||
|
||||
It is invalid to parameterize `Annotated` with less than two arguments.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
def _(x: Annotated):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
X = Annotated
|
||||
else:
|
||||
X = bool
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
def f(y: X):
|
||||
reveal_type(y) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression"
|
||||
def _(x: Annotated | bool):
|
||||
reveal_type(x) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[()]):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[int]):
|
||||
# `Annotated[T]` is invalid and will raise an error at runtime,
|
||||
# but we treat it the same as `T` to provide better diagnostics later on.
|
||||
# The subscription itself is still reported, regardless.
|
||||
# Same for the `(int,)` form below.
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[(int,)]):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
### Correctly parameterized
|
||||
|
||||
Inheriting from `Annotated[T, ...]` is equivalent to inheriting from `T` itself.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# TODO: False positive
|
||||
# error: [invalid-base]
|
||||
class C(Annotated[int, "foo"]): ...
|
||||
|
||||
# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
### Not parameterized
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# At runtime, this is an error.
|
||||
# error: [invalid-base]
|
||||
class C(Annotated): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
```
|
||||
@@ -34,7 +34,8 @@ If you define your own class named `Any`, using that in a type expression refers
|
||||
isn't a spelling of the Any type.
|
||||
|
||||
```py
|
||||
class Any: ...
|
||||
class Any:
|
||||
pass
|
||||
|
||||
x: Any
|
||||
|
||||
@@ -58,7 +59,8 @@ assignable to `int`.
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Subclass(Any): ...
|
||||
class Subclass(Any):
|
||||
pass
|
||||
|
||||
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
||||
|
||||
@@ -66,18 +68,8 @@ x: Subclass = 1 # error: [invalid-assignment]
|
||||
# TODO: no diagnostic
|
||||
y: int = Subclass() # error: [invalid-assignment]
|
||||
|
||||
def _(s: Subclass):
|
||||
reveal_type(s) # revealed: Subclass
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
`Any` cannot be parameterized:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
# error: [invalid-type-form] "Type `typing.Any` expected no type parameter"
|
||||
def f(x: Any[int]):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
def f() -> Subclass:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Subclass
|
||||
```
|
||||
|
||||
@@ -27,19 +27,19 @@ def f():
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-type-form]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-literal-parameter]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-literal-parameter]
|
||||
```
|
||||
|
||||
### Parameterized
|
||||
### Parametrized
|
||||
|
||||
`LiteralString` cannot be parameterized.
|
||||
`LiteralString` cannot be parametrized.
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
a: LiteralString[str] # error: [invalid-type-form]
|
||||
b: LiteralString["foo"] # error: [invalid-type-form]
|
||||
a: LiteralString[str] # error: [invalid-type-parameter]
|
||||
b: LiteralString["foo"] # error: [invalid-type-parameter]
|
||||
```
|
||||
|
||||
### As a base class
|
||||
@@ -89,26 +89,28 @@ vice versa.
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def _(flag: bool):
|
||||
foo_1: Literal["foo"] = "foo"
|
||||
bar_1: LiteralString = foo_1 # fine
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
foo_2 = "foo" if flag else "bar"
|
||||
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
|
||||
bar_2: LiteralString = foo_2 # fine
|
||||
foo_1: Literal["foo"] = "foo"
|
||||
bar_1: LiteralString = foo_1 # fine
|
||||
|
||||
foo_3: LiteralString = "foo" * 1_000_000_000
|
||||
bar_3: str = foo_2 # fine
|
||||
foo_2 = "foo" if coinflip() else "bar"
|
||||
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
|
||||
bar_2: LiteralString = foo_2 # fine
|
||||
|
||||
baz_1: str = str()
|
||||
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
|
||||
foo_3: LiteralString = "foo" * 1_000_000_000
|
||||
bar_3: str = foo_2 # fine
|
||||
|
||||
baz_2: LiteralString = "baz" * 1_000_000_000
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
baz_1: str = str()
|
||||
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
|
||||
|
||||
baz_3 = "foo" if flag else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
baz_2: LiteralString = "baz" * 1_000_000_000
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
|
||||
baz_3 = "foo" if coinflip() else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo"] | Literal[1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### Narrowing
|
||||
@@ -135,7 +137,7 @@ if "" < lorem == "ipsum":
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -21,7 +21,7 @@ reveal_type(stop())
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-form] "Type `typing.Never` expected no type parameter"
|
||||
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
a2: Never
|
||||
@@ -51,7 +51,7 @@ def f():
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# Typing-module aliases to other stdlib classes
|
||||
|
||||
The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but
|
||||
still need to be supported by a type checker.
|
||||
|
||||
## Currently unsupported
|
||||
|
||||
Support for most of these symbols is currently a TODO:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
def f(
|
||||
a: typing.List,
|
||||
b: typing.List[int],
|
||||
c: typing.Dict,
|
||||
d: typing.Dict[int, str],
|
||||
e: typing.DefaultDict,
|
||||
f: typing.DefaultDict[str, int],
|
||||
g: typing.Set,
|
||||
h: typing.Set[int],
|
||||
i: typing.FrozenSet,
|
||||
j: typing.FrozenSet[str],
|
||||
k: typing.OrderedDict,
|
||||
l: typing.OrderedDict[int, str],
|
||||
m: typing.Counter,
|
||||
n: typing.Counter[int],
|
||||
):
|
||||
reveal_type(a) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(b) # revealed: @Todo(typing.List alias)
|
||||
reveal_type(c) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(d) # revealed: @Todo(typing.Dict alias)
|
||||
reveal_type(e) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(f) # revealed: @Todo(typing.DefaultDict[] alias)
|
||||
reveal_type(g) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(h) # revealed: @Todo(typing.Set alias)
|
||||
reveal_type(i) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(j) # revealed: @Todo(typing.FrozenSet alias)
|
||||
reveal_type(k) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(l) # revealed: @Todo(typing.OrderedDict alias)
|
||||
reveal_type(m) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
reveal_type(n) # revealed: @Todo(typing.Counter[] alias)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
The aliases can be inherited from. Some of these are still partially or wholly TODOs.
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
class A(typing.Dict): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[dict], Unknown, Literal[object]]
|
||||
|
||||
class B(typing.List): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[list], Unknown, Literal[object]]
|
||||
|
||||
class C(typing.Set): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[set], Unknown, Literal[object]]
|
||||
|
||||
class D(typing.FrozenSet): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[frozenset], Unknown, Literal[object]]
|
||||
|
||||
class E(typing.DefaultDict): ...
|
||||
|
||||
reveal_type(E.__mro__) # revealed: tuple[Literal[E], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
|
||||
class F(typing.OrderedDict): ...
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
|
||||
class G(typing.Counter): ...
|
||||
|
||||
reveal_type(G.__mro__) # revealed: tuple[Literal[G], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
```
|
||||
@@ -3,56 +3,75 @@
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f(v: "int"):
|
||||
reveal_type(v) # revealed: int
|
||||
def f() -> "int":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
```py
|
||||
def f(v: "'int'"):
|
||||
reveal_type(v) # revealed: int
|
||||
def f() -> "'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Type expression
|
||||
|
||||
```py
|
||||
def f1(v: "int | str", w: "tuple[int, str]"):
|
||||
reveal_type(v) # revealed: int | str
|
||||
reveal_type(w) # revealed: tuple[int, str]
|
||||
def f1() -> "int | str":
|
||||
return 1
|
||||
|
||||
def f2() -> "tuple[int, str]":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: int | str
|
||||
reveal_type(f2()) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Partial
|
||||
|
||||
```py
|
||||
def f(v: tuple[int, "str"]):
|
||||
reveal_type(v) # revealed: tuple[int, str]
|
||||
def f() -> tuple[int, "str"]:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Deferred
|
||||
|
||||
```py
|
||||
def f(v: "Foo"):
|
||||
reveal_type(v) # revealed: Foo
|
||||
def f() -> "Foo":
|
||||
return Foo()
|
||||
|
||||
class Foo: ...
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred (undefined)
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
def f(v: "Foo"):
|
||||
reveal_type(v) # revealed: Unknown
|
||||
def f() -> "Foo":
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Partial deferred
|
||||
|
||||
```py
|
||||
def f(v: int | "Foo"):
|
||||
reveal_type(v) # revealed: int | Foo
|
||||
def f() -> int | "Foo":
|
||||
return 1
|
||||
|
||||
class Foo: ...
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: int | Foo
|
||||
```
|
||||
|
||||
## `typing.Literal`
|
||||
@@ -60,43 +79,65 @@ class Foo: ...
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'):
|
||||
reveal_type(v) # revealed: Literal["Foo", "Bar"]
|
||||
reveal_type(w) # revealed: Literal["Foo", "Bar"]
|
||||
def f1() -> Literal["Foo", "Bar"]:
|
||||
return "Foo"
|
||||
|
||||
class Foo: ...
|
||||
def f2() -> 'Literal["Foo", "Bar"]':
|
||||
return "Foo"
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
reveal_type(f1()) # revealed: Literal["Foo", "Bar"]
|
||||
reveal_type(f2()) # revealed: Literal["Foo", "Bar"]
|
||||
```
|
||||
|
||||
## Various string kinds
|
||||
|
||||
```py
|
||||
def f1(
|
||||
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
|
||||
a: r"int",
|
||||
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
|
||||
b: f"int",
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
c: b"int",
|
||||
d: "int",
|
||||
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
|
||||
e: "in" "t",
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
f: "\N{LATIN SMALL LETTER I}nt",
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
g: "\x69nt",
|
||||
h: """int""",
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
i: "b'int'",
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: int
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
reveal_type(h) # revealed: int
|
||||
reveal_type(i) # revealed: Unknown
|
||||
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
|
||||
def f1() -> r"int":
|
||||
return 1
|
||||
|
||||
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
|
||||
def f2() -> f"int":
|
||||
return 1
|
||||
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
def f3() -> b"int":
|
||||
return 1
|
||||
|
||||
def f4() -> "int":
|
||||
return 1
|
||||
|
||||
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
|
||||
def f5() -> "in" "t":
|
||||
return 1
|
||||
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
def f6() -> "\N{LATIN SMALL LETTER I}nt":
|
||||
return 1
|
||||
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
def f7() -> "\x69nt":
|
||||
return 1
|
||||
|
||||
def f8() -> """int""":
|
||||
return 1
|
||||
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
def f9() -> "b'int'":
|
||||
return 1
|
||||
|
||||
reveal_type(f1()) # revealed: Unknown
|
||||
reveal_type(f2()) # revealed: Unknown
|
||||
reveal_type(f3()) # revealed: Unknown
|
||||
reveal_type(f4()) # revealed: int
|
||||
reveal_type(f5()) # revealed: Unknown
|
||||
reveal_type(f6()) # revealed: Unknown
|
||||
reveal_type(f7()) # revealed: Unknown
|
||||
reveal_type(f8()) # revealed: int
|
||||
reveal_type(f9()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Various string kinds in `typing.Literal`
|
||||
@@ -104,8 +145,10 @@ def f1(
|
||||
```py
|
||||
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", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
def f() -> Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]:
|
||||
return "normal"
|
||||
|
||||
reveal_type(f()) # revealed: Literal["a", "b", "de", "f", "g", "h"] | Literal[b"c"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
@@ -132,7 +175,8 @@ c: "Foo"
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
|
||||
d: "Foo" = 1
|
||||
|
||||
class Foo: ...
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
c = Foo()
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# Unsupported special forms
|
||||
|
||||
## Not yet supported
|
||||
|
||||
Several special forms are unsupported by red-knot currently. However, we also don't emit
|
||||
false-positive errors if you use one in an annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Self, TypeVarTuple, Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, TypeAlias, Callable, TypeVar
|
||||
|
||||
P = ParamSpec("P")
|
||||
Ts = TypeVarTuple("Ts")
|
||||
R_co = TypeVar("R_co", covariant=True)
|
||||
|
||||
Alias: TypeAlias = int
|
||||
|
||||
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
reveal_type(Alias) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def h() -> TypeIs[int]: ...
|
||||
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(kwargs) # revealed: dict
|
||||
|
||||
return callback(42, *args, **kwargs)
|
||||
|
||||
class Foo:
|
||||
def method(self, x: Self):
|
||||
reveal_type(x) # revealed: @Todo(Unsupported or invalid type in a type expression)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
You can't inherit from most of these. `typing.Callable` is an exception.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
|
||||
|
||||
class A(Self): ... # error: [invalid-base]
|
||||
class B(Unpack): ... # error: [invalid-base]
|
||||
class C(TypeGuard): ... # error: [invalid-base]
|
||||
class D(TypeIs): ... # error: [invalid-base]
|
||||
class E(Concatenate): ... # error: [invalid-base]
|
||||
class F(Callable): ...
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for more typing aliases as base classes), Literal[object]]
|
||||
```
|
||||
|
||||
## Subscriptability
|
||||
|
||||
Some of these are not subscriptable:
|
||||
|
||||
```py
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
X: TypeAlias[T] = int # error: [invalid-type-form]
|
||||
|
||||
class Foo[T]:
|
||||
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
|
||||
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
|
||||
def method(self: Self[int]) -> Self[int]:
|
||||
reveal_type(self) # revealed: Unknown
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# Unsupported type qualifiers
|
||||
|
||||
## Not yet supported
|
||||
|
||||
Several type qualifiers are unsupported by red-knot currently. However, we also don't emit
|
||||
false-positive errors if you use one in an annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly, TypedDict
|
||||
|
||||
X: Final = 42
|
||||
Y: Final[int] = 42
|
||||
|
||||
class Foo:
|
||||
A: ClassVar[int] = 42
|
||||
|
||||
# TODO: `TypedDict` is actually valid as a base
|
||||
# error: [invalid-base]
|
||||
class Bar(TypedDict):
|
||||
x: Required[int]
|
||||
y: NotRequired[str]
|
||||
z: ReadOnly[bytes]
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
You can't inherit from a type qualifier.
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
||||
|
||||
class A(Final): ... # error: [invalid-base]
|
||||
class B(ClassVar): ... # error: [invalid-base]
|
||||
class C(Required): ... # error: [invalid-base]
|
||||
class D(NotRequired): ... # error: [invalid-base]
|
||||
class E(ReadOnly): ... # error: [invalid-base]
|
||||
```
|
||||
@@ -78,10 +78,20 @@ c: tuple[str | int, str] = ([], "foo")
|
||||
## PEP-604 annotations are supported
|
||||
|
||||
```py
|
||||
def foo(v: str | int | None, w: str | str | None, x: str | str):
|
||||
reveal_type(v) # revealed: str | int | None
|
||||
reveal_type(w) # revealed: str | None
|
||||
reveal_type(x) # revealed: str
|
||||
def foo() -> str | int | None:
|
||||
return None
|
||||
|
||||
reveal_type(foo()) # revealed: str | int | None
|
||||
|
||||
def bar() -> str | str | None:
|
||||
return None
|
||||
|
||||
reveal_type(bar()) # revealed: str | None
|
||||
|
||||
def baz() -> str | str:
|
||||
return "Hello, world!"
|
||||
|
||||
reveal_type(baz()) # revealed: str
|
||||
```
|
||||
|
||||
## Attribute expressions in type annotations are understood
|
||||
@@ -108,7 +118,8 @@ from __future__ import annotations
|
||||
|
||||
x: Foo
|
||||
|
||||
class Foo: ...
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
@@ -119,7 +130,8 @@ reveal_type(x) # revealed: Foo
|
||||
```pyi path=main.pyi
|
||||
x: Foo
|
||||
|
||||
class Foo: ...
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
|
||||
@@ -49,116 +49,134 @@ reveal_type(x) # revealed: int
|
||||
## Method union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
else:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
f = Foo()
|
||||
f += 12
|
||||
flag = bool_instance()
|
||||
|
||||
reveal_type(f) # revealed: str | int
|
||||
class Foo:
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
else:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | int
|
||||
```
|
||||
|
||||
## Partially bound `__iadd__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
f = Foo()
|
||||
class Foo:
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
f = Foo()
|
||||
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Partially bound with `__add__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: str) -> str:
|
||||
return "Hello, world!"
|
||||
if flag:
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
class Foo:
|
||||
def __add__(self, other: str) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(f) # revealed: int | str
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | str
|
||||
```
|
||||
|
||||
## Partially bound target union
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if flag1:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag2:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
if bool_instance():
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
## Target union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
flag = bool_instance()
|
||||
|
||||
reveal_type(f) # revealed: str | float
|
||||
class Foo:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | float
|
||||
```
|
||||
|
||||
## Partially bound target union with `__add__`
|
||||
|
||||
```py
|
||||
def f(flag: bool, flag2: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if flag:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
flag = bool_instance()
|
||||
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
if flag2:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
@@ -3,23 +3,27 @@
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
else:
|
||||
class C1:
|
||||
x = 2
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
x = 3
|
||||
else:
|
||||
class C1:
|
||||
x = 2
|
||||
x = 4
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
x = 3
|
||||
else:
|
||||
x = 4
|
||||
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
```
|
||||
|
||||
## Inherited attributes
|
||||
@@ -64,19 +68,24 @@ reveal_type(A.X) # revealed: Literal[42]
|
||||
In this example, the `x` attribute is not defined in the `C2` element of the union:
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class C1:
|
||||
x = 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C2: ...
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
class C2: ...
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
@@ -85,21 +94,26 @@ We raise the same diagnostic if the attribute is possibly-unbound in at least on
|
||||
union:
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag1: bool, flag2: bool):
|
||||
class C1:
|
||||
x = 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
x = 2
|
||||
class C1:
|
||||
x = 1
|
||||
|
||||
class C3:
|
||||
x = 3
|
||||
class C2:
|
||||
if bool_instance():
|
||||
x = 2
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
class C3:
|
||||
x = 3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
flag1 = bool_instance()
|
||||
flag2 = bool_instance()
|
||||
|
||||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## Unions with all paths unbound
|
||||
@@ -107,51 +121,16 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
||||
If the symbol is unbound in all elements of the union, we detect that:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class C1: ...
|
||||
class C2: ...
|
||||
C = C1 if flag else C2
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Objects of all types have a `__class__` method
|
||||
|
||||
```py
|
||||
import typing_extensions
|
||||
|
||||
reveal_type(typing_extensions.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
a = 42
|
||||
reveal_type(a.__class__) # revealed: Literal[int]
|
||||
|
||||
b = "42"
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
|
||||
c = b"42"
|
||||
reveal_type(c.__class__) # revealed: Literal[bytes]
|
||||
|
||||
d = True
|
||||
reveal_type(d.__class__) # revealed: Literal[bool]
|
||||
|
||||
e = (42, 42)
|
||||
reveal_type(e.__class__) # revealed: Literal[tuple]
|
||||
|
||||
def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
|
||||
reveal_type(a.__class__) # revealed: type[int]
|
||||
reveal_type(b.__class__) # revealed: Literal[str]
|
||||
reveal_type(c.__class__) # revealed: type[int] | type[str]
|
||||
|
||||
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
|
||||
# It would be incorrect to infer `Literal[type]` here,
|
||||
# as `c` could be some subclass of `str` with a custom metaclass.
|
||||
# All we know is that the metaclass must be a (non-strict) subclass of `type`.
|
||||
reveal_type(d.__class__) # revealed: type[type]
|
||||
|
||||
reveal_type(f.__class__) # revealed: Literal[FunctionType]
|
||||
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
class C1: ...
|
||||
class C2: ...
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
C = C1 if flag else C2
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[C1, C2]` has no attribute `x`"
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -281,12 +281,20 @@ reveal_type(42 + 4.2) # revealed: int
|
||||
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3 + 3j) # revealed: int
|
||||
|
||||
def _(x: bool, y: int):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: float
|
||||
def returns_int() -> int:
|
||||
return 42
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
x = returns_bool()
|
||||
y = returns_int()
|
||||
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
```
|
||||
|
||||
## With literal types
|
||||
|
||||
@@ -7,25 +7,29 @@ Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand
|
||||
evaluated.
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag or (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag and (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
if bool_instance() or (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if bool_instance() and (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## First expression is always evaluated
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if (x := 1) or flag:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if (x := 1) and flag:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
if (x := 1) or bool_instance():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if (x := 1) and bool_instance():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Statically known truthiness
|
||||
@@ -45,26 +49,30 @@ if True and (x := 1):
|
||||
## Later expressions can always use variables from earlier expressions
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
flag or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
# error: [unresolved-reference]
|
||||
flag or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# error: [unresolved-reference]
|
||||
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Nested expressions
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
if flag1 or ((x := 1) and flag2):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if ((y := 1) and flag1) or flag2:
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance() or ((x := 1) and bool_instance()):
|
||||
# error: [possibly-unresolved-reference]
|
||||
if (flag1 and (z := 1)) or reveal_type(z): # revealed: Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(z) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if ((y := 1) and bool_instance()) or bool_instance():
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(z) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -22,27 +22,29 @@ reveal_type(b) # revealed: Unknown
|
||||
## Possibly unbound `__call__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class PossiblyNotCallable:
|
||||
if flag:
|
||||
def __call__(self) -> int: ...
|
||||
def flag() -> bool: ...
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
class PossiblyNotCallable:
|
||||
if flag():
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound callable
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
def flag() -> bool: ...
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
if flag():
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-callable `__call__`
|
||||
@@ -59,14 +61,15 @@ reveal_type(a()) # revealed: Unknown
|
||||
## Possibly non-callable `__call__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class NonCallable:
|
||||
if flag:
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
def flag() -> bool: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
class NonCallable:
|
||||
if flag():
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
@@ -57,10 +57,12 @@ x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
## Potentially unbound function
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def foo() -> int:
|
||||
return 42
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
## Union of return types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def f() -> int:
|
||||
return 1
|
||||
else:
|
||||
def f() -> str:
|
||||
return "foo"
|
||||
reveal_type(f()) # revealed: int | str
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
else:
|
||||
|
||||
def f() -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(f()) # revealed: int | str
|
||||
```
|
||||
|
||||
## Calling with an unknown union
|
||||
@@ -18,10 +26,13 @@ def _(flag: bool):
|
||||
```py
|
||||
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
|
||||
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
@@ -33,14 +44,20 @@ reveal_type(f()) # revealed: Unknown | int
|
||||
Calling a union with a non-callable element should emit a diagnostic.
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
@@ -48,17 +65,23 @@ def _(flag: bool):
|
||||
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
f = "foo"
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# 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())
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
f = "foo"
|
||||
else:
|
||||
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# 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())
|
||||
```
|
||||
|
||||
## All non-callable union elements
|
||||
@@ -66,12 +89,16 @@ def _(flag: bool, flag2: bool):
|
||||
Calling a union with no callable elements can emit a simpler diagnostic.
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
f = "foo"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,31 +3,38 @@
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def _(a1: A, a2: A, o: object):
|
||||
n1 = None
|
||||
n2 = None
|
||||
def get_a() -> A: ...
|
||||
def get_object() -> object: ...
|
||||
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
a1 = get_a()
|
||||
a2 = get_a()
|
||||
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
n1 = None
|
||||
n2 = None
|
||||
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
o = get_object()
|
||||
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -312,9 +312,17 @@ reveal_type(1 <= 2j) # revealed: bool
|
||||
reveal_type(1 > 2j) # revealed: bool
|
||||
reveal_type(1 >= 2j) # revealed: bool
|
||||
|
||||
def f(x: bool, y: int):
|
||||
reveal_type(x < y) # revealed: bool
|
||||
reveal_type(y < x) # revealed: bool
|
||||
reveal_type(4.2 < x) # revealed: bool
|
||||
reveal_type(x < 4.2) # revealed: bool
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
x = bool_instance()
|
||||
y = int_instance()
|
||||
|
||||
reveal_type(x < y) # revealed: bool
|
||||
reveal_type(y < x) # revealed: bool
|
||||
reveal_type(4.2 < x) # revealed: bool
|
||||
reveal_type(x < 4.2) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -20,8 +20,10 @@ reveal_type(1 <= "" and 0 < 1) # revealed: bool
|
||||
|
||||
```py
|
||||
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
|
||||
def _(a: int, b: int):
|
||||
reveal_type(1 == a) # revealed: bool
|
||||
reveal_type(9 < a) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(1 == int_instance()) # revealed: bool
|
||||
reveal_type(9 < int_instance()) # revealed: bool
|
||||
reveal_type(int_instance() < int_instance()) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -14,19 +14,21 @@ class Child1(Base):
|
||||
|
||||
class Child2(Base): ...
|
||||
|
||||
def _(x: Base):
|
||||
c1 = Child1()
|
||||
def get_base() -> Base: ...
|
||||
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
x = get_base()
|
||||
c1 = Child1()
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
```
|
||||
|
||||
## Negative contributions
|
||||
@@ -71,15 +73,18 @@ if x != "abc":
|
||||
#### Integers
|
||||
|
||||
```py
|
||||
def _(x: int):
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
def get_int() -> int: ...
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
x = get_int()
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity comparisons
|
||||
@@ -87,15 +92,18 @@ def _(x: int):
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def _(o: object):
|
||||
a = A()
|
||||
n = None
|
||||
def get_object() -> object: ...
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
o = object()
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
@@ -111,13 +119,16 @@ class Container:
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
def get_object() -> object: ...
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
@@ -131,11 +142,14 @@ class Container:
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
def get_object() -> object: ...
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
## String literals
|
||||
|
||||
```py
|
||||
def _(x: str):
|
||||
reveal_type("abc" == "abc") # revealed: Literal[True]
|
||||
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
|
||||
reveal_type("abc" in "ab cd") # revealed: Literal[False]
|
||||
reveal_type("" not in "hello") # revealed: Literal[False]
|
||||
reveal_type("--" is "--") # revealed: bool
|
||||
reveal_type("A" is "B") # revealed: Literal[False]
|
||||
reveal_type("--" is not "--") # revealed: bool
|
||||
reveal_type("A" is not "B") # revealed: Literal[True]
|
||||
reveal_type(x < "...") # revealed: bool
|
||||
def str_instance() -> str: ...
|
||||
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
|
||||
reveal_type("abc" == "abc") # revealed: Literal[True]
|
||||
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
|
||||
reveal_type("abc" in "ab cd") # revealed: Literal[False]
|
||||
reveal_type("" not in "hello") # revealed: Literal[False]
|
||||
reveal_type("--" is "--") # revealed: bool
|
||||
reveal_type("A" is "B") # revealed: Literal[False]
|
||||
reveal_type("--" is not "--") # revealed: bool
|
||||
reveal_type("A" is not "B") # revealed: Literal[True]
|
||||
reveal_type(str_instance() < "...") # revealed: bool
|
||||
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -58,23 +58,28 @@ reveal_type(c >= d) # revealed: Literal[True]
|
||||
#### Results with Ambiguity
|
||||
|
||||
```py
|
||||
def _(x: bool, y: int):
|
||||
a = (x,)
|
||||
b = (y,)
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(a == b) # revealed: bool
|
||||
reveal_type(a != b) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
a = (bool_instance(),)
|
||||
b = (int_instance(),)
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
reveal_type(a == b) # revealed: bool
|
||||
reveal_type(a != b) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
```
|
||||
|
||||
#### Comparison Unsupported
|
||||
@@ -192,7 +197,7 @@ reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
|
||||
|
||||
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
|
||||
|
||||
> Example: `(<int instance>, "foo") == (<int instance>, "bar")`
|
||||
> Example: `(int_instance(), "foo") == (int_instance(), "bar")`
|
||||
|
||||
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
|
||||
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
|
||||
@@ -203,38 +208,42 @@ In contrast, with operators like `<` and `>`, the comparison must consider each
|
||||
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
|
||||
|
||||
```py
|
||||
def _(x: str, y: int):
|
||||
reveal_type("foo" == "bar") # revealed: Literal[False]
|
||||
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
|
||||
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
|
||||
reveal_type((y, "foo") == (y, "bar")) # revealed: Literal[False]
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
|
||||
a = (x, y, "foo")
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
reveal_type("foo" == "bar") # revealed: Literal[False]
|
||||
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
|
||||
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
|
||||
reveal_type((int_instance(), "foo") == (int_instance(), "bar")) # revealed: Literal[False]
|
||||
|
||||
b = (x, y, "bar")
|
||||
a = (str_instance(), int_instance(), "foo")
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
c = (x, y, "foo", "different_length")
|
||||
b = (str_instance(), int_instance(), "bar")
|
||||
|
||||
reveal_type(a == c) # revealed: Literal[False]
|
||||
reveal_type(a != c) # revealed: Literal[True]
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(a <= c) # revealed: bool
|
||||
reveal_type(a > c) # revealed: bool
|
||||
reveal_type(a >= c) # revealed: bool
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
c = (str_instance(), int_instance(), "foo", "different_length")
|
||||
reveal_type(a == c) # revealed: Literal[False]
|
||||
reveal_type(a != c) # revealed: Literal[True]
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(a <= c) # revealed: bool
|
||||
reveal_type(a > c) # revealed: bool
|
||||
reveal_type(a >= c) # revealed: bool
|
||||
```
|
||||
|
||||
#### Error Propagation
|
||||
@@ -243,36 +252,42 @@ Errors occurring within a tuple comparison should propagate outward. However, if
|
||||
comparison can clearly conclude before encountering an error, the error should not be raised.
|
||||
|
||||
```py
|
||||
def _(n: int, s: str):
|
||||
class A: ...
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
|
||||
A() < A()
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
|
||||
A() <= A()
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
|
||||
A() > A()
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
|
||||
A() >= A()
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
a = (0, n, A())
|
||||
def str_instance() -> str:
|
||||
return "hello"
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a < a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a <= a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a > a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a >= a) # revealed: Unknown
|
||||
class A: ...
|
||||
|
||||
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
|
||||
# and should terminate immediately.
|
||||
b = (99999, n, A())
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
|
||||
A() < A()
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
|
||||
A() <= A()
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
|
||||
A() > A()
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
|
||||
A() >= A()
|
||||
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
a = (0, int_instance(), A())
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a < a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a <= a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a > a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a >= a) # revealed: Unknown
|
||||
|
||||
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
|
||||
# and should terminate immediately.
|
||||
b = (99999, int_instance(), A())
|
||||
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Membership Test Comparisons
|
||||
@@ -280,20 +295,22 @@ def _(n: int, s: str):
|
||||
"Membership Test Comparisons" refers to the operators `in` and `not in`.
|
||||
|
||||
```py
|
||||
def _(n: int):
|
||||
a = (1, 2)
|
||||
b = ((3, 4), (1, 2))
|
||||
c = ((1, 2, 3), (4, 5, 6))
|
||||
d = ((n, n), (n, n))
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(a in b) # revealed: Literal[True]
|
||||
reveal_type(a not in b) # revealed: Literal[False]
|
||||
a = (1, 2)
|
||||
b = ((3, 4), (1, 2))
|
||||
c = ((1, 2, 3), (4, 5, 6))
|
||||
d = ((int_instance(), int_instance()), (int_instance(), int_instance()))
|
||||
|
||||
reveal_type(a in c) # revealed: Literal[False]
|
||||
reveal_type(a not in c) # revealed: Literal[True]
|
||||
reveal_type(a in b) # revealed: Literal[True]
|
||||
reveal_type(a not in b) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a in d) # revealed: bool
|
||||
reveal_type(a not in d) # revealed: bool
|
||||
reveal_type(a in c) # revealed: Literal[False]
|
||||
reveal_type(a not in c) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a in d) # revealed: bool
|
||||
reveal_type(a not in d) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity Comparisons
|
||||
|
||||
@@ -5,46 +5,49 @@
|
||||
Comparisons on union types need to consider all possible cases:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
one_or_two = 1 if flag else 2
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(one_or_two <= 2) # revealed: Literal[True]
|
||||
reveal_type(one_or_two <= 1) # revealed: bool
|
||||
reveal_type(one_or_two <= 0) # revealed: Literal[False]
|
||||
flag = bool_instance()
|
||||
one_or_two = 1 if flag else 2
|
||||
|
||||
reveal_type(2 >= one_or_two) # revealed: Literal[True]
|
||||
reveal_type(1 >= one_or_two) # revealed: bool
|
||||
reveal_type(0 >= one_or_two) # revealed: Literal[False]
|
||||
reveal_type(one_or_two <= 2) # revealed: Literal[True]
|
||||
reveal_type(one_or_two <= 1) # revealed: bool
|
||||
reveal_type(one_or_two <= 0) # revealed: Literal[False]
|
||||
|
||||
reveal_type(one_or_two < 1) # revealed: Literal[False]
|
||||
reveal_type(one_or_two < 2) # revealed: bool
|
||||
reveal_type(one_or_two < 3) # revealed: Literal[True]
|
||||
reveal_type(2 >= one_or_two) # revealed: Literal[True]
|
||||
reveal_type(1 >= one_or_two) # revealed: bool
|
||||
reveal_type(0 >= one_or_two) # revealed: Literal[False]
|
||||
|
||||
reveal_type(one_or_two > 0) # revealed: Literal[True]
|
||||
reveal_type(one_or_two > 1) # revealed: bool
|
||||
reveal_type(one_or_two > 2) # revealed: Literal[False]
|
||||
reveal_type(one_or_two < 1) # revealed: Literal[False]
|
||||
reveal_type(one_or_two < 2) # revealed: bool
|
||||
reveal_type(one_or_two < 3) # revealed: Literal[True]
|
||||
|
||||
reveal_type(one_or_two == 3) # revealed: Literal[False]
|
||||
reveal_type(one_or_two == 1) # revealed: bool
|
||||
reveal_type(one_or_two > 0) # revealed: Literal[True]
|
||||
reveal_type(one_or_two > 1) # revealed: bool
|
||||
reveal_type(one_or_two > 2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(one_or_two != 3) # revealed: Literal[True]
|
||||
reveal_type(one_or_two != 1) # revealed: bool
|
||||
reveal_type(one_or_two == 3) # revealed: Literal[False]
|
||||
reveal_type(one_or_two == 1) # revealed: bool
|
||||
|
||||
a_or_ab = "a" if flag else "ab"
|
||||
reveal_type(one_or_two != 3) # revealed: Literal[True]
|
||||
reveal_type(one_or_two != 1) # revealed: bool
|
||||
|
||||
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
|
||||
reveal_type("a" in a_or_ab) # revealed: Literal[True]
|
||||
a_or_ab = "a" if flag else "ab"
|
||||
|
||||
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
|
||||
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
|
||||
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
|
||||
reveal_type("a" in a_or_ab) # revealed: Literal[True]
|
||||
|
||||
reveal_type("b" in a_or_ab) # revealed: bool
|
||||
reveal_type("b" not in a_or_ab) # revealed: bool
|
||||
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
|
||||
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
|
||||
|
||||
one_or_none = 1 if flag else None
|
||||
reveal_type("b" in a_or_ab) # revealed: bool
|
||||
reveal_type("b" not in a_or_ab) # revealed: bool
|
||||
|
||||
reveal_type(one_or_none is None) # revealed: bool
|
||||
reveal_type(one_or_none is not None) # revealed: bool
|
||||
one_or_none = 1 if flag else None
|
||||
|
||||
reveal_type(one_or_none is None) # revealed: bool
|
||||
reveal_type(one_or_none is not None) # revealed: bool
|
||||
```
|
||||
|
||||
## Union on both sides of the comparison
|
||||
@@ -53,15 +56,18 @@ With unions on both sides, we need to consider the full cross product of options
|
||||
resulting (union) type:
|
||||
|
||||
```py
|
||||
def _(flag_s: bool, flag_l: bool):
|
||||
small = 1 if flag_s else 2
|
||||
large = 2 if flag_l else 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(small <= large) # revealed: Literal[True]
|
||||
reveal_type(small >= large) # revealed: bool
|
||||
flag_s, flag_l = bool_instance(), bool_instance()
|
||||
small = 1 if flag_s else 2
|
||||
large = 2 if flag_l else 3
|
||||
|
||||
reveal_type(small < large) # revealed: bool
|
||||
reveal_type(small > large) # revealed: Literal[False]
|
||||
reveal_type(small <= large) # revealed: Literal[True]
|
||||
reveal_type(small >= large) # revealed: bool
|
||||
|
||||
reveal_type(small < large) # revealed: bool
|
||||
reveal_type(small > large) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Unsupported operations
|
||||
@@ -71,9 +77,12 @@ back to `bool` for the result type instead of trying to infer something more pre
|
||||
(supported) variants:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = [1, 2] if flag else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
result = 1 in x # error: "Operator `in` is not supported"
|
||||
reveal_type(result) # revealed: bool
|
||||
flag = bool_instance()
|
||||
x = [1, 2] if flag else 1
|
||||
|
||||
result = 1 in x # error: "Operator `in` is not supported"
|
||||
reveal_type(result) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
# Comparison: Unsupported operators
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # revealed: bool
|
||||
class A: ...
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `object` and `int`")
|
||||
c = object() < 5
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(c) # revealed: bool
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `int` and `object`")
|
||||
d = 5 < object()
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(d) # revealed: bool
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # 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] | Literal["foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `object` and `int`")
|
||||
c = object() < 5
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(c) # revealed: bool
|
||||
|
||||
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
||||
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
||||
f = (1, 2) < (1, "hello")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `int` and `object`")
|
||||
d = 5 < object()
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(d) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (flag1, A()) < (flag2, A())
|
||||
reveal_type(g) # revealed: Unknown
|
||||
flag = bool_instance()
|
||||
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] | Literal["foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
||||
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
||||
f = (1, 2) < (1, "hello")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (bool_instance(), A()) < (bool_instance(), A())
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,35 +3,47 @@
|
||||
## Simple if-expression
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## If-expression with walrus operator
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
reveal_type(z) # revealed: Literal[0, 2]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
reveal_type(z) # revealed: Literal[0, 2]
|
||||
```
|
||||
|
||||
## Nested if-expression
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## None
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
@@ -3,115 +3,128 @@
|
||||
## Simple if
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
flag = bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
if flag:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Simple if-elif-else
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
else:
|
||||
r = y
|
||||
y = 5
|
||||
s = y
|
||||
x = y
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
else:
|
||||
r = y
|
||||
y = 5
|
||||
s = y
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
|
||||
# revealed: Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
# revealed: Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
|
||||
# revealed: Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
# revealed: Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
```
|
||||
|
||||
## Single symbol across if-elif-else
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
y = 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
y = 3
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## if-elif-else without else assignment
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## if-elif-else with intervening assignment
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
z = 3
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
y = 1
|
||||
z = 3
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## Nested if statement
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
if flag2:
|
||||
y = 1
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 0
|
||||
if flag:
|
||||
if flag2:
|
||||
y = 1
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## if-elif without else
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
y = 1
|
||||
y = 2
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
@@ -3,43 +3,39 @@
|
||||
## With wildcard
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
match 0:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Without wildcard
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
match 0:
|
||||
case 1:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
# revealed: Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
# revealed: Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
## Basic match
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
y = 1
|
||||
y = 2
|
||||
y = 1
|
||||
y = 2
|
||||
match 0:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
|
||||
match target:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
@@ -10,35 +10,42 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t
|
||||
## Incompatible declarations
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Partial declarations
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: int
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: int
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations with bad assignment
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
# error: [conflicting-declarations]
|
||||
# error: [invalid-assignment]
|
||||
x = b"foo"
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
# error: [conflicting-declarations]
|
||||
# error: [invalid-assignment]
|
||||
x = b"foo"
|
||||
```
|
||||
|
||||
@@ -49,44 +49,12 @@ def foo(
|
||||
try:
|
||||
help()
|
||||
except x as e:
|
||||
reveal_type(e) # revealed: AttributeError
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo(exception type)
|
||||
except y as f:
|
||||
reveal_type(f) # revealed: OSError | RuntimeError
|
||||
# TODO: should be `OSError | RuntimeError`
|
||||
reveal_type(f) # revealed: @Todo(exception type)
|
||||
except z as g:
|
||||
# TODO: should be `BaseException`
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
## Invalid exception handlers
|
||||
|
||||
```py
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except 3 as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except (ValueError, OSError, "foo", b"bar") as e:
|
||||
reveal_type(e) # revealed: ValueError | OSError | Unknown
|
||||
|
||||
def foo(
|
||||
x: type[str],
|
||||
y: tuple[type[OSError], type[RuntimeError], int],
|
||||
z: tuple[type[str], ...],
|
||||
):
|
||||
try:
|
||||
help()
|
||||
# error: [invalid-exception-caught]
|
||||
except x as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
# error: [invalid-exception-caught]
|
||||
except y as f:
|
||||
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
|
||||
except z as g:
|
||||
# TODO: should emit a diagnostic here:
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(exception type)
|
||||
```
|
||||
|
||||
@@ -1,66 +1,30 @@
|
||||
# `except*`
|
||||
# Except star
|
||||
|
||||
`except*` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
## `except*` with `BaseException`
|
||||
## Except\* with BaseException
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* BaseException as e:
|
||||
# TODO: should be `BaseExceptionGroup[BaseException]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## `except*` with specific exception
|
||||
## Except\* with specific exception
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* OSError as e:
|
||||
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
|
||||
# (needs homogenous tuples + generics)
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[OSError]`
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## `except*` with multiple exceptions
|
||||
## Except\* with multiple exceptions
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* (TypeError, AttributeError) as e:
|
||||
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
|
||||
# (needs homogenous tuples + generics)
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## `except*` with mix of `Exception`s and `BaseException`s
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* (KeyboardInterrupt, AttributeError) as e:
|
||||
# TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
## Invalid `except*` handlers
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* 3 as e: # error: [invalid-exception-caught]
|
||||
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
|
||||
try:
|
||||
help()
|
||||
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
|
||||
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
|
||||
# TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`.
|
||||
reveal_type(e) # revealed: BaseExceptionGroup
|
||||
```
|
||||
|
||||
@@ -9,4 +9,5 @@ try:
|
||||
print
|
||||
except as e: # error: [invalid-syntax]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
```
|
||||
|
||||
@@ -3,25 +3,26 @@
|
||||
## Boundness
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
always_bound = 1
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag:
|
||||
union = 1
|
||||
else:
|
||||
union = "abc"
|
||||
class A:
|
||||
always_bound = 1
|
||||
|
||||
if flag:
|
||||
possibly_unbound = "abc"
|
||||
if flag():
|
||||
union = 1
|
||||
else:
|
||||
union = "abc"
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
if flag():
|
||||
possibly_unbound = "abc"
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
reveal_type(A.union) # revealed: Literal[1] | Literal["abc"]
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -3,45 +3,54 @@
|
||||
## OR
|
||||
|
||||
```py
|
||||
def _(foo: str):
|
||||
reveal_type(True or False) # revealed: Literal[True]
|
||||
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
|
||||
reveal_type("" or "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type(False or "z") # revealed: Literal["z"]
|
||||
reveal_type(False or True) # revealed: Literal[True]
|
||||
reveal_type(False or False) # revealed: Literal[False]
|
||||
reveal_type(foo or False) # revealed: str | Literal[False]
|
||||
reveal_type(foo or True) # revealed: str | Literal[True]
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
reveal_type(True or False) # revealed: Literal[True]
|
||||
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
|
||||
reveal_type("" or "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type(False or "z") # revealed: Literal["z"]
|
||||
reveal_type(False or True) # revealed: Literal[True]
|
||||
reveal_type(False or False) # revealed: Literal[False]
|
||||
reveal_type(foo() or False) # revealed: str | Literal[False]
|
||||
reveal_type(foo() or True) # revealed: str | Literal[True]
|
||||
```
|
||||
|
||||
## AND
|
||||
|
||||
```py
|
||||
def _(foo: str):
|
||||
reveal_type(True and False) # revealed: Literal[False]
|
||||
reveal_type(False and True) # revealed: Literal[False]
|
||||
reveal_type(foo and False) # revealed: str | Literal[False]
|
||||
reveal_type(foo and True) # revealed: str | Literal[True]
|
||||
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" and "") # revealed: Literal[""]
|
||||
reveal_type("" and "y") # revealed: Literal[""]
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
reveal_type(True and False) # revealed: Literal[False]
|
||||
reveal_type(False and True) # revealed: Literal[False]
|
||||
reveal_type(foo() and False) # revealed: str | Literal[False]
|
||||
reveal_type(foo() and True) # revealed: str | Literal[True]
|
||||
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" and "") # revealed: Literal[""]
|
||||
reveal_type("" and "y") # revealed: Literal[""]
|
||||
```
|
||||
|
||||
## Simple function calls to bool
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x = True
|
||||
else:
|
||||
x = False
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
if returns_bool():
|
||||
x = True
|
||||
else:
|
||||
x = False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
```
|
||||
|
||||
## Complex
|
||||
|
||||
```py
|
||||
def foo() -> str:
|
||||
pass
|
||||
|
||||
reveal_type("x" and "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type("x" or "y" and "z") # revealed: Literal["x"]
|
||||
reveal_type("" and "y" or "z") # revealed: Literal["z"]
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
## Union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
reveal_type(1 if flag else 2) # revealed: Literal[1, 2]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
@@ -28,12 +30,14 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2]
|
||||
The test inside an if expression should not affect code outside of the expression.
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x: Literal[42, "hello"] = 42 if flag else "hello"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
```
|
||||
|
||||
@@ -211,7 +211,8 @@ reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
|
||||
### No `__len__`
|
||||
|
||||
```py
|
||||
class NoDunderLen: ...
|
||||
class NoDunderLen:
|
||||
pass
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(NoDunderLen())) # revealed: int
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
```py
|
||||
import builtins
|
||||
|
||||
x = builtins.chr
|
||||
reveal_type(x) # revealed: Literal[chr]
|
||||
x = builtins.copyright
|
||||
reveal_type(x) # revealed: Literal[copyright]
|
||||
```
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
## Maybe unbound
|
||||
|
||||
```py path=maybe_unbound.py
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
y = 3
|
||||
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
@@ -30,12 +31,13 @@ reveal_type(y) # revealed: Literal[3]
|
||||
## Maybe unbound annotated
|
||||
|
||||
```py path=maybe_unbound_annotated.py
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
y: int = 3
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
y: int = 3
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Literal[3]
|
||||
@@ -61,10 +63,10 @@ reveal_type(y) # revealed: int
|
||||
Importing a possibly undeclared name still gives us its declared type:
|
||||
|
||||
```py path=maybe_undeclared.py
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
if bool_instance():
|
||||
x: int
|
||||
```
|
||||
|
||||
@@ -81,12 +83,14 @@ def f(): ...
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
from c import f
|
||||
else:
|
||||
|
||||
def f(): ...
|
||||
```
|
||||
|
||||
@@ -107,10 +111,11 @@ x: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
flag = bool_instance()
|
||||
if flag:
|
||||
from c import x
|
||||
else:
|
||||
x = 1
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Known constants
|
||||
|
||||
## `typing.TYPE_CHECKING`
|
||||
|
||||
This constant is `True` when in type-checking mode, `False` otherwise. The symbol is defined to be
|
||||
`False` at runtime. In typeshed, it is annotated as `bool`. This test makes sure that we infer
|
||||
`Literal[True]` for it anyways.
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING
|
||||
import typing
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
reveal_type(typing.TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Aliased
|
||||
|
||||
Make sure that we still infer the correct type if the constant has been given a different name:
|
||||
|
||||
```py
|
||||
from typing import TYPE_CHECKING as TC
|
||||
|
||||
reveal_type(TC) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
### Must originate from `typing`
|
||||
|
||||
Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants
|
||||
with the same name:
|
||||
|
||||
```py path=constants.py
|
||||
TYPE_CHECKING: bool = False
|
||||
```
|
||||
|
||||
```py
|
||||
from constants import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: bool
|
||||
```
|
||||
|
||||
### `typing_extensions` re-export
|
||||
|
||||
This should behave in the same way as `typing.TYPE_CHECKING`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TYPE_CHECKING
|
||||
|
||||
reveal_type(TYPE_CHECKING) # revealed: Literal[True]
|
||||
```
|
||||
@@ -45,19 +45,19 @@ def f():
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid1: Literal[3 + 4]
|
||||
# error: [invalid-type-form]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid2: Literal[4 + 3j]
|
||||
# error: [invalid-type-form]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid3: Literal[(3, 4)]
|
||||
|
||||
hello = "hello"
|
||||
invalid4: Literal[
|
||||
1 + 2, # error: [invalid-type-form]
|
||||
1 + 2, # error: [invalid-literal-parameter]
|
||||
"foo",
|
||||
hello, # error: [invalid-type-form]
|
||||
(1, 2, 3), # error: [invalid-type-form]
|
||||
hello, # error: [invalid-literal-parameter]
|
||||
(1, 2, 3), # error: [invalid-literal-parameter]
|
||||
]
|
||||
```
|
||||
|
||||
@@ -91,13 +91,3 @@ a1: Literal[26]
|
||||
def f():
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
# error: [invalid-type-form] "`Literal` requires at least one argument when used in a type expression"
|
||||
def _(x: Literal):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
@@ -106,19 +106,23 @@ reveal_type(x)
|
||||
## With non-callable iterator
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
flag = bool_instance()
|
||||
|
||||
# revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
# revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
## Invalid iterable
|
||||
@@ -156,9 +160,13 @@ class Test2:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
def _(flag: bool):
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterator
|
||||
@@ -207,9 +215,13 @@ class Test2:
|
||||
def __iter__(self) -> TestIter3 | TestIter4:
|
||||
return TestIter3()
|
||||
|
||||
def _(flag: bool):
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: int | Exception | str | tuple[int, int] | bytes | memoryview
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has no `__iter__` method
|
||||
@@ -223,10 +235,12 @@ class Test:
|
||||
def __iter__(self) -> TestIter:
|
||||
return TestIter()
|
||||
|
||||
def _(flag: bool):
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
for x in Test() if flag else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
for x in Test() if coinflip() else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has invalid `__iter__` method
|
||||
@@ -244,10 +258,12 @@ class Test2:
|
||||
def __iter__(self) -> int:
|
||||
return 42
|
||||
|
||||
def _(flag: bool):
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if coinflip() else Test2():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union type as iterator where one union element has no `__next__` method
|
||||
|
||||
@@ -1,50 +1,59 @@
|
||||
# While loops
|
||||
|
||||
## Basic `while` loop
|
||||
## Basic While Loop
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## While with else (no break)
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## `while` with `else` (no `break`)
|
||||
## While with Else (may break)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1
|
||||
while flag:
|
||||
x = 2
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
x = 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
flag, flag2 = bool_instance(), bool_instance()
|
||||
x = 1
|
||||
y = 0
|
||||
while flag:
|
||||
x = 2
|
||||
if flag2:
|
||||
y = 4
|
||||
break
|
||||
else:
|
||||
y = x
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
|
||||
## `while` with `else` (may `break`)
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
x = 1
|
||||
y = 0
|
||||
while flag:
|
||||
x = 2
|
||||
if flag2:
|
||||
y = 4
|
||||
break
|
||||
else:
|
||||
y = x
|
||||
x = 3
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
|
||||
## Nested `while` loops
|
||||
## Nested while loops
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
@@ -69,50 +78,3 @@ else:
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
```
|
||||
|
||||
## Boundness
|
||||
|
||||
Make sure that the boundness information is correctly tracked in `while` loop control flow.
|
||||
|
||||
### Basic `while` loop
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
while flag:
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
x
|
||||
```
|
||||
|
||||
### `while` with `else` (no `break`)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
while flag:
|
||||
y = 1
|
||||
else:
|
||||
x = 1
|
||||
|
||||
# no error, `x` is always bound
|
||||
x
|
||||
# error: [possibly-unresolved-reference]
|
||||
y
|
||||
```
|
||||
|
||||
### `while` with `else` (may `break`)
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
while flag:
|
||||
x = 1
|
||||
if flag2:
|
||||
break
|
||||
else:
|
||||
y = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
x
|
||||
# error: [possibly-unresolved-reference]
|
||||
y
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ The following configuration will be attached to the *root* section (without any
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
target-version = "3.10"
|
||||
```
|
||||
|
||||
# Basic
|
||||
@@ -34,7 +34,7 @@ Here, we make sure that we can overwrite the global configuration in a child sec
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
target-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
@@ -55,7 +55,7 @@ Children in this section should all use the section configuration:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
target-version = "3.12"
|
||||
```
|
||||
|
||||
## Child
|
||||
|
||||
@@ -68,7 +68,7 @@ class B(metaclass=M2): ...
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: type[Unknown]
|
||||
reveal_type(C.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Conflict (2)
|
||||
@@ -85,7 +85,7 @@ class A(metaclass=M1): ...
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship"
|
||||
class B(A, metaclass=M2): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: type[Unknown]
|
||||
reveal_type(B.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Common metaclass
|
||||
@@ -129,7 +129,7 @@ class C(metaclass=M12): ...
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class D(A, B, C): ...
|
||||
|
||||
reveal_type(D.__class__) # revealed: type[Unknown]
|
||||
reveal_type(D.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unknown
|
||||
@@ -183,7 +183,7 @@ class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(A.__class__) # revealed: type[Unknown]
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## PEP 695 generic
|
||||
@@ -194,26 +194,3 @@ class A[T: str](metaclass=M): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Metaclasses of metaclasses
|
||||
|
||||
```py
|
||||
class Foo(type): ...
|
||||
class Bar(type, metaclass=Foo): ...
|
||||
class Baz(type, metaclass=Bar): ...
|
||||
class Spam(metaclass=Baz): ...
|
||||
|
||||
reveal_type(Spam.__class__) # revealed: Literal[Baz]
|
||||
reveal_type(Spam.__class__.__class__) # revealed: Literal[Bar]
|
||||
reveal_type(Spam.__class__.__class__.__class__) # revealed: Literal[Foo]
|
||||
|
||||
def test(x: Spam):
|
||||
reveal_type(x.__class__) # revealed: type[Spam]
|
||||
reveal_type(x.__class__.__class__) # revealed: type[Baz]
|
||||
reveal_type(x.__class__.__class__.__class__) # revealed: type[Bar]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Foo]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__.__class__) # revealed: type[type]
|
||||
|
||||
# revealed: type[type]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__.__class__.__class__.__class__.__class__)
|
||||
```
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
## Narrowing for `bool(..)` checks
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
def flag() -> bool: ...
|
||||
|
||||
x = 1 if flag else None
|
||||
x = 1 if flag() else None
|
||||
|
||||
# valid invocation, positive
|
||||
# valid invocation, positive
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# valid invocation, negative
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool(x is not None):
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
# no args/narrowing
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool():
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# valid invocation, negative
|
||||
# invalid invocation, too many positional args
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, 5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool(x is not None):
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
# no args/narrowing
|
||||
# invalid invocation, too many kwargs
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, y=5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if not bool():
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
|
||||
# invalid invocation, too many positional args
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, 5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
|
||||
# invalid invocation, too many kwargs
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
if bool(x is not None, y=5): # TODO diagnostic
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
@@ -9,67 +9,85 @@ Similarly, in `and` expressions, the right-hand side is evaluated only if the le
|
||||
## Narrowing in `or`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag else None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
x is None or reveal_type(x) # revealed: A
|
||||
reveal_type(x) # revealed: A | None
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
x is None or reveal_type(x) # revealed: A
|
||||
reveal_type(x) # revealed: A | None
|
||||
```
|
||||
|
||||
## Narrowing in `and`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag else None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
x is None and reveal_type(x) # revealed: None
|
||||
reveal_type(x) # revealed: A | None
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
x is None and reveal_type(x) # revealed: None
|
||||
reveal_type(x) # revealed: A | None
|
||||
```
|
||||
|
||||
## Multiple `and` arms
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag1 else None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag2 and isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
isinstance(x, A) and flag2 and reveal_type(x) # revealed: A
|
||||
reveal_type(x) and isinstance(x, A) and flag3 # revealed: A | None
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
bool_instance() and isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
isinstance(x, A) and bool_instance() and reveal_type(x) # revealed: A
|
||||
reveal_type(x) and isinstance(x, A) and bool_instance() # revealed: A | None
|
||||
```
|
||||
|
||||
## Multiple `or` arms
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag1 else None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag2 or isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
isinstance(x, A) or flag3 or reveal_type(x) # revealed: None
|
||||
reveal_type(x) or isinstance(x, A) or flag4 # revealed: A | None
|
||||
class A: ...
|
||||
|
||||
x: A | None = A() if bool_instance() else None
|
||||
|
||||
bool_instance() or isinstance(x, A) or reveal_type(x) # revealed: None
|
||||
isinstance(x, A) or bool_instance() or reveal_type(x) # revealed: None
|
||||
reveal_type(x) or isinstance(x, A) or bool_instance() # revealed: A | None
|
||||
```
|
||||
|
||||
## Multiple predicates
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
|
||||
class A: ...
|
||||
|
||||
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
|
||||
|
||||
x is None or isinstance(x, A) or reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Mix of `and` and `or`
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
|
||||
class A: ...
|
||||
|
||||
x: A | None | Literal[1] = A() if bool_instance() else None if bool_instance() else 1
|
||||
|
||||
isinstance(x, A) or x is not None and reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: A | B):
|
||||
if isinstance(x, A) and isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | A & ~B
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | A & ~B
|
||||
```
|
||||
|
||||
## Arms might not add narrowing constraints
|
||||
@@ -19,18 +23,25 @@ def _(x: A | B):
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(flag: bool, x: A | B):
|
||||
if isinstance(x, A) and flag:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if flag and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and bool_instance():
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if bool_instance() and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## Statically known arms
|
||||
@@ -39,35 +50,39 @@ def _(flag: bool, x: A | B):
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: A | B):
|
||||
if isinstance(x, A) and True:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
if True and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
x = instance()
|
||||
|
||||
if False and isinstance(x, A):
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
if isinstance(x, A) and True:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if False or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
if True and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if False and isinstance(x, A):
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if False or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## The type of multiple symbols can be narrowed down
|
||||
@@ -76,17 +91,22 @@ def _(x: A | B):
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: A | B, y: A | B):
|
||||
if isinstance(x, A) and isinstance(y, B):
|
||||
reveal_type(x) # revealed: A
|
||||
reveal_type(y) # revealed: B
|
||||
else:
|
||||
# No narrowing: Only-one or both checks might have failed
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(y, B):
|
||||
reveal_type(x) # revealed: A
|
||||
reveal_type(y) # revealed: B
|
||||
else:
|
||||
# No narrowing: Only-one or both checks might have failed
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
```
|
||||
|
||||
## Narrowing in `or` conditional
|
||||
@@ -96,11 +116,15 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, A) or isinstance(x, B):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## In `or`, all arms should add constraint in order to narrow
|
||||
@@ -110,11 +134,18 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(flag: bool, x: A | B | C):
|
||||
if isinstance(x, A) or isinstance(x, B) or flag:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B) or bool_instance():
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## in `or`, all arms should narrow the same set of symbols
|
||||
@@ -124,23 +155,28 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B | C, y: A | B | C):
|
||||
if isinstance(x, A) or isinstance(y, A):
|
||||
# The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here.
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
# The same for `y`
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A
|
||||
reveal_type(y) # revealed: B & ~A | C & ~A
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
|
||||
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(y, A):
|
||||
# The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here.
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
# The same for `y`
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A
|
||||
reveal_type(y) # revealed: B & ~A | C & ~A
|
||||
|
||||
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
|
||||
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
```
|
||||
|
||||
## mixing `and` and `not`
|
||||
@@ -150,12 +186,16 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, B) and not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B & ~C
|
||||
else:
|
||||
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
|
||||
reveal_type(x) # revealed: A & ~B | C
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) and not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B & ~C
|
||||
else:
|
||||
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
|
||||
reveal_type(x) # revealed: A & ~B | C
|
||||
```
|
||||
|
||||
## mixing `or` and `not`
|
||||
@@ -165,11 +205,15 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, B) or not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B | A & ~C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~B
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) or not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B | A & ~C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~B
|
||||
```
|
||||
|
||||
## `or` with nested `and`
|
||||
@@ -179,12 +223,16 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
|
||||
reveal_type(x) # revealed: A | B & ~C
|
||||
else:
|
||||
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
|
||||
reveal_type(x) # revealed: C & ~A
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
|
||||
reveal_type(x) # revealed: A | B & ~C
|
||||
else:
|
||||
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
|
||||
reveal_type(x) # revealed: C & ~A
|
||||
```
|
||||
|
||||
## `and` with nested `or`
|
||||
@@ -194,32 +242,41 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: A | B | C):
|
||||
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
|
||||
# A & (B | ~C) -> (A & B) | (A & ~C)
|
||||
reveal_type(x) # revealed: A & B | A & ~C
|
||||
else:
|
||||
# ~((A & B) | (A & ~C)) ->
|
||||
# ~(A & B) & ~(A & ~C) ->
|
||||
# (~A | ~B) & (~A | C) ->
|
||||
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
|
||||
# ~A | (~A & C) | (~B & C) ->
|
||||
# ~A | (C & ~B) ->
|
||||
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
|
||||
# A & (B | ~C) -> (A & B) | (A & ~C)
|
||||
reveal_type(x) # revealed: A & B | A & ~C
|
||||
else:
|
||||
# ~((A & B) | (A & ~C)) ->
|
||||
# ~(A & B) & ~(A & ~C) ->
|
||||
# (~A | ~B) & (~A | C) ->
|
||||
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
|
||||
# ~A | (~A & C) | (~B & C) ->
|
||||
# ~A | (C & ~B) ->
|
||||
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
|
||||
```
|
||||
|
||||
## Boolean expression internal narrowing
|
||||
|
||||
```py
|
||||
def _(x: str | None, y: str | None):
|
||||
if x is None and y is not x:
|
||||
reveal_type(y) # revealed: str
|
||||
def optional_string() -> str | None:
|
||||
return None
|
||||
|
||||
# Neither of the conditions alone is sufficient for narrowing y's type:
|
||||
if x is None:
|
||||
reveal_type(y) # revealed: str | None
|
||||
x = optional_string()
|
||||
y = optional_string()
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: str | None
|
||||
if x is None and y is not x:
|
||||
reveal_type(y) # revealed: str
|
||||
|
||||
# Neither of the conditions alone is sufficient for narrowing y's type:
|
||||
if x is None:
|
||||
reveal_type(y) # revealed: str | None
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: str | None
|
||||
```
|
||||
|
||||
@@ -3,47 +3,55 @@
|
||||
## Positive contributions become negative in elif-else blocks
|
||||
|
||||
```py
|
||||
def _(x: int):
|
||||
if x == 1:
|
||||
# cannot narrow; could be a subclass of `int`
|
||||
reveal_type(x) # revealed: int
|
||||
elif x == 2:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
elif x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
x = int_instance()
|
||||
|
||||
if x == 1:
|
||||
# cannot narrow; could be a subclass of `int`
|
||||
reveal_type(x) # revealed: int
|
||||
elif x == 2:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
elif x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
```
|
||||
|
||||
## Positive contributions become negative in elif-else blocks, with simplification
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x == 1:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
elif x == 2:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||
|
||||
if x == 1:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
elif x == 2:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## Multiple negative contributions using elif, with simplification
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
elif x != 2:
|
||||
# TODO should be `Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
elif x == 3:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
elif x != 2:
|
||||
# TODO should be `Literal[1]`
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
elif x == 3:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
@@ -3,64 +3,77 @@
|
||||
## `is None`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is` for other types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if y is x:
|
||||
reveal_type(y) # revealed: A
|
||||
else:
|
||||
reveal_type(y) # revealed: A | None
|
||||
flag = bool_instance()
|
||||
|
||||
class A: ...
|
||||
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
if y is x:
|
||||
reveal_type(y) # revealed: A
|
||||
else:
|
||||
reveal_type(y) # revealed: A | None
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
|
||||
## `is` in chained comparisons
|
||||
|
||||
```py
|
||||
def _(x_flag: bool, y_flag: bool):
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is x is False: # Interpreted as `(y is x) and (x is False)`
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is not x) or (x is not False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is x is False: # Interpreted as `(y is x) and (x is False)`
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is not x) or (x is not False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
```
|
||||
|
||||
## `is` in elif clause
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = None if flag1 else (1 if flag2 else True)
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
elif x is True:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = None if bool_instance() else (1 if bool_instance() else True)
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1] | Literal[True]
|
||||
if x is None:
|
||||
reveal_type(x) # revealed: None
|
||||
elif x is True:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -5,28 +5,34 @@
|
||||
The type guard removes `None` from the union type:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is not` for other singleton types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x is not False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
if x is not False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## `is not` for non-singleton types
|
||||
@@ -47,17 +53,20 @@ else:
|
||||
## `is not` for other types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: A | None
|
||||
else:
|
||||
reveal_type(y) # revealed: A
|
||||
class A: ...
|
||||
|
||||
x = A()
|
||||
y = x if bool_instance() else None
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: A | None
|
||||
else:
|
||||
reveal_type(y) # revealed: A
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
|
||||
## `is not` in chained comparisons
|
||||
@@ -65,20 +74,23 @@ def _(flag: bool):
|
||||
The type guard removes `False` from the union type of the tested value only.
|
||||
|
||||
```py
|
||||
def _(x_flag: bool, y_flag: bool):
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x_flag, y_flag = bool_instance(), bool_instance()
|
||||
x = True if x_flag else False
|
||||
y = True if y_flag else False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is x) or (x is False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
|
||||
if y is not x is not False: # Interpreted as `(y is not x) and (x is not False)`
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
reveal_type(y) # revealed: bool
|
||||
else:
|
||||
# The negation of the clause above is (y is x) or (x is False)
|
||||
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
reveal_type(y) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -3,45 +3,54 @@
|
||||
## Multiple negative contributions
|
||||
|
||||
```py
|
||||
def _(x: int):
|
||||
if x != 1:
|
||||
if x != 2:
|
||||
if x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
x = int_instance()
|
||||
|
||||
if x != 1:
|
||||
if x != 2:
|
||||
if x != 3:
|
||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||
```
|
||||
|
||||
## Multiple negative contributions with simplification
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x != 2:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
flag1, flag2 = bool_instance(), bool_instance()
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x != 2:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
## elif-else blocks
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2 if flag2 else 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != 1:
|
||||
x = 1 if bool_instance() else 2 if bool_instance() else 3
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x == 2:
|
||||
# TODO should be `Literal[2]`
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
if x == 2:
|
||||
# TODO should be `Literal[2]`
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
elif x == 3:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
elif x != 2:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
elif x == 3:
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
elif x != 2:
|
||||
# TODO should be Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1, 3]
|
||||
else:
|
||||
# TODO should be Never
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
@@ -5,25 +5,29 @@ The `not` operator negates a constraint.
|
||||
## `not is None`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if not x is None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
x = None if bool_instance() else 1
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
if not x is None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `not isinstance`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if not isinstance(x, (int)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = 1 if bool_instance() else "a"
|
||||
|
||||
if not isinstance(x, (int)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -3,66 +3,82 @@
|
||||
## `x != None`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
# TODO should be None
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
flag = bool_instance()
|
||||
x = None if flag else 1
|
||||
|
||||
if x != None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
# TODO should be None
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `!=` for other singleton types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = True if flag else False
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
# TODO should be Literal[False]
|
||||
reveal_type(x) # revealed: bool
|
||||
flag = bool_instance()
|
||||
x = True if flag else False
|
||||
|
||||
if x != False:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
# TODO should be Literal[False]
|
||||
reveal_type(x) # revealed: bool
|
||||
```
|
||||
|
||||
## `x != y` where `y` is of literal type
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else 2
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else 2
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## `x != y` where `y` is a single-valued type
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
class B: ...
|
||||
C = A if flag else B
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if C != A:
|
||||
reveal_type(C) # revealed: Literal[B]
|
||||
else:
|
||||
# TODO should be Literal[A]
|
||||
reveal_type(C) # revealed: Literal[A, B]
|
||||
flag = bool_instance()
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
C = A if flag else B
|
||||
|
||||
if C != A:
|
||||
reveal_type(C) # revealed: Literal[B]
|
||||
else:
|
||||
# TODO should be Literal[A]
|
||||
reveal_type(C) # revealed: Literal[A, B]
|
||||
```
|
||||
|
||||
## `x != y` where `y` has multiple single-valued options
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
x = 1 if flag1 else 2
|
||||
y = 2 if flag2 else 3
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
x = 1 if bool_instance() else 2
|
||||
y = 2 if bool_instance() else 3
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
# TODO should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## `!=` for non-single-valued types
|
||||
@@ -70,22 +86,34 @@ def _(flag1: bool, flag2: bool):
|
||||
Only single-valued types should narrow the type:
|
||||
|
||||
```py
|
||||
def _(flag: bool, a: int, y: int):
|
||||
x = a if flag else None
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: int | None
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
flag = bool_instance()
|
||||
x = int_instance() if flag else None
|
||||
y = int_instance()
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Mix of single-valued and non-single-valued types
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool, a: int):
|
||||
x = 1 if flag1 else 2
|
||||
y = 2 if flag2 else a
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = 1 if bool_instance() else 2
|
||||
y = 2 if bool_instance() else int_instance()
|
||||
|
||||
if x != y:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
@@ -5,19 +5,23 @@ Narrowing for `isinstance(object, classinfo)` expressions.
|
||||
## `classinfo` is a single type
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if isinstance(x, str):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(x, str):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
if isinstance(x, (int, object)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
## `classinfo` is a tuple of types
|
||||
@@ -26,48 +30,56 @@ Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, tup
|
||||
The former is equivalent to `isinstance(x, int | str)`:
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag1: bool, flag2: bool):
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if isinstance(x, (int, str)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance()
|
||||
|
||||
if isinstance(x, (int, bytes)):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (bytes, str)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
if isinstance(x, (int, str)):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
# 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] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
if isinstance(x, (int, bytes)):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||
if isinstance(y, (int, str)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal["a"]
|
||||
if isinstance(x, (bytes, str)):
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
|
||||
if isinstance(y, (int, bytes)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
|
||||
# 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] | Literal["a"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
if isinstance(y, (str, bytes)):
|
||||
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
|
||||
y = 1 if flag1 else "a" if flag2 else b"b"
|
||||
if isinstance(y, (int, str)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal["a"]
|
||||
|
||||
if isinstance(y, (int, bytes)):
|
||||
reveal_type(y) # revealed: Literal[1] | Literal[b"b"]
|
||||
|
||||
if isinstance(y, (str, bytes)):
|
||||
reveal_type(y) # revealed: Literal["a"] | Literal[b"b"]
|
||||
```
|
||||
|
||||
## `classinfo` is a nested tuple of types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if isinstance(x, (bool, (bytes, int))):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "a"
|
||||
|
||||
if isinstance(x, (bool, (bytes, int))):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
```
|
||||
|
||||
## Class types
|
||||
@@ -77,7 +89,9 @@ class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
x = object()
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
@@ -98,40 +112,50 @@ else:
|
||||
## No narrowing for instances of `builtins.type`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
t = type("t", (), {})
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
reveal_type(t) # revealed: type
|
||||
flag = bool_instance()
|
||||
|
||||
x = 1 if flag else "foo"
|
||||
t = type("t", (), {})
|
||||
|
||||
if isinstance(x, t):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
|
||||
# This isn't testing what we want it to test if we infer anything more precise here:
|
||||
reveal_type(t) # revealed: type
|
||||
x = 1 if flag else "foo"
|
||||
|
||||
if isinstance(x, t):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["foo"]
|
||||
```
|
||||
|
||||
## Do not use custom `isinstance` for narrowing
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
def isinstance(x, t):
|
||||
return True
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
flag = bool_instance()
|
||||
|
||||
def isinstance(x, t):
|
||||
return True
|
||||
|
||||
x = 1 if flag else "a"
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is aliased
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
isinstance_alias = isinstance
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = 1 if flag else "a"
|
||||
flag = bool_instance()
|
||||
|
||||
if isinstance_alias(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
isinstance_alias = isinstance
|
||||
|
||||
x = 1 if flag else "a"
|
||||
if isinstance_alias(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Do support narrowing if `isinstance` is imported
|
||||
@@ -139,38 +163,46 @@ def _(flag: bool):
|
||||
```py
|
||||
from builtins import isinstance as imported_isinstance
|
||||
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if imported_isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
if imported_isinstance(x, int):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Do not narrow if second argument is not a type
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "a"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "int"):
|
||||
reveal_type(x) # revealed: Literal[1] | Literal["a"]
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if isinstance(x, "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] | Literal["a"]
|
||||
```
|
||||
|
||||
## Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else "a"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
# 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] | Literal["a"]
|
||||
flag = bool_instance()
|
||||
x = 1 if flag else "a"
|
||||
|
||||
# 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] | Literal["a"]
|
||||
```
|
||||
|
||||
@@ -7,43 +7,45 @@ Narrowing for `issubclass(class, classinfo)` expressions.
|
||||
### Basic example
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
t = int if flag else str
|
||||
def flag() -> bool: ...
|
||||
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
t = int if flag() else str
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Proper narrowing in `elif` and `else` branches
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
t = int if flag1 else str if flag2 else bytes
|
||||
def flag() -> bool: ...
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
```
|
||||
|
||||
### Multiple derived classes
|
||||
@@ -54,28 +56,29 @@ class Derived1(Base): ...
|
||||
class Derived2(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _(flag1: bool, flag2: bool, flag3: bool):
|
||||
t1 = Derived1 if flag1 else Derived2
|
||||
def flag() -> bool: ...
|
||||
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
t1 = Derived1 if flag() else Derived2
|
||||
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
|
||||
t2 = Derived1 if flag2 else Base
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
t2 = Derived1 if flag() else Base
|
||||
|
||||
t3 = Derived1 if flag3 else Unrelated
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
t3 = Derived1 if flag() else Unrelated
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
```
|
||||
|
||||
### Narrowing for non-literals
|
||||
@@ -84,13 +87,16 @@ def _(flag1: bool, flag2: bool, flag3: bool):
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(t: type[object]):
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
def get_class() -> type[object]: ...
|
||||
|
||||
t = get_class()
|
||||
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
```
|
||||
|
||||
### Handling of `None`
|
||||
@@ -101,15 +107,16 @@ def _(t: type[object]):
|
||||
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
|
||||
from types import NoneType
|
||||
|
||||
def _(flag: bool):
|
||||
t = int if flag else NoneType
|
||||
def flag() -> bool: ...
|
||||
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
t = int if flag() else NoneType
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
@@ -119,13 +126,14 @@ def _(flag: bool):
|
||||
```py
|
||||
class Unrelated: ...
|
||||
|
||||
def _(flag1: bool, flag2: bool):
|
||||
t = int if flag1 else str if flag2 else bytes
|
||||
def flag() -> bool: ...
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
## Special cases
|
||||
@@ -140,7 +148,9 @@ to `issubclass`:
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
t = object()
|
||||
def get_object() -> object: ...
|
||||
|
||||
t = get_object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, A):
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
flag = bool_instance()
|
||||
|
||||
y = 0
|
||||
x = None if flag else 1
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
|
||||
match x:
|
||||
case None:
|
||||
y = x
|
||||
y = 0
|
||||
|
||||
reveal_type(y) # revealed: Literal[0] | None
|
||||
match x:
|
||||
case None:
|
||||
y = x
|
||||
|
||||
reveal_type(y) # revealed: Literal[0] | None
|
||||
```
|
||||
|
||||
@@ -3,49 +3,62 @@
|
||||
## After if-else statements, narrowing has no effect if the variable is not mutated in any branch
|
||||
|
||||
```py
|
||||
def _(x: int | None):
|
||||
if x is None:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
reveal_type(x) # revealed: int | None
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Narrowing can have a persistent effect if the variable is mutated in one branch
|
||||
|
||||
```py
|
||||
def _(x: int | None):
|
||||
if x is None:
|
||||
x = 10
|
||||
else:
|
||||
pass
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
x = 10
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## An if statement without an explicit `else` branch is equivalent to one with a no-op `else` branch
|
||||
|
||||
```py
|
||||
def _(x: int | None, y: int | None):
|
||||
if x is None:
|
||||
x = 0
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
if y is None:
|
||||
pass
|
||||
x = optional_int()
|
||||
y = optional_int()
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: int | None
|
||||
if x is None:
|
||||
x = 0
|
||||
|
||||
if y is None:
|
||||
pass
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: int | None
|
||||
```
|
||||
|
||||
## An if-elif without an explicit else branch is equivalent to one with an empty else branch
|
||||
|
||||
```py
|
||||
def _(x: int | None):
|
||||
if x is None:
|
||||
x = 0
|
||||
elif x > 50:
|
||||
x = 50
|
||||
def optional_int() -> int | None: ...
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
x = optional_int()
|
||||
|
||||
if x is None:
|
||||
x = 0
|
||||
elif x > 50:
|
||||
x = 50
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
@@ -6,14 +6,18 @@
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: A | B):
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
# It would be wrong to infer `B` here. The type
|
||||
# of `x` could be a subclass of `A`, so we need
|
||||
# to infer the full union type:
|
||||
reveal_type(x) # revealed: A | B
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
# It would be wrong to infer `B` here. The type
|
||||
# of `x` could be a subclass of `A`, so we need
|
||||
# to infer the full union type:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## `type(x) is not C`
|
||||
@@ -22,12 +26,16 @@ def _(x: A | B):
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: A | B):
|
||||
if type(x) is not A:
|
||||
# Same reasoning as above: no narrowing should occur here.
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is not A:
|
||||
# Same reasoning as above: no narrowing should occur here.
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## `type(x) == C`, `type(x) != C`
|
||||
@@ -46,12 +54,16 @@ class IsEqualToEverything(type):
|
||||
class A(metaclass=IsEqualToEverything): ...
|
||||
class B(metaclass=IsEqualToEverything): ...
|
||||
|
||||
def _(x: A | B):
|
||||
if type(x) == A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
def get_a_or_b() -> A | B:
|
||||
return B()
|
||||
|
||||
if type(x) != A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) == A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if type(x) != A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for custom `type` callable
|
||||
@@ -63,11 +75,15 @@ class B: ...
|
||||
def type(x):
|
||||
return int
|
||||
|
||||
def _(x: A | B):
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if type(x) is A:
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## No narrowing for multiple arguments
|
||||
@@ -75,11 +91,15 @@ def _(x: A | B):
|
||||
No narrowing should occur if `type` is used to dynamically create a class:
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## No narrowing for keyword arguments
|
||||
@@ -87,10 +107,14 @@ def _(x: str | int):
|
||||
`type` can't be used with a keyword argument:
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
# TODO: we could issue a diagnostic here
|
||||
if type(object=x) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
def get_str_or_int() -> str | int:
|
||||
return "test"
|
||||
|
||||
x = get_str_or_int()
|
||||
|
||||
# TODO: we could issue a diagnostic here
|
||||
if type(object=x) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
## Narrowing if `type` is aliased
|
||||
@@ -101,9 +125,13 @@ class B: ...
|
||||
|
||||
alias_for_type = type
|
||||
|
||||
def _(x: A | B):
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
def get_a_or_b() -> A | B:
|
||||
return A()
|
||||
|
||||
x = get_a_or_b()
|
||||
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
||||
## Limitations
|
||||
@@ -112,9 +140,13 @@ def _(x: A | B):
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
|
||||
def _(x: Base):
|
||||
if type(x) is Base:
|
||||
# Ideally, this could be narrower, but there is now way to
|
||||
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
||||
reveal_type(x) # revealed: Base
|
||||
def get_base() -> Base:
|
||||
return Base()
|
||||
|
||||
x = get_base()
|
||||
|
||||
if type(x) is Base:
|
||||
# Ideally, this could be narrower, but there is now way to
|
||||
# express a constraint like `Base & ~ProperSubtypeOf[Base]`.
|
||||
reveal_type(x) # revealed: Base
|
||||
```
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Narrowing in `while` loops
|
||||
|
||||
We only make sure that narrowing works for `while` loops in general, we do not exhaustively test all
|
||||
narrowing forms here, as they are covered in other tests.
|
||||
|
||||
Note how type narrowing works subtly different from `if` ... `else`, because the negated constraint
|
||||
is retained after the loop.
|
||||
|
||||
## Basic `while` loop
|
||||
|
||||
```py
|
||||
def next_item() -> int | None: ...
|
||||
|
||||
x = next_item()
|
||||
|
||||
while x is not None:
|
||||
reveal_type(x) # revealed: int
|
||||
x = next_item()
|
||||
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
## `while` loop with `else`
|
||||
|
||||
```py
|
||||
def next_item() -> int | None: ...
|
||||
|
||||
x = next_item()
|
||||
|
||||
while x is not None:
|
||||
reveal_type(x) # revealed: int
|
||||
x = next_item()
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
## Nested `while` loops
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def next_item() -> Literal[1, 2, 3]: ...
|
||||
|
||||
x = next_item()
|
||||
|
||||
while x != 1:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
|
||||
while x != 2:
|
||||
# TODO: this should be Literal[1, 3]; Literal[3] is only correct
|
||||
# in the first loop iteration
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
x = next_item()
|
||||
|
||||
x = next_item()
|
||||
```
|
||||
@@ -10,10 +10,10 @@ def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
chr = 1
|
||||
copyright = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | Literal[1]
|
||||
reveal_type(copyright) # revealed: Literal[copyright] | Literal[1]
|
||||
```
|
||||
|
||||
## Conditionally global or builtin, with annotation
|
||||
@@ -25,8 +25,8 @@ def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
chr: int = 1
|
||||
copyright: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
reveal_type(copyright) # revealed: Literal[copyright] | int
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
reveal_type(typing.__class__) # revealed: Literal[type]
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = coinflip()
|
||||
flag = bool_instance()
|
||||
x = 1
|
||||
|
||||
class C:
|
||||
@@ -24,14 +24,14 @@ reveal_type(C.y) # revealed: Literal[1]
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
if bool_instance():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if coinflip():
|
||||
if bool_instance():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
## Shadow after incompatible declarations is OK
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x: bytes = b"foo"
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
x: bytes = b"foo"
|
||||
```
|
||||
|
||||
@@ -22,10 +22,12 @@ reveal_type(x) # revealed: Unknown
|
||||
y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5"
|
||||
reveal_type(y) # revealed: Unknown
|
||||
|
||||
def _(n: int):
|
||||
a = b"abcde"[n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
def int_instance() -> int:
|
||||
return 42
|
||||
|
||||
a = b"abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -41,13 +43,15 @@ b[:4:0] # error: [zero-stepsize-in-slice]
|
||||
b[0::0] # error: [zero-stepsize-in-slice]
|
||||
b[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def _(m: int, n: int):
|
||||
byte_slice1 = b[m:n]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
def int_instance() -> int: ...
|
||||
|
||||
def _(s: bytes) -> bytes:
|
||||
byte_slice2 = s[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
byte_slice1 = b[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
|
||||
def bytes_instance() -> bytes: ...
|
||||
|
||||
byte_slice2 = bytes_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
@@ -21,66 +21,77 @@ reveal_type(Identity[0]) # revealed: str
|
||||
## Class getitem union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class UnionClassGetItem:
|
||||
if flag:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
else:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
class UnionClassGetItem:
|
||||
if bool_instance():
|
||||
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
else:
|
||||
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with class union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
return item
|
||||
|
||||
x = A if flag else B
|
||||
class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
x = A if bool_instance() else B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
```
|
||||
|
||||
## Class getitem with unbound method union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
class Spam:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
else:
|
||||
class Spam: ...
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||
# revealed: str
|
||||
reveal_type(Spam[42])
|
||||
if bool_instance():
|
||||
class Spam:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
else:
|
||||
class Spam: ...
|
||||
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||
# revealed: str
|
||||
reveal_type(Spam[42])
|
||||
```
|
||||
|
||||
## TODO: Class getitem non-class union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
class Eggs:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
else:
|
||||
Eggs = 1
|
||||
if bool_instance():
|
||||
class Eggs:
|
||||
def __class_getitem__(self, x: int) -> str:
|
||||
return "foo"
|
||||
|
||||
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
|
||||
else:
|
||||
Eggs = 1
|
||||
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
a = Eggs[42] # error: "Cannot subscript object of type `Literal[Eggs] | Literal[1]` with no `__getitem__` method"
|
||||
|
||||
# TODO: should _probably_ emit `str | Unknown`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -30,14 +30,18 @@ reveal_type(Identity()[0]) # revealed: int
|
||||
## Getitem union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Identity:
|
||||
if flag:
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
else:
|
||||
def __getitem__(self, index: int) -> str:
|
||||
return str(index)
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(Identity()[0]) # revealed: int | str
|
||||
class Identity:
|
||||
if bool_instance():
|
||||
|
||||
def __getitem__(self, index: int) -> int:
|
||||
return index
|
||||
else:
|
||||
|
||||
def __getitem__(self, index: int) -> str:
|
||||
return str(index)
|
||||
|
||||
reveal_type(Identity()[0]) # revealed: int | str
|
||||
```
|
||||
|
||||
@@ -19,10 +19,11 @@ reveal_type(a) # revealed: Unknown
|
||||
b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
def _(n: int):
|
||||
a = "abcde"[n]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
def int_instance() -> int: ...
|
||||
|
||||
a = "abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -73,14 +74,17 @@ s[:4:0] # error: [zero-stepsize-in-slice]
|
||||
s[0::0] # error: [zero-stepsize-in-slice]
|
||||
s[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def _(m: int, n: int, s2: str):
|
||||
substring1 = s[m:n]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
def int_instance() -> int: ...
|
||||
|
||||
substring2 = s2[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
substring1 = s[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
|
||||
def str_instance() -> str: ...
|
||||
|
||||
substring2 = str_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
@@ -67,60 +67,9 @@ t[:4:0] # error: [zero-stepsize-in-slice]
|
||||
t[0::0] # error: [zero-stepsize-in-slice]
|
||||
t[::0] # error: [zero-stepsize-in-slice]
|
||||
|
||||
def _(m: int, n: int):
|
||||
tuple_slice = t[m:n]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO:
|
||||
# * `tuple.__class_getitem__` is always bound on 3.9 (`sys.version_info`)
|
||||
# * `tuple[int, str]` is a valid base (generics)
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
|
||||
# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class A(tuple[int, str]): ...
|
||||
|
||||
# Runtime value: `(A, tuple, object)`
|
||||
# TODO: Generics
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `typing.Tuple`
|
||||
|
||||
### Correspondence with `tuple`
|
||||
|
||||
`typing.Tuple` can be used interchangeably with `tuple`:
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
class A: ...
|
||||
|
||||
def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]):
|
||||
reveal_type(c) # revealed: tuple
|
||||
reveal_type(d) # revealed: tuple[int, A]
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
### Inheritance
|
||||
|
||||
Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself
|
||||
is not a class.
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
class C(Tuple): ...
|
||||
|
||||
# Runtime value: `(C, tuple, typing.Generic, object)`
|
||||
# TODO: Add `Generic` to the MRO
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[tuple], Unknown, Literal[object]]
|
||||
def int_instance() -> int: ...
|
||||
|
||||
tuple_slice = t[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
target-version = "3.9"
|
||||
```
|
||||
|
||||
## The type of `sys.version_info`
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
# PEP 695 type aliases
|
||||
|
||||
PEP 695 type aliases are only available in Python 3.12 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
# Type aliases
|
||||
|
||||
## Basic
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def _(c: type[A]):
|
||||
reveal_type(c) # revealed: type[A]
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
```
|
||||
|
||||
## Nested class literal
|
||||
@@ -15,8 +17,10 @@ def _(c: type[A]):
|
||||
class A:
|
||||
class B: ...
|
||||
|
||||
def f(c: type[A.B]):
|
||||
reveal_type(c) # revealed: type[B]
|
||||
def f() -> type[A.B]:
|
||||
return A.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
```
|
||||
|
||||
## Deeply nested class literal
|
||||
@@ -26,8 +30,10 @@ class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f(c: type[A.B.C]):
|
||||
reveal_type(c) # revealed: type[C]
|
||||
def f() -> type[A.B.C]:
|
||||
return A.B.C
|
||||
|
||||
reveal_type(f()) # revealed: type[C]
|
||||
```
|
||||
|
||||
## Class literal from another module
|
||||
@@ -35,8 +41,10 @@ def f(c: type[A.B.C]):
|
||||
```py
|
||||
from a import A
|
||||
|
||||
def f(c: type[A]):
|
||||
reveal_type(c) # revealed: type[A]
|
||||
def f() -> type[A]:
|
||||
return A
|
||||
|
||||
reveal_type(f()) # revealed: type[A]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
@@ -48,8 +56,10 @@ class A: ...
|
||||
```py
|
||||
import a
|
||||
|
||||
def f(c: type[a.B]):
|
||||
reveal_type(c) # revealed: type[B]
|
||||
def f() -> type[a.B]:
|
||||
return a.B
|
||||
|
||||
reveal_type(f()) # revealed: type[B]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
@@ -63,8 +73,12 @@ import a.b
|
||||
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
def f(c: type[a.b.C]):
|
||||
reveal_type(c) # revealed: @Todo(unsupported type[X] special form)
|
||||
def f() -> type[a.b.C]:
|
||||
# TODO: no diagnostic
|
||||
# error: [unresolved-attribute]
|
||||
return a.b.C
|
||||
|
||||
reveal_type(f()) # revealed: @Todo(unsupported type[X] special form)
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
@@ -74,7 +88,7 @@ def f(c: type[a.b.C]):
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## New-style union of classes
|
||||
## Union of classes
|
||||
|
||||
```py
|
||||
class BasicUser: ...
|
||||
@@ -84,44 +98,11 @@ class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def _(u: type[BasicUser | ProUser | A.B.C]):
|
||||
# revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(u)
|
||||
```
|
||||
def get_user() -> type[BasicUser | ProUser | A.B.C]:
|
||||
return BasicUser
|
||||
|
||||
## Old-style union of classes
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
class BasicUser: ...
|
||||
class ProUser: ...
|
||||
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]):
|
||||
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(b) # revealed: type[str]
|
||||
reveal_type(c) # revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
```
|
||||
|
||||
## New-style and old-style unions in combination
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
class BasicUser: ...
|
||||
class ProUser: ...
|
||||
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]):
|
||||
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str]
|
||||
# revealed: type[BasicUser] | type[ProUser] | type[C]
|
||||
reveal_type(get_user())
|
||||
```
|
||||
|
||||
## Illegal parameters
|
||||
@@ -131,16 +112,6 @@ class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: [invalid-type-form]
|
||||
_: type[A, B]
|
||||
```
|
||||
|
||||
## As a base class
|
||||
|
||||
```py
|
||||
# TODO: this is a false positive
|
||||
# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
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]]
|
||||
def get_user() -> type[A, B]:
|
||||
return A
|
||||
```
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
# `type[Any]`
|
||||
|
||||
This file contains tests for non-fully-static `type[]` types, such as `type[Any]` and
|
||||
`type[Unknown]`.
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f(x: type[Any], y: type[str]):
|
||||
reveal_type(x) # revealed: type[Any]
|
||||
# TODO: could be `<object.__repr__ type> & Any`
|
||||
reveal_type(x.__repr__) # revealed: Any
|
||||
|
||||
# type[str] and type[Any] are assignable to each other
|
||||
a: type[str] = x
|
||||
b: type[Any] = y
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type[Any] = object
|
||||
x: type[Any] = type
|
||||
x: type[Any] = A
|
||||
x: type[Any] = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Bare type
|
||||
|
||||
The interpretation of bare `type` is not clear: existing wording in the spec does not match the
|
||||
behavior of mypy or pyright. For now we interpret it as simply "an instance of `builtins.type`",
|
||||
which is equivalent to `type[object]`. This is similar to the current behavior of mypy, and pyright
|
||||
in strict mode.
|
||||
|
||||
```py
|
||||
def f(x: type):
|
||||
reveal_type(x) # revealed: type
|
||||
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type = object
|
||||
x: type = type
|
||||
x: type = A
|
||||
x: type = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## type[object] != type[Any]
|
||||
|
||||
```py
|
||||
def f(x: type[object]):
|
||||
reveal_type(x) # revealed: type[object]
|
||||
# TODO: bound method types
|
||||
reveal_type(x.__repr__) # revealed: Literal[__repr__]
|
||||
|
||||
class A: ...
|
||||
|
||||
x: type[object] = object
|
||||
x: type[object] = type
|
||||
x: type[object] = A
|
||||
x: type[object] = A() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## The type of `Any` is `type[Any]`
|
||||
|
||||
`Any` represents an unknown set of possible runtime values. If `x` is of type `Any`, the type of
|
||||
`x.__class__` is also unknown and remains dynamic, *except* that we know it must be a class object
|
||||
of some kind. As such, the type of `x.__class__` is `type[Any]` rather than `Any`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from does_not_exist import SomethingUnknown # error: [unresolved-import]
|
||||
|
||||
reveal_type(SomethingUnknown) # revealed: Unknown
|
||||
|
||||
def test(x: Any, y: SomethingUnknown):
|
||||
reveal_type(x.__class__) # revealed: type[Any]
|
||||
reveal_type(x.__class__.__class__.__class__.__class__) # revealed: type[Any]
|
||||
reveal_type(y.__class__) # revealed: type[Unknown]
|
||||
reveal_type(y.__class__.__class__.__class__.__class__) # revealed: type[Unknown]
|
||||
```
|
||||
|
||||
## `type[Unknown]` has similar properties to `type[Any]`
|
||||
|
||||
```py
|
||||
import abc
|
||||
from typing import Any
|
||||
from does_not_exist import SomethingUnknown # error: [unresolved-import]
|
||||
|
||||
has_unknown_type = SomethingUnknown.__class__
|
||||
reveal_type(has_unknown_type) # revealed: type[Unknown]
|
||||
|
||||
def test(x: type[str], y: type[Any]):
|
||||
"""Both `type[Any]` and `type[Unknown]` are assignable to all `type[]` types"""
|
||||
a: type[Any] = x
|
||||
b: type[str] = y
|
||||
c: type[Any] = has_unknown_type
|
||||
d: type[str] = has_unknown_type
|
||||
|
||||
def test2(a: type[Any]):
|
||||
"""`type[Any]` and `type[Unknown]` are also assignable to all instances of `type` subclasses"""
|
||||
b: abc.ABCMeta = a
|
||||
b: abc.ABCMeta = has_unknown_type
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
# `typing.Type`
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Type` can be used interchangeably with `type`:
|
||||
|
||||
```py
|
||||
from typing import Type
|
||||
|
||||
class A: ...
|
||||
|
||||
def _(c: Type, d: Type[A]):
|
||||
reveal_type(c) # revealed: type
|
||||
reveal_type(d) # revealed: type[A]
|
||||
c = d # fine
|
||||
d = c # fine
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
Inheriting from `Type` results in a MRO with `builtins.type` and `typing.Generic`. `Type` itself is
|
||||
not a class.
|
||||
|
||||
```py
|
||||
from typing import Type
|
||||
|
||||
class C(Type): ...
|
||||
|
||||
# Runtime value: `(C, type, typing.Generic, object)`
|
||||
# TODO: Add `Generic` to the MRO
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[type], Literal[object]]
|
||||
```
|
||||
@@ -35,25 +35,29 @@ y = 1
|
||||
## Union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
p = 1
|
||||
q = 3.3
|
||||
r = "hello"
|
||||
s = "world"
|
||||
t = 0
|
||||
else:
|
||||
p = "hello"
|
||||
q = 4
|
||||
r = ""
|
||||
s = 0
|
||||
t = ""
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(not p) # revealed: Literal[False]
|
||||
reveal_type(not q) # revealed: bool
|
||||
reveal_type(not r) # revealed: bool
|
||||
reveal_type(not s) # revealed: bool
|
||||
reveal_type(not t) # revealed: Literal[True]
|
||||
flag = bool_instance()
|
||||
|
||||
if flag:
|
||||
p = 1
|
||||
q = 3.3
|
||||
r = "hello"
|
||||
s = "world"
|
||||
t = 0
|
||||
else:
|
||||
p = "hello"
|
||||
q = 4
|
||||
r = ""
|
||||
s = 0
|
||||
t = ""
|
||||
|
||||
reveal_type(not p) # revealed: Literal[False]
|
||||
reveal_type(not q) # revealed: bool
|
||||
reveal_type(not r) # revealed: bool
|
||||
reveal_type(not s) # revealed: bool
|
||||
reveal_type(not t) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Integer literal
|
||||
|
||||
@@ -21,23 +21,25 @@ with Manager() as f:
|
||||
## Union context manager
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Manager1:
|
||||
def __enter__(self) -> str:
|
||||
return "foo"
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
class Manager1:
|
||||
def __enter__(self) -> str:
|
||||
return "foo"
|
||||
|
||||
class Manager2:
|
||||
def __enter__(self) -> int:
|
||||
return 42
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
class Manager2:
|
||||
def __enter__(self) -> int:
|
||||
return 42
|
||||
|
||||
context_expr = Manager1() if flag else Manager2()
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str | int
|
||||
context_expr = Manager1() if coinflip() else Manager2()
|
||||
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str | int
|
||||
```
|
||||
|
||||
## Context manager without an `__enter__` or `__exit__` method
|
||||
@@ -101,34 +103,39 @@ with Manager():
|
||||
## Context expression with possibly-unbound union variants
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Manager1:
|
||||
def __enter__(self) -> str:
|
||||
return "foo"
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
class Manager1:
|
||||
def __enter__(self) -> str:
|
||||
return "foo"
|
||||
|
||||
class NotAContextManager: ...
|
||||
context_expr = Manager1() if flag else NotAContextManager()
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
class NotAContextManager: ...
|
||||
|
||||
context_expr = Manager1() if coinflip() else NotAContextManager()
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Context expression with "sometimes" callable `__enter__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Manager:
|
||||
if flag:
|
||||
def __enter__(self) -> str:
|
||||
return "abcd"
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
def __exit__(self, *args): ...
|
||||
class Manager:
|
||||
if coinflip():
|
||||
def __enter__(self) -> str:
|
||||
return "abcd"
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
with Manager() as f:
|
||||
reveal_type(f) # revealed: str
|
||||
def __exit__(self, *args): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
with Manager() as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
@@ -166,12 +166,12 @@ pub(crate) mod tests {
|
||||
.context("Failed to write test files")?;
|
||||
|
||||
let mut search_paths = SearchPathSettings::new(src_root);
|
||||
search_paths.typeshed = self.custom_typeshed;
|
||||
search_paths.custom_typeshed = self.custom_typeshed;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: self.python_version,
|
||||
target_version: self.python_version,
|
||||
search_paths,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -29,14 +29,12 @@ mod util;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
/// Creates a new registry with all known semantic lints.
|
||||
pub fn default_lint_registry() -> LintRegistry {
|
||||
let mut registry = LintRegistryBuilder::default();
|
||||
register_lints(&mut registry);
|
||||
register_semantic_lints(&mut registry);
|
||||
registry.build()
|
||||
}
|
||||
|
||||
/// Register all known semantic lints.
|
||||
pub fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
types::register_lints(registry);
|
||||
pub fn register_semantic_lints(registry: &mut LintRegistryBuilder) {
|
||||
types::register_type_lints(registry);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,8 @@ pub struct LintMetadata {
|
||||
|
||||
pub status: LintStatus,
|
||||
|
||||
/// The source file in which the lint is declared.
|
||||
pub file: &'static str,
|
||||
|
||||
/// The 1-based line number in the source `file` where the lint is declared.
|
||||
pub line: u32,
|
||||
/// Location where this lint is declared: `file_name:line`
|
||||
pub source: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
@@ -77,7 +74,7 @@ impl LintMetadata {
|
||||
self.summary
|
||||
}
|
||||
|
||||
/// Returns the documentation line by line with one leading space and all trailing whitespace removed.
|
||||
/// Returns the documentation line by line with leading and trailing whitespace removed.
|
||||
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
|
||||
self.raw_documentation
|
||||
.lines()
|
||||
@@ -97,12 +94,8 @@ impl LintMetadata {
|
||||
&self.status
|
||||
}
|
||||
|
||||
pub fn file(&self) -> &str {
|
||||
self.file
|
||||
}
|
||||
|
||||
pub fn line(&self) -> u32 {
|
||||
self.line
|
||||
pub fn source(&self) -> &str {
|
||||
self.source
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,43 +107,30 @@ pub const fn lint_metadata_defaults() -> LintMetadata {
|
||||
raw_documentation: "",
|
||||
default_level: Level::Error,
|
||||
status: LintStatus::preview("0.0.0"),
|
||||
file: "",
|
||||
line: 1,
|
||||
source: "",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum LintStatus {
|
||||
/// The lint has been added to the linter, but is not yet stable.
|
||||
/// The rule has been added to the linter, but is not yet stable.
|
||||
Preview {
|
||||
/// The version in which the lint was added.
|
||||
/// When the rule was added to preview
|
||||
since: &'static str,
|
||||
},
|
||||
|
||||
/// The lint is stable.
|
||||
Stable {
|
||||
/// The version in which the lint was stabilized.
|
||||
since: &'static str,
|
||||
},
|
||||
/// Stable rule that was added in the version defined by `since`.
|
||||
Stable { since: &'static str },
|
||||
|
||||
/// The lint is deprecated and no longer recommended for use.
|
||||
/// The rule has been deprecated [`since`] (version) and will be removed in the future.
|
||||
Deprecated {
|
||||
/// The version in which the lint was deprecated.
|
||||
since: &'static str,
|
||||
|
||||
/// The reason why the lint has been deprecated.
|
||||
///
|
||||
/// This should explain why the lint has been deprecated and if there's a replacement lint that users
|
||||
/// can use instead.
|
||||
reason: &'static str,
|
||||
},
|
||||
|
||||
/// The lint has been removed and can no longer be used.
|
||||
/// The rule has been removed [`since`] (version) and using it will result in an error.
|
||||
Removed {
|
||||
/// The version in which the lint was removed.
|
||||
since: &'static str,
|
||||
|
||||
/// The reason why the lint has been removed.
|
||||
reason: &'static str,
|
||||
},
|
||||
}
|
||||
@@ -177,31 +157,6 @@ impl LintStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Declares a lint rule with the given metadata.
|
||||
///
|
||||
/// ```rust
|
||||
/// use red_knot_python_semantic::declare_lint;
|
||||
/// use red_knot_python_semantic::lint::{LintStatus, Level};
|
||||
///
|
||||
/// declare_lint! {
|
||||
/// /// ## What it does
|
||||
/// /// Checks for references to names that are not defined.
|
||||
/// ///
|
||||
/// /// ## Why is this bad?
|
||||
/// /// Using an undefined variable will raise a `NameError` at runtime.
|
||||
/// ///
|
||||
/// /// ## Example
|
||||
/// ///
|
||||
/// /// ```python
|
||||
/// /// print(x) # NameError: name 'x' is not defined
|
||||
/// /// ```
|
||||
/// pub(crate) static UNRESOLVED_REFERENCE = {
|
||||
/// summary: "detects references to names that are not defined",
|
||||
/// status: LintStatus::preview("1.0.0"),
|
||||
/// default_level: Level::Warn,
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! declare_lint {
|
||||
(
|
||||
@@ -220,8 +175,7 @@ macro_rules! declare_lint {
|
||||
summary: $summary,
|
||||
raw_documentation: concat!($($doc,)+ "\n"),
|
||||
status: $status,
|
||||
file: file!(),
|
||||
line: line!(),
|
||||
source: concat!(file!(), ":", line!()),
|
||||
$( $key: $value, )*
|
||||
..$crate::lint::lint_metadata_defaults()
|
||||
};
|
||||
@@ -352,7 +306,7 @@ impl LintRegistry {
|
||||
/// Returns an iterator over all known aliases and to their target lints.
|
||||
///
|
||||
/// This iterator includes aliases that point to removed lints.
|
||||
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + '_ {
|
||||
pub fn aliases(&self) -> impl Iterator<Item = (LintName, LintId)> + use<'_> {
|
||||
self.by_name.iter().filter_map(|(key, value)| {
|
||||
if let LintEntry::Alias(alias) = value {
|
||||
Some((LintName::of(key), *alias))
|
||||
@@ -363,7 +317,7 @@ impl LintRegistry {
|
||||
}
|
||||
|
||||
/// Iterates over all removed lints.
|
||||
pub fn removed(&self) -> impl Iterator<Item = LintId> + '_ {
|
||||
pub fn removed(&self) -> impl Iterator<Item = LintId> + use<'_> {
|
||||
self.by_name.iter().filter_map(|(_, value)| {
|
||||
if let LintEntry::Removed(metadata) = value {
|
||||
Some(*metadata)
|
||||
@@ -430,12 +384,12 @@ impl RuleSelection {
|
||||
}
|
||||
|
||||
/// Returns an iterator over all enabled lints.
|
||||
pub fn enabled(&self) -> impl Iterator<Item = LintId> + '_ {
|
||||
pub fn enabled(&self) -> impl Iterator<Item = LintId> + use<'_> {
|
||||
self.lints.keys().copied()
|
||||
}
|
||||
|
||||
/// Returns an iterator over all enabled lints and their severity.
|
||||
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + '_ {
|
||||
pub fn iter(&self) -> impl ExactSizeIterator<Item = (LintId, Severity)> + use<'_> {
|
||||
self.lints.iter().map(|(&lint, &severity)| (lint, severity))
|
||||
}
|
||||
|
||||
|
||||
@@ -283,9 +283,9 @@ fn query_stdlib_version(
|
||||
let Some(module_name) = stdlib_path_to_module_name(relative_path) else {
|
||||
return TypeshedVersionsQueryResult::DoesNotExist;
|
||||
};
|
||||
let ResolverContext { db, python_version } = context;
|
||||
let ResolverContext { db, target_version } = context;
|
||||
|
||||
typeshed_versions(*db).query_module(&module_name, *python_version)
|
||||
typeshed_versions(*db).query_module(&module_name, *target_version)
|
||||
}
|
||||
|
||||
/// Enumeration describing the various ways in which validation of a search path might fail.
|
||||
@@ -658,7 +658,7 @@ mod tests {
|
||||
let TestCase {
|
||||
db, src, stdlib, ..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
@@ -779,7 +779,7 @@ mod tests {
|
||||
#[should_panic(expected = "Extension must be `pyi`; got `py`")]
|
||||
fn stdlib_path_invalid_join_py() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
@@ -791,7 +791,7 @@ mod tests {
|
||||
#[should_panic(expected = "Extension must be `pyi`; got `rs`")]
|
||||
fn stdlib_path_invalid_join_rs() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
@@ -822,7 +822,7 @@ mod tests {
|
||||
#[test]
|
||||
fn relativize_stdlib_path_errors() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(MockedTypeshed::default())
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
|
||||
let root = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap();
|
||||
@@ -867,11 +867,11 @@ mod tests {
|
||||
|
||||
fn typeshed_test_case(
|
||||
typeshed: MockedTypeshed,
|
||||
python_version: PythonVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> (TestDb, SearchPath) {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(typeshed)
|
||||
.with_python_version(python_version)
|
||||
.with_custom_typeshed(typeshed)
|
||||
.with_target_version(target_version)
|
||||
.build();
|
||||
let stdlib = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap();
|
||||
(db, stdlib)
|
||||
|
||||
@@ -160,7 +160,7 @@ impl SearchPaths {
|
||||
let SearchPathSettings {
|
||||
extra_paths,
|
||||
src_root,
|
||||
typeshed,
|
||||
custom_typeshed,
|
||||
site_packages: site_packages_paths,
|
||||
} = settings;
|
||||
|
||||
@@ -180,13 +180,17 @@ impl SearchPaths {
|
||||
tracing::debug!("Adding first-party search path '{src_root}'");
|
||||
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
|
||||
|
||||
let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
|
||||
let typeshed = canonicalize(typeshed, system);
|
||||
tracing::debug!("Adding custom-stdlib search path '{typeshed}'");
|
||||
let (typeshed_versions, stdlib_path) = if let Some(custom_typeshed) = custom_typeshed {
|
||||
let custom_typeshed = canonicalize(custom_typeshed, system);
|
||||
tracing::debug!("Adding custom-stdlib search path '{custom_typeshed}'");
|
||||
|
||||
files.try_add_root(db.upcast(), &typeshed, FileRootKind::LibrarySearchPath);
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
&custom_typeshed,
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
|
||||
let versions_path = typeshed.join("stdlib/VERSIONS");
|
||||
let versions_path = custom_typeshed.join("stdlib/VERSIONS");
|
||||
|
||||
let versions_content = system.read_to_string(&versions_path).map_err(|error| {
|
||||
SearchPathValidationError::FailedToReadVersionsFile {
|
||||
@@ -197,7 +201,7 @@ impl SearchPaths {
|
||||
|
||||
let parsed: TypeshedVersions = versions_content.parse()?;
|
||||
|
||||
let search_path = SearchPath::custom_stdlib(db, &typeshed)?;
|
||||
let search_path = SearchPath::custom_stdlib(db, &custom_typeshed)?;
|
||||
|
||||
(parsed, search_path)
|
||||
} else {
|
||||
@@ -526,10 +530,10 @@ struct ModuleNameIngredient<'db> {
|
||||
/// attempt to resolve the module name
|
||||
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, ModuleKind)> {
|
||||
let program = Program::get(db);
|
||||
let python_version = program.python_version(db);
|
||||
let resolver_state = ResolverContext::new(db, python_version);
|
||||
let target_version = program.target_version(db);
|
||||
let resolver_state = ResolverContext::new(db, target_version);
|
||||
let is_builtin_module =
|
||||
ruff_python_stdlib::sys::is_builtin_module(python_version.minor, name.as_str());
|
||||
ruff_python_stdlib::sys::is_builtin_module(target_version.minor, name.as_str());
|
||||
|
||||
for search_path in search_paths(db) {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
@@ -686,12 +690,12 @@ impl PackageKind {
|
||||
|
||||
pub(super) struct ResolverContext<'db> {
|
||||
pub(super) db: &'db dyn Db,
|
||||
pub(super) python_version: PythonVersion,
|
||||
pub(super) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl<'db> ResolverContext<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, python_version: PythonVersion) -> Self {
|
||||
Self { db, python_version }
|
||||
pub(super) fn new(db: &'db dyn Db, target_version: PythonVersion) -> Self {
|
||||
Self { db, target_version }
|
||||
}
|
||||
|
||||
pub(super) fn vendored(&self) -> &VendoredFileSystem {
|
||||
@@ -767,8 +771,8 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
@@ -785,8 +789,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -838,8 +842,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
@@ -883,8 +887,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&[
|
||||
@@ -927,8 +931,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY39)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&[
|
||||
@@ -969,8 +973,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY39)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
|
||||
@@ -993,8 +997,8 @@ mod tests {
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1018,7 +1022,7 @@ mod tests {
|
||||
fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_vendored_typeshed()
|
||||
.with_python_version(PythonVersion::default())
|
||||
.with_target_version(PythonVersion::default())
|
||||
.build();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
@@ -1286,11 +1290,11 @@ mod tests {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::PY38,
|
||||
target_version: PythonVersion::PY38,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
typeshed: Some(custom_typeshed),
|
||||
custom_typeshed: Some(custom_typeshed),
|
||||
site_packages: SitePackages::Known(vec![site_packages]),
|
||||
},
|
||||
},
|
||||
@@ -1329,7 +1333,7 @@ mod tests {
|
||||
fn deleting_an_unrelated_file_doesnt_change_module_resolution() {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")])
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
@@ -1416,8 +1420,8 @@ mod tests {
|
||||
site_packages,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1464,8 +1468,8 @@ mod tests {
|
||||
src,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1504,8 +1508,8 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_mocked_typeshed(TYPESHED)
|
||||
.with_python_version(PythonVersion::PY38)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1791,11 +1795,11 @@ not_a_directory
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version: PythonVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
typeshed: None,
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![
|
||||
venv_site_packages,
|
||||
system_site_packages,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub(crate) struct TestCase<T> {
|
||||
// so this is a single directory instead of a `Vec` of directories,
|
||||
// like it is in `ruff_db::Program`.
|
||||
pub(crate) site_packages: SystemPathBuf,
|
||||
pub(crate) python_version: PythonVersion,
|
||||
pub(crate) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
/// A `(file_name, file_contents)` tuple
|
||||
@@ -67,7 +67,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// ```rs
|
||||
/// let test_case = TestCaseBuilder::new()
|
||||
/// .with_src_files(...)
|
||||
/// .with_python_version(...)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
@@ -85,13 +85,13 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// const TYPESHED = MockedTypeshed { ... };
|
||||
///
|
||||
/// let test_case = resolver_test_case()
|
||||
/// .with_mocked_typeshed(TYPESHED)
|
||||
/// .with_python_version(...)
|
||||
/// .with_custom_typeshed(TYPESHED)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
///
|
||||
/// let test_case2 = resolver_test_case()
|
||||
/// .with_vendored_typeshed()
|
||||
/// .with_python_version(...)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
@@ -100,7 +100,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// to `()`.
|
||||
pub(crate) struct TestCaseBuilder<T> {
|
||||
typeshed_option: T,
|
||||
python_version: PythonVersion,
|
||||
target_version: PythonVersion,
|
||||
first_party_files: Vec<FileSpec>,
|
||||
site_packages_files: Vec<FileSpec>,
|
||||
}
|
||||
@@ -118,9 +118,9 @@ impl<T> TestCaseBuilder<T> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the Python version the module resolver should assume
|
||||
pub(crate) fn with_python_version(mut self, python_version: PythonVersion) -> Self {
|
||||
self.python_version = python_version;
|
||||
/// Specify the target Python version the module resolver should assume
|
||||
pub(crate) fn with_target_version(mut self, target_version: PythonVersion) -> Self {
|
||||
self.target_version = target_version;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn new() -> TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
Self {
|
||||
typeshed_option: UnspecifiedTypeshed,
|
||||
python_version: PythonVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
first_party_files: vec![],
|
||||
site_packages_files: vec![],
|
||||
}
|
||||
@@ -156,33 +156,33 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn with_vendored_typeshed(self) -> TestCaseBuilder<VendoredTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use a mock typeshed directory for this test case
|
||||
pub(crate) fn with_mocked_typeshed(
|
||||
pub(crate) fn with_custom_typeshed(
|
||||
self,
|
||||
typeshed: MockedTypeshed,
|
||||
) -> TestCaseBuilder<MockedTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
|
||||
TestCaseBuilder {
|
||||
typeshed_option: typeshed,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
@@ -194,15 +194,15 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
src,
|
||||
stdlib: _,
|
||||
site_packages,
|
||||
python_version,
|
||||
} = self.with_mocked_typeshed(MockedTypeshed::default()).build();
|
||||
target_version,
|
||||
} = self.with_custom_typeshed(MockedTypeshed::default()).build();
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
src,
|
||||
stdlib: (),
|
||||
site_packages,
|
||||
python_version,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +211,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
pub(crate) fn build(self) -> TestCase<SystemPathBuf> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
@@ -226,11 +226,11 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
target_version,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
typeshed: Some(typeshed.clone()),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
},
|
||||
},
|
||||
@@ -242,7 +242,7 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
src,
|
||||
stdlib: typeshed.join("stdlib"),
|
||||
site_packages,
|
||||
python_version,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
pub(crate) fn build(self) -> TestCase<VendoredPathBuf> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
python_version,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
@@ -282,7 +282,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
python_version,
|
||||
target_version,
|
||||
search_paths: SearchPathSettings {
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
..SearchPathSettings::new(src.clone())
|
||||
@@ -296,7 +296,7 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
src,
|
||||
stdlib: VendoredPathBuf::from("stdlib"),
|
||||
site_packages,
|
||||
python_version,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +112,10 @@ impl TypeshedVersions {
|
||||
pub(in crate::module_resolver) fn query_module(
|
||||
&self,
|
||||
module: &ModuleName,
|
||||
python_version: PythonVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
if let Some(range) = self.exact(module) {
|
||||
if range.contains(python_version) {
|
||||
if range.contains(target_version) {
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
} else {
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
@@ -125,7 +125,7 @@ impl TypeshedVersions {
|
||||
while let Some(module_to_try) = module {
|
||||
if let Some(range) = self.exact(&module_to_try) {
|
||||
return {
|
||||
if range.contains(python_version) {
|
||||
if range.contains(target_version) {
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
} else {
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::Db;
|
||||
|
||||
#[salsa::input(singleton)]
|
||||
pub struct Program {
|
||||
pub python_version: PythonVersion,
|
||||
pub target_version: PythonVersion,
|
||||
|
||||
#[return_ref]
|
||||
pub(crate) search_paths: SearchPaths,
|
||||
@@ -19,16 +19,16 @@ pub struct Program {
|
||||
impl Program {
|
||||
pub fn from_settings(db: &dyn Db, settings: &ProgramSettings) -> anyhow::Result<Self> {
|
||||
let ProgramSettings {
|
||||
python_version,
|
||||
target_version,
|
||||
search_paths,
|
||||
} = settings;
|
||||
|
||||
tracing::info!("Python version: Python {python_version}");
|
||||
tracing::info!("Target version: Python {target_version}");
|
||||
|
||||
let search_paths = SearchPaths::from_settings(db, search_paths)
|
||||
.with_context(|| "Invalid search path settings")?;
|
||||
|
||||
Ok(Program::builder(settings.python_version, search_paths)
|
||||
Ok(Program::builder(settings.target_version, search_paths)
|
||||
.durability(Durability::HIGH)
|
||||
.new(db))
|
||||
}
|
||||
@@ -56,7 +56,7 @@ impl Program {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct ProgramSettings {
|
||||
pub python_version: PythonVersion,
|
||||
pub target_version: PythonVersion,
|
||||
pub search_paths: SearchPathSettings,
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ pub struct SearchPathSettings {
|
||||
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
pub typeshed: Option<SystemPathBuf>,
|
||||
pub custom_typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
pub site_packages: SitePackages,
|
||||
@@ -86,7 +86,7 @@ impl SearchPathSettings {
|
||||
Self {
|
||||
src_root,
|
||||
extra_paths: vec![],
|
||||
typeshed: None,
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::fmt;
|
||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||
/// this does not necessarily represent a Python version that we actually support.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct PythonVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
@@ -67,42 +68,3 @@ impl fmt::Display for PythonVersion {
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for PythonVersion {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let as_str = String::deserialize(deserializer)?;
|
||||
|
||||
if let Some((major, minor)) = as_str.split_once('.') {
|
||||
let major = major
|
||||
.parse()
|
||||
.map_err(|err| serde::de::Error::custom(format!("invalid major version: {err}")))?;
|
||||
let minor = minor
|
||||
.parse()
|
||||
.map_err(|err| serde::de::Error::custom(format!("invalid minor version: {err}")))?;
|
||||
|
||||
Ok((major, minor).into())
|
||||
} else {
|
||||
let major = as_str.parse().map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"invalid python-version: {err}, expected: `major.minor`"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok((major, 0).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for PythonVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Div;
|
||||
use std::sync::Arc;
|
||||
|
||||
use except_handlers::TryNodeContextStackManager;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_index::IndexVec;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
@@ -86,10 +83,6 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
|
||||
impl<'db> SemanticIndexBuilder<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, file: File, parsed: &'db ParsedModule) -> Self {
|
||||
let source = source_text(db.upcast(), file);
|
||||
let approximated_expressions = (source.len() as f64 * 0.05) as usize;
|
||||
let approximated_definitions = (source.len() as f64 * 0.015) as usize;
|
||||
|
||||
let mut builder = Self {
|
||||
db,
|
||||
file,
|
||||
@@ -108,19 +101,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
scope_ids_by_scope: IndexVec::new(),
|
||||
use_def_maps: IndexVec::new(),
|
||||
|
||||
scopes_by_expression: FxHashMap::with_capacity_and_hasher(
|
||||
approximated_expressions,
|
||||
FxBuildHasher::default(),
|
||||
),
|
||||
scopes_by_expression: FxHashMap::default(),
|
||||
scopes_by_node: FxHashMap::default(),
|
||||
definitions_by_node: FxHashMap::with_capacity_and_hasher(
|
||||
approximated_definitions,
|
||||
FxBuildHasher::default(),
|
||||
),
|
||||
expressions_by_node: FxHashMap::with_capacity_and_hasher(
|
||||
approximated_expressions,
|
||||
FxBuildHasher::default(),
|
||||
),
|
||||
definitions_by_node: FxHashMap::default(),
|
||||
expressions_by_node: FxHashMap::default(),
|
||||
};
|
||||
|
||||
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
|
||||
@@ -564,19 +548,6 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.scope_ids_by_scope.shrink_to_fit();
|
||||
self.scopes_by_node.shrink_to_fit();
|
||||
|
||||
// let source_len = source_text(self.db.upcast(), self.file).len();
|
||||
// let expressions = self.scopes_by_expression.len();
|
||||
// let definitions = self.definitions_by_node.len();
|
||||
//
|
||||
// let expression_rel = NonZeroUsize::new(source_len)
|
||||
// .map(|size| (expressions as f64).div(size.get() as f64))
|
||||
// .unwrap_or_default();
|
||||
// let definitions_rel = NonZeroUsize::new(source_len)
|
||||
// .map(|size| (definitions as f64).div(size.get() as f64))
|
||||
// .unwrap_or_default();
|
||||
//
|
||||
// println!("{source_len},{expressions},{expression_rel},{definitions},{definitions_rel}");
|
||||
|
||||
SemanticIndex {
|
||||
symbol_tables,
|
||||
scopes: self.scopes,
|
||||
@@ -862,7 +833,6 @@ where
|
||||
self.visit_expr(test);
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(test);
|
||||
|
||||
// Save aside any break states from an outer loop
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
@@ -882,7 +852,6 @@ where
|
||||
// We may execute the `else` clause without ever executing the body, so merge in
|
||||
// the pre-loop state before visiting `else`.
|
||||
self.flow_merge(pre_loop);
|
||||
self.record_negated_constraint(constraint);
|
||||
self.visit_body(orelse);
|
||||
|
||||
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_resolver::file_to_module;
|
||||
@@ -466,33 +465,6 @@ pub enum DefinitionKind<'db> {
|
||||
TypeVarTuple(AstNodeRef<ast::TypeParamTypeVarTuple>),
|
||||
}
|
||||
|
||||
impl Ranged for DefinitionKind<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
DefinitionKind::Import(alias) => alias.range(),
|
||||
DefinitionKind::ImportFrom(import) => import.alias().range(),
|
||||
DefinitionKind::Function(function) => function.name.range(),
|
||||
DefinitionKind::Class(class) => class.name.range(),
|
||||
DefinitionKind::TypeAlias(type_alias) => type_alias.name.range(),
|
||||
DefinitionKind::NamedExpression(named) => named.target.range(),
|
||||
DefinitionKind::Assignment(assignment) => assignment.name().range(),
|
||||
DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(),
|
||||
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.target.range(),
|
||||
DefinitionKind::For(for_stmt) => for_stmt.target().range(),
|
||||
DefinitionKind::Comprehension(comp) => comp.target().range(),
|
||||
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
|
||||
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
|
||||
DefinitionKind::Parameter(parameter) => parameter.parameter.name.range(),
|
||||
DefinitionKind::WithItem(with_item) => with_item.target().range(),
|
||||
DefinitionKind::MatchPattern(match_pattern) => match_pattern.identifier.range(),
|
||||
DefinitionKind::ExceptHandler(handler) => handler.node().range(),
|
||||
DefinitionKind::TypeVar(type_var) => type_var.name.range(),
|
||||
DefinitionKind::ParamSpec(param_spec) => param_spec.name.range(),
|
||||
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.name.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
match self {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
//! The **declared type** represents the code author's declaration (usually through a type
|
||||
//! annotation) that a given variable should not be assigned any type outside the declared type. In
|
||||
//! our model, declared types are also control-flow-sensitive; we allow the code author to
|
||||
//! explicitly redeclare the same variable with a different type. So for a given binding of a
|
||||
//! explicitly re-declare the same variable with a different type. So for a given binding of a
|
||||
//! variable, we will want to ask which declarations of that variable can reach that binding, in
|
||||
//! order to determine whether the binding is permitted, or should be a type error. For example:
|
||||
//!
|
||||
@@ -62,7 +62,7 @@
|
||||
//! assignable to `str`. This is the purpose of declared types: they prevent accidental assignment
|
||||
//! of the wrong type to a variable.
|
||||
//!
|
||||
//! But in some cases it is useful to "shadow" or "redeclare" a variable with a new type, and we
|
||||
//! But in some cases it is useful to "shadow" or "re-declare" a variable with a new type, and we
|
||||
//! permit this, as long as it is done with an explicit re-annotation. So `path: Path =
|
||||
//! Path(path)`, with the explicit `: Path` annotation, is permitted.
|
||||
//!
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user