Compare commits
38 Commits
charlie/tr
...
alex/optim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
742a63d340 | ||
|
|
3feb3dfb6d | ||
|
|
5c36ab23b3 | ||
|
|
f4206d524a | ||
|
|
ff387ad74a | ||
|
|
4abaf61b50 | ||
|
|
0804030ee9 | ||
|
|
26230b1ed3 | ||
|
|
295ae836fd | ||
|
|
9677364847 | ||
|
|
8e45bac3c1 | ||
|
|
7366a9e951 | ||
|
|
15aa74206e | ||
|
|
77c2f4c6cb | ||
|
|
758926eecd | ||
|
|
6433b88ffa | ||
|
|
f619783066 | ||
|
|
ff05428ce6 | ||
|
|
4f2529f353 | ||
|
|
6f9ea73ac9 | ||
|
|
12dd27da52 | ||
|
|
e0e1e9535e | ||
|
|
7173c7ea3f | ||
|
|
5013752c6c | ||
|
|
b2b9d91859 | ||
|
|
c483b59ddd | ||
|
|
57218753be | ||
|
|
2ada8b6634 | ||
|
|
0edd97dd41 | ||
|
|
f8f4ca8fbc | ||
|
|
3d35dbd334 | ||
|
|
a652b411b8 | ||
|
|
4dac3d105d | ||
|
|
77ad107617 | ||
|
|
b925ae5061 | ||
|
|
9333f15433 | ||
|
|
c429ef8407 | ||
|
|
8716b4e230 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -794,7 +794,7 @@ jobs:
|
||||
echo '```console' > "$GITHUB_STEP_SUMMARY"
|
||||
# Enable color output for pre-commit and remove it for the summary
|
||||
# Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default
|
||||
SKIP=cargo-fmt,clippy,dev-generate-all uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
SKIP=cargo-fmt uvx --python="${PYTHON_VERSION}" pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \
|
||||
tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1
|
||||
exit_code="${PIPESTATUS[0]}"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
|
||||
3
.github/workflows/typing_conformance.yaml
vendored
3
.github/workflows/typing_conformance.yaml
vendored
@@ -59,9 +59,6 @@ jobs:
|
||||
|
||||
- name: Compute diagnostic diff
|
||||
shell: bash
|
||||
env:
|
||||
# TODO: Remove this once we fixed the remaining panics in the conformance suite.
|
||||
TY_MAX_PARALLELISM: 1
|
||||
run: |
|
||||
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
|
||||
|
||||
|
||||
@@ -135,6 +135,3 @@ repos:
|
||||
rev: v0.11.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
||||
ci:
|
||||
skip: [cargo-fmt, dev-generate-all]
|
||||
|
||||
@@ -125,7 +125,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -127,7 +127,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -263,6 +263,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -129,7 +129,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -265,6 +265,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -129,7 +129,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -265,6 +265,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -126,7 +126,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -262,6 +262,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -126,7 +126,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -262,6 +262,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -125,7 +125,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -125,7 +125,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -125,7 +125,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -261,6 +261,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -238,7 +238,7 @@ linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.functions_names = [
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
@@ -374,6 +374,7 @@ linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
|
||||
@@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
13109,
|
||||
13116,
|
||||
);
|
||||
|
||||
static TANJUN: Benchmark = Benchmark::new(
|
||||
|
||||
31
crates/ruff_linter/resources/test/fixtures/airflow/AIR303.py
vendored
Normal file
31
crates/ruff_linter/resources/test/fixtures/airflow/AIR303.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from airflow.lineage.hook import HookLineageCollector
|
||||
|
||||
# airflow.lineage.hook
|
||||
hlc = HookLineageCollector()
|
||||
hlc.create_asset("there")
|
||||
hlc.create_asset("should", "be", "no", "posarg")
|
||||
hlc.create_asset(name="but", uri="kwargs are ok")
|
||||
hlc.create_asset()
|
||||
|
||||
HookLineageCollector().create_asset(name="but", uri="kwargs are ok")
|
||||
HookLineageCollector().create_asset("there")
|
||||
HookLineageCollector().create_asset("should", "be", "no", "posarg")
|
||||
|
||||
args = ["uri_value"]
|
||||
hlc.create_asset(*args)
|
||||
HookLineageCollector().create_asset(*args)
|
||||
|
||||
# Literal unpacking
|
||||
hlc.create_asset(*["literal_uri"])
|
||||
HookLineageCollector().create_asset(*["literal_uri"])
|
||||
|
||||
# starred args with keyword args
|
||||
hlc.create_asset(*args, extra="value")
|
||||
HookLineageCollector().create_asset(*args, extra="value")
|
||||
|
||||
# Double-starred keyword arguments
|
||||
kwargs = {"uri": "value", "name": "test"}
|
||||
hlc.create_asset(**kwargs)
|
||||
HookLineageCollector().create_asset(**kwargs)
|
||||
@@ -1,5 +1,5 @@
|
||||
from abc import abstractmethod
|
||||
from typing import overload, cast
|
||||
from typing import overload, cast, TypeVar
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
@@ -256,3 +256,15 @@ class C:
|
||||
"""Docstring."""
|
||||
msg = t"{x}..."
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
###
|
||||
# Unused arguments with `**kwargs`.
|
||||
###
|
||||
|
||||
def f(
|
||||
default: object = None, # noqa: ARG001
|
||||
**kwargs: object,
|
||||
) -> None:
|
||||
TypeVar(**kwargs)
|
||||
|
||||
|
||||
@@ -53,3 +53,25 @@ for i in items:
|
||||
items = [1, 2, 3, 4]
|
||||
for i in items:
|
||||
items[i]
|
||||
|
||||
|
||||
# A case with multiple uses of the value to show off the secondary annotations
|
||||
for instrument in ORCHESTRA:
|
||||
data = json.dumps(
|
||||
{
|
||||
"instrument": instrument,
|
||||
"section": ORCHESTRA[instrument],
|
||||
}
|
||||
)
|
||||
|
||||
print(f"saving data for {instrument} in {ORCHESTRA[instrument]}")
|
||||
|
||||
with open(f"{instrument}/{ORCHESTRA[instrument]}.txt", "w") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
# This should still suppress the error
|
||||
for ( # noqa: PLC0206
|
||||
instrument
|
||||
) in ORCHESTRA:
|
||||
print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
|
||||
@@ -73,3 +73,7 @@ foo == 1 or foo == 1.0 # Different types, same hashed value
|
||||
foo == False or foo == 0 # Different types, same hashed value
|
||||
|
||||
foo == 0.0 or foo == 0j # Different types, same hashed value
|
||||
|
||||
foo == "bar" or foo == "bar" # All members identical
|
||||
|
||||
foo == "bar" or foo == "bar" or foo == "buzz" # All but one members identical
|
||||
|
||||
51
crates/ruff_linter/resources/test/fixtures/ruff/RUF067/modules/__init__.py
vendored
Normal file
51
crates/ruff_linter/resources/test/fixtures/ruff/RUF067/modules/__init__.py
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
"""This is the module docstring."""
|
||||
|
||||
# convenience imports:
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ["MY_CONSTANT"]
|
||||
__all__ += ["foo"]
|
||||
__all__: list[str] = __all__
|
||||
__all__ = __all__ = __all__
|
||||
|
||||
MY_CONSTANT = 5
|
||||
"""This is an important constant."""
|
||||
|
||||
os.environ["FOO"] = 1
|
||||
|
||||
|
||||
def foo():
|
||||
return Path("foo.py")
|
||||
|
||||
def __getattr__(name): # ok
|
||||
return name
|
||||
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
||||
|
||||
if os.environ["FOO"] != "1": # RUF067
|
||||
MY_CONSTANT = 4 # ok, don't flag nested statements
|
||||
|
||||
if TYPE_CHECKING: # ok
|
||||
MY_CONSTANT = 3
|
||||
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING: # ok
|
||||
MY_CONSTANT = 2
|
||||
|
||||
__version__ = "1.2.3" # ok
|
||||
|
||||
def __dir__(): # ok
|
||||
return ["foo"]
|
||||
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(__path__, __name__) # ok
|
||||
__path__ = unknown.extend_path(__path__, __name__) # also ok
|
||||
|
||||
# non-`extend_path` assignments are not allowed
|
||||
__path__ = 5 # RUF067
|
||||
|
||||
# also allow `__author__`
|
||||
__author__ = "The Author" # ok
|
||||
54
crates/ruff_linter/resources/test/fixtures/ruff/RUF067/modules/okay.py
vendored
Normal file
54
crates/ruff_linter/resources/test/fixtures/ruff/RUF067/modules/okay.py
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
The code here is not in an `__init__.py` file and should not trigger the
|
||||
lint.
|
||||
"""
|
||||
|
||||
# convenience imports:
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ["MY_CONSTANT"]
|
||||
__all__ += ["foo"]
|
||||
__all__: list[str] = __all__
|
||||
__all__ = __all__ = __all__
|
||||
|
||||
MY_CONSTANT = 5
|
||||
"""This is an important constant."""
|
||||
|
||||
os.environ["FOO"] = 1
|
||||
|
||||
|
||||
def foo():
|
||||
return Path("foo.py")
|
||||
|
||||
def __getattr__(name): # ok
|
||||
return name
|
||||
|
||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
||||
|
||||
if os.environ["FOO"] != "1": # RUF067
|
||||
MY_CONSTANT = 4 # ok, don't flag nested statements
|
||||
|
||||
if TYPE_CHECKING: # ok
|
||||
MY_CONSTANT = 3
|
||||
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING: # ok
|
||||
MY_CONSTANT = 2
|
||||
|
||||
__version__ = "1.2.3" # ok
|
||||
|
||||
def __dir__(): # ok
|
||||
return ["foo"]
|
||||
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(__path__, __name__) # ok
|
||||
__path__ = unknown.extend_path(__path__, __name__) # also ok
|
||||
|
||||
# non-`extend_path` assignments are not allowed
|
||||
__path__ = 5 # RUF067
|
||||
|
||||
# also allow `__author__`
|
||||
__author__ = "The Author" # ok
|
||||
@@ -1043,7 +1043,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
]) && flake8_gettext::is_gettext_func_call(
|
||||
checker,
|
||||
func,
|
||||
&checker.settings().flake8_gettext.functions_names,
|
||||
&checker.settings().flake8_gettext.function_names,
|
||||
) {
|
||||
if checker.is_rule_enabled(Rule::FStringInGetTextFuncCall) {
|
||||
flake8_gettext::rules::f_string_in_gettext_func_call(checker, args);
|
||||
@@ -1278,6 +1278,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::Airflow3SuggestedUpdate) {
|
||||
airflow::rules::airflow_3_0_suggested_update_expr(checker, expr);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::Airflow3IncompatibleFunctionSignature) {
|
||||
airflow::rules::airflow_3_incompatible_function_signature(checker, expr);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryCastToInt) {
|
||||
ruff::rules::unnecessary_cast_to_int(checker, call);
|
||||
}
|
||||
|
||||
@@ -1630,4 +1630,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::NonEmptyInitModule) {
|
||||
ruff::rules::non_empty_init_module(checker, stmt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) fn unresolved_references(checker: &Checker) {
|
||||
}
|
||||
|
||||
// Allow __path__.
|
||||
if checker.path.ends_with("__init__.py") {
|
||||
if checker.in_init_module() {
|
||||
if reference.name(checker.source()) == "__path__" {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//! represents the lint-rule analysis phase. In the future, these steps may be separated into
|
||||
//! distinct passes over the AST.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
use std::path::Path;
|
||||
|
||||
use itertools::Itertools;
|
||||
@@ -198,6 +198,8 @@ pub(crate) struct Checker<'a> {
|
||||
parsed_type_annotation: Option<&'a ParsedAnnotation>,
|
||||
/// The [`Path`] to the file under analysis.
|
||||
path: &'a Path,
|
||||
/// Whether `path` points to an `__init__.py` file.
|
||||
in_init_module: OnceCell<bool>,
|
||||
/// The [`Path`] to the package containing the current file.
|
||||
package: Option<PackageRoot<'a>>,
|
||||
/// The module representation of the current file (e.g., `foo.bar`).
|
||||
@@ -274,6 +276,7 @@ impl<'a> Checker<'a> {
|
||||
noqa_line_for,
|
||||
noqa,
|
||||
path,
|
||||
in_init_module: OnceCell::new(),
|
||||
package,
|
||||
module,
|
||||
source_type,
|
||||
@@ -482,9 +485,11 @@ impl<'a> Checker<'a> {
|
||||
self.context.settings
|
||||
}
|
||||
|
||||
/// The [`Path`] to the file under analysis.
|
||||
pub(crate) const fn path(&self) -> &'a Path {
|
||||
self.path
|
||||
/// Returns whether the file under analysis is an `__init__.py` file.
|
||||
pub(crate) fn in_init_module(&self) -> bool {
|
||||
*self
|
||||
.in_init_module
|
||||
.get_or_init(|| self.path.ends_with("__init__.py"))
|
||||
}
|
||||
|
||||
/// The [`Path`] to the package containing the current file.
|
||||
@@ -1873,6 +1878,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
} else {
|
||||
self.visit_non_type_definition(value);
|
||||
}
|
||||
} else {
|
||||
// Ex: typing.TypeVar(**kwargs)
|
||||
self.visit_non_type_definition(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3171,7 +3179,7 @@ impl<'a> Checker<'a> {
|
||||
// F822
|
||||
if self.is_rule_enabled(Rule::UndefinedExport) {
|
||||
if is_undefined_export_in_dunder_init_enabled(self.settings())
|
||||
|| !self.path.ends_with("__init__.py")
|
||||
|| !self.in_init_module()
|
||||
{
|
||||
self.report_diagnostic(
|
||||
pyflakes::rules::UndefinedExport {
|
||||
|
||||
@@ -1060,6 +1060,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "064") => rules::ruff::rules::NonOctalPermissions,
|
||||
(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,
|
||||
(Ruff, "066") => rules::ruff::rules::PropertyWithoutReturn,
|
||||
(Ruff, "067") => rules::ruff::rules::NonEmptyInitModule,
|
||||
|
||||
(Ruff, "100") => rules::ruff::rules::UnusedNOQA,
|
||||
(Ruff, "101") => rules::ruff::rules::RedirectedNOQA,
|
||||
@@ -1123,6 +1124,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Airflow, "002") => rules::airflow::rules::AirflowDagNoScheduleArgument,
|
||||
(Airflow, "301") => rules::airflow::rules::Airflow3Removal,
|
||||
(Airflow, "302") => rules::airflow::rules::Airflow3MovedToProvider,
|
||||
(Airflow, "303") => rules::airflow::rules::Airflow3IncompatibleFunctionSignature,
|
||||
(Airflow, "311") => rules::airflow::rules::Airflow3SuggestedUpdate,
|
||||
(Airflow, "312") => rules::airflow::rules::Airflow3SuggestedToMoveToProvider,
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ mod tests {
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_zendesk.py"))]
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_standard.py"))]
|
||||
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR302_try.py"))]
|
||||
#[test_case(Rule::Airflow3IncompatibleFunctionSignature, Path::new("AIR303.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_args.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_names.py"))]
|
||||
#[test_case(Rule::Airflow3SuggestedUpdate, Path::new("AIR311_try.py"))]
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::{Arguments, Expr, ExprAttribute, ExprCall, Identifier};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for Airflow function calls that will raise a runtime error in Airflow 3.0
|
||||
/// due to function signature changes, such as functions that changed to accept only
|
||||
/// keyword arguments, parameter reordering, or parameter type changes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Airflow 3.0 introduces changes to function signatures. Code that
|
||||
/// worked in Airflow 2.x will raise a runtime error if not updated in Airflow
|
||||
/// 3.0.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from airflow.lineage.hook import HookLineageCollector
|
||||
///
|
||||
/// collector = HookLineageCollector()
|
||||
/// # Passing positional arguments will raise a runtime error in Airflow 3.0
|
||||
/// collector.create_asset("s3://bucket/key")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from airflow.lineage.hook import HookLineageCollector
|
||||
///
|
||||
/// collector = HookLineageCollector()
|
||||
/// # Passing arguments as keyword arguments instead of positional arguments
|
||||
/// collector.create_asset(uri="s3://bucket/key")
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.14.11")]
|
||||
pub(crate) struct Airflow3IncompatibleFunctionSignature {
|
||||
function_name: String,
|
||||
change_type: FunctionSignatureChangeType,
|
||||
}
|
||||
|
||||
impl Violation for Airflow3IncompatibleFunctionSignature {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Airflow3IncompatibleFunctionSignature {
|
||||
function_name,
|
||||
change_type,
|
||||
} = self;
|
||||
match change_type {
|
||||
FunctionSignatureChangeType::KeywordOnly { .. } => {
|
||||
format!("`{function_name}` signature is changed in Airflow 3.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Airflow3IncompatibleFunctionSignature { change_type, .. } = self;
|
||||
match change_type {
|
||||
FunctionSignatureChangeType::KeywordOnly { message } => Some(message.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AIR303
|
||||
pub(crate) fn airflow_3_incompatible_function_signature(checker: &Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::AIRFLOW) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::Call(ExprCall {
|
||||
func, arguments, ..
|
||||
}) = expr
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Resolve the qualified name: try variable assignments first, then fall back to direct
|
||||
// constructor calls.
|
||||
let qualified_name = typing::resolve_assignment(value, checker.semantic()).or_else(|| {
|
||||
value
|
||||
.as_call_expr()
|
||||
.and_then(|call| checker.semantic().resolve_qualified_name(&call.func))
|
||||
});
|
||||
|
||||
let Some(qualified_name) = qualified_name else {
|
||||
return;
|
||||
};
|
||||
|
||||
check_keyword_only_method(checker, &qualified_name, attr, arguments);
|
||||
}
|
||||
|
||||
fn check_keyword_only_method(
|
||||
checker: &Checker,
|
||||
qualified_name: &QualifiedName,
|
||||
attr: &Identifier,
|
||||
arguments: &Arguments,
|
||||
) {
|
||||
let has_positional_args =
|
||||
arguments.find_positional(0).is_some() || arguments.args.iter().any(Expr::is_starred_expr);
|
||||
|
||||
if let ["airflow", "lineage", "hook", "HookLineageCollector"] = qualified_name.segments() {
|
||||
if attr.as_str() == "create_asset" && has_positional_args {
|
||||
checker.report_diagnostic(
|
||||
Airflow3IncompatibleFunctionSignature {
|
||||
function_name: attr.to_string(),
|
||||
change_type: FunctionSignatureChangeType::KeywordOnly {
|
||||
message: "Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)",
|
||||
},
|
||||
},
|
||||
attr.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum FunctionSignatureChangeType {
|
||||
/// Function signature changed to only accept keyword arguments.
|
||||
KeywordOnly { message: &'static str },
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub(crate) use dag_schedule_argument::*;
|
||||
pub(crate) use function_signature_change_in_3::*;
|
||||
pub(crate) use moved_to_provider_in_3::*;
|
||||
pub(crate) use removal_in_3::*;
|
||||
pub(crate) use suggested_to_move_to_provider_in_3::*;
|
||||
@@ -6,6 +7,7 @@ pub(crate) use suggested_to_update_3_0::*;
|
||||
pub(crate) use task_variable_name::*;
|
||||
|
||||
mod dag_schedule_argument;
|
||||
mod function_signature_change_in_3;
|
||||
mod moved_to_provider_in_3;
|
||||
mod removal_in_3;
|
||||
mod suggested_to_move_to_provider_in_3;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:7:5
|
||||
|
|
||||
5 | # airflow.lineage.hook
|
||||
6 | hlc = HookLineageCollector()
|
||||
7 | hlc.create_asset("there")
|
||||
| ^^^^^^^^^^^^
|
||||
8 | hlc.create_asset("should", "be", "no", "posarg")
|
||||
9 | hlc.create_asset(name="but", uri="kwargs are ok")
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:8:5
|
||||
|
|
||||
6 | hlc = HookLineageCollector()
|
||||
7 | hlc.create_asset("there")
|
||||
8 | hlc.create_asset("should", "be", "no", "posarg")
|
||||
| ^^^^^^^^^^^^
|
||||
9 | hlc.create_asset(name="but", uri="kwargs are ok")
|
||||
10 | hlc.create_asset()
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:13:24
|
||||
|
|
||||
12 | HookLineageCollector().create_asset(name="but", uri="kwargs are ok")
|
||||
13 | HookLineageCollector().create_asset("there")
|
||||
| ^^^^^^^^^^^^
|
||||
14 | HookLineageCollector().create_asset("should", "be", "no", "posarg")
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:14:24
|
||||
|
|
||||
12 | HookLineageCollector().create_asset(name="but", uri="kwargs are ok")
|
||||
13 | HookLineageCollector().create_asset("there")
|
||||
14 | HookLineageCollector().create_asset("should", "be", "no", "posarg")
|
||||
| ^^^^^^^^^^^^
|
||||
15 |
|
||||
16 | args = ["uri_value"]
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:17:5
|
||||
|
|
||||
16 | args = ["uri_value"]
|
||||
17 | hlc.create_asset(*args)
|
||||
| ^^^^^^^^^^^^
|
||||
18 | HookLineageCollector().create_asset(*args)
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:18:24
|
||||
|
|
||||
16 | args = ["uri_value"]
|
||||
17 | hlc.create_asset(*args)
|
||||
18 | HookLineageCollector().create_asset(*args)
|
||||
| ^^^^^^^^^^^^
|
||||
19 |
|
||||
20 | # Literal unpacking
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:21:5
|
||||
|
|
||||
20 | # Literal unpacking
|
||||
21 | hlc.create_asset(*["literal_uri"])
|
||||
| ^^^^^^^^^^^^
|
||||
22 | HookLineageCollector().create_asset(*["literal_uri"])
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:22:24
|
||||
|
|
||||
20 | # Literal unpacking
|
||||
21 | hlc.create_asset(*["literal_uri"])
|
||||
22 | HookLineageCollector().create_asset(*["literal_uri"])
|
||||
| ^^^^^^^^^^^^
|
||||
23 |
|
||||
24 | # starred args with keyword args
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:25:5
|
||||
|
|
||||
24 | # starred args with keyword args
|
||||
25 | hlc.create_asset(*args, extra="value")
|
||||
| ^^^^^^^^^^^^
|
||||
26 | HookLineageCollector().create_asset(*args, extra="value")
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
|
||||
AIR303 `create_asset` signature is changed in Airflow 3.0
|
||||
--> AIR303.py:26:24
|
||||
|
|
||||
24 | # starred args with keyword args
|
||||
25 | hlc.create_asset(*args, extra="value")
|
||||
26 | HookLineageCollector().create_asset(*args, extra="value")
|
||||
| ^^^^^^^^^^^^
|
||||
27 |
|
||||
28 | # Double-starred keyword arguments
|
||||
|
|
||||
help: Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)
|
||||
@@ -38,6 +38,10 @@ use crate::checkers::ast::Checker;
|
||||
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.flake8-gettext.function-names`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html)
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -38,6 +38,10 @@ use crate::checkers::ast::Checker;
|
||||
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.flake8-gettext.function-names`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html)
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -37,6 +37,10 @@ use crate::checkers::ast::Checker;
|
||||
/// _("Hello, %s!") % name # Looks for "Hello, %s!".
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.flake8-gettext.function-names`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html)
|
||||
#[derive(ViolationMetadata)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug, Clone, CacheKey)]
|
||||
pub struct Settings {
|
||||
pub functions_names: Vec<Name>,
|
||||
pub function_names: Vec<Name>,
|
||||
}
|
||||
|
||||
pub fn default_func_names() -> Vec<Name> {
|
||||
@@ -19,7 +19,7 @@ pub fn default_func_names() -> Vec<Name> {
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
functions_names: default_func_names(),
|
||||
function_names: default_func_names(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ impl Display for Settings {
|
||||
formatter = f,
|
||||
namespace = "linter.flake8_gettext",
|
||||
fields = [
|
||||
self.functions_names | array
|
||||
self.function_names | array
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -32,6 +32,13 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// "dog"
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// Setting `lint.flake8-implicit-str-concat.allow-multiline = false` will disable this rule because
|
||||
/// it would leave no allowed way to write a multi-line string.
|
||||
///
|
||||
/// - `lint.flake8-implicit-str-concat.allow-multiline`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.201")]
|
||||
pub(crate) struct ExplicitStringConcatenation;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{FixAvailability, Violation};
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `ByteString` has been deprecated since Python 3.9 and will be removed in
|
||||
/// Python 3.14. The Python documentation recommends using either
|
||||
/// Python 3.17. The Python documentation recommends using either
|
||||
/// `collections.abc.Buffer` (or the `typing_extensions` backport
|
||||
/// on Python <3.12) or a union like `bytes | bytearray | memoryview` instead.
|
||||
///
|
||||
|
||||
@@ -36,6 +36,10 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// - Instead of `example-module-name` or `example module name`, use `example_module_name`.
|
||||
/// - Instead of `ExampleModule`, use `example_module`.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pep8-naming.ignore-names`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#package-and-module-names
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.248")]
|
||||
|
||||
@@ -58,6 +58,11 @@ use crate::rules::pydocstyle::settings::Convention;
|
||||
/// """
|
||||
/// return distance / time
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydoclint.ignore-one-line-docstrings`
|
||||
/// - `lint.pydocstyle.convention`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.14.1")]
|
||||
pub(crate) struct DocstringExtraneousParameter {
|
||||
@@ -113,6 +118,12 @@ impl Violation for DocstringExtraneousParameter {
|
||||
/// """
|
||||
/// return distance / time
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydoclint.ignore-one-line-docstrings`
|
||||
/// - `lint.pydocstyle.convention`
|
||||
/// - `lint.pydocstyle.property-decorators`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.5.6")]
|
||||
pub(crate) struct DocstringMissingReturns;
|
||||
@@ -165,6 +176,11 @@ impl Violation for DocstringMissingReturns {
|
||||
/// for _ in range(n):
|
||||
/// print("Hello!")
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydoclint.ignore-one-line-docstrings`
|
||||
/// - `lint.pydocstyle.convention`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.5.6")]
|
||||
pub(crate) struct DocstringExtraneousReturns;
|
||||
@@ -218,6 +234,11 @@ impl Violation for DocstringExtraneousReturns {
|
||||
/// for i in range(1, n + 1):
|
||||
/// yield i
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydoclint.ignore-one-line-docstrings`
|
||||
/// - `lint.pydocstyle.convention`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.5.7")]
|
||||
pub(crate) struct DocstringMissingYields;
|
||||
@@ -270,6 +291,11 @@ impl Violation for DocstringMissingYields {
|
||||
/// for _ in range(n):
|
||||
/// print("Hello!")
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydoclint.ignore-one-line-docstrings`
|
||||
/// - `lint.pydocstyle.convention`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.5.7")]
|
||||
pub(crate) struct DocstringExtraneousYields;
|
||||
@@ -342,6 +368,11 @@ impl Violation for DocstringExtraneousYields {
|
||||
/// except ZeroDivisionError as exc:
|
||||
/// raise FasterThanLightError from exc
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydoclint.ignore-one-line-docstrings`
|
||||
/// - `lint.pydocstyle.convention`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.5.5")]
|
||||
pub(crate) struct DocstringMissingException {
|
||||
@@ -410,6 +441,11 @@ impl Violation for DocstringMissingException {
|
||||
/// It may often be desirable to document *all* exceptions that a function
|
||||
/// could possibly raise, even those which are not explicitly raised using
|
||||
/// `raise` statements in the function body.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydoclint.ignore-one-line-docstrings`
|
||||
/// - `lint.pydocstyle.convention`
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.5.5")]
|
||||
pub(crate) struct DocstringExtraneousException {
|
||||
|
||||
@@ -38,6 +38,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// foobar.__doc__ # "Docstring for foo\bar."
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [Python documentation: String and Bytes literals](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals)
|
||||
|
||||
@@ -34,6 +34,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -33,6 +33,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// """Return the mean of the given values."""
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
@@ -80,6 +84,10 @@ impl Violation for BlankLineBeforeFunction {
|
||||
/// return sum(values) / len(values)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -25,6 +25,10 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
/// """Return the mean of the given values."""
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -63,6 +63,10 @@ use crate::docstrings::Docstring;
|
||||
/// factorial.__doc__ # "Return the factorial of n."
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [Python documentation: `typing.overload`](https://docs.python.org/3/library/typing.html#typing.overload)
|
||||
|
||||
@@ -44,6 +44,10 @@ use crate::{Edit, Fix};
|
||||
/// The rule is also incompatible with the [formatter] when using
|
||||
/// `format.indent-style="tab"`.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
@@ -93,6 +97,10 @@ impl Violation for DocstringTabIndentation {
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
@@ -146,6 +154,10 @@ impl AlwaysFixableViolation for UnderIndentation {
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -37,6 +37,10 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -27,6 +27,10 @@ use crate::rules::pydocstyle::helpers::ends_with_backslash;
|
||||
/// """Return the factorial of n."""
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -24,6 +24,10 @@ use crate::docstrings::Docstring;
|
||||
/// """Return the mean of the given values."""
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -55,6 +55,10 @@ use crate::checkers::ast::Checker;
|
||||
/// ## Notebook behavior
|
||||
/// This rule is ignored for Jupyter Notebooks.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
|
||||
@@ -139,6 +143,10 @@ impl Violation for UndocumentedPublicModule {
|
||||
/// self.points += points
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
|
||||
@@ -366,6 +374,10 @@ impl Violation for UndocumentedPublicFunction {
|
||||
/// __all__ = ["player", "game"]
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
|
||||
@@ -480,6 +492,10 @@ impl Violation for UndocumentedMagicMethod {
|
||||
/// bar.__doc__ # "Class Bar."
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
|
||||
|
||||
@@ -32,6 +32,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// documentation generators, or custom introspection utilities that rely on
|
||||
/// specific docstring formatting.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
///
|
||||
|
||||
@@ -1066,6 +1066,10 @@ impl AlwaysFixableViolation for MissingBlankLineAfterLastSection {
|
||||
/// raise FasterThanLightError from exc
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
|
||||
@@ -1317,6 +1321,10 @@ impl Violation for UndocumentedParam {
|
||||
/// raise FasterThanLightError from exc
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [PEP 287 – reStructuredText Docstring Format](https://peps.python.org/pep-0287/)
|
||||
|
||||
@@ -31,6 +31,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent quotes, making the rule redundant.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `lint.pydocstyle.ignore-decorators`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
|
||||
@@ -389,7 +389,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) {
|
||||
}
|
||||
}
|
||||
|
||||
let in_init = checker.path().ends_with("__init__.py");
|
||||
let in_init = checker.in_init_module();
|
||||
let fix_init = !checker.settings().ignore_init_module_imports;
|
||||
let preview_mode = is_dunder_init_fix_unused_import_enabled(checker.settings());
|
||||
let dunder_all_exprs = find_dunder_all_exprs(checker.semantic());
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::{
|
||||
self as ast, Expr, ExprContext,
|
||||
self as ast, Expr, ExprContext, StmtFor,
|
||||
token::parenthesized_range,
|
||||
visitor::{self, Visitor},
|
||||
};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};
|
||||
use ruff_python_semantic::analyze::typing::is_dict;
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::checkers::ast::{Checker, DiagnosticGuard};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for dictionary iterations that extract the dictionary value
|
||||
@@ -47,20 +48,26 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "0.8.0")]
|
||||
pub(crate) struct DictIndexMissingItems;
|
||||
pub(crate) struct DictIndexMissingItems<'a> {
|
||||
key: &'a str,
|
||||
dict: &'a str,
|
||||
}
|
||||
|
||||
impl Violation for DictIndexMissingItems {
|
||||
impl Violation for DictIndexMissingItems<'_> {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Extracting value from dictionary without calling `.items()`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { key, dict } = self;
|
||||
Some(format!("Use `for {key}, value in {dict}.items()` instead"))
|
||||
}
|
||||
}
|
||||
|
||||
/// PLC0206
|
||||
pub(crate) fn dict_index_missing_items(checker: &Checker, stmt_for: &ast::StmtFor) {
|
||||
let ast::StmtFor {
|
||||
target, iter, body, ..
|
||||
} = stmt_for;
|
||||
pub(crate) fn dict_index_missing_items(checker: &Checker, stmt_for: &StmtFor) {
|
||||
let StmtFor { iter, body, .. } = stmt_for;
|
||||
|
||||
// Extract the name of the iteration object (e.g., `obj` in `for key in obj:`).
|
||||
let Some(dict_name) = extract_dict_name(iter) else {
|
||||
@@ -77,40 +84,46 @@ pub(crate) fn dict_index_missing_items(checker: &Checker, stmt_for: &ast::StmtFo
|
||||
return;
|
||||
}
|
||||
|
||||
let has_violation = {
|
||||
let mut visitor = SubscriptVisitor::new(target, dict_name);
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
visitor.has_violation
|
||||
};
|
||||
|
||||
if has_violation {
|
||||
checker.report_diagnostic(DictIndexMissingItems, stmt_for.range());
|
||||
}
|
||||
SubscriptVisitor::new(stmt_for, dict_name, checker).visit_body(body);
|
||||
}
|
||||
|
||||
/// A visitor to detect subscript operations on a target dictionary.
|
||||
struct SubscriptVisitor<'a> {
|
||||
struct SubscriptVisitor<'a, 'b> {
|
||||
/// The target of the for loop (e.g., `key` in `for key in obj:`).
|
||||
target: &'a Expr,
|
||||
/// The name of the iterated object (e.g., `obj` in `for key in obj:`).
|
||||
dict_name: &'a ast::ExprName,
|
||||
/// Whether a violation has been detected.
|
||||
has_violation: bool,
|
||||
/// The range to use for the primary diagnostic.
|
||||
range: TextRange,
|
||||
/// The [`Checker`] used to emit diagnostics.
|
||||
checker: &'a Checker<'b>,
|
||||
/// The [`DiagnosticGuard`] used to attach additional annotations for each subscript.
|
||||
///
|
||||
/// The guard is initially `None` and then set to `Some` when the first subscript is found.
|
||||
guard: Option<DiagnosticGuard<'a, 'b>>,
|
||||
}
|
||||
|
||||
impl<'a> SubscriptVisitor<'a> {
|
||||
fn new(target: &'a Expr, dict_name: &'a ast::ExprName) -> Self {
|
||||
impl<'a, 'b> SubscriptVisitor<'a, 'b> {
|
||||
fn new(stmt_for: &'a StmtFor, dict_name: &'a ast::ExprName, checker: &'a Checker<'b>) -> Self {
|
||||
let StmtFor { target, iter, .. } = stmt_for;
|
||||
let range = {
|
||||
let target_start =
|
||||
parenthesized_range(target.into(), stmt_for.into(), checker.tokens())
|
||||
.map_or(target.start(), TextRange::start);
|
||||
TextRange::new(target_start, iter.end())
|
||||
};
|
||||
|
||||
Self {
|
||||
target,
|
||||
dict_name,
|
||||
has_violation: false,
|
||||
range,
|
||||
checker,
|
||||
guard: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for SubscriptVisitor<'a> {
|
||||
impl<'a> Visitor<'a> for SubscriptVisitor<'a, '_> {
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
// Given `obj[key]`, `value` must be `obj` and `slice` must be `key`.
|
||||
if let Expr::Subscript(ast::ExprSubscript {
|
||||
@@ -134,7 +147,17 @@ impl<'a> Visitor<'a> for SubscriptVisitor<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
self.has_violation = true;
|
||||
let guard = self.guard.get_or_insert_with(|| {
|
||||
self.checker.report_diagnostic(
|
||||
DictIndexMissingItems {
|
||||
key: self.checker.locator().slice(self.target),
|
||||
dict: self.checker.locator().slice(self.dict_name),
|
||||
},
|
||||
self.range,
|
||||
)
|
||||
});
|
||||
|
||||
guard.secondary_annotation("", expr);
|
||||
} else {
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
|
||||
@@ -140,6 +140,18 @@ pub(crate) fn repeated_equality_comparison(checker: &Checker, bool_op: &ast::Exp
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((&first, rest)) = comparators.split_first() {
|
||||
let first_comparable = ComparableExpr::from(first);
|
||||
|
||||
if rest
|
||||
.iter()
|
||||
.all(|&c| ComparableExpr::from(c) == first_comparable)
|
||||
{
|
||||
// Do not flag if all members are identical
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if we can determine that all the values are hashable, we can use a set
|
||||
// TODO: improve with type inference
|
||||
let all_hashable = comparators
|
||||
|
||||
@@ -77,7 +77,7 @@ pub(crate) fn useless_import_alias(checker: &Checker, alias: &Alias) {
|
||||
}
|
||||
|
||||
// A re-export in __init__.py is probably intentional.
|
||||
if checker.path().ends_with("__init__.py") {
|
||||
if checker.in_init_module() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ pub(crate) fn useless_import_from_alias(
|
||||
}
|
||||
|
||||
// A re-export in __init__.py is probably intentional.
|
||||
if checker.path().ends_with("__init__.py") {
|
||||
if checker.in_init_module() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,72 +2,107 @@
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
PLC0206 Extracting value from dictionary without calling `.items()`
|
||||
--> dict_index_missing_items.py:9:1
|
||||
--> dict_index_missing_items.py:9:5
|
||||
|
|
||||
8 | # Errors
|
||||
9 | / for instrument in ORCHESTRA:
|
||||
10 | | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
| |___________________________________________________^
|
||||
8 | # Errors
|
||||
9 | for instrument in ORCHESTRA:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
10 | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
| ---------------------
|
||||
11 |
|
||||
12 | for instrument in ORCHESTRA:
|
||||
12 | for instrument in ORCHESTRA:
|
||||
|
|
||||
help: Use `for instrument, value in ORCHESTRA.items()` instead
|
||||
|
||||
PLC0206 Extracting value from dictionary without calling `.items()`
|
||||
--> dict_index_missing_items.py:12:1
|
||||
--> dict_index_missing_items.py:12:5
|
||||
|
|
||||
10 | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
10 | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
11 |
|
||||
12 | / for instrument in ORCHESTRA:
|
||||
13 | | ORCHESTRA[instrument]
|
||||
| |_________________________^
|
||||
12 | for instrument in ORCHESTRA:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
13 | ORCHESTRA[instrument]
|
||||
| ---------------------
|
||||
14 |
|
||||
15 | for instrument in ORCHESTRA.keys():
|
||||
15 | for instrument in ORCHESTRA.keys():
|
||||
|
|
||||
help: Use `for instrument, value in ORCHESTRA.items()` instead
|
||||
|
||||
PLC0206 Extracting value from dictionary without calling `.items()`
|
||||
--> dict_index_missing_items.py:15:1
|
||||
--> dict_index_missing_items.py:15:5
|
||||
|
|
||||
13 | ORCHESTRA[instrument]
|
||||
13 | ORCHESTRA[instrument]
|
||||
14 |
|
||||
15 | / for instrument in ORCHESTRA.keys():
|
||||
16 | | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
| |___________________________________________________^
|
||||
15 | for instrument in ORCHESTRA.keys():
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
16 | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
| ---------------------
|
||||
17 |
|
||||
18 | for instrument in ORCHESTRA.keys():
|
||||
18 | for instrument in ORCHESTRA.keys():
|
||||
|
|
||||
help: Use `for instrument, value in ORCHESTRA.items()` instead
|
||||
|
||||
PLC0206 Extracting value from dictionary without calling `.items()`
|
||||
--> dict_index_missing_items.py:18:1
|
||||
--> dict_index_missing_items.py:18:5
|
||||
|
|
||||
16 | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
16 | print(f"{instrument}: {ORCHESTRA[instrument]}")
|
||||
17 |
|
||||
18 | / for instrument in ORCHESTRA.keys():
|
||||
19 | | ORCHESTRA[instrument]
|
||||
| |_________________________^
|
||||
18 | for instrument in ORCHESTRA.keys():
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
19 | ORCHESTRA[instrument]
|
||||
| ---------------------
|
||||
20 |
|
||||
21 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
21 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
|
|
||||
help: Use `for instrument, value in ORCHESTRA.items()` instead
|
||||
|
||||
PLC0206 Extracting value from dictionary without calling `.items()`
|
||||
--> dict_index_missing_items.py:21:1
|
||||
--> dict_index_missing_items.py:21:5
|
||||
|
|
||||
19 | ORCHESTRA[instrument]
|
||||
19 | ORCHESTRA[instrument]
|
||||
20 |
|
||||
21 | / for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
22 | | print(f"{instrument}: {temp_orchestra[instrument]}")
|
||||
| |________________________________________________________^
|
||||
21 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
22 | print(f"{instrument}: {temp_orchestra[instrument]}")
|
||||
| --------------------------
|
||||
23 |
|
||||
24 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
24 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
|
|
||||
help: Use `for instrument, value in temp_orchestra.items()` instead
|
||||
|
||||
PLC0206 Extracting value from dictionary without calling `.items()`
|
||||
--> dict_index_missing_items.py:24:1
|
||||
--> dict_index_missing_items.py:24:5
|
||||
|
|
||||
22 | print(f"{instrument}: {temp_orchestra[instrument]}")
|
||||
22 | print(f"{instrument}: {temp_orchestra[instrument]}")
|
||||
23 |
|
||||
24 | / for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
25 | | temp_orchestra[instrument]
|
||||
| |______________________________^
|
||||
24 | for instrument in (temp_orchestra := {"violin": "strings", "oboe": "woodwind"}):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
25 | temp_orchestra[instrument]
|
||||
| --------------------------
|
||||
26 |
|
||||
27 | # # OK
|
||||
27 | # # OK
|
||||
|
|
||||
help: Use `for instrument, value in temp_orchestra.items()` instead
|
||||
|
||||
PLC0206 Extracting value from dictionary without calling `.items()`
|
||||
--> dict_index_missing_items.py:59:5
|
||||
|
|
||||
58 | # A case with multiple uses of the value to show off the secondary annotations
|
||||
59 | for instrument in ORCHESTRA:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
60 | data = json.dumps(
|
||||
61 | {
|
||||
62 | "instrument": instrument,
|
||||
63 | "section": ORCHESTRA[instrument],
|
||||
| ---------------------
|
||||
64 | }
|
||||
65 | )
|
||||
66 |
|
||||
67 | print(f"saving data for {instrument} in {ORCHESTRA[instrument]}")
|
||||
| ---------------------
|
||||
68 |
|
||||
69 | with open(f"{instrument}/{ORCHESTRA[instrument]}.txt", "w") as f:
|
||||
| ---------------------
|
||||
70 | f.write(data)
|
||||
|
|
||||
help: Use `for instrument, value in ORCHESTRA.items()` instead
|
||||
|
||||
@@ -436,6 +436,7 @@ help: Merge multiple comparisons
|
||||
73 + foo in {False, 0} # Different types, same hashed value
|
||||
74 |
|
||||
75 | foo == 0.0 or foo == 0j # Different types, same hashed value
|
||||
76 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PLR1714 [*] Consider merging multiple comparisons: `foo in {0.0, 0j}`.
|
||||
@@ -445,6 +446,8 @@ PLR1714 [*] Consider merging multiple comparisons: `foo in {0.0, 0j}`.
|
||||
74 |
|
||||
75 | foo == 0.0 or foo == 0j # Different types, same hashed value
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
76 |
|
||||
77 | foo == "bar" or foo == "bar" # All members identical
|
||||
|
|
||||
help: Merge multiple comparisons
|
||||
72 |
|
||||
@@ -452,4 +455,23 @@ help: Merge multiple comparisons
|
||||
74 |
|
||||
- foo == 0.0 or foo == 0j # Different types, same hashed value
|
||||
75 + foo in {0.0, 0j} # Different types, same hashed value
|
||||
76 |
|
||||
77 | foo == "bar" or foo == "bar" # All members identical
|
||||
78 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
PLR1714 [*] Consider merging multiple comparisons: `foo in {"bar", "bar", "buzz"}`.
|
||||
--> repeated_equality_comparison.py:79:1
|
||||
|
|
||||
77 | foo == "bar" or foo == "bar" # All members identical
|
||||
78 |
|
||||
79 | foo == "bar" or foo == "bar" or foo == "buzz" # All but one members identical
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Merge multiple comparisons
|
||||
76 |
|
||||
77 | foo == "bar" or foo == "bar" # All members identical
|
||||
78 |
|
||||
- foo == "bar" or foo == "bar" or foo == "buzz" # All but one members identical
|
||||
79 + foo in {"bar", "bar", "buzz"} # All but one members identical
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -34,6 +34,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// different values when introspecting types at runtime. However, in most cases,
|
||||
/// the fix should be safe to apply.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `target-version`
|
||||
///
|
||||
/// [PEP 646]: https://peps.python.org/pep-0646/
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "0.10.0")]
|
||||
|
||||
@@ -78,6 +78,10 @@ use super::{
|
||||
/// This rule only applies to generic classes and does not include generic functions. See
|
||||
/// [`non-pep695-generic-function`][UP047] for the function version.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `target-version`
|
||||
///
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [PEP 696]: https://peps.python.org/pep-0696/
|
||||
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
|
||||
|
||||
@@ -71,6 +71,10 @@ use super::{DisplayTypeVars, TypeVarReferenceVisitor, check_type_vars, in_nested
|
||||
/// This rule only applies to generic functions and does not include generic classes. See
|
||||
/// [`non-pep695-generic-class`][UP046] for the class version.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `target-version`
|
||||
///
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [PEP 696]: https://peps.python.org/pep-0696/
|
||||
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
|
||||
|
||||
@@ -78,6 +78,10 @@ use super::{
|
||||
/// new type parameters are restricted in scope to their associated aliases. See
|
||||
/// [`private-type-parameter`][UP049] for a rule to update these names.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `target-version`
|
||||
///
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
|
||||
/// [UP046]: https://docs.astral.sh/ruff/rules/non-pep695-generic-class/
|
||||
|
||||
@@ -72,6 +72,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
/// As such, migrating to `enum.StrEnum` will introduce a behavior change for
|
||||
/// code that relies on the Python 3.11 behavior.
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - `target-version`
|
||||
///
|
||||
/// ## References
|
||||
/// - [enum.StrEnum](https://docs.python.org/3/library/enum.html#enum.StrEnum)
|
||||
///
|
||||
|
||||
@@ -119,6 +119,8 @@ mod tests {
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
|
||||
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]
|
||||
#[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/__init__.py"))]
|
||||
#[test_case(Rule::NonEmptyInitModule, Path::new("RUF067/modules/okay.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
@@ -136,6 +138,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
ruff: super::settings::Settings {
|
||||
parenthesize_tuple_in_subscript: true,
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
|
||||
},
|
||||
@@ -151,6 +154,7 @@ mod tests {
|
||||
&LinterSettings {
|
||||
ruff: super::settings::Settings {
|
||||
parenthesize_tuple_in_subscript: false,
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
unresolved_target_version: PythonVersion::PY310.into(),
|
||||
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
|
||||
@@ -714,4 +718,26 @@ mod tests {
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strictly_empty_init_modules_ruf067() -> Result<()> {
|
||||
assert_diagnostics_diff!(
|
||||
Path::new("ruff/RUF067/modules/__init__.py"),
|
||||
&LinterSettings {
|
||||
ruff: super::settings::Settings {
|
||||
strictly_empty_init_modules: false,
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
..LinterSettings::for_rule(Rule::NonEmptyInitModule)
|
||||
},
|
||||
&LinterSettings {
|
||||
ruff: super::settings::Settings {
|
||||
strictly_empty_init_modules: true,
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
..LinterSettings::for_rule(Rule::NonEmptyInitModule)
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(crate) use mutable_dataclass_default::*;
|
||||
pub(crate) use mutable_fromkeys_value::*;
|
||||
pub(crate) use needless_else::*;
|
||||
pub(crate) use never_union::*;
|
||||
pub(crate) use non_empty_init_module::*;
|
||||
pub(crate) use non_octal_permissions::*;
|
||||
pub(crate) use none_not_at_end_of_union::*;
|
||||
pub(crate) use parenthesize_chained_operators::*;
|
||||
@@ -99,6 +100,7 @@ mod mutable_dataclass_default;
|
||||
mod mutable_fromkeys_value;
|
||||
mod needless_else;
|
||||
mod never_union;
|
||||
mod non_empty_init_module;
|
||||
mod non_octal_permissions;
|
||||
mod none_not_at_end_of_union;
|
||||
mod parenthesize_chained_operators;
|
||||
|
||||
259
crates/ruff_linter/src/rules/ruff/rules/non_empty_init_module.rs
Normal file
259
crates/ruff_linter/src/rules/ruff/rules/non_empty_init_module.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::analyze::typing::is_type_checking_block;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{Violation, checkers::ast::Checker};
|
||||
|
||||
/// ## What it does
|
||||
///
|
||||
/// Detects the presence of code in `__init__.py` files.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// `__init__.py` files are often empty or only contain simple code to modify a module's API. As
|
||||
/// such, it's easy to overlook them and their possible side effects when debugging.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// Instead of defining `MyClass` directly in `__init__.py`:
|
||||
///
|
||||
/// ```python
|
||||
/// """My module docstring."""
|
||||
///
|
||||
///
|
||||
/// class MyClass:
|
||||
/// def my_method(self): ...
|
||||
/// ```
|
||||
///
|
||||
/// move the definition to another file, import it, and include it in `__all__`:
|
||||
///
|
||||
/// ```python
|
||||
/// """My module docstring."""
|
||||
///
|
||||
/// from submodule import MyClass
|
||||
///
|
||||
/// __all__ = ["MyClass"]
|
||||
/// ```
|
||||
///
|
||||
/// Code in `__init__.py` files is also run at import time and can cause surprising slowdowns. To
|
||||
/// disallow any code in `__init__.py` files, you can enable the
|
||||
/// [`lint.ruff.strictly-empty-init-modules`] setting. In this case:
|
||||
///
|
||||
/// ```python
|
||||
/// from submodule import MyClass
|
||||
///
|
||||
/// __all__ = ["MyClass"]
|
||||
/// ```
|
||||
///
|
||||
/// the only fix is entirely emptying the file:
|
||||
///
|
||||
/// ```python
|
||||
/// ```
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
/// In non-strict mode, this rule allows several common patterns in `__init__.py` files:
|
||||
///
|
||||
/// - Imports
|
||||
/// - Assignments to `__all__`, `__path__`, `__version__`, and `__author__`
|
||||
/// - Module-level and attribute docstrings
|
||||
/// - `if TYPE_CHECKING` blocks
|
||||
/// - [PEP-562] module-level `__getattr__` and `__dir__` functions
|
||||
///
|
||||
/// ## Options
|
||||
///
|
||||
/// - [`lint.ruff.strictly-empty-init-modules`]
|
||||
///
|
||||
/// ## References
|
||||
///
|
||||
/// - [`flake8-empty-init-modules`](https://github.com/samueljsb/flake8-empty-init-modules/)
|
||||
///
|
||||
/// [PEP-562]: https://peps.python.org/pep-0562/
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(preview_since = "0.14.11")]
|
||||
pub(crate) struct NonEmptyInitModule {
|
||||
strictly_empty_init_modules: bool,
|
||||
}
|
||||
|
||||
impl Violation for NonEmptyInitModule {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
if self.strictly_empty_init_modules {
|
||||
"`__init__` module should not contain any code".to_string()
|
||||
} else {
|
||||
"`__init__` module should only contain docstrings and re-exports".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF067
|
||||
pub(crate) fn non_empty_init_module(checker: &Checker, stmt: &Stmt) {
|
||||
if !checker.in_init_module() {
|
||||
return;
|
||||
}
|
||||
|
||||
let semantic = checker.semantic();
|
||||
|
||||
// Only flag top-level statements
|
||||
if !semantic.at_top_level() {
|
||||
return;
|
||||
}
|
||||
|
||||
let strictly_empty_init_modules = checker.settings().ruff.strictly_empty_init_modules;
|
||||
|
||||
if !strictly_empty_init_modules {
|
||||
// Even though module-level attributes are disallowed, we still allow attribute docstrings
|
||||
// to avoid needing two `noqa` comments in a case like:
|
||||
//
|
||||
// ```py
|
||||
// MY_CONSTANT = 1 # noqa: RUF067
|
||||
// "A very important constant"
|
||||
// ```
|
||||
if semantic.in_pep_257_docstring() || semantic.in_attribute_docstring() {
|
||||
return;
|
||||
}
|
||||
|
||||
match stmt {
|
||||
// Allow imports
|
||||
Stmt::Import(_) | Stmt::ImportFrom(_) => return,
|
||||
|
||||
// Allow PEP-562 module `__getattr__` and `__dir__`
|
||||
Stmt::FunctionDef(func) if matches!(&*func.name, "__getattr__" | "__dir__") => return,
|
||||
|
||||
// Allow `TYPE_CHECKING` blocks
|
||||
Stmt::If(stmt_if) if is_type_checking_block(stmt_if, semantic) => return,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(assignment) = Assignment::from_stmt(stmt) {
|
||||
// Allow assignments to `__all__`.
|
||||
//
|
||||
// TODO(brent) should we allow additional cases here? Beyond simple assignments, you could
|
||||
// also append or extend `__all__`.
|
||||
//
|
||||
// This is actually going slightly beyond the upstream rule already, which only checks for
|
||||
// `Stmt::Assign`.
|
||||
if assignment.is_assignment_to("__all__") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow legacy namespace packages with assignments like:
|
||||
//
|
||||
// ```py
|
||||
// __path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
// ```
|
||||
if assignment.is_assignment_to("__path__") && assignment.is_pkgutil_extend_path() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow assignments to `__version__`.
|
||||
if assignment.is_assignment_to("__version__") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow assignments to `__author__`.
|
||||
if assignment.is_assignment_to("__author__") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checker.report_diagnostic(
|
||||
NonEmptyInitModule {
|
||||
strictly_empty_init_modules,
|
||||
},
|
||||
stmt.range(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Any assignment statement, including plain assignment, annotated assignments, and augmented
|
||||
/// assignments.
|
||||
struct Assignment<'a> {
|
||||
targets: &'a [Expr],
|
||||
value: Option<&'a Expr>,
|
||||
}
|
||||
|
||||
impl<'a> Assignment<'a> {
|
||||
fn from_stmt(stmt: &'a Stmt) -> Option<Self> {
|
||||
let (targets, value) = match stmt {
|
||||
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
|
||||
(targets.as_slice(), Some(&**value))
|
||||
}
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
|
||||
(std::slice::from_ref(&**target), value.as_deref())
|
||||
}
|
||||
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
|
||||
(std::slice::from_ref(&**target), Some(&**value))
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Self { targets, value })
|
||||
}
|
||||
|
||||
/// Returns whether all of the assignment targets match `name`.
|
||||
///
|
||||
/// For example, both of the following would be allowed for a `name` of `__all__`:
|
||||
///
|
||||
/// ```py
|
||||
/// __all__ = ["foo"]
|
||||
/// __all__ = __all__ = ["foo"]
|
||||
/// ```
|
||||
///
|
||||
/// but not:
|
||||
///
|
||||
/// ```py
|
||||
/// __all__ = another_list = ["foo"]
|
||||
/// ```
|
||||
fn is_assignment_to(&self, name: &str) -> bool {
|
||||
self.targets
|
||||
.iter()
|
||||
.all(|target| target.as_name_expr().is_some_and(|expr| expr.id == name))
|
||||
}
|
||||
|
||||
/// Returns `true` if the value being assigned is a call to `pkgutil.extend_path`.
|
||||
///
|
||||
/// For example, both of the following would return true:
|
||||
///
|
||||
/// ```py
|
||||
/// __path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||
/// __path__ = other.extend_path(__path__, __name__)
|
||||
/// ```
|
||||
///
|
||||
/// We're intentionally a bit less strict here, not requiring that the receiver of the
|
||||
/// `extend_path` call is the typical `__import__('pkgutil')` or `pkgutil`.
|
||||
fn is_pkgutil_extend_path(&self) -> bool {
|
||||
let Some(Expr::Call(ast::ExprCall {
|
||||
func: extend_func,
|
||||
arguments: extend_arguments,
|
||||
..
|
||||
})) = self.value
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Expr::Attribute(ast::ExprAttribute {
|
||||
attr: maybe_extend_path,
|
||||
..
|
||||
}) = &**extend_func
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Test that this is an `extend_path(__path__, __name__)` call
|
||||
if maybe_extend_path != "extend_path" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(Expr::Name(path)) = extend_arguments.find_argument_value("path", 0) else {
|
||||
return false;
|
||||
};
|
||||
let Some(Expr::Name(name)) = extend_arguments.find_argument_value("name", 1) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
path.id() == "__path__" && name.id() == "__name__"
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
|
||||
/// ## References
|
||||
/// - [Typing documentation: Legal parameters for `Literal` at type check time](https://typing.python.org/en/latest/spec/literal.html#legal-parameters-for-literal-at-type-check-time)
|
||||
///
|
||||
/// [PEP 586](https://peps.python.org/pep-0586/)
|
||||
/// [PEP 586]: https://peps.python.org/pep-0586/
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "0.10.0")]
|
||||
pub(crate) struct UnnecessaryNestedLiteral;
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::fmt;
|
||||
#[derive(Debug, Clone, CacheKey, Default)]
|
||||
pub struct Settings {
|
||||
pub parenthesize_tuple_in_subscript: bool,
|
||||
pub strictly_empty_init_modules: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for Settings {
|
||||
@@ -16,6 +17,7 @@ impl fmt::Display for Settings {
|
||||
namespace = "linter.ruff",
|
||||
fields = [
|
||||
self.parenthesize_tuple_in_subscript,
|
||||
self.strictly_empty_init_modules,
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:12:1
|
||||
|
|
||||
10 | __all__ = __all__ = __all__
|
||||
11 |
|
||||
12 | MY_CONSTANT = 5
|
||||
| ^^^^^^^^^^^^^^^
|
||||
13 | """This is an important constant."""
|
||||
|
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:15:1
|
||||
|
|
||||
13 | """This is an important constant."""
|
||||
14 |
|
||||
15 | os.environ["FOO"] = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:18:1
|
||||
|
|
||||
18 | / def foo():
|
||||
19 | | return Path("foo.py")
|
||||
| |_________________________^
|
||||
20 |
|
||||
21 | def __getattr__(name): # ok
|
||||
|
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:26:1
|
||||
|
|
||||
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
||||
25 |
|
||||
26 | / if os.environ["FOO"] != "1": # RUF067
|
||||
27 | | MY_CONSTANT = 4 # ok, don't flag nested statements
|
||||
| |___________________^
|
||||
28 |
|
||||
29 | if TYPE_CHECKING: # ok
|
||||
|
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:48:1
|
||||
|
|
||||
47 | # non-`extend_path` assignments are not allowed
|
||||
48 | __path__ = 5 # RUF067
|
||||
| ^^^^^^^^^^^^
|
||||
49 |
|
||||
50 | # also allow `__author__`
|
||||
|
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.ruff.strictly_empty_init_modules = false
|
||||
+linter.ruff.strictly_empty_init_modules = true
|
||||
|
||||
--- Summary ---
|
||||
Removed: 5
|
||||
Added: 24
|
||||
|
||||
--- Removed ---
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:12:1
|
||||
|
|
||||
10 | __all__ = __all__ = __all__
|
||||
11 |
|
||||
12 | MY_CONSTANT = 5
|
||||
| ^^^^^^^^^^^^^^^
|
||||
13 | """This is an important constant."""
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:15:1
|
||||
|
|
||||
13 | """This is an important constant."""
|
||||
14 |
|
||||
15 | os.environ["FOO"] = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:18:1
|
||||
|
|
||||
18 | / def foo():
|
||||
19 | | return Path("foo.py")
|
||||
| |_________________________^
|
||||
20 |
|
||||
21 | def __getattr__(name): # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:26:1
|
||||
|
|
||||
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
||||
25 |
|
||||
26 | / if os.environ["FOO"] != "1": # RUF067
|
||||
27 | | MY_CONSTANT = 4 # ok, don't flag nested statements
|
||||
| |___________________^
|
||||
28 |
|
||||
29 | if TYPE_CHECKING: # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should only contain docstrings and re-exports
|
||||
--> __init__.py:48:1
|
||||
|
|
||||
47 | # non-`extend_path` assignments are not allowed
|
||||
48 | __path__ = 5 # RUF067
|
||||
| ^^^^^^^^^^^^
|
||||
49 |
|
||||
50 | # also allow `__author__`
|
||||
|
|
||||
|
||||
|
||||
|
||||
--- Added ---
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:1:1
|
||||
|
|
||||
1 | """This is the module docstring."""
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
2 |
|
||||
3 | # convenience imports:
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:4:1
|
||||
|
|
||||
3 | # convenience imports:
|
||||
4 | import os
|
||||
| ^^^^^^^^^
|
||||
5 | from pathlib import Path
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:5:1
|
||||
|
|
||||
3 | # convenience imports:
|
||||
4 | import os
|
||||
5 | from pathlib import Path
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
6 |
|
||||
7 | __all__ = ["MY_CONSTANT"]
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:7:1
|
||||
|
|
||||
5 | from pathlib import Path
|
||||
6 |
|
||||
7 | __all__ = ["MY_CONSTANT"]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
8 | __all__ += ["foo"]
|
||||
9 | __all__: list[str] = __all__
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:8:1
|
||||
|
|
||||
7 | __all__ = ["MY_CONSTANT"]
|
||||
8 | __all__ += ["foo"]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
9 | __all__: list[str] = __all__
|
||||
10 | __all__ = __all__ = __all__
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:9:1
|
||||
|
|
||||
7 | __all__ = ["MY_CONSTANT"]
|
||||
8 | __all__ += ["foo"]
|
||||
9 | __all__: list[str] = __all__
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
10 | __all__ = __all__ = __all__
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:10:1
|
||||
|
|
||||
8 | __all__ += ["foo"]
|
||||
9 | __all__: list[str] = __all__
|
||||
10 | __all__ = __all__ = __all__
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
11 |
|
||||
12 | MY_CONSTANT = 5
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:12:1
|
||||
|
|
||||
10 | __all__ = __all__ = __all__
|
||||
11 |
|
||||
12 | MY_CONSTANT = 5
|
||||
| ^^^^^^^^^^^^^^^
|
||||
13 | """This is an important constant."""
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:13:1
|
||||
|
|
||||
12 | MY_CONSTANT = 5
|
||||
13 | """This is an important constant."""
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
14 |
|
||||
15 | os.environ["FOO"] = 1
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:15:1
|
||||
|
|
||||
13 | """This is an important constant."""
|
||||
14 |
|
||||
15 | os.environ["FOO"] = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:18:1
|
||||
|
|
||||
18 | / def foo():
|
||||
19 | | return Path("foo.py")
|
||||
| |_________________________^
|
||||
20 |
|
||||
21 | def __getattr__(name): # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:21:1
|
||||
|
|
||||
19 | return Path("foo.py")
|
||||
20 |
|
||||
21 | / def __getattr__(name): # ok
|
||||
22 | | return name
|
||||
| |_______________^
|
||||
23 |
|
||||
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:24:1
|
||||
|
|
||||
22 | return name
|
||||
23 |
|
||||
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
25 |
|
||||
26 | if os.environ["FOO"] != "1": # RUF067
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:26:1
|
||||
|
|
||||
24 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) # ok
|
||||
25 |
|
||||
26 | / if os.environ["FOO"] != "1": # RUF067
|
||||
27 | | MY_CONSTANT = 4 # ok, don't flag nested statements
|
||||
| |___________________^
|
||||
28 |
|
||||
29 | if TYPE_CHECKING: # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:29:1
|
||||
|
|
||||
27 | MY_CONSTANT = 4 # ok, don't flag nested statements
|
||||
28 |
|
||||
29 | / if TYPE_CHECKING: # ok
|
||||
30 | | MY_CONSTANT = 3
|
||||
| |___________________^
|
||||
31 |
|
||||
32 | import typing
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:32:1
|
||||
|
|
||||
30 | MY_CONSTANT = 3
|
||||
31 |
|
||||
32 | import typing
|
||||
| ^^^^^^^^^^^^^
|
||||
33 |
|
||||
34 | if typing.TYPE_CHECKING: # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:34:1
|
||||
|
|
||||
32 | import typing
|
||||
33 |
|
||||
34 | / if typing.TYPE_CHECKING: # ok
|
||||
35 | | MY_CONSTANT = 2
|
||||
| |___________________^
|
||||
36 |
|
||||
37 | __version__ = "1.2.3" # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:37:1
|
||||
|
|
||||
35 | MY_CONSTANT = 2
|
||||
36 |
|
||||
37 | __version__ = "1.2.3" # ok
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
38 |
|
||||
39 | def __dir__(): # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:39:1
|
||||
|
|
||||
37 | __version__ = "1.2.3" # ok
|
||||
38 |
|
||||
39 | / def __dir__(): # ok
|
||||
40 | | return ["foo"]
|
||||
| |__________________^
|
||||
41 |
|
||||
42 | import pkgutil
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:42:1
|
||||
|
|
||||
40 | return ["foo"]
|
||||
41 |
|
||||
42 | import pkgutil
|
||||
| ^^^^^^^^^^^^^^
|
||||
43 |
|
||||
44 | __path__ = pkgutil.extend_path(__path__, __name__) # ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:44:1
|
||||
|
|
||||
42 | import pkgutil
|
||||
43 |
|
||||
44 | __path__ = pkgutil.extend_path(__path__, __name__) # ok
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
45 | __path__ = unknown.extend_path(__path__, __name__) # also ok
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:45:1
|
||||
|
|
||||
44 | __path__ = pkgutil.extend_path(__path__, __name__) # ok
|
||||
45 | __path__ = unknown.extend_path(__path__, __name__) # also ok
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
46 |
|
||||
47 | # non-`extend_path` assignments are not allowed
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:48:1
|
||||
|
|
||||
47 | # non-`extend_path` assignments are not allowed
|
||||
48 | __path__ = 5 # RUF067
|
||||
| ^^^^^^^^^^^^
|
||||
49 |
|
||||
50 | # also allow `__author__`
|
||||
|
|
||||
|
||||
|
||||
RUF067 `__init__` module should not contain any code
|
||||
--> __init__.py:51:1
|
||||
|
|
||||
50 | # also allow `__author__`
|
||||
51 | __author__ = "The Author" # ok
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
@@ -1479,7 +1479,7 @@ pub struct Flake8GetTextOptions {
|
||||
impl Flake8GetTextOptions {
|
||||
pub fn into_settings(self) -> flake8_gettext::settings::Settings {
|
||||
flake8_gettext::settings::Settings {
|
||||
functions_names: self
|
||||
function_names: self
|
||||
.function_names
|
||||
.unwrap_or_else(flake8_gettext::settings::default_func_names)
|
||||
.into_iter()
|
||||
@@ -3497,6 +3497,17 @@ pub struct RuffOptions {
|
||||
note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section of the configuration."
|
||||
)]
|
||||
pub allowed_markup_calls: Option<Vec<String>>,
|
||||
/// Whether to require `__init__.py` files to contain no code at all, including imports and
|
||||
/// docstrings (see `RUF067`).
|
||||
#[option(
|
||||
default = r#"false"#,
|
||||
value_type = "bool",
|
||||
example = r#"
|
||||
# Make it a violation to include any code, including imports and docstrings in `__init__.py`
|
||||
strictly-empty-init-modules = true
|
||||
"#
|
||||
)]
|
||||
pub strictly_empty_init_modules: Option<bool>,
|
||||
}
|
||||
|
||||
impl RuffOptions {
|
||||
@@ -3505,6 +3516,7 @@ impl RuffOptions {
|
||||
parenthesize_tuple_in_subscript: self
|
||||
.parenthesize_tuple_in_subscript
|
||||
.unwrap_or_default(),
|
||||
strictly_empty_init_modules: self.strictly_empty_init_modules.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@ crates shared with Ruff, such as `ruff_db`, `ruff_python_ast`, and `ruff_python_
|
||||
annotations for the Python standard library.
|
||||
- `ty_wasm`: library crate for exposing ty as a WebAssembly module. Powers the
|
||||
[ty Playground](https://play.ty.dev/).
|
||||
- `ty_completion_eval`: Framework for evaluating completion suggestions returned by the ty LSP.
|
||||
- `ty_module_resolver`: The module resolver, which allows resolving imports to their modules.
|
||||
- `ty_static`: Lists the known environment variables used by `ty`.
|
||||
- `ty_combine`: Utility crate containing the `Combine` trait, which is used to combine `Options`.
|
||||
|
||||
## Writing tests
|
||||
|
||||
|
||||
2
crates/ty/docs/cli.md
generated
2
crates/ty/docs/cli.md
generated
@@ -91,7 +91,7 @@ over all configuration files.</p>
|
||||
<li><code>3.13</code></li>
|
||||
<li><code>3.14</code></li>
|
||||
</ul></dd><dt id="ty-check--quiet"><a href="#ty-check--quiet"><code>--quiet</code></a>, <code>-q</code></dt><dd><p>Use quiet output (or <code>-qq</code> for silent output)</p>
|
||||
</dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-gitignore</code> to disable</p>
|
||||
</dd><dt id="ty-check--respect-ignore-files"><a href="#ty-check--respect-ignore-files"><code>--respect-ignore-files</code></a></dt><dd><p>Respect file exclusions via <code>.gitignore</code> and other standard ignore files. Use <code>--no-respect-ignore-files</code> to disable</p>
|
||||
</dd><dt id="ty-check--typeshed"><a href="#ty-check--typeshed"><code>--typeshed</code></a>, <code>--custom-typeshed-dir</code> <i>path</i></dt><dd><p>Custom directory to use for stdlib typeshed stubs</p>
|
||||
</dd><dt id="ty-check--verbose"><a href="#ty-check--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output (or <code>-vv</code> and <code>-vvv</code> for more verbose output)</p>
|
||||
</dd><dt id="ty-check--warn"><a href="#ty-check--warn"><code>--warn</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'warn'. Can be specified multiple times.</p>
|
||||
|
||||
@@ -142,7 +142,7 @@ pub(crate) struct CheckCommand {
|
||||
pub(crate) watch: bool,
|
||||
|
||||
/// Respect file exclusions via `.gitignore` and other standard ignore files.
|
||||
/// Use `--no-respect-gitignore` to disable.
|
||||
/// Use `--no-respect-ignore-files` to disable.
|
||||
#[arg(
|
||||
long,
|
||||
overrides_with("no_respect_ignore_files"),
|
||||
|
||||
@@ -12,6 +12,7 @@ use ruff_python_codegen::Stylist;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
use rustc_hash::FxHashSet;
|
||||
use ty_module_resolver::{KnownModule, ModuleName};
|
||||
use ty_python_semantic::HasType;
|
||||
use ty_python_semantic::types::UnionType;
|
||||
use ty_python_semantic::{
|
||||
Completion as SemanticCompletion, NameKind, SemanticModel,
|
||||
@@ -60,6 +61,7 @@ pub fn completion<'db>(
|
||||
completions.extend(semantic_completions);
|
||||
if scoped.is_some() {
|
||||
add_keyword_completions(db, &mut completions);
|
||||
add_argument_completions(db, &model, &context.cursor, &mut completions);
|
||||
}
|
||||
if settings.auto_import {
|
||||
if let Some(scoped) = scoped {
|
||||
@@ -75,8 +77,6 @@ pub fn completion<'db>(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
add_function_arg_completions(db, file, &context.cursor, &mut completions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +356,7 @@ impl<'db> Completion<'db> {
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypeGuard(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_) => CompletionKind::Value,
|
||||
@@ -418,6 +419,25 @@ impl<'db> Completion<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn argument(name: &str, ty: Option<Type<'db>>, documentation: Option<&str>) -> Self {
|
||||
let insert = Some(format!("{name}=").into_boxed_str());
|
||||
let documentation = documentation.map(|d| Docstring::new(d.to_owned()));
|
||||
|
||||
Completion {
|
||||
name: name.into(),
|
||||
qualified: None,
|
||||
insert,
|
||||
ty,
|
||||
kind: Some(CompletionKind::Variable),
|
||||
module_name: None,
|
||||
import: None,
|
||||
builtin: false,
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
documentation,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when this completion refers to the
|
||||
/// `NotImplemented` builtin.
|
||||
fn is_notimplemented(&self, db: &dyn Db) -> bool {
|
||||
@@ -1062,7 +1082,77 @@ enum Sort {
|
||||
Lower,
|
||||
}
|
||||
|
||||
/// Detect and construct completions for unset function arguments.
|
||||
/// Detect and add completions for unset arguments.
|
||||
fn add_argument_completions<'db>(
|
||||
db: &'db dyn Db,
|
||||
model: &SemanticModel<'db>,
|
||||
cursor: &ContextCursor<'_>,
|
||||
completions: &mut Completions<'db>,
|
||||
) {
|
||||
for node in cursor.covering_node(cursor.range).ancestors() {
|
||||
match node {
|
||||
ast::AnyNodeRef::ExprCall(call) => {
|
||||
if call.arguments.range().contains_range(cursor.range) {
|
||||
add_function_arg_completions(db, model.file(), cursor, completions);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ast::AnyNodeRef::StmtClassDef(class_def) => {
|
||||
if let Some(arguments) = class_def.arguments.as_deref()
|
||||
&& arguments.range().contains_range(cursor.range)
|
||||
{
|
||||
add_class_arg_completions(model, class_def, completions);
|
||||
}
|
||||
return;
|
||||
}
|
||||
node => {
|
||||
if node.is_statement() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect and add completions for unset class arguments.
|
||||
///
|
||||
/// Some arguments we know are always valid and thus they are easy
|
||||
/// to provide. The `metaclass` keyword is always valid.
|
||||
/// For `typing.TypedDict` subclasses, we add
|
||||
/// `TypedDict` specific keywords like `total`.
|
||||
fn add_class_arg_completions<'db>(
|
||||
model: &SemanticModel<'db>,
|
||||
class_def: &ast::StmtClassDef,
|
||||
completions: &mut Completions<'db>,
|
||||
) {
|
||||
let is_set = |name| {
|
||||
class_def
|
||||
.arguments
|
||||
.as_ref()
|
||||
.is_some_and(|args| args.find_keyword(name).is_some())
|
||||
};
|
||||
|
||||
if !is_set("metaclass") {
|
||||
let ty = Some(KnownClass::Type.to_subclass_of(model.db()));
|
||||
completions.add(Completion::argument("metaclass", ty, None));
|
||||
}
|
||||
|
||||
let is_typed_dict = class_def
|
||||
.inferred_type(model)
|
||||
.and_then(Type::as_class_literal)
|
||||
.is_some_and(|t| t.is_typed_dict(model.db()));
|
||||
|
||||
// TODO: Handle PEP 728 that adds two extra keywords,
|
||||
// closed and extra_items.
|
||||
//
|
||||
// See https://peps.python.org/pep-0728/
|
||||
if is_typed_dict && !is_set("total") {
|
||||
let ty = Some(KnownClass::Bool.to_instance(model.db()));
|
||||
completions.add(Completion::argument("total", ty, None));
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect and add completions for unset function arguments.
|
||||
///
|
||||
/// Suggestions are only provided if the cursor is currently inside a
|
||||
/// function call and the function arguments have not 1) already been
|
||||
@@ -1073,18 +1163,15 @@ fn add_function_arg_completions<'db>(
|
||||
cursor: &ContextCursor<'_>,
|
||||
completions: &mut Completions<'db>,
|
||||
) {
|
||||
// But be careful: this isn't as simple as just finding a call
|
||||
// expression. We also have to make sure we are in the "arguments"
|
||||
// portion of the call. Otherwise we risk incorrectly returning
|
||||
// something for `(<CURSOR>)(arg1, arg2)`-style expressions.
|
||||
if !cursor
|
||||
.covering_node(TextRange::empty(cursor.offset))
|
||||
.ancestors()
|
||||
.take_while(|node| !node.is_statement())
|
||||
.any(|node| node.is_arguments())
|
||||
{
|
||||
return;
|
||||
}
|
||||
debug_assert!(
|
||||
cursor
|
||||
.covering_node(cursor.range)
|
||||
.ancestors()
|
||||
.take_while(|node| !node.is_statement())
|
||||
.any(|node| node.is_arguments()),
|
||||
"Should only be called if we're already certain we're in an arguments node to avoid \
|
||||
adding completions for something like `(<CURSOR>)(arg1, arg2)`-style expressions"
|
||||
);
|
||||
|
||||
let Some(sig_help) = signature_help(db, file, cursor.offset) else {
|
||||
return;
|
||||
@@ -1097,25 +1184,11 @@ fn add_function_arg_completions<'db>(
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = Name::new(&p.name);
|
||||
let documentation = p
|
||||
.documentation
|
||||
.as_ref()
|
||||
.map(|d| Docstring::new(d.to_owned()));
|
||||
let insert = Some(format!("{name}=").into_boxed_str());
|
||||
completions.add(Completion {
|
||||
name,
|
||||
qualified: None,
|
||||
insert,
|
||||
ty: p.ty,
|
||||
kind: Some(CompletionKind::Variable),
|
||||
module_name: None,
|
||||
import: None,
|
||||
builtin: false,
|
||||
is_type_check_only: false,
|
||||
is_definitively_raisable: false,
|
||||
documentation,
|
||||
});
|
||||
completions.add(Completion::argument(
|
||||
&p.name,
|
||||
p.ty,
|
||||
p.documentation.as_deref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2075,6 +2148,7 @@ fn token_suffix_by_kinds<const N: usize>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ruff_python_ast::helpers::is_dunder;
|
||||
use ruff_python_ast::token::{TokenKind, Tokens};
|
||||
use ruff_python_parser::{Mode, ParseOptions};
|
||||
use ty_module_resolver::ModuleName;
|
||||
@@ -3030,6 +3104,7 @@ class Foo(<CURSOR>):
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3047,6 +3122,7 @@ class Bar: ...
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3064,6 +3140,7 @@ class Bar: ...
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
@@ -3079,9 +3156,159 @@ class Foo(<CURSOR>",
|
||||
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
||||
Bar
|
||||
Foo
|
||||
metaclass=
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_metaclass() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
class Foo(meta<CURSOR>",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("metaclass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_metaclass_set() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
class Foo(metaclass=x, meta<CURSOR>",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.not_contains("metaclass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_metaclass_generic() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
class Foo[T](meta<CURSOR>",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("metaclass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_typed_dict_total() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
from typing import TypedDict
|
||||
|
||||
class Foo(TypedDict, tot<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("total");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_typed_dict_total_alias() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
from typing import TypedDict as TD
|
||||
|
||||
class Foo(TD, tot<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("total");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_typed_dict_total_set() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
from typing import TypedDict
|
||||
|
||||
class Foo(TypedDict, total=False, tot<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.not_contains("total");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_typed_dict_total_subclass() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
from typing import TypedDict
|
||||
|
||||
class Foo(TypedDict):
|
||||
x: int
|
||||
|
||||
class Bar(Foo, to<CURSOR>)
|
||||
",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("total");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_typed_dict_total_pep695_generic() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
from typing import TypedDict
|
||||
|
||||
class Foo[T](TypedDict, to<CURSOR>)
|
||||
",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("total");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_typed_dict_total_typevar_generic() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
from typing import Generic, TypeVar, TypedDict
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class Foo(TypedDict, Generic[T], to<CURSOR>)
|
||||
",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("total");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_init1() {
|
||||
let builder = completion_test_builder(
|
||||
@@ -3754,6 +3981,28 @@ bar(<CURSOR>
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_attribute_argument_no_arg_completions() {
|
||||
let builder = completion_test_builder(
|
||||
"\
|
||||
class A:
|
||||
class B:
|
||||
class C: ...
|
||||
|
||||
def f(aaaa): ...
|
||||
|
||||
f(A.B.<CURSOR>)
|
||||
",
|
||||
);
|
||||
|
||||
builder
|
||||
.skip_keywords()
|
||||
.skip_builtins()
|
||||
.build()
|
||||
.contains("C")
|
||||
.not_contains("aaaa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate1() {
|
||||
let builder = completion_test_builder(
|
||||
@@ -4026,7 +4275,10 @@ b.a.<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
builder.build().not_contains("a").contains("x");
|
||||
assert_snapshot!(builder.skip_dunders().build().snapshot(),
|
||||
@r"
|
||||
x
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -7537,6 +7789,7 @@ TypedDi<CURSOR>
|
||||
settings: CompletionSettings,
|
||||
skip_builtins: bool,
|
||||
skip_keywords: bool,
|
||||
skip_dunders: bool,
|
||||
type_signatures: bool,
|
||||
imports: bool,
|
||||
module_names: bool,
|
||||
@@ -7558,6 +7811,7 @@ TypedDi<CURSOR>
|
||||
.iter()
|
||||
.filter(|c| !self.skip_builtins || !c.builtin)
|
||||
.filter(|c| !self.skip_keywords || c.kind != Some(CompletionKind::Keyword))
|
||||
.filter(|c| !self.skip_dunders || !is_dunder(&c.name))
|
||||
.filter(|c| {
|
||||
self.predicate
|
||||
.as_ref()
|
||||
@@ -7621,6 +7875,16 @@ TypedDi<CURSOR>
|
||||
self
|
||||
}
|
||||
|
||||
/// When set, dunder completions are skipped.
|
||||
/// This is useful to reduce noise for snapshot tests
|
||||
/// when filtering on methods and attributes.
|
||||
///
|
||||
/// Not enabled by default.
|
||||
fn skip_dunders(mut self) -> CompletionTestBuilder {
|
||||
self.skip_dunders = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// When set, type signatures of each completion item are
|
||||
/// included in the snapshot. This is useful when one wants
|
||||
/// to specifically test types, but it usually best to leave
|
||||
@@ -7769,6 +8033,7 @@ TypedDi<CURSOR>
|
||||
settings: CompletionSettings::default(),
|
||||
skip_builtins: false,
|
||||
skip_keywords: false,
|
||||
skip_dunders: false,
|
||||
type_signatures: false,
|
||||
imports: false,
|
||||
module_names: false,
|
||||
|
||||
@@ -72,7 +72,7 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type definition]: Go to type definition
|
||||
--> main.py:4:1
|
||||
|
|
||||
@@ -82,15 +82,15 @@ mod tests {
|
||||
| ^^ Clicking here
|
||||
|
|
||||
info: Found 1 type definition
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| -------
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
");
|
||||
"#);
|
||||
}
|
||||
|
||||
// this is a slightly different case to the one above,
|
||||
@@ -137,7 +137,7 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type definition]: Go to type definition
|
||||
--> main.py:4:1
|
||||
|
|
||||
@@ -147,16 +147,15 @@ mod tests {
|
||||
| ^^ Clicking here
|
||||
|
|
||||
info: Found 1 type definition
|
||||
--> stdlib/typing.pyi:781:1
|
||||
|
|
||||
779 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
|
||||
780 |
|
||||
781 | Generic: type[_Generic]
|
||||
| -------
|
||||
782 |
|
||||
783 | class _ProtocolMeta(ABCMeta):
|
||||
|
|
||||
");
|
||||
--> stdlib/typing.pyi:1268:1
|
||||
|
|
||||
1266 | def __class_getitem__(cls, args: TypeVar | tuple[TypeVar, ...]) -> _Final: ...
|
||||
1267 |
|
||||
1268 | Generic: type[_Generic]
|
||||
| -------
|
||||
1269 | """Abstract base class for generic types.
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -797,13 +796,13 @@ mod tests {
|
||||
| -------
|
||||
5 | """some docs"""
|
||||
|
|
||||
::: stdlib/types.pyi:950:11
|
||||
::: stdlib/types.pyi:974:11
|
||||
|
|
||||
948 | if sys.version_info >= (3, 10):
|
||||
949 | @final
|
||||
950 | class NoneType:
|
||||
972 | if sys.version_info >= (3, 10):
|
||||
973 | @final
|
||||
974 | class NoneType:
|
||||
| --------
|
||||
951 | """The type of the None singleton."""
|
||||
975 | """The type of the None singleton."""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
@@ -851,13 +850,13 @@ mod tests {
|
||||
| -------
|
||||
5 | """some docs"""
|
||||
|
|
||||
::: stdlib/types.pyi:950:11
|
||||
::: stdlib/types.pyi:974:11
|
||||
|
|
||||
948 | if sys.version_info >= (3, 10):
|
||||
949 | @final
|
||||
950 | class NoneType:
|
||||
972 | if sys.version_info >= (3, 10):
|
||||
973 | @final
|
||||
974 | class NoneType:
|
||||
| --------
|
||||
951 | """The type of the None singleton."""
|
||||
975 | """The type of the None singleton."""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
@@ -1610,13 +1609,13 @@ def function():
|
||||
916 | """str(object='') -> str
|
||||
917 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
::: stdlib/types.pyi:950:11
|
||||
::: stdlib/types.pyi:974:11
|
||||
|
|
||||
948 | if sys.version_info >= (3, 10):
|
||||
949 | @final
|
||||
950 | class NoneType:
|
||||
972 | if sys.version_info >= (3, 10):
|
||||
973 | @final
|
||||
974 | class NoneType:
|
||||
| --------
|
||||
951 | """The type of the None singleton."""
|
||||
975 | """The type of the None singleton."""
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -723,13 +723,13 @@ mod tests {
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:6:5
|
||||
@@ -801,13 +801,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:10:6
|
||||
@@ -881,13 +881,13 @@ mod tests {
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:6
|
||||
@@ -919,13 +919,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:24
|
||||
@@ -1061,13 +1061,13 @@ mod tests {
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:6
|
||||
@@ -1099,13 +1099,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:24
|
||||
@@ -1259,13 +1259,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:11
|
||||
@@ -1297,13 +1297,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:23
|
||||
@@ -1486,13 +1486,13 @@ mod tests {
|
||||
x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3))
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:6
|
||||
@@ -1524,13 +1524,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:25
|
||||
@@ -1562,13 +1562,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:47
|
||||
@@ -1735,13 +1735,13 @@ mod tests {
|
||||
w[: int] = z
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:6:5
|
||||
@@ -2442,13 +2442,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/types.pyi:950:11
|
||||
--> stdlib/types.pyi:974:11
|
||||
|
|
||||
948 | if sys.version_info >= (3, 10):
|
||||
949 | @final
|
||||
950 | class NoneType:
|
||||
972 | if sys.version_info >= (3, 10):
|
||||
973 | @final
|
||||
974 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
951 | """The type of the None singleton."""
|
||||
975 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:5:20
|
||||
@@ -5525,13 +5525,13 @@ mod tests {
|
||||
y[: Literal[1, 2, 3, "hello"] | None] = x
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:13:9
|
||||
@@ -5615,13 +5615,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/types.pyi:950:11
|
||||
--> stdlib/types.pyi:974:11
|
||||
|
|
||||
948 | if sys.version_info >= (3, 10):
|
||||
949 | @final
|
||||
950 | class NoneType:
|
||||
972 | if sys.version_info >= (3, 10):
|
||||
973 | @final
|
||||
974 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
951 | """The type of the None singleton."""
|
||||
975 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:13:37
|
||||
@@ -6294,12 +6294,12 @@ mod tests {
|
||||
a[: <module 'foo'>] = foo
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/types.pyi:423:7
|
||||
--> stdlib/types.pyi:431:7
|
||||
|
|
||||
422 | @disjoint_base
|
||||
423 | class ModuleType:
|
||||
430 | @disjoint_base
|
||||
431 | class ModuleType:
|
||||
| ^^^^^^^^^^
|
||||
424 | """Create a module object.
|
||||
432 | """Create a module object.
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:6
|
||||
@@ -6342,13 +6342,13 @@ mod tests {
|
||||
a[: <special-form 'Literal["a", "b", "c"]'>] = Literal['a', 'b', 'c']
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:351:1
|
||||
--> stdlib/typing.pyi:487:1
|
||||
|
|
||||
349 | Final: _SpecialForm
|
||||
350 |
|
||||
351 | Literal: _SpecialForm
|
||||
485 | """
|
||||
486 |
|
||||
487 | Literal: _SpecialForm
|
||||
| ^^^^^^^
|
||||
352 | TypedDict: _SpecialForm
|
||||
488 | """Special typing form to define literal types (a.k.a. value types).
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:20
|
||||
@@ -6430,13 +6430,13 @@ mod tests {
|
||||
a[: <wrapper-descriptor '__get__' of 'function' objects>] = FunctionType.__get__
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/types.pyi:670:7
|
||||
--> stdlib/types.pyi:690:7
|
||||
|
|
||||
669 | @final
|
||||
670 | class WrapperDescriptorType:
|
||||
689 | @final
|
||||
690 | class WrapperDescriptorType:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
671 | @property
|
||||
672 | def __name__(self) -> str: ...
|
||||
691 | @property
|
||||
692 | def __name__(self) -> str: ...
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:6
|
||||
@@ -6482,13 +6482,13 @@ mod tests {
|
||||
a[: <method-wrapper '__call__' of function 'f'>] = f.__call__
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/types.pyi:684:7
|
||||
--> stdlib/types.pyi:704:7
|
||||
|
|
||||
683 | @final
|
||||
684 | class MethodWrapperType:
|
||||
703 | @final
|
||||
704 | class MethodWrapperType:
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
685 | @property
|
||||
686 | def __self__(self) -> object: ...
|
||||
705 | @property
|
||||
706 | def __self__(self) -> object: ...
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:6
|
||||
@@ -6500,13 +6500,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/types.pyi:134:9
|
||||
--> stdlib/types.pyi:139:9
|
||||
|
|
||||
132 | ) -> Self: ...
|
||||
133 |
|
||||
134 | def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
137 | ) -> Self: ...
|
||||
138 |
|
||||
139 | def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||
| ^^^^^^^^
|
||||
135 | """Call self as a function."""
|
||||
140 | """Call self as a function."""
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:22
|
||||
@@ -6573,14 +6573,14 @@ mod tests {
|
||||
Y[: <NewType pseudo-class 'N'>] = N
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:615:11
|
||||
|
|
||||
613 | TypeGuard: _SpecialForm
|
||||
614 |
|
||||
615 | class NewType:
|
||||
| ^^^^^^^
|
||||
616 | """NewType creates simple unique types with almost zero runtime overhead.
|
||||
|
|
||||
--> stdlib/typing.pyi:1040:11
|
||||
|
|
||||
1038 | """
|
||||
1039 |
|
||||
1040 | class NewType:
|
||||
| ^^^^^^^
|
||||
1041 | """NewType creates simple unique types with almost zero runtime overhead.
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:6
|
||||
|
|
||||
@@ -6614,15 +6614,15 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:637:28
|
||||
|
|
||||
635 | """
|
||||
636 |
|
||||
637 | def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm
|
||||
| ^^^^
|
||||
638 | if sys.version_info >= (3, 11):
|
||||
639 | @staticmethod
|
||||
|
|
||||
--> stdlib/typing.pyi:1062:28
|
||||
|
|
||||
1060 | """
|
||||
1061 |
|
||||
1062 | def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm
|
||||
| ^^^^
|
||||
1063 | if sys.version_info >= (3, 11):
|
||||
1064 | @staticmethod
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:44
|
||||
|
|
||||
@@ -6635,15 +6635,15 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:637:39
|
||||
|
|
||||
635 | """
|
||||
636 |
|
||||
637 | def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm
|
||||
| ^^
|
||||
638 | if sys.version_info >= (3, 11):
|
||||
639 | @staticmethod
|
||||
|
|
||||
--> stdlib/typing.pyi:1062:39
|
||||
|
|
||||
1060 | """
|
||||
1061 |
|
||||
1062 | def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm
|
||||
| ^^
|
||||
1063 | if sys.version_info >= (3, 11):
|
||||
1064 | @staticmethod
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:56
|
||||
|
|
||||
@@ -6656,14 +6656,14 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:615:11
|
||||
|
|
||||
613 | TypeGuard: _SpecialForm
|
||||
614 |
|
||||
615 | class NewType:
|
||||
| ^^^^^^^
|
||||
616 | """NewType creates simple unique types with almost zero runtime overhead.
|
||||
|
|
||||
--> stdlib/typing.pyi:1040:11
|
||||
|
|
||||
1038 | """
|
||||
1039 |
|
||||
1040 | class NewType:
|
||||
| ^^^^^^^
|
||||
1041 | """NewType creates simple unique types with almost zero runtime overhead.
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:6:6
|
||||
|
|
||||
@@ -6749,7 +6749,7 @@ mod tests {
|
||||
Strange = Protocol[T]",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
from typing import Protocol, TypeVar
|
||||
T = TypeVar([name=]'T')
|
||||
Strange[: <special-form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
@@ -6774,13 +6774,13 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:341:1
|
||||
--> stdlib/typing.pyi:346:1
|
||||
|
|
||||
340 | Union: _SpecialForm
|
||||
341 | Protocol: _SpecialForm
|
||||
344 | """
|
||||
345 |
|
||||
346 | Protocol: _SpecialForm
|
||||
| ^^^^^^^^
|
||||
342 | Callable: _SpecialForm
|
||||
343 | Type: _SpecialForm
|
||||
347 | """Base class for protocol classes.
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:26
|
||||
@@ -6807,7 +6807,7 @@ mod tests {
|
||||
4 | Strange[: <special-form 'typing.Protocol[T]'>] = Protocol[T]
|
||||
| ^
|
||||
|
|
||||
");
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6823,14 +6823,14 @@ mod tests {
|
||||
P = ParamSpec([name=]'P')
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:552:17
|
||||
--> stdlib/typing.pyi:901:17
|
||||
|
|
||||
550 | def __new__(
|
||||
551 | cls,
|
||||
552 | name: str,
|
||||
899 | def __new__(
|
||||
900 | cls,
|
||||
901 | name: str,
|
||||
| ^^^^
|
||||
553 | *,
|
||||
554 | bound: Any | None = None, # AnnotationForm
|
||||
902 | *,
|
||||
903 | bound: Any | None = None, # AnnotationForm
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:3:16
|
||||
@@ -6855,14 +6855,14 @@ mod tests {
|
||||
A = TypeAliasType([name=]'A', [value=]str)
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:2037:26
|
||||
--> stdlib/typing.pyi:2546:26
|
||||
|
|
||||
2035 | """
|
||||
2036 |
|
||||
2037 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
|
||||
2544 | """
|
||||
2545 |
|
||||
2546 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
|
||||
| ^^^^
|
||||
2038 | @property
|
||||
2039 | def __value__(self) -> Any: ... # AnnotationForm
|
||||
2547 | @property
|
||||
2548 | def __value__(self) -> Any: ... # AnnotationForm
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:3:20
|
||||
@@ -6873,14 +6873,14 @@ mod tests {
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:2037:37
|
||||
--> stdlib/typing.pyi:2546:37
|
||||
|
|
||||
2035 | """
|
||||
2036 |
|
||||
2037 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
|
||||
2544 | """
|
||||
2545 |
|
||||
2546 | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ...
|
||||
| ^^^^^
|
||||
2038 | @property
|
||||
2039 | def __value__(self) -> Any: ... # AnnotationForm
|
||||
2547 | @property
|
||||
2548 | def __value__(self) -> Any: ... # AnnotationForm
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:3:32
|
||||
@@ -6905,14 +6905,14 @@ mod tests {
|
||||
Ts = TypeVarTuple([name=]'Ts')
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/typing.pyi:412:30
|
||||
--> stdlib/typing.pyi:761:30
|
||||
|
|
||||
410 | def has_default(self) -> bool: ...
|
||||
411 | if sys.version_info >= (3, 13):
|
||||
412 | def __new__(cls, name: str, *, default: Any = ...) -> Self: ... # AnnotationForm
|
||||
759 | def has_default(self) -> bool: ...
|
||||
760 | if sys.version_info >= (3, 13):
|
||||
761 | def __new__(cls, name: str, *, default: Any = ...) -> Self: ... # AnnotationForm
|
||||
| ^^^^
|
||||
413 | elif sys.version_info >= (3, 12):
|
||||
414 | def __new__(cls, name: str) -> Self: ...
|
||||
762 | elif sys.version_info >= (3, 12):
|
||||
763 | def __new__(cls, name: str) -> Self: ...
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:3:20
|
||||
|
||||
@@ -16,7 +16,6 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
|
||||
return args
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
|
||||
reveal_type(args) # revealed: P@i.args
|
||||
reveal_type(kwargs) # revealed: P@i.kwargs
|
||||
|
||||
@@ -402,40 +402,48 @@ python-version = "3.12"
|
||||
`generic_list.py`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from typing import Literal, Sequence
|
||||
|
||||
def f[T](x: T) -> list[T]:
|
||||
return [x]
|
||||
|
||||
a = f("a")
|
||||
reveal_type(a) # revealed: list[str]
|
||||
x1 = f("a")
|
||||
reveal_type(x1) # revealed: list[str]
|
||||
|
||||
b: list[int | Literal["a"]] = f("a")
|
||||
reveal_type(b) # revealed: list[int | Literal["a"]]
|
||||
x2: list[int | Literal["a"]] = f("a")
|
||||
reveal_type(x2) # revealed: list[int | Literal["a"]]
|
||||
|
||||
c: list[int | str] = f("a")
|
||||
reveal_type(c) # revealed: list[int | str]
|
||||
x3: list[int | str] = f("a")
|
||||
reveal_type(x3) # revealed: list[int | str]
|
||||
|
||||
d: list[int | tuple[int, int]] = f((1, 2))
|
||||
reveal_type(d) # revealed: list[int | tuple[int, int]]
|
||||
x4: list[int | tuple[int, int]] = f((1, 2))
|
||||
reveal_type(x4) # revealed: list[int | tuple[int, int]]
|
||||
|
||||
e: list[int] = f(True)
|
||||
reveal_type(e) # revealed: list[int]
|
||||
x5: list[int] = f(True)
|
||||
reveal_type(x5) # revealed: list[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `list[int | str]` is not assignable to `list[int]`"
|
||||
g: list[int] = f("a")
|
||||
x6: list[int] = f("a")
|
||||
|
||||
# error: [invalid-assignment] "Object of type `list[str]` is not assignable to `tuple[int]`"
|
||||
h: tuple[int] = f("a")
|
||||
x7: tuple[int] = f("a")
|
||||
|
||||
def f2[T: int](x: T) -> T:
|
||||
return x
|
||||
|
||||
i: int = f2(True)
|
||||
reveal_type(i) # revealed: Literal[True]
|
||||
x8: int = f2(True)
|
||||
reveal_type(x8) # revealed: Literal[True]
|
||||
|
||||
j: int | str = f2(True)
|
||||
reveal_type(j) # revealed: Literal[True]
|
||||
x9: int | str = f2(True)
|
||||
reveal_type(x9) # revealed: Literal[True]
|
||||
|
||||
# TODO: We could choose a concrete type here.
|
||||
x10: list[int | str] | list[int | None] = [1, 2, 3]
|
||||
reveal_type(x10) # revealed: list[Unknown | int]
|
||||
|
||||
# TODO: And here similarly.
|
||||
x11: Sequence[int | str] | Sequence[int | None] = [1, 2, 3]
|
||||
reveal_type(x11) # revealed: list[Unknown | int]
|
||||
```
|
||||
|
||||
A function's arguments are also inferred using the type context:
|
||||
@@ -610,6 +618,73 @@ x1: X[int | None] = X()
|
||||
reveal_type(x1) # revealed: X[None]
|
||||
```
|
||||
|
||||
## Declared type preference sees through subtyping
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Similarly, if the inferred type is a subtype of the declared type, we prefer declared type
|
||||
assignments that are in non-covariant position.
|
||||
|
||||
```py
|
||||
from collections import defaultdict
|
||||
from typing import Any, Iterable, Literal, MutableSequence, Sequence
|
||||
|
||||
x1: Sequence[Any] = [1, 2, 3]
|
||||
reveal_type(x1) # revealed: list[int]
|
||||
|
||||
x2: MutableSequence[Any] = [1, 2, 3]
|
||||
reveal_type(x2) # revealed: list[Any]
|
||||
|
||||
x3: Iterable[Any] = [1, 2, 3]
|
||||
reveal_type(x3) # revealed: list[int]
|
||||
|
||||
x4: Iterable[Iterable[Any]] = [[1, 2, 3]]
|
||||
reveal_type(x4) # revealed: list[list[int]]
|
||||
|
||||
x5: list[Iterable[Any]] = [[1, 2, 3]]
|
||||
reveal_type(x5) # revealed: list[Iterable[Any]]
|
||||
|
||||
x6: Iterable[list[Any]] = [[1, 2, 3]]
|
||||
reveal_type(x6) # revealed: list[list[Any]]
|
||||
|
||||
class X[T]:
|
||||
value: T
|
||||
|
||||
def __init__(self, value: T): ...
|
||||
|
||||
class A[T](X[T]): ...
|
||||
|
||||
def a[T](value: T) -> A[T]:
|
||||
return A(value)
|
||||
|
||||
x7: A[object] = A(1)
|
||||
reveal_type(x7) # revealed: A[object]
|
||||
|
||||
x8: X[object] = A(1)
|
||||
reveal_type(x8) # revealed: A[object]
|
||||
|
||||
x9: X[object] | None = A(1)
|
||||
reveal_type(x9) # revealed: A[object]
|
||||
|
||||
x10: X[object] | None = a(1)
|
||||
reveal_type(x10) # revealed: A[object]
|
||||
|
||||
def f[T](x: T) -> list[list[T]]:
|
||||
return [[x]]
|
||||
|
||||
x11: Sequence[Sequence[Any]] = f(1)
|
||||
reveal_type(x11) # revealed: list[list[int]]
|
||||
|
||||
x12: Sequence[list[Any]] = f(1)
|
||||
reveal_type(x12) # revealed: list[list[Any]]
|
||||
|
||||
x13: dict[int, dict[str, int]] = defaultdict(dict)
|
||||
reveal_type(x13) # revealed: defaultdict[int, dict[str, int]]
|
||||
```
|
||||
|
||||
## Narrow generic unions
|
||||
|
||||
```toml
|
||||
|
||||
@@ -2002,6 +2002,64 @@ def _(ns: argparse.Namespace):
|
||||
ns.whatever = 42
|
||||
```
|
||||
|
||||
### `__setattr__` is a fallback for explicitly defined attributes
|
||||
|
||||
When a class has both a custom `__setattr__` method and explicitly defined attributes, the
|
||||
`__setattr__` method is treated as a fallback. The type of the explicit attribute takes precedence
|
||||
over the `__setattr__` parameter type.
|
||||
|
||||
This matches the behavior of other type checkers and reflects the common pattern in libraries like
|
||||
PyTorch, where `__setattr__` may have a narrow type signature but forwards to
|
||||
`super().__setattr__()` for attributes that don't match.
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
class Tensor: ...
|
||||
|
||||
class Module:
|
||||
def __setattr__(self, name: str, value: Union[Tensor, "Module"]) -> None:
|
||||
super().__setattr__(name, value)
|
||||
|
||||
class MyModule(Module):
|
||||
some_param: int # Explicit attribute with type `int`
|
||||
|
||||
def use_module(m: MyModule, param: int) -> None:
|
||||
# This is allowed because `some_param` is explicitly defined with type `int`,
|
||||
# even though `__setattr__` only accepts `Union[Tensor, Module]`.
|
||||
m.some_param = param
|
||||
|
||||
# But assigning to an attribute that's not explicitly defined will still
|
||||
# use `__setattr__` for validation.
|
||||
# error: [unresolved-attribute] "Cannot assign object of type `int` to attribute `undefined_param` on type `MyModule` with custom `__setattr__` method."
|
||||
m.undefined_param = param
|
||||
```
|
||||
|
||||
### `__setattr__` returning `Never` blocks all assignments
|
||||
|
||||
When `__setattr__` returns `Never` (indicating an immutable class), all attribute assignments are
|
||||
blocked, even if the value type doesn't match `__setattr__`'s parameter type.
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
class Immutable:
|
||||
x: float
|
||||
|
||||
def __setattr__(self, name: str, value: int) -> NoReturn:
|
||||
raise AttributeError("Immutable")
|
||||
|
||||
def _(obj: Immutable) -> None:
|
||||
# Even though `"foo"` doesn't match `__setattr__`'s `value: int` parameter,
|
||||
# we still detect that `__setattr__` returns `Never` and block the assignment.
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `x` on type `Immutable` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||
obj.x = "foo"
|
||||
|
||||
# Same for assignments that would match `__setattr__`'s parameter type.
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `x` on type `Immutable` whose `__setattr__` method returns `Never`/`NoReturn`"
|
||||
obj.x = 42
|
||||
```
|
||||
|
||||
## Objects of all types have a `__class__` method
|
||||
|
||||
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
||||
|
||||
@@ -32,11 +32,11 @@ reveal_type(l3) # revealed: list[int | str]
|
||||
|
||||
def _(l: list[int] | None = None):
|
||||
l1 = l or list()
|
||||
reveal_type(l1) # revealed: list[int] | list[Unknown]
|
||||
reveal_type(l1) # revealed: (list[int] & ~AlwaysFalsy) | list[Unknown]
|
||||
|
||||
l2: list[int] = l or list()
|
||||
# it would be better if this were `list[int]`? (https://github.com/astral-sh/ty/issues/136)
|
||||
reveal_type(l2) # revealed: list[int] | list[Unknown]
|
||||
reveal_type(l2) # revealed: (list[int] & ~AlwaysFalsy) | list[Unknown]
|
||||
|
||||
def f[T](x: T, cond: bool) -> T | list[T]:
|
||||
return x if cond else [x]
|
||||
@@ -91,7 +91,7 @@ def get_data() -> dict | None:
|
||||
def wrap_data() -> list[dict]:
|
||||
if not (res := get_data()):
|
||||
return list1({})
|
||||
reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown]]
|
||||
reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy]
|
||||
# `list[dict[Unknown, Unknown] & ~AlwaysFalsy]` and `list[dict[Unknown, Unknown]]` are incompatible,
|
||||
# but the return type check passes here because the type of `list1(res)` is inferred
|
||||
# by bidirectional type inference using the annotated return type, and the type of `res` is not used.
|
||||
@@ -100,7 +100,7 @@ def wrap_data() -> list[dict]:
|
||||
def wrap_data2() -> list[dict] | None:
|
||||
if not (res := get_data()):
|
||||
return None
|
||||
reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown]]
|
||||
reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy]
|
||||
return list1(res)
|
||||
|
||||
def deco[T](func: Callable[[], T]) -> Callable[[], T]:
|
||||
@@ -111,7 +111,7 @@ def outer() -> Callable[[], list[dict]]:
|
||||
def inner() -> list[dict]:
|
||||
if not (res := get_data()):
|
||||
return list1({})
|
||||
reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown]]
|
||||
reveal_type(list1(res)) # revealed: list[dict[Unknown, Unknown] & ~AlwaysFalsy]
|
||||
return list1(res)
|
||||
return inner
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ def _(
|
||||
h: Literal[42] | Not[Literal[42]],
|
||||
i: Not[Literal[42]] | Literal[42],
|
||||
):
|
||||
reveal_type(a) # revealed: object
|
||||
reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy
|
||||
reveal_type(b) # revealed: object
|
||||
reveal_type(c) # revealed: object
|
||||
reveal_type(d) # revealed: object
|
||||
|
||||
@@ -13,7 +13,7 @@ reveal_type(1 is not 1) # revealed: bool
|
||||
reveal_type(1 is 2) # revealed: Literal[False]
|
||||
reveal_type(1 is not 7) # revealed: Literal[True]
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported between objects of type `Literal[1]` and `Literal[""]`"
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown | Literal[True]
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True]
|
||||
```
|
||||
|
||||
## Integer instance
|
||||
|
||||
@@ -37,7 +37,7 @@ class C:
|
||||
return self
|
||||
|
||||
x = A() < B() < C()
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B
|
||||
|
||||
y = 0 < 1 < A() < 3
|
||||
reveal_type(y) # revealed: Literal[False] | A
|
||||
|
||||
@@ -386,3 +386,52 @@ def _(target: int, flag: NotBoolable):
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## Matching on enum | None without covering None
|
||||
|
||||
When matching on a union of an enum and None, code after the match should still be reachable if None
|
||||
is not covered by any case, even when all enum members are covered.
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
|
||||
class Answer(Enum):
|
||||
YES = 1
|
||||
NO = 2
|
||||
|
||||
def _(answer: Answer | None):
|
||||
y = 0
|
||||
match answer:
|
||||
case Answer.YES:
|
||||
y = 1
|
||||
case Answer.NO:
|
||||
y = 2
|
||||
|
||||
# The match is not exhaustive because None is not covered,
|
||||
# so y could still be 0
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
|
||||
def _(answer: Answer | None):
|
||||
match answer:
|
||||
case Answer.YES:
|
||||
return 1
|
||||
case Answer.NO:
|
||||
return 2
|
||||
|
||||
# Code here is reachable because None is not covered
|
||||
reveal_type(answer) # revealed: None
|
||||
return 3
|
||||
|
||||
class Foo: ...
|
||||
|
||||
def _(answer: Answer | None):
|
||||
match answer:
|
||||
case Answer.YES:
|
||||
return
|
||||
case Answer.NO:
|
||||
return
|
||||
|
||||
# New assignments after the match should not be `Never`
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
@@ -525,6 +525,42 @@ c.name = None
|
||||
c.name = 42
|
||||
```
|
||||
|
||||
### Overriding properties in subclasses
|
||||
|
||||
When a subclass overrides a property, accessing other inherited properties from within the
|
||||
overriding property methods should still work correctly.
|
||||
|
||||
```py
|
||||
class Base:
|
||||
_value: float = 0.0
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, v: float) -> None:
|
||||
self._value = v
|
||||
|
||||
@property
|
||||
def other(self) -> float:
|
||||
return self.value
|
||||
|
||||
@other.setter
|
||||
def other(self, v: float) -> None:
|
||||
self.value = v
|
||||
|
||||
class Derived(Base):
|
||||
@property
|
||||
def other(self) -> float:
|
||||
return self.value
|
||||
|
||||
@other.setter
|
||||
def other(self, v: float) -> None:
|
||||
reveal_type(self.value) # revealed: int | float
|
||||
self.value = v
|
||||
```
|
||||
|
||||
### Properties with no setters
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
@@ -10,8 +10,8 @@ def _(foo: str):
|
||||
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]
|
||||
reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False]
|
||||
reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True]
|
||||
```
|
||||
|
||||
## AND
|
||||
@@ -20,8 +20,8 @@ def _(foo: str):
|
||||
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(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False]
|
||||
reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | 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[""]
|
||||
|
||||
@@ -790,6 +790,44 @@ static_assert(not is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
```
|
||||
|
||||
## TypeGuard
|
||||
|
||||
`TypeGuard[T]` is covariant in `T`. The typing spec doesn't explicitly call this out, but it follows
|
||||
from similar logic to invariance of `TypeIs` except without the negative case.
|
||||
|
||||
Formally, suppose we have types `A` and `B` with `B < A`. Take `x: object` to be the value that all
|
||||
subsequent `TypeGuard`s are narrowing.
|
||||
|
||||
We can assign `p: TypeGuard[A] = q` where `q: TypeGuard[B]` because
|
||||
|
||||
- if `q` is `False`, then no constraints were learned on `x` before and none are now learned, so
|
||||
nothing changes
|
||||
- if `q` is `True`, then we know `x: B`. From `B < A`, we conclude `x: A`.
|
||||
|
||||
We _cannot_ assign `p: TypeGuard[B] = q` where `q: TypeGuard[A]` because if `q` is `True`, we would
|
||||
be concluding `x: B` from `x: A`, which is an unsafe downcast.
|
||||
|
||||
```py
|
||||
from typing import TypeGuard
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
class C[T]:
|
||||
def check(x: object) -> TypeGuard[T]:
|
||||
# this is a bad check, but we only care about it type-checking
|
||||
return False
|
||||
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
```
|
||||
|
||||
## Type aliases
|
||||
|
||||
The variance of the type alias matches the variance of the value type (RHS type).
|
||||
|
||||
@@ -310,7 +310,7 @@ x11: list[Literal[1] | Literal[2] | Literal[3]] = [1, 2, 3]
|
||||
reveal_type(x11) # revealed: list[Literal[1, 2, 3]]
|
||||
|
||||
x12: Y[Y[Literal[1]]] = [[1]]
|
||||
reveal_type(x12) # revealed: list[list[Literal[1]]]
|
||||
reveal_type(x12) # revealed: list[Y[Literal[1]]]
|
||||
|
||||
x13: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)]
|
||||
reveal_type(x13) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]]
|
||||
@@ -347,3 +347,58 @@ reveal_type(x21) # revealed: X[Literal[1]]
|
||||
x22: X[Literal[1]] | None = x(1)
|
||||
reveal_type(x22) # revealed: X[Literal[1]]
|
||||
```
|
||||
|
||||
## Literal annotations see through subtyping
|
||||
|
||||
```py
|
||||
from typing import Any, Iterable, Literal, MutableSequence, Sequence
|
||||
|
||||
x1: Sequence[Literal[1, 2, 3]] = [1, 2, 3]
|
||||
reveal_type(x1) # revealed: list[Literal[1, 2, 3]]
|
||||
|
||||
x2: MutableSequence[Literal[1, 2, 3]] = [1, 2, 3]
|
||||
reveal_type(x2) # revealed: list[Literal[1, 2, 3]]
|
||||
|
||||
x3: Iterable[Literal[1, 2, 3]] = [1, 2, 3]
|
||||
reveal_type(x3) # revealed: list[Literal[1, 2, 3]]
|
||||
|
||||
class Sup1[T]:
|
||||
value: T
|
||||
|
||||
class Sub1[T](Sup1[T]): ...
|
||||
|
||||
def sub1[T](value: T) -> Sub1[T]:
|
||||
return Sub1()
|
||||
|
||||
x4: Sub1[Literal[1]] = sub1(1)
|
||||
reveal_type(x4) # revealed: Sub1[Literal[1]]
|
||||
|
||||
x5: Sup1[Literal[1]] = sub1(1)
|
||||
reveal_type(x5) # revealed: Sub1[Literal[1]]
|
||||
|
||||
x6: Sup1[Literal[1]] | None = sub1(1)
|
||||
reveal_type(x6) # revealed: Sub1[Literal[1]]
|
||||
|
||||
x7: Sup1[Literal[1]] | None = sub1(1)
|
||||
reveal_type(x7) # revealed: Sub1[Literal[1]]
|
||||
|
||||
class Sup2A[T, U]:
|
||||
value: tuple[T, U]
|
||||
|
||||
class Sup2B[T, U]:
|
||||
value: tuple[T, U]
|
||||
|
||||
class Sub2[T, U](Sup2A[T, Any], Sup2B[Any, U]): ...
|
||||
|
||||
def sub2[T, U](x: T, y: U) -> Sub2[T, U]:
|
||||
return Sub2()
|
||||
|
||||
x8 = sub2(1, 2)
|
||||
reveal_type(x8) # revealed: Sub2[int, int]
|
||||
|
||||
x9: Sup2A[Literal[1], Literal[2]] = sub2(1, 2)
|
||||
reveal_type(x9) # revealed: Sub2[Literal[1], int]
|
||||
|
||||
x10: Sup2B[Literal[1], Literal[2]] = sub2(1, 2)
|
||||
reveal_type(x10) # revealed: Sub2[int, Literal[2]]
|
||||
```
|
||||
|
||||
@@ -105,8 +105,8 @@ reveal_type(y) # revealed: Unknown
|
||||
|
||||
```py
|
||||
def one(x: int | None):
|
||||
assert (y := x), reveal_type(y) # revealed: int | None
|
||||
reveal_type(y) # revealed: int
|
||||
assert (y := x), reveal_type(y) # revealed: (int & ~AlwaysTruthy) | None
|
||||
reveal_type(y) # revealed: int & ~AlwaysFalsy
|
||||
|
||||
def two(x: int | None):
|
||||
assert isinstance((y := x), int), reveal_type(y) # revealed: None
|
||||
|
||||
@@ -104,14 +104,14 @@ class C:
|
||||
value: str | None
|
||||
|
||||
def foo(c: C):
|
||||
# The truthiness check `c.value` narrows to `str`.
|
||||
# The truthiness check `c.value` narrows to `str & ~AlwaysFalsy`.
|
||||
# The subsequent `len(c.value)` doesn't narrow further since `str` is not narrowable by len().
|
||||
if c.value and len(c.value):
|
||||
reveal_type(c.value) # revealed: str
|
||||
reveal_type(c.value) # revealed: str & ~AlwaysFalsy
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `len` is incorrect: Expected `Sized`, found `str | None`"
|
||||
if len(c.value) and c.value:
|
||||
reveal_type(c.value) # revealed: str
|
||||
reveal_type(c.value) # revealed: str & ~AlwaysFalsy
|
||||
|
||||
if c.value is None or not len(c.value):
|
||||
reveal_type(c.value) # revealed: str | None
|
||||
@@ -279,6 +279,101 @@ def _(t9: tuple[int | None, str] | tuple[str, int]):
|
||||
reveal_type(t9) # revealed: tuple[int | None, str] | tuple[str, int]
|
||||
```
|
||||
|
||||
### Tagged unions of tuples (equality narrowing)
|
||||
|
||||
Narrow unions of tuples based on literal tag elements using `==` comparison:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]):
|
||||
if x[0] == "tag1":
|
||||
reveal_type(x) # revealed: tuple[Literal["tag1"], A]
|
||||
reveal_type(x[1]) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[Literal["tag2"], B, C]
|
||||
reveal_type(x[1]) # revealed: B
|
||||
reveal_type(x[2]) # revealed: C
|
||||
|
||||
def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]):
|
||||
if x[0] != "tag1":
|
||||
reveal_type(x) # revealed: tuple[Literal["tag2"], B, C]
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[Literal["tag1"], A]
|
||||
|
||||
# With int literals
|
||||
def _(x: tuple[Literal[1], A] | tuple[Literal[2], B]):
|
||||
if x[0] == 1:
|
||||
reveal_type(x) # revealed: tuple[Literal[1], A]
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[Literal[2], B]
|
||||
|
||||
# With bytes literals
|
||||
def _(x: tuple[Literal[b"a"], A] | tuple[Literal[b"b"], B]):
|
||||
if x[0] == b"a":
|
||||
reveal_type(x) # revealed: tuple[Literal[b"a"], A]
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[Literal[b"b"], B]
|
||||
|
||||
# Multiple tuple variants
|
||||
def _(x: tuple[Literal["a"], A] | tuple[Literal["b"], B] | tuple[Literal["c"], C]):
|
||||
if x[0] == "a":
|
||||
reveal_type(x) # revealed: tuple[Literal["a"], A]
|
||||
elif x[0] == "b":
|
||||
reveal_type(x) # revealed: tuple[Literal["b"], B]
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[Literal["c"], C]
|
||||
|
||||
# Using index 1 instead of 0
|
||||
def _(x: tuple[A, Literal["tag1"]] | tuple[B, Literal["tag2"]]):
|
||||
if x[1] == "tag1":
|
||||
reveal_type(x) # revealed: tuple[A, Literal["tag1"]]
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[B, Literal["tag2"]]
|
||||
```
|
||||
|
||||
Narrowing is restricted to `Literal` tag elements. If any tuple has a non-literal type at the
|
||||
discriminating index, we can't safely narrow with equality:
|
||||
|
||||
```py
|
||||
def _(x: tuple[Literal["tag1"], A] | tuple[str, B]):
|
||||
# Can't narrow because second tuple has `str` (not literal) at index 0
|
||||
if x[0] == "tag1":
|
||||
reveal_type(x) # revealed: tuple[Literal["tag1"], A] | tuple[str, B]
|
||||
else:
|
||||
# But we *can* narrow with inequality
|
||||
reveal_type(x) # revealed: tuple[str, B]
|
||||
```
|
||||
|
||||
If the index is out of bounds for any tuple in the union, we also skip narrowing (a diagnostic will
|
||||
be emitted elsewhere for the out-of-bounds access):
|
||||
|
||||
```py
|
||||
def _(x: tuple[A, Literal["a"]] | tuple[B]):
|
||||
# error: [index-out-of-bounds]
|
||||
if x[1] == "a":
|
||||
# Can't narrow because index 1 is out of bounds for second tuple
|
||||
reveal_type(x) # revealed: tuple[A, Literal["a"]] | tuple[B]
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[A, Literal["a"]] | tuple[B]
|
||||
```
|
||||
|
||||
We can still narrow tuples when non-tuple types are present in the union:
|
||||
|
||||
```py
|
||||
def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B] | list[int]):
|
||||
if x[0] == "tag1":
|
||||
# A list of ints could have int subclasses in it,
|
||||
# and int subclasses could have custom `__eq__` methods such that they
|
||||
# compare equal to `"tag1"`, so `list[int]` cannot be narrowed out of this
|
||||
# union.
|
||||
reveal_type(x) # revealed: tuple[Literal["tag1"], A] | list[int]
|
||||
```
|
||||
|
||||
### String subscript
|
||||
|
||||
```py
|
||||
|
||||
@@ -299,7 +299,7 @@ def f(l: list[str | None] | None):
|
||||
def f(a: A):
|
||||
if a:
|
||||
def _():
|
||||
reveal_type(a) # revealed: A
|
||||
reveal_type(a) # revealed: A & ~AlwaysFalsy
|
||||
a.x = None
|
||||
```
|
||||
|
||||
|
||||
@@ -365,12 +365,12 @@ def f(
|
||||
if isinstance(c, bool):
|
||||
reveal_type(c) # revealed: Never
|
||||
else:
|
||||
reveal_type(c) # revealed: P
|
||||
reveal_type(c) # revealed: P & ~AlwaysTruthy
|
||||
|
||||
if isinstance(d, bool):
|
||||
reveal_type(d) # revealed: Never
|
||||
else:
|
||||
reveal_type(d) # revealed: P
|
||||
reveal_type(d) # revealed: P & ~AlwaysFalsy
|
||||
```
|
||||
|
||||
## Narrowing if an object of type `Any` or `Unknown` is used as the second argument
|
||||
|
||||
@@ -58,9 +58,9 @@ and `tuple[()]` in the negative case (see <https://github.com/astral-sh/ty/issue
|
||||
```py
|
||||
def _(x: tuple[int, ...]):
|
||||
if len(x):
|
||||
reveal_type(x) # revealed: tuple[int, ...]
|
||||
reveal_type(x) # revealed: tuple[int, ...] & ~AlwaysFalsy
|
||||
else:
|
||||
reveal_type(x) # revealed: tuple[int, ...]
|
||||
reveal_type(x) # revealed: tuple[int, ...] & ~AlwaysTruthy
|
||||
```
|
||||
|
||||
## Unions of narrowable types
|
||||
@@ -70,9 +70,9 @@ from typing import Literal
|
||||
|
||||
def _(x: Literal["foo", ""] | tuple[int, ...]):
|
||||
if len(x):
|
||||
reveal_type(x) # revealed: Literal["foo"] | tuple[int, ...]
|
||||
reveal_type(x) # revealed: Literal["foo"] | (tuple[int, ...] & ~AlwaysFalsy)
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[""] | tuple[int, ...]
|
||||
reveal_type(x) # revealed: Literal[""] | (tuple[int, ...] & ~AlwaysTruthy)
|
||||
```
|
||||
|
||||
## Types that are not narrowed
|
||||
@@ -119,13 +119,13 @@ def _(lines: list[str]):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
reveal_type(line) # revealed: str
|
||||
reveal_type(line) # revealed: str & ~AlwaysFalsy
|
||||
value = line if len(line) < 3 else ""
|
||||
reveal_type(value) # revealed: str
|
||||
reveal_type(value) # revealed: (str & ~AlwaysFalsy) | Literal[""]
|
||||
|
||||
if len(value):
|
||||
# `Literal[""]` is removed, `str & ~AlwaysFalsy` is unchanged
|
||||
reveal_type(value) # revealed: str
|
||||
reveal_type(value) # revealed: str & ~AlwaysFalsy
|
||||
# Accessing value[0] is safe here
|
||||
_ = value[0]
|
||||
```
|
||||
|
||||
@@ -375,3 +375,70 @@ try:
|
||||
except ValueError:
|
||||
pass
|
||||
```
|
||||
|
||||
## Narrowing tagged unions of tuples
|
||||
|
||||
Narrow unions of tuples based on literal tag elements in `match` statements:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def _(x: tuple[Literal["tag1"], A] | tuple[Literal["tag2"], B, C]):
|
||||
match x[0]:
|
||||
case "tag1":
|
||||
reveal_type(x) # revealed: tuple[Literal["tag1"], A]
|
||||
reveal_type(x[1]) # revealed: A
|
||||
case "tag2":
|
||||
reveal_type(x) # revealed: tuple[Literal["tag2"], B, C]
|
||||
reveal_type(x[1]) # revealed: B
|
||||
reveal_type(x[2]) # revealed: C
|
||||
case _:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
# With int literals
|
||||
def _(x: tuple[Literal[1], A] | tuple[Literal[2], B]):
|
||||
match x[0]:
|
||||
case 1:
|
||||
reveal_type(x) # revealed: tuple[Literal[1], A]
|
||||
case 2:
|
||||
reveal_type(x) # revealed: tuple[Literal[2], B]
|
||||
case _:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
# With bytes literals
|
||||
def _(x: tuple[Literal[b"a"], A] | tuple[Literal[b"b"], B]):
|
||||
match x[0]:
|
||||
case b"a":
|
||||
reveal_type(x) # revealed: tuple[Literal[b"a"], A]
|
||||
case b"b":
|
||||
reveal_type(x) # revealed: tuple[Literal[b"b"], B]
|
||||
case _:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
# Using index 1 instead of 0
|
||||
def _(x: tuple[A, Literal["tag1"]] | tuple[B, Literal["tag2"]]):
|
||||
match x[1]:
|
||||
case "tag1":
|
||||
reveal_type(x) # revealed: tuple[A, Literal["tag1"]]
|
||||
case "tag2":
|
||||
reveal_type(x) # revealed: tuple[B, Literal["tag2"]]
|
||||
case _:
|
||||
reveal_type(x) # revealed: Never
|
||||
```
|
||||
|
||||
Narrowing is restricted to `Literal` tag elements:
|
||||
|
||||
```py
|
||||
def _(x: tuple[Literal["tag1"], A] | tuple[str, B]):
|
||||
match x[0]:
|
||||
case "tag1":
|
||||
# Can't narrow because second tuple has `str` (not literal) at index 0
|
||||
reveal_type(x) # revealed: tuple[Literal["tag1"], A] | tuple[str, B]
|
||||
case _:
|
||||
# But we *can* narrow with inequality
|
||||
reveal_type(x) # revealed: tuple[str, B]
|
||||
```
|
||||
|
||||
@@ -82,19 +82,19 @@ class B: ...
|
||||
|
||||
def f(x: A | B):
|
||||
if x:
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy)
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy)
|
||||
|
||||
if x and not x:
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy)
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if x or not x:
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy)
|
||||
```
|
||||
|
||||
### Truthiness of Types
|
||||
@@ -111,9 +111,9 @@ x = int if flag() else str
|
||||
reveal_type(x) # revealed: <class 'int'> | <class 'str'>
|
||||
|
||||
if x:
|
||||
reveal_type(x) # revealed: <class 'int'> | <class 'str'>
|
||||
reveal_type(x) # revealed: (<class 'int'> & ~AlwaysFalsy) | (<class 'str'> & ~AlwaysFalsy)
|
||||
else:
|
||||
reveal_type(x) # revealed: <class 'int'> | <class 'str'>
|
||||
reveal_type(x) # revealed: (<class 'int'> & ~AlwaysTruthy) | (<class 'str'> & ~AlwaysTruthy)
|
||||
```
|
||||
|
||||
## Determined Truthiness
|
||||
@@ -179,9 +179,9 @@ if isinstance(x, str) and not isinstance(x, B):
|
||||
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"]
|
||||
|
||||
if z:
|
||||
reveal_type(z) # revealed: (A & str & ~B) | Literal[42, "hello"]
|
||||
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"]
|
||||
else:
|
||||
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, ""]
|
||||
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""]
|
||||
```
|
||||
|
||||
## Narrowing Multiple Variables
|
||||
@@ -219,7 +219,7 @@ x = A()
|
||||
|
||||
if x and not x:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A
|
||||
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
|
||||
else:
|
||||
y = x
|
||||
reveal_type(y) # revealed: A
|
||||
@@ -264,16 +264,16 @@ def _(
|
||||
):
|
||||
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
|
||||
if ta:
|
||||
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
|
||||
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
|
||||
|
||||
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
|
||||
if af:
|
||||
reveal_type(af) # revealed: type[AmbiguousClass]
|
||||
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `MetaDeferred`"
|
||||
if d:
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(d) # revealed: type[DeferredClass]
|
||||
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
||||
|
||||
tf = TruthyClass if flag else FalsyClass
|
||||
reveal_type(tf) # revealed: <class 'TruthyClass'> | <class 'FalsyClass'>
|
||||
@@ -296,12 +296,12 @@ def _(x: Literal[0, 1]):
|
||||
reveal_type(x and A()) # revealed: Literal[0] | A
|
||||
|
||||
def _(x: str):
|
||||
reveal_type(x or A()) # revealed: str | A
|
||||
reveal_type(x and A()) # revealed: str | A
|
||||
reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A
|
||||
reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A
|
||||
|
||||
def _(x: bool | str):
|
||||
reveal_type(x or A()) # revealed: Literal[True] | str | A
|
||||
reveal_type(x and A()) # revealed: Literal[False] | str | A
|
||||
reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A
|
||||
reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A
|
||||
|
||||
class Falsy:
|
||||
def __bool__(self) -> Literal[False]:
|
||||
|
||||
@@ -12,21 +12,19 @@ from typing_extensions import TypeGuard, TypeIs
|
||||
def _(
|
||||
a: TypeGuard[str],
|
||||
b: TypeIs[str | int],
|
||||
c: TypeGuard[Intersection[complex, Not[int], Not[float]]],
|
||||
c: TypeGuard[bool],
|
||||
d: TypeIs[tuple[TypeOf[bytes]]],
|
||||
e: TypeGuard, # error: [invalid-type-form]
|
||||
f: TypeIs, # error: [invalid-type-form]
|
||||
):
|
||||
# TODO: Should be `TypeGuard[str]`
|
||||
reveal_type(a) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(a) # revealed: TypeGuard[str]
|
||||
reveal_type(b) # revealed: TypeIs[str | int]
|
||||
# TODO: Should be `TypeGuard[complex & ~int & ~float]`
|
||||
reveal_type(c) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(c) # revealed: TypeGuard[bool]
|
||||
reveal_type(d) # revealed: TypeIs[tuple[<class 'bytes'>]]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
|
||||
# TODO: error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeGuard[str]`"
|
||||
# error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeGuard[str]`"
|
||||
def _(a) -> TypeGuard[str]: ...
|
||||
|
||||
# error: [invalid-return-type] "Function always implicitly returns `None`, which is not assignable to return type `TypeIs[str]`"
|
||||
@@ -38,8 +36,7 @@ def g(a) -> TypeIs[str]:
|
||||
return True
|
||||
|
||||
def _(a: object):
|
||||
# TODO: Should be `TypeGuard[str @ a]`
|
||||
reveal_type(f(a)) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(f(a)) # revealed: TypeGuard[str @ a]
|
||||
reveal_type(g(a)) # revealed: TypeIs[str @ a]
|
||||
```
|
||||
|
||||
@@ -96,6 +93,72 @@ def _(a: int) -> TypeIs[str]: ...
|
||||
def _(a: bool | str) -> TypeIs[int]: ...
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
Methods narrow the first positional argument after `self` or `cls`
|
||||
|
||||
```py
|
||||
from typing import TypeGuard
|
||||
|
||||
class C:
|
||||
def f(self, x: object) -> TypeGuard[str]:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def g(cls, x: object) -> TypeGuard[int]:
|
||||
return True
|
||||
# TODO: this could error at definition time
|
||||
def h(self) -> TypeGuard[str]:
|
||||
return True
|
||||
# TODO: this could error at definition time
|
||||
@classmethod
|
||||
def j(cls) -> TypeGuard[int]:
|
||||
return True
|
||||
|
||||
def _(x: object):
|
||||
if C().f(x):
|
||||
reveal_type(x) # revealed: str
|
||||
if C.f(C(), x):
|
||||
# TODO: should be str
|
||||
reveal_type(x) # revealed: object
|
||||
if C.g(x):
|
||||
reveal_type(x) # revealed: int
|
||||
if C().g(x):
|
||||
reveal_type(x) # revealed: int
|
||||
if C().h(): # error: [invalid-type-guard-call] "Type guard call does not have a target"
|
||||
pass
|
||||
if C.j(): # error: [invalid-type-guard-call] "Type guard call does not have a target"
|
||||
pass
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
def is_int(val: object) -> TypeIs[int]:
|
||||
return isinstance(val, int)
|
||||
|
||||
class A:
|
||||
def is_int(self, val: object) -> TypeIs[int]:
|
||||
return isinstance(val, int)
|
||||
|
||||
@classmethod
|
||||
def is_int2(cls, val: object) -> TypeIs[int]:
|
||||
return isinstance(val, int)
|
||||
|
||||
def _(x: object):
|
||||
if is_int(x):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
if A().is_int(x):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
if A().is_int2(x):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
if A.is_int2(x):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Arguments to special forms
|
||||
|
||||
`TypeGuard` and `TypeIs` accept exactly one type argument.
|
||||
@@ -105,15 +168,14 @@ from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
a = 123
|
||||
|
||||
# TODO: error: [invalid-type-form]
|
||||
# error: [invalid-type-form] "Special form `typing.TypeGuard` expected exactly one type parameter"
|
||||
def f(_) -> TypeGuard[int, str]: ...
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.TypeIs` expected exactly one type parameter"
|
||||
# error: [invalid-type-form] "Variable of type `Literal[123]` is not allowed in a type expression"
|
||||
def g(_) -> TypeIs[a, str]: ...
|
||||
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(f(0)) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(f(0)) # revealed: Unknown
|
||||
reveal_type(g(0)) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -126,9 +188,10 @@ from typing_extensions import Literal, TypeGuard, TypeIs, assert_never
|
||||
|
||||
def _(a: object, flag: bool) -> TypeGuard[str]:
|
||||
if flag:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `TypeGuard[str]`, found `Literal[0]`"
|
||||
return 0
|
||||
|
||||
# TODO: error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `Literal["foo"]`"
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `TypeGuard[str]`, found `Literal["foo"]`"
|
||||
return "foo"
|
||||
|
||||
# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`"
|
||||
@@ -193,8 +256,7 @@ def is_bar(a: object) -> TypeIs[Bar]:
|
||||
|
||||
def _(a: Foo | Bar):
|
||||
if guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
reveal_type(a) # revealed: Foo
|
||||
else:
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
|
||||
@@ -204,6 +266,26 @@ def _(a: Foo | Bar):
|
||||
reveal_type(a) # revealed: Foo & ~Bar
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import TypeGuard, reveal_type
|
||||
|
||||
class P:
|
||||
pass
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B:
|
||||
pass
|
||||
|
||||
def is_b(val: object) -> TypeGuard[B]:
|
||||
return isinstance(val, B)
|
||||
|
||||
def _(x: P):
|
||||
if isinstance(x, A) or is_b(x):
|
||||
reveal_type(x) # revealed: B | (P & A)
|
||||
```
|
||||
|
||||
Attribute and subscript narrowing is supported:
|
||||
|
||||
```py
|
||||
@@ -215,23 +297,17 @@ class C(Generic[T]):
|
||||
v: T
|
||||
|
||||
def _(a: tuple[Foo, Bar] | tuple[Bar, Foo], c: C[Any]):
|
||||
# TODO: Should be `TypeGuard[Foo @ a[1]]`
|
||||
if reveal_type(guard_foo(a[1])): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
# TODO: Should be `tuple[Bar, Foo]`
|
||||
if reveal_type(guard_foo(a[1])): # revealed: TypeGuard[Foo @ a[1]]
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a[1]) # revealed: Bar | Foo
|
||||
reveal_type(a[1]) # revealed: Foo
|
||||
|
||||
if reveal_type(is_bar(a[0])): # revealed: TypeIs[Bar @ a[0]]
|
||||
# TODO: Should be `tuple[Bar, Bar & Foo]`
|
||||
reveal_type(a) # revealed: tuple[Foo, Bar] | tuple[Bar, Foo]
|
||||
reveal_type(a[0]) # revealed: Bar
|
||||
|
||||
# TODO: Should be `TypeGuard[Foo @ c.v]`
|
||||
if reveal_type(guard_foo(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
|
||||
if reveal_type(guard_foo(c.v)): # revealed: TypeGuard[Foo @ c.v]
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(c.v) # revealed: Any
|
||||
reveal_type(c.v) # revealed: Foo
|
||||
|
||||
if reveal_type(is_bar(c.v)): # revealed: TypeIs[Bar @ c.v]
|
||||
reveal_type(c) # revealed: C[Any]
|
||||
@@ -246,8 +322,7 @@ def _(a: Foo | Bar):
|
||||
c = is_bar(a)
|
||||
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
# TODO: Should be `TypeGuard[Foo @ a]`
|
||||
reveal_type(b) # revealed: @Todo(`TypeGuard[]` special form)
|
||||
reveal_type(b) # revealed: TypeGuard[Foo @ a]
|
||||
reveal_type(c) # revealed: TypeIs[Bar @ a]
|
||||
|
||||
if b:
|
||||
@@ -345,25 +420,82 @@ class Baz(Bar): ...
|
||||
def guard_foo(a: object) -> TypeGuard[Foo]:
|
||||
return True
|
||||
|
||||
def guard_bar(a: object) -> TypeGuard[Bar]:
|
||||
return True
|
||||
|
||||
def is_bar(a: object) -> TypeIs[Bar]:
|
||||
return True
|
||||
|
||||
def does_not_narrow_in_negative_case(a: Foo | Bar):
|
||||
if not guard_foo(a):
|
||||
# TODO: Should be `Bar`
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
else:
|
||||
reveal_type(a) # revealed: Foo | Bar
|
||||
reveal_type(a) # revealed: Foo
|
||||
|
||||
def narrowed_type_must_be_exact(a: object, b: Baz):
|
||||
if guard_foo(b):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(b) # revealed: Baz
|
||||
reveal_type(b) # revealed: Foo
|
||||
|
||||
if isinstance(a, Baz) and is_bar(a):
|
||||
reveal_type(a) # revealed: Baz
|
||||
|
||||
if isinstance(a, Bar) and guard_foo(a):
|
||||
# TODO: Should be `Foo`
|
||||
reveal_type(a) # revealed: Bar
|
||||
reveal_type(a) # revealed: Foo
|
||||
if guard_bar(a):
|
||||
reveal_type(a) # revealed: Bar
|
||||
```
|
||||
|
||||
## TypeGuard overrides normal constraints
|
||||
|
||||
TypeGuard constraints override any previous narrowing, but additional "regular" constraints can be
|
||||
added on to TypeGuard constraints.
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def f(x: object) -> TypeGuard[A]:
|
||||
return True
|
||||
|
||||
def g(x: object) -> TypeGuard[B]:
|
||||
return True
|
||||
|
||||
def h(x: object) -> TypeIs[C]:
|
||||
return True
|
||||
|
||||
def _(x: object):
|
||||
if f(x) and g(x) and h(x):
|
||||
reveal_type(x) # revealed: B & C
|
||||
```
|
||||
|
||||
## Boolean logic with TypeGuard and TypeIs
|
||||
|
||||
TypeGuard constraints need to properly distribute through boolean operations.
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeGuard, TypeIs
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def f(x: object) -> TypeIs[A]:
|
||||
return True
|
||||
|
||||
def g(x: object) -> TypeGuard[B]:
|
||||
return True
|
||||
|
||||
def h(x: object) -> TypeIs[C]:
|
||||
return True
|
||||
|
||||
def _(x: object):
|
||||
# g(x) or h(x) should give B | C
|
||||
# Then f(x) and (...) should distribute: (f(x) and g(x)) or (f(x) and h(x))
|
||||
# Which is (Regular(A) & TypeGuard(B)) | (Regular(A) & Regular(C))
|
||||
# TypeGuard clobbers in the first branch, giving: B | (A & C)
|
||||
if f(x) and (g(x) or h(x)):
|
||||
reveal_type(x) # revealed: B | (A & C)
|
||||
```
|
||||
|
||||
@@ -140,7 +140,7 @@ type IntOrStr = int | str
|
||||
|
||||
def f(x: IntOrStr, y: str | bytes):
|
||||
z = x or y
|
||||
reveal_type(z) # revealed: int | str | bytes
|
||||
reveal_type(z) # revealed: (int & ~AlwaysFalsy) | str | bytes
|
||||
```
|
||||
|
||||
## Multiple layers of union aliases
|
||||
|
||||
@@ -91,14 +91,14 @@ error[missing-argument]: No argument provided for required parameter `arg` of bo
|
||||
7 | from typing_extensions import deprecated
|
||||
|
|
||||
info: Parameter declared here
|
||||
--> stdlib/typing_extensions.pyi:1001:28
|
||||
--> stdlib/typing_extensions.pyi:1301:28
|
||||
|
|
||||
999 | stacklevel: int
|
||||
1000 | def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
|
||||
1001 | def __call__(self, arg: _T, /) -> _T: ...
|
||||
1299 | stacklevel: int
|
||||
1300 | def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
|
||||
1301 | def __call__(self, arg: _T, /) -> _T: ...
|
||||
| ^^^^^^^
|
||||
1002 |
|
||||
1003 | @final
|
||||
1302 |
|
||||
1303 | @final
|
||||
|
|
||||
info: rule `missing-argument` is enabled by default
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@ reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
|
||||
|
||||
reveal_type(tuple([1])) # revealed: tuple[Unknown | int, ...]
|
||||
|
||||
x1: tuple[int, ...] = tuple([1])
|
||||
reveal_type(x1) # revealed: tuple[int, ...]
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(tuple[int]([1])) # revealed: tuple[int]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user