Compare commits

..

1 Commits

Author SHA1 Message Date
Brent Westbrook
5810961b7f wip add ScopeKind::DunderClassCell like ruff 2025-08-25 13:55:17 -04:00
226 changed files with 2615 additions and 7562 deletions

View File

@@ -34,7 +34,7 @@ task_group()
setup()
from airflow.decorators import teardown
from airflow.io.path import ObjectStoragePath
from airflow.io.store import attach
from airflow.io.storage import attach
from airflow.models import DAG as DAGFromModel
from airflow.models import (
Connection,
@@ -74,36 +74,3 @@ DatasetOrTimeSchedule()
# airflow.utils.dag_parsing_context
get_parsing_context()
from airflow.decorators.base import (
DecoratedMappedOperator,
DecoratedOperator,
TaskDecorator,
get_unique_task_id,
task_decorator_factory,
)
# airflow.decorators.base
DecoratedMappedOperator()
DecoratedOperator()
TaskDecorator()
get_unique_task_id()
task_decorator_factory()
from airflow.models import Param
# airflow.models
Param()
from airflow.sensors.base import (
BaseSensorOperator,
PokeReturnValue,
poke_mode_only,
)
# airflow.sensors.base
BaseSensorOperator()
PokeReturnValue()
poke_mode_only()

View File

@@ -9,6 +9,7 @@ from airflow.operators.empty import EmptyOperator
from airflow.operators.latest_only import LatestOnlyOperator
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
from airflow.operators.weekday import BranchDayOfWeekOperator
from airflow.sensors.date_time import DateTimeSensor
FSHook()
PackageIndexHook()
@@ -21,6 +22,7 @@ EmptyOperator()
LatestOnlyOperator()
BranchDayOfWeekOperator()
DateTimeSensor()
from airflow.operators.python import (
BranchPythonOperator,
@@ -28,23 +30,16 @@ from airflow.operators.python import (
PythonVirtualenvOperator,
ShortCircuitOperator,
)
from airflow.sensors.bash import BashSensor
from airflow.sensors.date_time import DateTimeSensor
BranchPythonOperator()
PythonOperator()
PythonVirtualenvOperator()
ShortCircuitOperator()
BashSensor()
DateTimeSensor()
from airflow.sensors.date_time import DateTimeSensorAsync
from airflow.sensors.external_task import (
ExternalTaskMarker,
ExternalTaskSensor,
)
from airflow.sensors.time_sensor import (
TimeSensor,
TimeSensorAsync,
)
from airflow.sensors.filesystem import FileSensor
from airflow.sensors.python import PythonSensor
BranchPythonOperator()
PythonOperator()
@@ -54,13 +49,6 @@ DateTimeSensorAsync()
ExternalTaskMarker()
ExternalTaskSensor()
FileSensor()
PythonSensor()
from airflow.sensors.time_sensor import (
TimeSensor,
TimeSensorAsync,
)
TimeSensor()
TimeSensorAsync()

View File

@@ -1,75 +0,0 @@
from typing import Optional
import httpx
def foo():
client = httpx.Client()
client.close() # Ok
client.delete() # Ok
client.get() # Ok
client.head() # Ok
client.options() # Ok
client.patch() # Ok
client.post() # Ok
client.put() # Ok
client.request() # Ok
client.send() # Ok
client.stream() # Ok
client.anything() # Ok
client.build_request() # Ok
client.is_closed # Ok
async def foo():
client = httpx.Client()
client.close() # ASYNC212
client.delete() # ASYNC212
client.get() # ASYNC212
client.head() # ASYNC212
client.options() # ASYNC212
client.patch() # ASYNC212
client.post() # ASYNC212
client.put() # ASYNC212
client.request() # ASYNC212
client.send() # ASYNC212
client.stream() # ASYNC212
client.anything() # Ok
client.build_request() # Ok
client.is_closed # Ok
async def foo(client: httpx.Client):
client.request() # ASYNC212
client.anything() # Ok
async def foo(client: httpx.Client | None):
client.request() # ASYNC212
client.anything() # Ok
async def foo(client: Optional[httpx.Client]):
client.request() # ASYNC212
client.anything() # Ok
async def foo():
client: httpx.Client = ...
client.request() # ASYNC212
client.anything() # Ok
global_client = httpx.Client()
async def foo():
global_client.request() # ASYNC212
global_client.anything() # Ok
async def foo():
async with httpx.AsyncClient() as client:
await client.get() # Ok

View File

@@ -17,50 +17,3 @@ info(f"{__name__}")
# Don't trigger for t-strings
info(t"{name}")
info(t"{__name__}")
count = 5
total = 9
directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
logging.info(f"{count} out of {total} files in {directory_path} checked")
x = 99
fmt = "08d"
logger.info(f"{x:{'08d'}}")
logger.info(f"{x:>10} {x:{fmt}}")
logging.info(f"")
logging.info(f"This message doesn't have any variables.")
obj = {"key": "value"}
logging.info(f"Object: {obj!r}")
items_count = 3
logging.warning(f"Items: {items_count:d}")
data = {"status": "active"}
logging.info(f"Processing {len(data)} items")
logging.info(f"Status: {data.get('status', 'unknown').upper()}")
result = 123
logging.info(f"Calculated result: {result + 100}")
temperature = 123
logging.info(f"Temperature: {temperature:.1f}°C")
class FilePath:
def __init__(self, name: str):
self.name = name
logging.info(f"No changes made to {file_path.name}.")
user = "tron"
balance = 123.45
logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}")
import logging
x = 1
logging.error(f"{x} -> %s", x)

View File

@@ -1,10 +0,0 @@
"""Test f-string argument order."""
import logging
logger = logging.getLogger(__name__)
X = 1
Y = 2
logger.error(f"{X} -> %s", Y)
logger.error(f"{Y} -> %s", X)

View File

@@ -151,39 +151,3 @@ def f():
pass
except Exception as _:
pass
# OK, `__class__` in this case is not the special `__class__` cell, so we don't
# emit a diagnostic. (It has its own special semantics -- see
# https://github.com/astral-sh/ruff/pull/20048#discussion_r2298338048 -- but
# those aren't relevant here.)
class A:
__class__ = 1
# The following three cases are flagged because they declare local `__class__`
# variables that don't refer to the special `__class__` cell.
class A:
def set_class(self, cls):
__class__ = cls # F841
class A:
class B:
def set_class(self, cls):
__class__ = cls # F841
class A:
def foo():
class B:
print(__class__)
def set_class(self, cls):
__class__ = cls # F841
# OK, the `__class__` cell is nonlocal and declared as such.
class NonlocalDunderClass:
def foo():
nonlocal __class__
__class__ = 1

View File

@@ -44,8 +44,3 @@ def f():
def g():
nonlocal x
x = 2
# OK
class A:
def method(self):
nonlocal __class__

View File

@@ -124,19 +124,3 @@ def fun_with_python_syntax():
...
return Foo
@dataclass
class C:
def __post_init__(self, x: tuple[int, ...] = (
1,
2,
)) -> None:
self.x = x
@dataclass
class D:
def __post_init__(self, x: int = """
""") -> None:
self.x = x

View File

@@ -660,9 +660,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::BlockingHttpCallInAsyncFunction) {
flake8_async::rules::blocking_http_call(checker, call);
}
if checker.is_rule_enabled(Rule::BlockingHttpCallHttpxInAsyncFunction) {
flake8_async::rules::blocking_http_call_httpx(checker, call);
}
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
flake8_async::rules::blocking_open_call(checker, call);
}
@@ -1049,6 +1046,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::PyPath,
Rule::Glob,
Rule::OsListdir,
Rule::OsSymlink,
]) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
}

View File

@@ -703,10 +703,7 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false,
ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async,
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
}
}
false
@@ -717,10 +714,7 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
}
}
false
@@ -731,7 +725,7 @@ impl SemanticSyntaxContext for Checker<'_> {
match scope.kind {
ScopeKind::Class(_) | ScopeKind::Generator { .. } => return false,
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
ScopeKind::Module | ScopeKind::Type | ScopeKind::DunderClassCell => {}
ScopeKind::Module | ScopeKind::Type => {}
}
}
false
@@ -1098,24 +1092,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}
// Here we add the implicit scope surrounding a method which allows code in the
// method to access `__class__` at runtime. See the `ScopeKind::DunderClassCell`
// docs for more information.
let added_dunder_class_scope = if self.semantic.current_scope().kind.is_class() {
self.semantic.push_scope(ScopeKind::DunderClassCell);
let binding_id = self.semantic.push_binding(
TextRange::default(),
BindingKind::DunderClassCell,
BindingFlags::empty(),
);
self.semantic
.current_scope_mut()
.add("__class__", binding_id);
true
} else {
false
};
self.semantic.push_scope(ScopeKind::Type);
if let Some(type_params) = type_params {
@@ -1179,9 +1155,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
self.semantic.pop_scope(); // Function scope
self.semantic.pop_definition();
self.semantic.pop_scope(); // Type parameter scope
if added_dunder_class_scope {
self.semantic.pop_scope(); // `__class__` cell closure scope
}
self.add_binding(
name,
stmt.identifier(),

View File

@@ -336,7 +336,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
(Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction),
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),

View File

@@ -40,11 +40,6 @@ pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSet
settings.preview.is_enabled()
}
/// <https://github.com/astral-sh/ruff/pull/19303>
pub(crate) const fn is_fix_f_string_logging_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/16719
pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()

View File

@@ -354,10 +354,7 @@ impl Renamer {
))
}
// Avoid renaming builtins and other "special" bindings.
BindingKind::FutureImport
| BindingKind::Builtin
| BindingKind::Export(_)
| BindingKind::DunderClassCell => None,
BindingKind::FutureImport | BindingKind::Builtin | BindingKind::Export(_) => None,
// By default, replace the binding's name with the target name.
BindingKind::Annotation
| BindingKind::Argument

View File

@@ -215,12 +215,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
version: "0.0.1",
}
}
["airflow", "sensors", "bash", "BashSensor"] => ProviderReplacement::AutoImport {
module: "airflow.providers.standard.sensor.bash",
name: "BashSensor",
provider: "standard",
version: "0.0.1",
},
[
"airflow",
"sensors",
@@ -249,12 +243,6 @@ fn check_names_moved_to_provider(checker: &Checker, expr: &Expr, ranged: TextRan
provider: "standard",
version: "0.0.2",
},
["airflow", "sensors", "python", "PythonSensor"] => ProviderReplacement::AutoImport {
module: "airflow.providers.standard.sensors.python",
name: "PythonSensor",
provider: "standard",
version: "0.0.1",
},
[
"airflow",
"sensors",

View File

@@ -227,26 +227,13 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.sdk",
name: (*rest).to_string(),
},
[
"airflow",
"decorators",
"base",
rest @ ("DecoratedMappedOperator"
| "DecoratedOperator"
| "TaskDecorator"
| "get_unique_task_id"
| "task_decorator_factory"),
] => Replacement::SourceModuleMoved {
module: "airflow.sdk.bases.decorator",
name: (*rest).to_string(),
},
// airflow.io
["airflow", "io", "path", "ObjectStoragePath"] => Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: "ObjectStoragePath".to_string(),
},
["airflow", "io", "store", "attach"] => Replacement::SourceModuleMoved {
["airflow", "io", "storage", "attach"] => Replacement::SourceModuleMoved {
module: "airflow.sdk.io",
name: "attach".to_string(),
},
@@ -258,10 +245,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
name: (*rest).to_string(),
}
}
["airflow", "models", "Param"] => Replacement::AutoImport {
module: "airflow.sdk.definitions.param",
name: "Param",
},
// airflow.models.baseoperator
[
@@ -277,30 +260,16 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
module: "airflow.sdk",
name: "BaseOperatorLink",
},
// airflow.model..DAG
["airflow", "models", .., "DAG"] => Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: "DAG".to_string(),
},
// airflow.sensors.base
[
"airflow",
"sensors",
"base",
rest @ ("BaseSensorOperator" | "PokeReturnValue" | "poke_mode_only"),
] => Replacement::SourceModuleMoved {
module: "airflow.sdk",
name: (*rest).to_string(),
},
// airflow.timetables
["airflow", "timetables", "datasets", "DatasetOrTimeSchedule"] => Replacement::AutoImport {
module: "airflow.timetables.assets",
name: "AssetOrTimeSchedule",
},
// airflow.utils
[
"airflow",

View File

@@ -312,7 +312,7 @@ help: Use `teardown` from `airflow.sdk` instead.
34 34 | setup()
35 |-from airflow.decorators import teardown
36 35 | from airflow.io.path import ObjectStoragePath
37 36 | from airflow.io.store import attach
37 36 | from airflow.io.storage import attach
38 37 | from airflow.models import DAG as DAGFromModel
--------------------------------------------------------------------------------
43 42 | from airflow.models.baseoperator import chain, chain_linear, cross_downstream
@@ -338,7 +338,7 @@ help: Use `ObjectStoragePath` from `airflow.sdk` instead.
34 34 | setup()
35 35 | from airflow.decorators import teardown
36 |-from airflow.io.path import ObjectStoragePath
37 36 | from airflow.io.store import attach
37 36 | from airflow.io.storage import attach
38 37 | from airflow.models import DAG as DAGFromModel
39 38 | from airflow.models import (
--------------------------------------------------------------------------------
@@ -350,7 +350,7 @@ help: Use `ObjectStoragePath` from `airflow.sdk` instead.
47 47 | # airflow.decorators
48 48 | teardown()
AIR311 [*] `airflow.io.store.attach` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
AIR311 [*] `airflow.io.storage.attach` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:52:1
|
50 | # # airflow.io
@@ -366,7 +366,7 @@ help: Use `attach` from `airflow.sdk.io` instead.
34 34 | setup()
35 35 | from airflow.decorators import teardown
36 36 | from airflow.io.path import ObjectStoragePath
37 |-from airflow.io.store import attach
37 |-from airflow.io.storage import attach
38 37 | from airflow.models import DAG as DAGFromModel
39 38 | from airflow.models import (
40 39 | Connection,
@@ -391,7 +391,7 @@ AIR311 [*] `airflow.models.Connection` is removed in Airflow 3.0; It still works
help: Use `Connection` from `airflow.sdk` instead.
Unsafe fix
37 37 | from airflow.io.store import attach
37 37 | from airflow.io.storage import attach
38 38 | from airflow.models import DAG as DAGFromModel
39 39 | from airflow.models import (
40 |- Connection,
@@ -614,8 +614,6 @@ AIR311 [*] `airflow.utils.dag_parsing_context.get_parsing_context` is removed in
75 | # airflow.utils.dag_parsing_context
76 | get_parsing_context()
| ^^^^^^^^^^^^^^^^^^^
77 |
78 | from airflow.decorators.base import (
|
help: Use `get_parsing_context` from `airflow.sdk` instead.
@@ -628,211 +626,3 @@ help: Use `get_parsing_context` from `airflow.sdk` instead.
71 71 |
72 72 | # airflow.timetables.datasets
73 73 | DatasetOrTimeSchedule()
AIR311 [*] `airflow.decorators.base.DecoratedMappedOperator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:87:1
|
86 | # airflow.decorators.base
87 | DecoratedMappedOperator()
| ^^^^^^^^^^^^^^^^^^^^^^^
88 | DecoratedOperator()
89 | TaskDecorator()
|
help: Use `DecoratedMappedOperator` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
76 76 | get_parsing_context()
77 77 |
78 78 | from airflow.decorators.base import (
79 |- DecoratedMappedOperator,
80 79 | DecoratedOperator,
81 80 | TaskDecorator,
82 81 | get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import DecoratedMappedOperator
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.DecoratedOperator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:88:1
|
86 | # airflow.decorators.base
87 | DecoratedMappedOperator()
88 | DecoratedOperator()
| ^^^^^^^^^^^^^^^^^
89 | TaskDecorator()
90 | get_unique_task_id()
|
help: Use `DecoratedOperator` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
77 77 |
78 78 | from airflow.decorators.base import (
79 79 | DecoratedMappedOperator,
80 |- DecoratedOperator,
81 80 | TaskDecorator,
82 81 | get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import DecoratedOperator
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.TaskDecorator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:89:1
|
87 | DecoratedMappedOperator()
88 | DecoratedOperator()
89 | TaskDecorator()
| ^^^^^^^^^^^^^
90 | get_unique_task_id()
91 | task_decorator_factory()
|
help: Use `TaskDecorator` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
78 78 | from airflow.decorators.base import (
79 79 | DecoratedMappedOperator,
80 80 | DecoratedOperator,
81 |- TaskDecorator,
82 81 | get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import TaskDecorator
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.get_unique_task_id` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:90:1
|
88 | DecoratedOperator()
89 | TaskDecorator()
90 | get_unique_task_id()
| ^^^^^^^^^^^^^^^^^^
91 | task_decorator_factory()
|
help: Use `get_unique_task_id` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
79 79 | DecoratedMappedOperator,
80 80 | DecoratedOperator,
81 81 | TaskDecorator,
82 |- get_unique_task_id,
83 82 | task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import get_unique_task_id
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.decorators.base.task_decorator_factory` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:91:1
|
89 | TaskDecorator()
90 | get_unique_task_id()
91 | task_decorator_factory()
| ^^^^^^^^^^^^^^^^^^^^^^
|
help: Use `task_decorator_factory` from `airflow.sdk.bases.decorator` instead.
Unsafe fix
80 80 | DecoratedOperator,
81 81 | TaskDecorator,
82 82 | get_unique_task_id,
83 |- task_decorator_factory,
84 83 | )
84 |+from airflow.sdk.bases.decorator import task_decorator_factory
85 85 |
86 86 | # airflow.decorators.base
87 87 | DecoratedMappedOperator()
AIR311 [*] `airflow.models.Param` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:97:1
|
96 | # airflow.models
97 | Param()
| ^^^^^
|
help: Use `Param` from `airflow.sdk.definitions.param` instead.
Unsafe fix
91 91 | task_decorator_factory()
92 92 |
93 93 |
94 |-from airflow.models import Param
94 |+from airflow.sdk.definitions.param import Param
95 95 |
96 96 | # airflow.models
97 97 | Param()
AIR311 [*] `airflow.sensors.base.BaseSensorOperator` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:107:1
|
106 | # airflow.sensors.base
107 | BaseSensorOperator()
| ^^^^^^^^^^^^^^^^^^
108 | PokeReturnValue()
109 | poke_mode_only()
|
help: Use `BaseSensorOperator` from `airflow.sdk` instead.
Unsafe fix
98 98 |
99 99 |
100 100 | from airflow.sensors.base import (
101 |- BaseSensorOperator,
102 101 | PokeReturnValue,
103 102 | poke_mode_only,
104 103 | )
104 |+from airflow.sdk import BaseSensorOperator
105 105 |
106 106 | # airflow.sensors.base
107 107 | BaseSensorOperator()
AIR311 [*] `airflow.sensors.base.PokeReturnValue` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:108:1
|
106 | # airflow.sensors.base
107 | BaseSensorOperator()
108 | PokeReturnValue()
| ^^^^^^^^^^^^^^^
109 | poke_mode_only()
|
help: Use `PokeReturnValue` from `airflow.sdk` instead.
Unsafe fix
99 99 |
100 100 | from airflow.sensors.base import (
101 101 | BaseSensorOperator,
102 |- PokeReturnValue,
103 102 | poke_mode_only,
104 103 | )
104 |+from airflow.sdk import PokeReturnValue
105 105 |
106 106 | # airflow.sensors.base
107 107 | BaseSensorOperator()
AIR311 [*] `airflow.sensors.base.poke_mode_only` is removed in Airflow 3.0; It still works in Airflow 3.0 but is expected to be removed in a future version.
--> AIR311_names.py:109:1
|
107 | BaseSensorOperator()
108 | PokeReturnValue()
109 | poke_mode_only()
| ^^^^^^^^^^^^^^
|
help: Use `poke_mode_only` from `airflow.sdk` instead.
Unsafe fix
100 100 | from airflow.sensors.base import (
101 101 | BaseSensorOperator,
102 102 | PokeReturnValue,
103 |- poke_mode_only,
104 103 | )
104 |+from airflow.sdk import poke_mode_only
105 105 |
106 106 | # airflow.sensors.base
107 107 | BaseSensorOperator()

View File

@@ -23,7 +23,6 @@ mod tests {
#[test_case(Rule::AsyncZeroSleep, Path::new("ASYNC115.py"))]
#[test_case(Rule::LongSleepNotForever, Path::new("ASYNC116.py"))]
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
#[test_case(Rule::BlockingHttpCallHttpxInAsyncFunction, Path::new("ASYNC212.py"))]
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]

View File

@@ -1,145 +0,0 @@
use ruff_python_ast::{self as ast, Expr, ExprCall};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional};
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not use blocking httpx clients.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking HTTP call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// HTTP response, negating the benefits of asynchronous programming.
///
/// Instead of using the blocking `httpx` client, use the asynchronous client.
///
/// ## Example
/// ```python
/// import httpx
///
///
/// async def fetch():
/// client = httpx.Client()
/// response = client.get(...)
/// ```
///
/// Use instead:
/// ```python
/// import httpx
///
///
/// async def fetch():
/// async with httpx.AsyncClient() as client:
/// response = await client.get(...)
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct BlockingHttpCallHttpxInAsyncFunction {
name: String,
call: String,
}
impl Violation for BlockingHttpCallHttpxInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"Blocking httpx method {name}.{call}() in async context, use httpx.AsyncClient",
name = self.name,
call = self.call,
)
}
}
struct HttpxClientChecker;
impl TypeChecker for HttpxClientChecker {
fn match_annotation(
annotation: &ruff_python_ast::Expr,
semantic: &ruff_python_semantic::SemanticModel,
) -> bool {
// match base annotation directly
if semantic
.resolve_qualified_name(annotation)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
{
return true;
}
// otherwise traverse any union or optional annotation
let mut found = false;
traverse_union_and_optional(
&mut |inner_expr, _| {
if semantic
.resolve_qualified_name(inner_expr)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["httpx", "Client"])
})
{
found = true;
}
},
semantic,
annotation,
);
found
}
fn match_initializer(
initializer: &ruff_python_ast::Expr,
semantic: &ruff_python_semantic::SemanticModel,
) -> bool {
let Expr::Call(ExprCall { func, .. }) = initializer else {
return false;
};
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
}
}
/// ASYNC212
pub(crate) fn blocking_http_call_httpx(checker: &Checker, call: &ExprCall) {
let semantic = checker.semantic();
if !semantic.in_async_context() {
return;
}
let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else {
return;
};
let Some(name) = value.as_name_expr() else {
return;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return;
};
if check_type::<HttpxClientChecker>(binding, semantic) {
if matches!(
attr.id.as_str(),
"close"
| "delete"
| "get"
| "head"
| "options"
| "patch"
| "post"
| "put"
| "request"
| "send"
| "stream"
) {
checker.report_diagnostic(
BlockingHttpCallHttpxInAsyncFunction {
name: name.id.to_string(),
call: attr.id.to_string(),
},
call.func.range(),
);
}
}
}

View File

@@ -2,7 +2,6 @@ pub(crate) use async_busy_wait::*;
pub(crate) use async_function_with_timeout::*;
pub(crate) use async_zero_sleep::*;
pub(crate) use blocking_http_call::*;
pub(crate) use blocking_http_call_httpx::*;
pub(crate) use blocking_open_call::*;
pub(crate) use blocking_process_invocation::*;
pub(crate) use blocking_sleep::*;
@@ -14,7 +13,6 @@ mod async_busy_wait;
mod async_function_with_timeout;
mod async_zero_sleep;
mod blocking_http_call;
mod blocking_http_call_httpx;
mod blocking_open_call;
mod blocking_process_invocation;
mod blocking_sleep;

View File

@@ -1,168 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC212 Blocking httpx method client.close() in async context, use httpx.AsyncClient
--> ASYNC212.py:27:5
|
25 | async def foo():
26 | client = httpx.Client()
27 | client.close() # ASYNC212
| ^^^^^^^^^^^^
28 | client.delete() # ASYNC212
29 | client.get() # ASYNC212
|
ASYNC212 Blocking httpx method client.delete() in async context, use httpx.AsyncClient
--> ASYNC212.py:28:5
|
26 | client = httpx.Client()
27 | client.close() # ASYNC212
28 | client.delete() # ASYNC212
| ^^^^^^^^^^^^^
29 | client.get() # ASYNC212
30 | client.head() # ASYNC212
|
ASYNC212 Blocking httpx method client.get() in async context, use httpx.AsyncClient
--> ASYNC212.py:29:5
|
27 | client.close() # ASYNC212
28 | client.delete() # ASYNC212
29 | client.get() # ASYNC212
| ^^^^^^^^^^
30 | client.head() # ASYNC212
31 | client.options() # ASYNC212
|
ASYNC212 Blocking httpx method client.head() in async context, use httpx.AsyncClient
--> ASYNC212.py:30:5
|
28 | client.delete() # ASYNC212
29 | client.get() # ASYNC212
30 | client.head() # ASYNC212
| ^^^^^^^^^^^
31 | client.options() # ASYNC212
32 | client.patch() # ASYNC212
|
ASYNC212 Blocking httpx method client.options() in async context, use httpx.AsyncClient
--> ASYNC212.py:31:5
|
29 | client.get() # ASYNC212
30 | client.head() # ASYNC212
31 | client.options() # ASYNC212
| ^^^^^^^^^^^^^^
32 | client.patch() # ASYNC212
33 | client.post() # ASYNC212
|
ASYNC212 Blocking httpx method client.patch() in async context, use httpx.AsyncClient
--> ASYNC212.py:32:5
|
30 | client.head() # ASYNC212
31 | client.options() # ASYNC212
32 | client.patch() # ASYNC212
| ^^^^^^^^^^^^
33 | client.post() # ASYNC212
34 | client.put() # ASYNC212
|
ASYNC212 Blocking httpx method client.post() in async context, use httpx.AsyncClient
--> ASYNC212.py:33:5
|
31 | client.options() # ASYNC212
32 | client.patch() # ASYNC212
33 | client.post() # ASYNC212
| ^^^^^^^^^^^
34 | client.put() # ASYNC212
35 | client.request() # ASYNC212
|
ASYNC212 Blocking httpx method client.put() in async context, use httpx.AsyncClient
--> ASYNC212.py:34:5
|
32 | client.patch() # ASYNC212
33 | client.post() # ASYNC212
34 | client.put() # ASYNC212
| ^^^^^^^^^^
35 | client.request() # ASYNC212
36 | client.send() # ASYNC212
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:35:5
|
33 | client.post() # ASYNC212
34 | client.put() # ASYNC212
35 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
36 | client.send() # ASYNC212
37 | client.stream() # ASYNC212
|
ASYNC212 Blocking httpx method client.send() in async context, use httpx.AsyncClient
--> ASYNC212.py:36:5
|
34 | client.put() # ASYNC212
35 | client.request() # ASYNC212
36 | client.send() # ASYNC212
| ^^^^^^^^^^^
37 | client.stream() # ASYNC212
|
ASYNC212 Blocking httpx method client.stream() in async context, use httpx.AsyncClient
--> ASYNC212.py:37:5
|
35 | client.request() # ASYNC212
36 | client.send() # ASYNC212
37 | client.stream() # ASYNC212
| ^^^^^^^^^^^^^
38 |
39 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:45:5
|
44 | async def foo(client: httpx.Client):
45 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
46 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:50:5
|
49 | async def foo(client: httpx.Client | None):
50 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
51 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:55:5
|
54 | async def foo(client: Optional[httpx.Client]):
55 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
56 | client.anything() # Ok
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:61:5
|
59 | async def foo():
60 | client: httpx.Client = ...
61 | client.request() # ASYNC212
| ^^^^^^^^^^^^^^
62 | client.anything() # Ok
|
ASYNC212 Blocking httpx method global_client.request() in async context, use httpx.AsyncClient
--> ASYNC212.py:69:5
|
68 | async def foo():
69 | global_client.request() # ASYNC212
| ^^^^^^^^^^^^^^^^^^^^^
70 | global_client.anything() # Ok
|

View File

@@ -22,7 +22,6 @@ mod tests {
#[test_case(Path::new("G002.py"))]
#[test_case(Path::new("G003.py"))]
#[test_case(Path::new("G004.py"))]
#[test_case(Path::new("G004_arg_order.py"))]
#[test_case(Path::new("G010.py"))]
#[test_case(Path::new("G101_1.py"))]
#[test_case(Path::new("G101_2.py"))]
@@ -49,24 +48,4 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::LoggingFString, Path::new("G004.py"))]
#[test_case(Rule::LoggingFString, Path::new("G004_arg_order.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_logging_format").join(path).as_path(),
&settings::LinterSettings {
logger_objects: vec!["logging_setup.logger".to_string()],
preview: settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,11 +1,9 @@
use ruff_python_ast::InterpolatedStringElement;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator, StringFlags};
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator};
use ruff_python_semantic::analyze::logging;
use ruff_python_stdlib::logging::LoggingLevel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_fix_f_string_logging_enabled;
use crate::registry::Rule;
use crate::rules::flake8_logging_format::violations::{
LoggingExcInfo, LoggingExtraAttrClash, LoggingFString, LoggingPercentFormat,
@@ -13,87 +11,6 @@ use crate::rules::flake8_logging_format::violations::{
};
use crate::{Edit, Fix};
fn logging_f_string(
checker: &Checker,
msg: &Expr,
f_string: &ast::ExprFString,
arguments: &Arguments,
msg_pos: usize,
) {
// Report the diagnostic up-front so we can attach a fix later only when preview is enabled.
let mut diagnostic = checker.report_diagnostic(LoggingFString, msg.range());
// Preview gate for the automatic fix.
if !is_fix_f_string_logging_enabled(checker.settings()) {
return;
}
// If there are existing positional arguments after the message, bail out.
// This could indicate a mistake or complex usage we shouldn't try to fix.
if arguments.args.len() > msg_pos + 1 {
return;
}
let mut format_string = String::new();
let mut args: Vec<&str> = Vec::new();
// Try to reuse the first part's quote style when building the replacement.
// Default to double quotes if we can't determine it.
let quote_str = f_string
.value
.f_strings()
.next()
.map(|f| f.flags.quote_str())
.unwrap_or("\"");
for f in f_string.value.f_strings() {
for element in &f.elements {
match element {
InterpolatedStringElement::Literal(lit) => {
// If the literal text contains a '%' placeholder, bail out: mixing
// f-string interpolation with '%' placeholders is ambiguous for our
// automatic conversion, so don't offer a fix for this case.
if lit.value.as_ref().contains('%') {
return;
}
format_string.push_str(lit.value.as_ref());
}
InterpolatedStringElement::Interpolation(interpolated) => {
if interpolated.format_spec.is_some()
|| !matches!(
interpolated.conversion,
ruff_python_ast::ConversionFlag::None
)
{
return;
}
match interpolated.expression.as_ref() {
Expr::Name(name) => {
format_string.push_str("%s");
args.push(name.id.as_str());
}
_ => return,
}
}
}
}
}
if args.is_empty() {
return;
}
let replacement = format!(
"{q}{format_string}{q}, {args}",
q = quote_str,
format_string = format_string,
args = args.join(", ")
);
let fix = Fix::safe_edit(Edit::range_replacement(replacement, msg.range()));
diagnostic.set_fix(fix);
}
/// Returns `true` if the attribute is a reserved attribute on the `logging` module's `LogRecord`
/// class.
fn is_reserved_attr(attr: &str) -> bool {
@@ -125,7 +42,7 @@ fn is_reserved_attr(attr: &str) -> bool {
}
/// Check logging messages for violations.
fn check_msg(checker: &Checker, msg: &Expr, arguments: &Arguments, msg_pos: usize) {
fn check_msg(checker: &Checker, msg: &Expr) {
match msg {
// Check for string concatenation and percent format.
Expr::BinOp(ast::ExprBinOp { op, .. }) => match op {
@@ -138,10 +55,8 @@ fn check_msg(checker: &Checker, msg: &Expr, arguments: &Arguments, msg_pos: usiz
_ => {}
},
// Check for f-strings.
Expr::FString(f_string) => {
if checker.is_rule_enabled(Rule::LoggingFString) {
logging_f_string(checker, msg, f_string, arguments, msg_pos);
}
Expr::FString(_) => {
checker.report_diagnostic_if_enabled(LoggingFString, msg.range());
}
// Check for .format() calls.
Expr::Call(ast::ExprCall { func, .. }) => {
@@ -253,7 +168,7 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) {
// G001, G002, G003, G004
let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall));
if let Some(format_arg) = call.arguments.find_argument_value("msg", msg_pos) {
check_msg(checker, format_arg, &call.arguments, msg_pos);
check_msg(checker, format_arg);
}
// G010

View File

@@ -9,7 +9,6 @@ G004 Logging statement uses f-string
| ^^^^^^^^^^^^^^^
5 | logging.log(logging.INFO, f"Hello {name}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:5:27
@@ -21,7 +20,6 @@ G004 Logging statement uses f-string
6 |
7 | _LOGGER = logging.getLogger()
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:8:14
@@ -32,7 +30,6 @@ G004 Logging statement uses f-string
9 |
10 | logging.getLogger().info(f"{name}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:10:26
@@ -44,7 +41,6 @@ G004 Logging statement uses f-string
11 |
12 | from logging import info
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:14:6
@@ -55,7 +51,6 @@ G004 Logging statement uses f-string
| ^^^^^^^^^
15 | info(f"{__name__}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:15:6
@@ -66,156 +61,3 @@ G004 Logging statement uses f-string
16 |
17 | # Don't trigger for t-strings
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:24:14
|
22 | total = 9
23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
24 | logging.info(f"{count} out of {total} files in {directory_path} checked")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:30:13
|
28 | x = 99
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
| ^^^^^^^^^^^^^^
31 | logger.info(f"{x:>10} {x:{fmt}}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:31:13
|
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
31 | logger.info(f"{x:>10} {x:{fmt}}")
| ^^^^^^^^^^^^^^^^^^^^
32 |
33 | logging.info(f"")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:33:14
|
31 | logger.info(f"{x:>10} {x:{fmt}}")
32 |
33 | logging.info(f"")
| ^^^
34 | logging.info(f"This message doesn't have any variables.")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:34:14
|
33 | logging.info(f"")
34 | logging.info(f"This message doesn't have any variables.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 |
36 | obj = {"key": "value"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:37:14
|
36 | obj = {"key": "value"}
37 | logging.info(f"Object: {obj!r}")
| ^^^^^^^^^^^^^^^^^^
38 |
39 | items_count = 3
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:40:17
|
39 | items_count = 3
40 | logging.warning(f"Items: {items_count:d}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^
41 |
42 | data = {"status": "active"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:43:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:44:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:48:14
|
47 | result = 123
48 | logging.info(f"Calculated result: {result + 100}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | temperature = 123
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:51:14
|
50 | temperature = 123
51 | logging.info(f"Temperature: {temperature:.1f}°C")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 |
53 | class FilePath:
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:57:14
|
55 | self.name = name
56 |
57 | logging.info(f"No changes made to {file_path.name}.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 |
59 | user = "tron"
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:61:15
|
59 | user = "tron"
60 | balance = 123.45
61 | logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 |
63 | import logging
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:66:15
|
65 | x = 1
66 | logging.error(f"{x} -> %s", x)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -1,23 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
---
G004 Logging statement uses f-string
--> G004_arg_order.py:9:14
|
7 | X = 1
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
| ^^^^^^^^^^^^
10 | logger.error(f"{Y} -> %s", X)
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_arg_order.py:10:14
|
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
10 | logger.error(f"{Y} -> %s", X)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -1,291 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
---
G004 [*] Logging statement uses f-string
--> G004.py:4:14
|
3 | name = "world"
4 | logging.info(f"Hello {name}")
| ^^^^^^^^^^^^^^^
5 | logging.log(logging.INFO, f"Hello {name}")
|
help: Convert to lazy `%` formatting
Safe fix
1 1 | import logging
2 2 |
3 3 | name = "world"
4 |-logging.info(f"Hello {name}")
4 |+logging.info("Hello %s", name)
5 5 | logging.log(logging.INFO, f"Hello {name}")
6 6 |
7 7 | _LOGGER = logging.getLogger()
G004 [*] Logging statement uses f-string
--> G004.py:5:27
|
3 | name = "world"
4 | logging.info(f"Hello {name}")
5 | logging.log(logging.INFO, f"Hello {name}")
| ^^^^^^^^^^^^^^^
6 |
7 | _LOGGER = logging.getLogger()
|
help: Convert to lazy `%` formatting
Safe fix
2 2 |
3 3 | name = "world"
4 4 | logging.info(f"Hello {name}")
5 |-logging.log(logging.INFO, f"Hello {name}")
5 |+logging.log(logging.INFO, "Hello %s", name)
6 6 |
7 7 | _LOGGER = logging.getLogger()
8 8 | _LOGGER.info(f"{__name__}")
G004 [*] Logging statement uses f-string
--> G004.py:8:14
|
7 | _LOGGER = logging.getLogger()
8 | _LOGGER.info(f"{__name__}")
| ^^^^^^^^^^^^^
9 |
10 | logging.getLogger().info(f"{name}")
|
help: Convert to lazy `%` formatting
Safe fix
5 5 | logging.log(logging.INFO, f"Hello {name}")
6 6 |
7 7 | _LOGGER = logging.getLogger()
8 |-_LOGGER.info(f"{__name__}")
8 |+_LOGGER.info("%s", __name__)
9 9 |
10 10 | logging.getLogger().info(f"{name}")
11 11 |
G004 [*] Logging statement uses f-string
--> G004.py:10:26
|
8 | _LOGGER.info(f"{__name__}")
9 |
10 | logging.getLogger().info(f"{name}")
| ^^^^^^^^^
11 |
12 | from logging import info
|
help: Convert to lazy `%` formatting
Safe fix
7 7 | _LOGGER = logging.getLogger()
8 8 | _LOGGER.info(f"{__name__}")
9 9 |
10 |-logging.getLogger().info(f"{name}")
10 |+logging.getLogger().info("%s", name)
11 11 |
12 12 | from logging import info
13 13 |
G004 [*] Logging statement uses f-string
--> G004.py:14:6
|
12 | from logging import info
13 |
14 | info(f"{name}")
| ^^^^^^^^^
15 | info(f"{__name__}")
|
help: Convert to lazy `%` formatting
Safe fix
11 11 |
12 12 | from logging import info
13 13 |
14 |-info(f"{name}")
14 |+info("%s", name)
15 15 | info(f"{__name__}")
16 16 |
17 17 | # Don't trigger for t-strings
G004 [*] Logging statement uses f-string
--> G004.py:15:6
|
14 | info(f"{name}")
15 | info(f"{__name__}")
| ^^^^^^^^^^^^^
16 |
17 | # Don't trigger for t-strings
|
help: Convert to lazy `%` formatting
Safe fix
12 12 | from logging import info
13 13 |
14 14 | info(f"{name}")
15 |-info(f"{__name__}")
15 |+info("%s", __name__)
16 16 |
17 17 | # Don't trigger for t-strings
18 18 | info(t"{name}")
G004 [*] Logging statement uses f-string
--> G004.py:24:14
|
22 | total = 9
23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
24 | logging.info(f"{count} out of {total} files in {directory_path} checked")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
Safe fix
21 21 | count = 5
22 22 | total = 9
23 23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
24 |-logging.info(f"{count} out of {total} files in {directory_path} checked")
24 |+logging.info("%s out of %s files in %s checked", count, total, directory_path)
25 25 |
26 26 |
27 27 |
G004 Logging statement uses f-string
--> G004.py:30:13
|
28 | x = 99
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
| ^^^^^^^^^^^^^^
31 | logger.info(f"{x:>10} {x:{fmt}}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:31:13
|
29 | fmt = "08d"
30 | logger.info(f"{x:{'08d'}}")
31 | logger.info(f"{x:>10} {x:{fmt}}")
| ^^^^^^^^^^^^^^^^^^^^
32 |
33 | logging.info(f"")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:33:14
|
31 | logger.info(f"{x:>10} {x:{fmt}}")
32 |
33 | logging.info(f"")
| ^^^
34 | logging.info(f"This message doesn't have any variables.")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:34:14
|
33 | logging.info(f"")
34 | logging.info(f"This message doesn't have any variables.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35 |
36 | obj = {"key": "value"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:37:14
|
36 | obj = {"key": "value"}
37 | logging.info(f"Object: {obj!r}")
| ^^^^^^^^^^^^^^^^^^
38 |
39 | items_count = 3
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:40:17
|
39 | items_count = 3
40 | logging.warning(f"Items: {items_count:d}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^
41 |
42 | data = {"status": "active"}
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:43:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:44:14
|
42 | data = {"status": "active"}
43 | logging.info(f"Processing {len(data)} items")
44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:48:14
|
47 | result = 123
48 | logging.info(f"Calculated result: {result + 100}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | temperature = 123
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:51:14
|
50 | temperature = 123
51 | logging.info(f"Temperature: {temperature:.1f}°C")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52 |
53 | class FilePath:
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:57:14
|
55 | self.name = name
56 |
57 | logging.info(f"No changes made to {file_path.name}.")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 |
59 | user = "tron"
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:61:15
|
59 | user = "tron"
60 | balance = 123.45
61 | logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 |
63 | import logging
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004.py:66:15
|
65 | x = 1
66 | logging.error(f"{x} -> %s", x)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -1,23 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs
---
G004 Logging statement uses f-string
--> G004_arg_order.py:9:14
|
7 | X = 1
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
| ^^^^^^^^^^^^
10 | logger.error(f"{Y} -> %s", X)
|
help: Convert to lazy `%` formatting
G004 Logging statement uses f-string
--> G004_arg_order.py:10:14
|
8 | Y = 2
9 | logger.error(f"{X} -> %s", Y)
10 | logger.error(f"{Y} -> %s", X)
| ^^^^^^^^^^^^
|
help: Convert to lazy `%` formatting

View File

@@ -327,16 +327,10 @@ impl Violation for LoggingStringConcat {
pub(crate) struct LoggingFString;
impl Violation for LoggingFString {
const FIX_AVAILABILITY: crate::FixAvailability = crate::FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Logging statement uses f-string".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Convert to lazy `%` formatting".to_string())
}
}
/// ## What it does

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -43,7 +43,7 @@ use ruff_text_size::Ranged;
/// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd)
/// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -46,7 +46,7 @@ use crate::{FixAvailability, Violation};
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.makedirs`](https://docs.python.org/3/library/os.html#os.makedirs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -47,7 +47,7 @@ use crate::{FixAvailability, Violation};
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
/// - [Python documentation: `os.mkdir`](https://docs.python.org/3/library/os.html#os.mkdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -1,15 +1,9 @@
use ruff_diagnostics::{Edit, Fix};
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `os.path.abspath`.
@@ -40,18 +34,13 @@ use crate::{FixAvailability, Violation};
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is always marked as unsafe because `Path.resolve()` resolves symlinks, while
/// `os.path.abspath()` does not. If resolving symlinks is important, you may need to use
/// `Path.absolute()`. However, `Path.absolute()` also does not remove any `..` components in a
/// path, unlike `os.path.abspath()` and `Path.resolve()`, so if that specific combination of
/// behaviors is required, there's no existing `pathlib` alternative. See CPython issue
/// [#69200](https://github.com/python/cpython/issues/69200).
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -74,44 +63,12 @@ pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&s
if segments != ["os", "path", "abspath"] {
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("path", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsPathAbspath, call.func.range());
if has_unknown_keywords_or_starred_expr(&call.arguments, &["path"]) {
return;
}
if !is_fix_os_path_abspath_enabled(checker.settings()) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.resolve()")
} else {
format!("{binding}({arg_code}).resolve()")
};
Ok(Fix::unsafe_edits(
Edit::range_replacement(replacement, range),
[import_edit],
))
});
check_os_pathlib_single_arg_calls(
checker,
call,
"resolve()",
"path",
is_fix_os_path_abspath_enabled(checker.settings()),
OsPathAbspath,
);
}

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent)
/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists)
/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -39,7 +39,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute)
/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir)
/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file)
/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -40,7 +40,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::{ExprCall, PythonVersion};
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -45,7 +45,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir)
/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -44,7 +44,7 @@ use crate::{FixAvailability, Violation};
/// ## References
/// - [Python documentation: `Path.symlink_to`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.symlink_to)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -42,7 +42,7 @@ use ruff_python_ast::ExprCall;
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Unsafe fix
Safe fix
1 1 | import os
2 2 | import os.path
3 |+import pathlib

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Unsafe fix
Safe fix
1 1 | import os as foo
2 2 | import os.path as foo_p
3 |+import pathlib

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Unsafe fix
Safe fix
2 2 | from os import remove, unlink, getcwd, readlink, stat
3 3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 4 | from os.path import isabs, join, basename, dirname, samefile, splitext

View File

@@ -13,7 +13,7 @@ PTH100 [*] `os.path.abspath()` should be replaced by `Path.resolve()`
|
help: Replace with `Path(...).resolve()`
Unsafe fix
Safe fix
7 7 | from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
8 8 | from os.path import join as xjoin, basename as xbasename, dirname as xdirname
9 9 | from os.path import samefile as xsamefile, splitext as xsplitext

View File

@@ -43,7 +43,7 @@ use crate::Violation;
/// - [Python documentation: `Path.owner`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.owner)
/// - [Python documentation: `os.stat`](https://docs.python.org/3/library/os.html#os.stat)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -89,7 +89,7 @@ impl Violation for OsStat {
/// - [Python documentation: `PurePath.joinpath`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.joinpath)
/// - [Python documentation: `os.path.join`](https://docs.python.org/3/library/os.path.html#os.path.join)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -160,7 +160,7 @@ pub(crate) enum Joiner {
/// - [Python documentation: `Path.suffixes`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffixes)
/// - [Python documentation: `os.path.splitext`](https://docs.python.org/3/library/os.path.html#os.path.splitext)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -205,7 +205,7 @@ impl Violation for OsPathSplitext {
/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open)
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
@@ -298,7 +298,7 @@ impl Violation for PyPath {
/// - [Python documentation: `Path.iterdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.iterdir)
/// - [Python documentation: `os.listdir`](https://docs.python.org/3/library/os.html#os.listdir)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]

View File

@@ -737,7 +737,6 @@ mod tests {
/// A re-implementation of the Pyflakes test runner.
/// Note that all tests marked with `#[ignore]` should be considered TODOs.
#[track_caller]
fn flakes(contents: &str, expected: &[Rule]) {
let contents = dedent(contents);
let source_type = PySourceType::default();

View File

@@ -251,63 +251,3 @@ F841 Local variable `value` is assigned to but never used
128 | print(key)
|
help: Remove assignment to unused variable `value`
F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:168:9
|
166 | class A:
167 | def set_class(self, cls):
168 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`
Unsafe fix
165 165 | # variables that don't refer to the special `__class__` cell.
166 166 | class A:
167 167 | def set_class(self, cls):
168 |- __class__ = cls # F841
168 |+ pass # F841
169 169 |
170 170 |
171 171 | class A:
F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:174:13
|
172 | class B:
173 | def set_class(self, cls):
174 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`
Unsafe fix
171 171 | class A:
172 172 | class B:
173 173 | def set_class(self, cls):
174 |- __class__ = cls # F841
174 |+ pass # F841
175 175 |
176 176 |
177 177 | class A:
F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:182:17
|
180 | print(__class__)
181 | def set_class(self, cls):
182 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`
Unsafe fix
179 179 | class B:
180 180 | print(__class__)
181 181 | def set_class(self, cls):
182 |- __class__ = cls # F841
182 |+ pass # F841
183 183 |
184 184 |
185 185 | # OK, the `__class__` cell is nonlocal and declared as such.

View File

@@ -289,65 +289,3 @@ help: Remove assignment to unused variable `_`
152 |- except Exception as _:
152 |+ except Exception:
153 153 | pass
154 154 |
155 155 |
F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:168:9
|
166 | class A:
167 | def set_class(self, cls):
168 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`
Unsafe fix
165 165 | # variables that don't refer to the special `__class__` cell.
166 166 | class A:
167 167 | def set_class(self, cls):
168 |- __class__ = cls # F841
168 |+ pass # F841
169 169 |
170 170 |
171 171 | class A:
F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:174:13
|
172 | class B:
173 | def set_class(self, cls):
174 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`
Unsafe fix
171 171 | class A:
172 172 | class B:
173 173 | def set_class(self, cls):
174 |- __class__ = cls # F841
174 |+ pass # F841
175 175 |
176 176 |
177 177 | class A:
F841 [*] Local variable `__class__` is assigned to but never used
--> F841_0.py:182:17
|
180 | print(__class__)
181 | def set_class(self, cls):
182 | __class__ = cls # F841
| ^^^^^^^^^
|
help: Remove assignment to unused variable `__class__`
Unsafe fix
179 179 | class B:
180 180 | print(__class__)
181 181 | def set_class(self, cls):
182 |- __class__ = cls # F841
182 |+ pass # F841
183 183 |
184 184 |
185 185 | # OK, the `__class__` cell is nonlocal and declared as such.

View File

@@ -73,8 +73,7 @@ pub(crate) fn non_ascii_name(checker: &Checker, binding: &Binding) {
| BindingKind::SubmoduleImport(_)
| BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::UnboundException(_)
| BindingKind::DunderClassCell => {
| BindingKind::UnboundException(_) => {
return;
}
};

View File

@@ -186,7 +186,7 @@ fn use_initvar(
let indentation = indentation_at_offset(post_init_def.start(), checker.source())
.context("Failed to calculate leading indentation of `__post_init__` method")?;
let content = textwrap::indent_first_line(&content, indentation);
let content = textwrap::indent(&content, indentation);
let initvar_edit = Edit::insertion(
content.into_owned(),

View File

@@ -455,57 +455,3 @@ help: Use `dataclasses.InitVar` instead
122 123 | ,
123 124 | ) -> None:
124 125 | ...
RUF033 [*] `__post_init__` method with argument defaults
--> RUF033.py:131:50
|
129 | @dataclass
130 | class C:
131 | def __post_init__(self, x: tuple[int, ...] = (
| __________________________________________________^
132 | | 1,
133 | | 2,
134 | | )) -> None:
| |_____^
135 | self.x = x
|
help: Use `dataclasses.InitVar` instead
Unsafe fix
128 128 |
129 129 | @dataclass
130 130 | class C:
131 |- def __post_init__(self, x: tuple[int, ...] = (
131 |+ x: InitVar[tuple[int, ...]] = (
132 132 | 1,
133 133 | 2,
134 |- )) -> None:
134 |+ )
135 |+ def __post_init__(self, x: tuple[int, ...]) -> None:
135 136 | self.x = x
136 137 |
137 138 |
RUF033 [*] `__post_init__` method with argument defaults
--> RUF033.py:140:38
|
138 | @dataclass
139 | class D:
140 | def __post_init__(self, x: int = """
| ______________________________________^
141 | | """) -> None:
| |_______^
142 | self.x = x
|
help: Use `dataclasses.InitVar` instead
Unsafe fix
137 137 |
138 138 | @dataclass
139 139 | class D:
140 |- def __post_init__(self, x: int = """
141 |- """) -> None:
140 |+ x: InitVar[int] = """
141 |+ """
142 |+ def __post_init__(self, x: int) -> None:
142 143 | self.x = x

View File

@@ -446,7 +446,7 @@ impl Ranged for Binding<'_> {
/// ID uniquely identifying a [Binding] in a program.
///
/// Using a `u32` to identify [Binding]s should be sufficient because Ruff only supports documents with a
/// size smaller than or equal to `u32::MAX`. A document with the size of `u32::MAX` must have fewer than `u32::MAX`
/// size smaller than or equal to `u32::max`. A document with the size of `u32::max` must have fewer than `u32::max`
/// bindings because bindings must be separated by whitespace (and have an assignment).
#[newtype_index]
pub struct BindingId;
@@ -672,24 +672,6 @@ pub enum BindingKind<'a> {
/// Stores the ID of the binding that was shadowed in the enclosing
/// scope, if any.
UnboundException(Option<BindingId>),
/// A binding to `__class__` in the implicit closure created around every method in a class
/// body, if any method refers to either `__class__` or `super`.
///
/// ```python
/// class C:
/// __class__ # NameError: name '__class__' is not defined
///
/// def f():
/// print(__class__) # allowed
///
/// def g():
/// nonlocal __class__ # also allowed because the scope is *not* the function scope
/// ```
///
/// See <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object> for more
/// details.
DunderClassCell,
}
bitflags! {

View File

@@ -404,11 +404,22 @@ impl<'a> SemanticModel<'a> {
}
}
let mut seen_function = false;
let mut import_starred = false;
let mut class_variables_visible = true;
for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() {
let scope = &self.scopes[scope_id];
if scope.kind.is_class() {
// Allow usages of `__class__` within methods, e.g.:
//
// ```python
// class Foo:
// def __init__(self):
// print(__class__)
// ```
if seen_function && matches!(name.id.as_str(), "__class__") {
return ReadResult::ImplicitGlobal;
}
// Do not allow usages of class symbols unless it is the immediate parent
// (excluding type scopes), e.g.:
//
@@ -431,13 +442,7 @@ impl<'a> SemanticModel<'a> {
// Allow class variables to be visible for an additional scope level
// when a type scope is seen — this covers the type scope present between
// function and class definitions and their parent class scope.
//
// Also allow an additional level beyond that to cover the implicit
// `__class__` closure created around methods and enclosing the type scope.
class_variables_visible = matches!(
(scope.kind, index),
(ScopeKind::Type, 0) | (ScopeKind::DunderClassCell, 1)
);
class_variables_visible = scope.kind.is_type() && index == 0;
if let Some(binding_id) = scope.get(name.id.as_str()) {
// Mark the binding as used.
@@ -609,6 +614,7 @@ impl<'a> SemanticModel<'a> {
}
}
seen_function |= scope.kind.is_function();
import_starred = import_starred || scope.uses_star_imports();
}
@@ -652,19 +658,21 @@ impl<'a> SemanticModel<'a> {
}
}
let mut seen_function = false;
let mut class_variables_visible = true;
for (index, scope_id) in self.scopes.ancestor_ids(scope_id).enumerate() {
let scope = &self.scopes[scope_id];
if scope.kind.is_class() {
if seen_function && matches!(symbol, "__class__") {
return None;
}
if !class_variables_visible {
continue;
}
}
class_variables_visible = matches!(
(scope.kind, index),
(ScopeKind::Type, 0) | (ScopeKind::DunderClassCell, 1)
);
class_variables_visible = scope.kind.is_type() && index == 0;
seen_function |= scope.kind.is_function();
if let Some(binding_id) = scope.get(symbol) {
match self.bindings[binding_id].kind {
@@ -778,15 +786,15 @@ impl<'a> SemanticModel<'a> {
}
if scope.kind.is_class() {
if seen_function && matches!(symbol, "__class__") {
return None;
}
if !class_variables_visible {
continue;
}
}
class_variables_visible = matches!(
(scope.kind, index),
(ScopeKind::Type, 0) | (ScopeKind::DunderClassCell, 1)
);
class_variables_visible = scope.kind.is_type() && index == 0;
seen_function |= scope.kind.is_function();
if let Some(binding_id) = scope.get(symbol) {
@@ -1345,12 +1353,11 @@ impl<'a> SemanticModel<'a> {
self.scopes[scope_id].parent
}
/// Returns the first parent of the given [`Scope`] that is not of [`ScopeKind::Type`] or
/// [`ScopeKind::DunderClassCell`], if any.
/// Returns the first parent of the given [`Scope`] that is not of [`ScopeKind::Type`], if any.
pub fn first_non_type_parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> {
let mut current_scope = scope;
while let Some(parent) = self.parent_scope(current_scope) {
if matches!(parent.kind, ScopeKind::Type | ScopeKind::DunderClassCell) {
if parent.kind.is_type() {
current_scope = parent;
} else {
return Some(parent);
@@ -1359,15 +1366,11 @@ impl<'a> SemanticModel<'a> {
None
}
/// Returns the first parent of the given [`ScopeId`] that is not of [`ScopeKind::Type`] or
/// [`ScopeKind::DunderClassCell`], if any.
/// Returns the first parent of the given [`ScopeId`] that is not of [`ScopeKind::Type`], if any.
pub fn first_non_type_parent_scope_id(&self, scope_id: ScopeId) -> Option<ScopeId> {
let mut current_scope_id = scope_id;
while let Some(parent_id) = self.parent_scope_id(current_scope_id) {
if matches!(
self.scopes[parent_id].kind,
ScopeKind::Type | ScopeKind::DunderClassCell
) {
if self.scopes[parent_id].kind.is_type() {
current_scope_id = parent_id;
} else {
return Some(parent_id);
@@ -2646,16 +2649,16 @@ pub enum ReadResult {
/// The `x` in `print(x)` is resolved to the binding of `x` in `x = 1`.
Resolved(BindingId),
/// The read reference is resolved to a context-specific, implicit global (e.g., `__qualname__`
/// The read reference is resolved to a context-specific, implicit global (e.g., `__class__`
/// within a class scope).
///
/// For example, given:
/// ```python
/// class C:
/// print(__qualname__)
/// print(__class__)
/// ```
///
/// The `__qualname__` in `print(__qualname__)` is resolved to the implicit global `__qualname__`.
/// The `__class__` in `print(__class__)` is resolved to the implicit global `__class__`.
ImplicitGlobal,
/// The read reference is unresolved, but at least one of the containing scopes contains a

View File

@@ -166,49 +166,9 @@ bitflags! {
}
}
#[derive(Clone, Copy, Debug, is_macro::Is)]
#[derive(Debug, is_macro::Is)]
pub enum ScopeKind<'a> {
Class(&'a ast::StmtClassDef),
/// The implicit `__class__` scope surrounding a method which allows code in the
/// method to access `__class__` at runtime. The closure sits in between the class
/// scope and the function scope.
///
/// Parameter defaults in methods cannot access `__class__`:
///
/// ```pycon
/// >>> class Bar:
/// ... def method(self, x=__class__): ...
/// ...
/// Traceback (most recent call last):
/// File "<python-input-6>", line 1, in <module>
/// class Bar:
/// def method(self, x=__class__): ...
/// File "<python-input-6>", line 2, in Bar
/// def method(self, x=__class__): ...
/// ^^^^^^^^^
/// NameError: name '__class__' is not defined
/// ```
///
/// However, type parameters in methods *can* access `__class__`:
///
/// ```pycon
/// >>> class Foo:
/// ... def bar[T: __class__](): ...
/// ...
/// >>> Foo.bar.__type_params__[0].__bound__
/// <class '__main__.Foo'>
/// ```
///
/// Note that this is still not 100% accurate! At runtime, the implicit `__class__`
/// closure is only added if the name `super` (has to be a name -- `builtins.super`
/// and similar don't count!) or the name `__class__` is used in any method of the
/// class. However, accurately emulating that would be both complex and probably
/// quite expensive unless we moved to a double-traversal of each scope similar to
/// ty. It would also only matter in extreme and unlikely edge cases. So we ignore
/// that subtlety for now.
///
/// See <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>.
DunderClassCell,
Function(&'a ast::StmtFunctionDef),
Generator {
kind: GeneratorKind,

View File

@@ -71,66 +71,6 @@ pub fn indent<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> {
Cow::Owned(result)
}
/// Indent only the first line by the given prefix.
///
/// This function is useful when you want to indent the first line of a multi-line
/// expression while preserving the relative indentation of subsequent lines.
///
/// # Examples
///
/// ```
/// # use ruff_python_trivia::textwrap::indent_first_line;
///
/// assert_eq!(indent_first_line("First line.\nSecond line.\n", " "),
/// " First line.\nSecond line.\n");
/// ```
///
/// When indenting, trailing whitespace is stripped from the prefix.
/// This means that empty lines remain empty afterwards:
///
/// ```
/// # use ruff_python_trivia::textwrap::indent_first_line;
///
/// assert_eq!(indent_first_line("\n\n\nSecond line.\n", " "),
/// "\n\n\nSecond line.\n");
/// ```
///
/// Leading and trailing whitespace coming from the text itself is
/// kept unchanged:
///
/// ```
/// # use ruff_python_trivia::textwrap::indent_first_line;
///
/// assert_eq!(indent_first_line(" \t Foo ", "->"), "-> \t Foo ");
/// ```
pub fn indent_first_line<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> {
if prefix.is_empty() {
return Cow::Borrowed(text);
}
let mut lines = text.universal_newlines();
let Some(first_line) = lines.next() else {
return Cow::Borrowed(text);
};
let mut result = String::with_capacity(text.len() + prefix.len());
// Indent only the first line
if first_line.trim_whitespace().is_empty() {
result.push_str(prefix.trim_whitespace_end());
} else {
result.push_str(prefix);
}
result.push_str(first_line.as_full_str());
// Add remaining lines without indentation
for line in lines {
result.push_str(line.as_full_str());
}
Cow::Owned(result)
}
/// Removes common leading whitespace from each line.
///
/// This function will look at each non-empty line and determine the
@@ -469,61 +409,6 @@ mod tests {
assert_eq!(dedent(text), text);
}
#[test]
fn indent_first_line_empty() {
assert_eq!(indent_first_line("\n", " "), "\n");
}
#[test]
#[rustfmt::skip]
fn indent_first_line_nonempty() {
let text = [
" foo\n",
"bar\n",
" baz\n",
].join("");
let expected = [
"// foo\n",
"bar\n",
" baz\n",
].join("");
assert_eq!(indent_first_line(&text, "// "), expected);
}
#[test]
#[rustfmt::skip]
fn indent_first_line_empty_line() {
let text = [
" foo",
"bar",
"",
" baz",
].join("\n");
let expected = [
"// foo",
"bar",
"",
" baz",
].join("\n");
assert_eq!(indent_first_line(&text, "// "), expected);
}
#[test]
#[rustfmt::skip]
fn indent_first_line_mixed_newlines() {
let text = [
" foo\r\n",
"bar\n",
" baz\r",
].join("");
let expected = [
"// foo\r\n",
"bar\n",
" baz\r",
].join("");
assert_eq!(indent_first_line(&text, "// "), expected);
}
#[test]
#[rustfmt::skip]
fn adjust_indent() {

155
crates/ty/docs/rules.md generated
View File

@@ -36,7 +36,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20call-non-callable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L113)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L110)
</small>
**What it does**
@@ -58,7 +58,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-argument-forms) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L157)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L154)
</small>
**What it does**
@@ -88,7 +88,7 @@ f(int) # error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-declarations) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L183)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L180)
</small>
**What it does**
@@ -117,7 +117,7 @@ a = 1
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20conflicting-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L208)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L205)
</small>
**What it does**
@@ -147,7 +147,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-class-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L234)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L231)
</small>
**What it does**
@@ -177,7 +177,7 @@ class B(A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L299)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L296)
</small>
**What it does**
@@ -202,7 +202,7 @@ class B(A, A): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L320)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L317)
</small>
**What it does**
@@ -306,7 +306,7 @@ def test(): -> "Literal[5]":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20inconsistent-mro) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L523)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L520)
</small>
**What it does**
@@ -334,7 +334,7 @@ class C(A, B): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20index-out-of-bounds) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L547)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L544)
</small>
**What it does**
@@ -358,7 +358,7 @@ t[3] # IndexError: tuple index out of range
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L352)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L349)
</small>
**What it does**
@@ -422,7 +422,7 @@ class D(A, B, C): ...
**Known problems**
Classes that have "dynamic" definitions of `__slots__` (definitions do not consist
of string literals, or tuples of string literals) are not currently considered disjoint
of string literals, or tuples of string literals) are not currently considered solid
bases by ty.
Additionally, this check is not exhaustive: many C extensions (including several in
@@ -445,7 +445,7 @@ an atypical memory layout.
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-argument-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L592)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L589)
</small>
**What it does**
@@ -470,7 +470,7 @@ func("foo") # error: [invalid-argument-type]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-assignment) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L632)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L629)
</small>
**What it does**
@@ -496,7 +496,7 @@ a: int = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-attribute-access) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1687)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663)
</small>
**What it does**
@@ -528,7 +528,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L654)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L651)
</small>
**What it does**
@@ -562,7 +562,7 @@ asyncio.run(main())
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L684)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L681)
</small>
**What it does**
@@ -584,7 +584,7 @@ class A(42): ... # error: [invalid-base]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-context-manager) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L735)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L732)
</small>
**What it does**
@@ -609,7 +609,7 @@ with 1:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-declaration) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L756)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L753)
</small>
**What it does**
@@ -636,7 +636,7 @@ a: str
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-exception-caught) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L779)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L776)
</small>
**What it does**
@@ -678,7 +678,7 @@ except ZeroDivisionError:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-generic-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L815)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L812)
</small>
**What it does**
@@ -709,7 +709,7 @@ class C[U](Generic[T]): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L564)
</small>
**What it does**
@@ -738,7 +738,7 @@ alice["height"] # KeyError: 'height'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-legacy-type-variable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L841)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L838)
</small>
**What it does**
@@ -771,7 +771,7 @@ def f(t: TypeVar("U")): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-metaclass) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L911)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L887)
</small>
**What it does**
@@ -803,7 +803,7 @@ class B(metaclass=f): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L497)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L494)
</small>
**What it does**
@@ -828,37 +828,12 @@ in a class's bases list.
TypeError: can only inherit from a NamedTuple type and Generic
```
## `invalid-newtype`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L890)
</small>
**What it does**
Checks for the creation of invalid `NewType`s
**Why is this bad?**
There are several requirements that you must follow when creating a `NewType`.
**Examples**
```python
from typing import NewType
Foo = NewType("Foo", int) # okay
Bar = NewType(get_name(), int) # error: NewType name must be a string literal
```
## `invalid-overload`
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L938)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L914)
</small>
**What it does**
@@ -906,7 +881,7 @@ def foo(x: int) -> int: ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-parameter-default) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L981)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L957)
</small>
**What it does**
@@ -930,7 +905,7 @@ def f(a: int = ''): ...
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-protocol) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L434)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L431)
</small>
**What it does**
@@ -962,7 +937,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-raise) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1001)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L977)
</small>
Checks for `raise` statements that raise non-exceptions or use invalid
@@ -1009,7 +984,7 @@ def g():
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-return-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L613)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L610)
</small>
**What it does**
@@ -1032,7 +1007,7 @@ def func() -> int:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-super-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1044)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1020)
</small>
**What it does**
@@ -1086,7 +1061,7 @@ TODO #14889
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L869)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L866)
</small>
**What it does**
@@ -1111,7 +1086,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-checking-constant) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1083)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1059)
</small>
**What it does**
@@ -1139,7 +1114,7 @@ TYPE_CHECKING = ''
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-form) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1083)
</small>
**What it does**
@@ -1167,7 +1142,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1159)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1135)
</small>
**What it does**
@@ -1199,7 +1174,7 @@ f(10) # Error
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1131)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1107)
</small>
**What it does**
@@ -1231,7 +1206,7 @@ class C:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-variable-constraints) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1187)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1163)
</small>
**What it does**
@@ -1264,7 +1239,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1216)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1192)
</small>
**What it does**
@@ -1287,7 +1262,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1786)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1762)
</small>
**What it does**
@@ -1318,7 +1293,7 @@ alice["age"] # KeyError
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20no-matching-overload) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1235)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1211)
</small>
**What it does**
@@ -1345,7 +1320,7 @@ func("string") # error: [no-matching-overload]
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20non-subscriptable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1258)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1234)
</small>
**What it does**
@@ -1367,7 +1342,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20not-iterable) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1276)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1252)
</small>
**What it does**
@@ -1391,7 +1366,7 @@ for i in 34: # TypeError: 'int' object is not iterable
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20parameter-already-assigned) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1327)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1303)
</small>
**What it does**
@@ -1445,7 +1420,7 @@ def test(): -> "int":
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20static-assert-error) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1663)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1639)
</small>
**What it does**
@@ -1473,7 +1448,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20subclass-of-final-class) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1418)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394)
</small>
**What it does**
@@ -1500,7 +1475,7 @@ class B(A): ... # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20too-many-positional-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1463)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1439)
</small>
**What it does**
@@ -1525,7 +1500,7 @@ f("foo") # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20type-assertion-failure) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1441)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417)
</small>
**What it does**
@@ -1551,7 +1526,7 @@ def _(x: int):
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unavailable-implicit-super-arguments) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1484)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1460)
</small>
**What it does**
@@ -1595,7 +1570,7 @@ class A:
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unknown-argument) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1541)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1517)
</small>
**What it does**
@@ -1620,7 +1595,7 @@ f(x=1, y=2) # Error raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1562)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1538)
</small>
**What it does**
@@ -1646,7 +1621,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1584)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1560)
</small>
**What it does**
@@ -1669,7 +1644,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1603)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1579)
</small>
**What it does**
@@ -1692,7 +1667,7 @@ print(x) # NameError: name 'x' is not defined
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-bool-conversion) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1296)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1272)
</small>
**What it does**
@@ -1727,7 +1702,7 @@ b1 < b2 < b1 # exception raised here
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-operator) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1622)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1598)
</small>
**What it does**
@@ -1753,7 +1728,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
<small>
Default level: [`error`](../rules.md#rule-levels "This lint has a default level of 'error'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20zero-stepsize-in-slice) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1644)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1620)
</small>
**What it does**
@@ -1776,7 +1751,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L462)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L459)
</small>
**What it does**
@@ -1815,7 +1790,7 @@ class SubProto(BaseProto, Protocol):
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L278)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L275)
</small>
**What it does**
@@ -1868,7 +1843,7 @@ a = 20 / 0 # type: ignore
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-attribute) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1348)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1324)
</small>
**What it does**
@@ -1894,7 +1869,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-implicit-call) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L131)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L128)
</small>
**What it does**
@@ -1924,7 +1899,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unbound-import) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1370)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1346)
</small>
**What it does**
@@ -1954,7 +1929,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20redundant-cast) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1715)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1691)
</small>
**What it does**
@@ -1979,7 +1954,7 @@ cast(int, f()) # Redundant
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20undefined-reveal) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1523)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1499)
</small>
**What it does**
@@ -2030,7 +2005,7 @@ a = 20 / 0 # ty: ignore[division-by-zero]
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1736)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1712)
</small>
**What it does**
@@ -2084,7 +2059,7 @@ def g():
<small>
Default level: [`warn`](../rules.md#rule-levels "This lint has a default level of 'warn'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L702)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L699)
</small>
**What it does**
@@ -2121,7 +2096,7 @@ class D(C): ... # error: [unsupported-base]
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20division-by-zero) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L260)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L257)
</small>
**What it does**
@@ -2143,7 +2118,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
<small>
Default level: [`ignore`](../rules.md#rule-levels "This lint has a default level of 'ignore'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-unresolved-reference) ·
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1396)
[View source](https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1372)
</small>
**What it does**

View File

@@ -263,9 +263,6 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
3 |
4 | stat = add(10, 15)
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -480,7 +477,7 @@ fn check_specific_paths() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command(),
@r"
@r###"
success: false
exit_code: 1
----- stdout -----
@@ -492,9 +489,6 @@ fn check_specific_paths() -> anyhow::Result<()> {
3 |
4 | print(z)
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -504,9 +498,6 @@ fn check_specific_paths() -> anyhow::Result<()> {
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -514,7 +505,7 @@ fn check_specific_paths() -> anyhow::Result<()> {
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
"
"###
);
// Now check only the `tests` and `other.py` files.
@@ -533,9 +524,6 @@ fn check_specific_paths() -> anyhow::Result<()> {
3 |
4 | print(z)
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -545,9 +533,6 @@ fn check_specific_paths() -> anyhow::Result<()> {
2 | import does_not_exist # error: unresolved-import
| ^^^^^^^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default

View File

@@ -333,10 +333,6 @@ import bar",
| ^^^
2 | import bar
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/strange-venv-location/lib/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -382,11 +378,6 @@ fn lib64_site_packages_directory_on_unix() -> anyhow::Result<()> {
1 | import foo, bar, baz
| ^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/.venv/lib/python3.13/site-packages (site-packages)
info: 4. <temp_dir>/.venv/lib64/python3.13/site-packages (site-packages)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -1282,9 +1273,6 @@ home = ./
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -1297,9 +1285,6 @@ home = ./
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -1312,9 +1297,6 @@ home = ./
| ^^^^^^^^
5 | from package1 import BaseConda
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -1326,9 +1308,6 @@ home = ./
5 | from package1 import BaseConda
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -1737,10 +1716,6 @@ fn default_root_tests_package() -> anyhow::Result<()> {
4 |
5 | print(f"{foo} {bar}")
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. <temp_dir>/src (first-party code)
info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default

View File

@@ -89,7 +89,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
// Assert that there's an `unresolved-reference` diagnostic (error)
// and an unresolved-import (error) diagnostic by default.
assert_cmd_snapshot!(case.command(), @r"
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -101,9 +101,6 @@ fn cli_rule_severity() -> anyhow::Result<()> {
3 |
4 | y = 4 / 0
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -121,7 +118,7 @@ fn cli_rule_severity() -> anyhow::Result<()> {
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
"###);
assert_cmd_snapshot!(
case
@@ -144,9 +141,6 @@ fn cli_rule_severity() -> anyhow::Result<()> {
3 |
4 | y = 4 / 0
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` was selected on the command line

View File

@@ -165,11 +165,16 @@ impl<'db> DefinitionsOrTargets<'db> {
ty_python_semantic::types::TypeDefinition::Module(module) => {
ResolvedDefinition::Module(module.file(db)?)
}
ty_python_semantic::types::TypeDefinition::Class(definition)
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
ty_python_semantic::types::TypeDefinition::Class(definition) => {
ResolvedDefinition::Definition(definition)
}
ty_python_semantic::types::TypeDefinition::Function(definition) => {
ResolvedDefinition::Definition(definition)
}
ty_python_semantic::types::TypeDefinition::TypeVar(definition) => {
ResolvedDefinition::Definition(definition)
}
ty_python_semantic::types::TypeDefinition::TypeAlias(definition) => {
ResolvedDefinition::Definition(definition)
}
};

View File

@@ -199,13 +199,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:911:7
--> stdlib/builtins.pyi:901:7
|
910 | @disjoint_base
911 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
912 | """str(object='') -> str
913 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:1
@@ -227,13 +228,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:911:7
--> stdlib/builtins.pyi:901:7
|
910 | @disjoint_base
911 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
912 | """str(object='') -> str
913 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:2:10
@@ -342,13 +344,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:911:7
--> stdlib/builtins.pyi:901:7
|
910 | @disjoint_base
911 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
912 | """str(object='') -> str
913 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:6
@@ -376,13 +379,14 @@ mod tests {
// is an int. Navigating to `str` would match pyright's behavior.
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:344:7
--> stdlib/builtins.pyi:337:7
|
343 | @disjoint_base
344 | class int:
335 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
336 |
337 | class int:
| ^^^
345 | """int([x]) -> integer
346 | int(x, base=10) -> integer
338 | """int([x]) -> integer
339 | int(x, base=10) -> integer
|
info: Source
--> main.py:4:6
@@ -407,15 +411,16 @@ f(**kwargs<CURSOR>)
"#,
);
assert_snapshot!(test.goto_type_definition(), @r#"
assert_snapshot!(test.goto_type_definition(), @r###"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:2917:7
--> stdlib/builtins.pyi:2901:7
|
2916 | @disjoint_base
2917 | class dict(MutableMapping[_KT, _VT]):
2899 | """See PEP 585"""
2900 |
2901 | class dict(MutableMapping[_KT, _VT]):
| ^^^^
2918 | """dict() -> new empty dictionary
2919 | dict(mapping) -> new dictionary initialized from a mapping object's
2902 | """dict() -> new empty dictionary
2903 | dict(mapping) -> new dictionary initialized from a mapping object's
|
info: Source
--> main.py:6:5
@@ -425,7 +430,7 @@ f(**kwargs<CURSOR>)
6 | f(**kwargs)
| ^^^^^^
|
"#);
"###);
}
#[test]
@@ -439,13 +444,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:911:7
--> stdlib/builtins.pyi:901:7
|
910 | @disjoint_base
911 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
912 | """str(object='') -> str
913 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:5
@@ -531,13 +537,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:911:7
--> stdlib/builtins.pyi:901:7
|
910 | @disjoint_base
911 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
912 | """str(object='') -> str
913 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:15
@@ -561,13 +568,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:941:11
--> stdlib/types.pyi:922:11
|
939 | if sys.version_info >= (3, 10):
940 | @final
941 | class NoneType:
920 | if sys.version_info >= (3, 10):
921 | @final
922 | class NoneType:
| ^^^^^^^^
942 | """The type of the None singleton."""
923 | """The type of the None singleton."""
|
info: Source
--> main.py:3:5
@@ -578,13 +585,14 @@ f(**kwargs<CURSOR>)
|
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:911:7
--> stdlib/builtins.pyi:901:7
|
910 | @disjoint_base
911 | class str(Sequence[str]):
899 | def __getitem__(self, key: int, /) -> str | int | None: ...
900 |
901 | class str(Sequence[str]):
| ^^^
912 | """str(object='') -> str
913 | str(bytes_or_buffer[, encoding[, errors]]) -> str
902 | """str(object='') -> str
903 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:5

View File

@@ -1,128 +1,62 @@
use std::{fmt, vec};
use crate::Db;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::{AnyNodeRef, Expr, Stmt};
use ruff_text_size::{Ranged, TextRange, TextSize};
use std::fmt;
use std::fmt::Formatter;
use ty_python_semantic::types::{Type, inlay_hint_function_argument_details};
use ty_python_semantic::{HasType, SemanticModel};
#[derive(Debug, Clone)]
pub struct InlayHint {
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct InlayHint<'db> {
pub position: TextSize,
pub kind: InlayHintKind,
pub label: InlayHintLabel,
pub content: InlayHintContent<'db>,
}
impl InlayHint {
fn variable_type(position: TextSize, ty: Type, db: &dyn Db) -> Self {
let label_parts = vec![
": ".into(),
InlayHintLabelPart::new(ty.display(db).to_string()),
];
Self {
position,
kind: InlayHintKind::Type,
label: InlayHintLabel { parts: label_parts },
}
}
fn call_argument_name(position: TextSize, name: &str) -> Self {
let label_parts = vec![InlayHintLabelPart::new(name), "=".into()];
Self {
position,
kind: InlayHintKind::CallArgumentName,
label: InlayHintLabel { parts: label_parts },
}
}
pub fn display(&self) -> InlayHintDisplay<'_> {
InlayHintDisplay { inlay_hint: self }
impl<'db> InlayHint<'db> {
pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> {
self.content.display(db)
}
}
#[derive(Debug, Clone)]
pub enum InlayHintKind {
Type,
CallArgumentName,
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum InlayHintContent<'db> {
Type(Type<'db>),
CallArgumentName(String),
}
#[derive(Debug, Clone)]
pub struct InlayHintLabel {
parts: Vec<InlayHintLabelPart>,
}
impl InlayHintLabel {
pub fn parts(&self) -> &[InlayHintLabelPart] {
&self.parts
impl<'db> InlayHintContent<'db> {
pub const fn display(&self, db: &'db dyn Db) -> DisplayInlayHint<'_, 'db> {
DisplayInlayHint { db, hint: self }
}
}
pub struct InlayHintDisplay<'a> {
inlay_hint: &'a InlayHint,
pub struct DisplayInlayHint<'a, 'db> {
db: &'db dyn Db,
hint: &'a InlayHintContent<'db>,
}
impl fmt::Display for InlayHintDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
for part in &self.inlay_hint.label.parts {
write!(f, "{}", part.text)?;
}
Ok(())
}
}
#[derive(Default, Debug, Clone)]
pub struct InlayHintLabelPart {
text: String,
target: Option<crate::NavigationTarget>,
}
impl InlayHintLabelPart {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
target: None,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn target(&self) -> Option<&crate::NavigationTarget> {
self.target.as_ref()
}
}
impl From<String> for InlayHintLabelPart {
fn from(s: String) -> Self {
Self {
text: s,
target: None,
impl fmt::Display for DisplayInlayHint<'_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.hint {
InlayHintContent::Type(ty) => {
write!(f, ": {}", ty.display(self.db))
}
InlayHintContent::CallArgumentName(name) => {
write!(f, "{name}=")
}
}
}
}
impl From<&str> for InlayHintLabelPart {
fn from(s: &str) -> Self {
Self {
text: s.to_string(),
target: None,
}
}
}
pub fn inlay_hints(
db: &dyn Db,
pub fn inlay_hints<'db>(
db: &'db dyn Db,
file: File,
range: TextRange,
settings: &InlayHintSettings,
) -> Vec<InlayHint> {
) -> Vec<InlayHint<'db>> {
let mut visitor = InlayHintVisitor::new(db, file, range, settings);
let ast = parsed_module(db, file).load(db);
@@ -172,7 +106,7 @@ impl Default for InlayHintSettings {
struct InlayHintVisitor<'a, 'db> {
db: &'db dyn Db,
model: SemanticModel<'db>,
hints: Vec<InlayHint>,
hints: Vec<InlayHint<'db>>,
in_assignment: bool,
range: TextRange,
settings: &'a InlayHintSettings,
@@ -194,11 +128,13 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
if !self.settings.variable_types {
return;
}
self.hints
.push(InlayHint::variable_type(position, ty, self.db));
self.hints.push(InlayHint {
position,
content: InlayHintContent::Type(ty),
});
}
fn add_call_argument_name(&mut self, position: TextSize, name: &str) {
fn add_call_argument_name(&mut self, position: TextSize, name: String) {
if !self.settings.call_argument_names {
return;
}
@@ -207,8 +143,10 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
return;
}
self.hints
.push(InlayHint::call_argument_name(position, name));
self.hints.push(InlayHint {
position,
content: InlayHintContent::CallArgumentName(name),
});
}
}
@@ -274,7 +212,10 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() {
if let Some(name) = argument_names.get(&index) {
self.add_call_argument_name(arg_or_keyword.range().start(), name);
self.add_call_argument_name(
arg_or_keyword.range().start(),
name.to_string(),
);
}
self.visit_expr(arg_or_keyword.value());
}
@@ -381,7 +322,7 @@ mod tests {
for hint in hints {
let end_position = (hint.position.to_u32() as usize) + offset;
let hint_str = format!("[{}]", hint.display());
let hint_str = format!("[{}]", hint.display(&self.db));
buf.insert_str(end_position, &hint_str);
offset += hint_str.len();
}

View File

@@ -30,7 +30,7 @@ pub use document_symbols::document_symbols;
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
pub use goto_references::goto_references;
pub use hover::hover;
pub use inlay_hints::{InlayHintKind, InlayHintLabel, InlayHintSettings, inlay_hints};
pub use inlay_hints::{InlayHintContent, InlayHintSettings, inlay_hints};
pub use markup::MarkupKind;
pub use references::ReferencesMode;
pub use rename::{can_rename, rename};

View File

@@ -137,22 +137,6 @@ from unittest.mock import MagicMock
x: int = MagicMock()
```
## Runtime properties
`typing.Any` is a class at runtime on Python 3.11+, and `typing_extensions.Any` is always a class.
On earlier versions of Python, `typing.Any` was an instance of `typing._SpecialForm`, but this is
not currently modeled by ty. We currently infer `Any` has having all attributes a class would have
on all versions of Python:
```py
from typing import Any
from ty_extensions import TypeOf, static_assert, is_assignable_to
reveal_type(Any.__base__) # revealed: type | None
reveal_type(Any.__bases__) # revealed: tuple[type, ...]
static_assert(is_assignable_to(TypeOf[Any], type))
```
## Invalid
`Any` cannot be parameterized:
@@ -160,32 +144,7 @@ static_assert(is_assignable_to(TypeOf[Any], type))
```py
from typing import Any
# error: [invalid-type-form] "Special form `typing.Any` expected no type parameter"
# error: [invalid-type-form] "Type `typing.Any` expected no type parameter"
def f(x: Any[int]):
reveal_type(x) # revealed: Unknown
```
`Any` cannot be called (this leads to a `TypeError` at runtime):
```py
Any() # error: [call-non-callable] "Object of type `typing.Any` is not callable"
```
`Any` also cannot be used as a metaclass (under the hood, this leads to an implicit call to `Any`):
```py
class F(metaclass=Any): ... # error: [invalid-metaclass] "Metaclass type `typing.Any` is not callable"
```
And `Any` cannot be used in `isinstance()` checks:
```py
# error: [invalid-argument-type] "`typing.Any` cannot be used with `isinstance()`: This call will raise `TypeError` at runtime"
isinstance("", Any)
```
But `issubclass()` checks are fine:
```py
issubclass(object, Any) # no error!
```

View File

@@ -398,7 +398,7 @@ def f_okay(c: Callable[[], None]):
c.__qualname__ = "my_callable"
result = getattr_static(c, "__qualname__")
reveal_type(result) # revealed: property
reveal_type(result) # revealed: Never
if isinstance(result, property) and result.fset:
c.__qualname__ = "my_callable" # okay
```

View File

@@ -48,24 +48,6 @@ def _(
reveal_type(h_) # revealed: Unknown
reveal_type(i_) # revealed: Unknown
reveal_type(j_) # revealed: Unknown
# Inspired by the conformance test suite at
# https://github.com/python/typing/blob/d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc/conformance/tests/aliases_implicit.py#L88-L122
B = [x for x in range(42)]
C = {x for x in range(42)}
D = {x: y for x, y in enumerate(range(42))}
E = (x for x in range(42))
def _(
b: B, # error: [invalid-type-form]
c: C, # error: [invalid-type-form]
d: D, # error: [invalid-type-form]
e: E, # error: [invalid-type-form]
):
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: Unknown
```
## Invalid AST nodes

View File

@@ -1,5 +1,7 @@
# NewType
Currently, ty doesn't support `typing.NewType` in type annotations.
## Valid forms
```py
@@ -10,44 +12,13 @@ X = GenericAlias(type, ())
A = NewType("A", int)
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
# to be compatible with `type`
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `A`"
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `NewType`"
B = GenericAlias(A, ())
def _(
a: A,
b: B,
):
reveal_type(a) # revealed: A
reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions)
reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
```
## Subtyping
```py
from typing_extensions import NewType
Foo = NewType("Foo", int)
Bar = NewType("Bar", Foo)
Foo(42)
Foo(Foo(42)) # allowed: `Foo` is a subtype of `int`.
Foo(Bar(Foo(42))) # allowed: `Bar` is a subtype of `int`.
Foo(True) # allowed: `bool` is a subtype of `int`.
Foo("fourty-two") # error: [invalid-argument-type]
def f(_: int): ...
def g(_: Foo): ...
def h(_: Bar): ...
f(42)
f(Foo(42))
f(Bar(Foo(42)))
g(42) # error: [invalid-argument-type]
g(Foo(42))
g(Bar(Foo(42)))
h(42) # error: [invalid-argument-type]
h(Foo(42)) # error: [invalid-argument-type]
h(Bar(Foo(42)))
```

View File

@@ -75,7 +75,7 @@ a: tuple[()] = (1, 2)
# error: [invalid-assignment] "Object of type `tuple[Literal["foo"]]` is not assignable to `tuple[int]`"
b: tuple[int] = ("foo",)
# error: [invalid-assignment]
# error: [invalid-assignment] "Object of type `tuple[list[Unknown], Literal["foo"]]` is not assignable to `tuple[str | int, str]`"
c: tuple[str | int, str] = ([], "foo")
```

View File

@@ -1038,49 +1038,6 @@ def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, An
reveal_type(f(*(any_any,))) # revealed: Unknown
```
### `Unknown` passed into an overloaded function annotated with protocols
`Foo.join()` here has similar annotations to `str.join()` in typeshed:
`module.pyi`:
```pyi
from typing_extensions import Iterable, overload, LiteralString, Protocol
from ty_extensions import Unknown, is_assignable_to
class Foo:
@overload
def join(self, iterable: Iterable[LiteralString], /) -> LiteralString: ...
@overload
def join(self, iterable: Iterable[str], /) -> str: ...
```
`main.py`:
```py
from module import Foo
from typing_extensions import LiteralString
def f(a: Foo, b: list[str], c: list[LiteralString], e):
reveal_type(e) # revealed: Unknown
# TODO: we should select the second overload here and reveal `str`
# (the incorrect result is due to missing logic in protocol subtyping/assignability)
reveal_type(a.join(b)) # revealed: LiteralString
reveal_type(a.join(c)) # revealed: LiteralString
# since both overloads match and they have return types that are not equivalent,
# step (5) of the overload evaluation algorithm says we must evaluate the result of the
# call as `Unknown`.
#
# Note: although the spec does not state as such (since intersections in general are not
# specified currently), `(str | LiteralString) & Unknown` might also be a reasonable type
# here (the union of all overload returns, intersected with `Unknown`) -- here that would
# simplify to `str & Unknown`.
reveal_type(a.join(e)) # revealed: Unknown
```
### Multiple arguments
`overloaded.pyi`:

View File

@@ -46,7 +46,7 @@ def delete():
del d # error: [unresolved-reference] "Name `d` used when not defined"
delete()
reveal_type(d) # revealed: list[@Todo(list literal element type)]
reveal_type(d) # revealed: list[Unknown]
def delete_element():
# When the `del` target isn't a name, it doesn't force local resolution.
@@ -62,7 +62,7 @@ def delete_global():
delete_global()
# Again, the variable should have been removed, but we don't check it.
reveal_type(d) # revealed: list[@Todo(list literal element type)]
reveal_type(d) # revealed: list[Unknown]
def delete_nonlocal():
e = 2

View File

@@ -1,230 +0,0 @@
# Identical type display names in diagnostics
ty prints the fully qualified name to disambiguate objects with the same name.
## Nested class
`test.py`:
```py
class A:
class B:
pass
class C:
class B:
pass
a: A.B = C.B() # error: [invalid-assignment] "Object of type `test.C.B` is not assignable to `test.A.B`"
```
## Nested class in function
`test.py`:
```py
class B:
pass
def f(b: B):
class B:
pass
# error: [invalid-assignment] "Object of type `test.<locals of function 'f'>.B` is not assignable to `test.B`"
b = B()
```
## Class from different modules
```py
import a
import b
df: a.DataFrame = b.DataFrame() # error: [invalid-assignment] "Object of type `b.DataFrame` is not assignable to `a.DataFrame`"
def _(dfs: list[b.DataFrame]):
# TODO should be"Object of type `list[b.DataFrame]` is not assignable to `list[a.DataFrame]`
# error: [invalid-assignment] "Object of type `list[DataFrame]` is not assignable to `list[DataFrame]`"
dataframes: list[a.DataFrame] = dfs
```
`a.py`:
```py
class DataFrame:
pass
```
`b.py`:
```py
class DataFrame:
pass
```
## Enum from different modules
```py
import status_a
import status_b
# error: [invalid-assignment] "Object of type `Literal[status_b.Status.ACTIVE]` is not assignable to `status_a.Status`"
s: status_a.Status = status_b.Status.ACTIVE
```
`status_a.py`:
```py
from enum import Enum
class Status(Enum):
ACTIVE = 1
INACTIVE = 2
```
`status_b.py`:
```py
from enum import Enum
class Status(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
```
## Nested enum
`test.py`:
```py
from enum import Enum
class A:
class B(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class C:
class B(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
# error: [invalid-assignment] "Object of type `Literal[test.C.B.ACTIVE]` is not assignable to `test.A.B`"
a: A.B = C.B.ACTIVE
```
## Class literals
```py
import cls_a
import cls_b
# error: [invalid-assignment] "Object of type `<class 'cls_b.Config'>` is not assignable to `type[cls_a.Config]`"
config_class: type[cls_a.Config] = cls_b.Config
```
`cls_a.py`:
```py
class Config:
pass
```
`cls_b.py`:
```py
class Config:
pass
```
## Generic aliases
```py
import generic_a
import generic_b
# TODO should be error: [invalid-assignment] "Object of type `<class 'generic_b.Container[int]'>` is not assignable to `type[generic_a.Container[int]]`"
container: type[generic_a.Container[int]] = generic_b.Container[int]
```
`generic_a.py`:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Container(Generic[T]):
pass
```
`generic_b.py`:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Container(Generic[T]):
pass
```
## Protocols
```py
from typing import Protocol
import proto_a
import proto_b
# TODO should be error: [invalid-assignment] "Object of type `proto_b.Drawable` is not assignable to `proto_a.Drawable`"
def _(drawable_b: proto_b.Drawable):
drawable: proto_a.Drawable = drawable_b
```
`proto_a.py`:
```py
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
```
`proto_b.py`:
```py
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> int: ...
```
## TypedDict
```py
from typing import TypedDict
import dict_a
import dict_b
def _(b_person: dict_b.Person):
# TODO should be error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`"
person_var: dict_a.Person = b_person
```
`dict_a.py`:
```py
from typing import TypedDict
class Person(TypedDict):
name: str
```
`dict_b.py`:
```py
from typing import TypedDict
class Person(TypedDict):
name: bytes
```

View File

@@ -33,6 +33,12 @@ def _():
def _():
assert_never(None) # error: [type-assertion-failure]
def _():
assert_never([]) # error: [type-assertion-failure]
def _():
assert_never({}) # error: [type-assertion-failure]
def _():
assert_never(()) # error: [type-assertion-failure]

View File

@@ -785,7 +785,7 @@ from subexporter import *
# TODO: Should be `list[str]`
# TODO: Should we avoid including `Unknown` for this case?
reveal_type(__all__) # revealed: Unknown | list[@Todo(list literal element type)]
reveal_type(__all__) # revealed: Unknown | list[Unknown]
__all__.append("B")

View File

@@ -103,7 +103,7 @@ class E( # error: [instance-layout-conflict]
): ...
```
## A single "disjoint base"
## A single "solid base"
```py
class A:
@@ -152,15 +152,14 @@ class Baz(Foo, Bar): ... # fine
<!-- snapshot-diagnostics -->
Certain classes implemented in C extensions also have an extended instance memory layout, in the
same way as classes that define non-empty `__slots__`. CPython internally calls all such classes
with a unique instance memory layout "solid bases", but [PEP 800](https://peps.python.org/pep-0800/)
calls these classes "disjoint bases", and this is the term we generally use. The `@disjoint_base`
decorator introduced by this PEP provides a generalised way for type checkers to identify such
classes.
same way as classes that define non-empty `__slots__`. (CPython internally calls all such classes
with a unique instance memory layout "solid bases", and we also borrow this term.) There is
currently no generalized way for ty to detect such a C-extension class, as there is currently no way
of expressing the fact that a class is a solid base in a stub file. However, ty special-cases
certain builtin classes in order to detect that attempting to combine them in a single MRO would
fail:
```py
from typing_extensions import disjoint_base
# fmt: off
class A( # error: [instance-layout-conflict]
@@ -184,17 +183,6 @@ class E( # error: [instance-layout-conflict]
class F(int, str, bytes, bytearray): ... # error: [instance-layout-conflict]
@disjoint_base
class G: ...
@disjoint_base
class H: ...
class I( # error: [instance-layout-conflict]
G,
H
): ...
# fmt: on
```
@@ -205,9 +193,9 @@ We avoid emitting an `instance-layout-conflict` diagnostic for this class defini
class Foo(range, str): ... # error: [subclass-of-final-class]
```
## Multiple "disjoint bases" where one is a subclass of the other
## Multiple "solid bases" where one is a subclass of the other
A class is permitted to multiple-inherit from multiple disjoint bases if one is a subclass of the
A class is permitted to multiple-inherit from multiple solid bases if one is a subclass of the
other:
```py

View File

@@ -3,12 +3,5 @@
## Empty dictionary
```py
reveal_type({}) # revealed: dict[@Todo(dict literal key type), @Todo(dict literal value type)]
```
## Dict comprehensions
```py
# revealed: dict[@Todo(dict comprehension key type), @Todo(dict comprehension value type)]
reveal_type({x: y for x, y in enumerate(range(42))})
reveal_type({}) # revealed: dict[Unknown, Unknown]
```

View File

@@ -1,6 +0,0 @@
# Generator expressions
```py
# revealed: GeneratorType[@Todo(generator expression yield type), @Todo(generator expression send type), @Todo(generator expression return type)]
reveal_type((x for x in range(42)))
```

View File

@@ -3,11 +3,5 @@
## Empty list
```py
reveal_type([]) # revealed: list[@Todo(list literal element type)]
```
## List comprehensions
```py
reveal_type([x for x in range(42)]) # revealed: list[@Todo(list comprehension element type)]
reveal_type([]) # revealed: list[Unknown]
```

View File

@@ -3,11 +3,5 @@
## Basic set
```py
reveal_type({1, 2}) # revealed: set[@Todo(set literal element type)]
```
## Set comprehensions
```py
reveal_type({x for x in range(42)}) # revealed: set[@Todo(set comprehension element type)]
reveal_type({1, 2}) # revealed: set[Unknown]
```

View File

@@ -207,7 +207,7 @@ dd[0] = 0
cm: ChainMap[int, int] = ChainMap({1: 1}, {0: 0})
cm[0] = 0
# TODO: should be ChainMap[int, int]
reveal_type(cm) # revealed: ChainMap[@Todo(dict literal key type), @Todo(dict literal value type)]
reveal_type(cm) # revealed: ChainMap[Unknown, Unknown]
reveal_type(l[0]) # revealed: Literal[0]
reveal_type(d[0]) # revealed: Literal[0]

View File

@@ -318,7 +318,7 @@ def f(l: list[str | None]):
l: list[str | None] = [None]
def _():
# TODO: should be `str | None`
reveal_type(l[0]) # revealed: @Todo(list literal element type)
reveal_type(l[0]) # revealed: Unknown
def _():
def _():

View File

@@ -75,32 +75,6 @@ def f(x: T):
reveal_type(b) # revealed: str
```
## Scoping
PEP 695 type aliases delay runtime evaluation of their right-hand side, so they are a lazy (not
eager) nested scope.
```py
type Alias = Foo | str
def f(x: Alias):
reveal_type(x) # revealed: Foo | str
class Foo:
pass
```
But narrowing of names used in the type alias is still respected:
```py
def _(flag: bool):
t = int if flag else None
if t is not None:
type Alias = t | str
def f(x: Alias):
reveal_type(x) # revealed: int | str
```
## Generic type aliases
```py
@@ -204,17 +178,13 @@ def f(x: OptNestedInt) -> None:
### Invalid self-referential
```py
# TODO emit a diagnostic on these two lines
# TODO emit a diagnostic here
type IntOr = int | IntOr
type OrInt = OrInt | int
def f(x: IntOr, y: OrInt):
def f(x: IntOr):
reveal_type(x) # revealed: int
reveal_type(y) # revealed: int
if not isinstance(x, int):
reveal_type(x) # revealed: Never
if not isinstance(y, int):
reveal_type(y) # revealed: Never
```
### Mutually recursive
@@ -238,42 +208,3 @@ from ty_extensions import Intersection
def h(x: Intersection[A, B]):
reveal_type(x) # revealed: tuple[B] | None
```
### Union inside generic
#### With old-style union
```py
from typing import Union
type A = list[Union["A", str]]
def f(x: A):
reveal_type(x) # revealed: list[A | str]
for item in x:
reveal_type(item) # revealed: list[A | str] | str
```
#### With new-style union
```py
type A = list["A" | str]
def f(x: A):
reveal_type(x) # revealed: list[A | str]
for item in x:
reveal_type(item) # revealed: list[A | str] | str
```
#### With Optional
```py
from typing import Optional, Union
type A = list[Optional[Union["A", str]]]
def f(x: A):
reveal_type(x) # revealed: list[A | str | None]
for item in x:
reveal_type(item) # revealed: list[A | str | None] | str | None
```

View File

@@ -95,20 +95,6 @@ class NotAProtocol: ...
reveal_type(is_protocol(NotAProtocol)) # revealed: Literal[False]
```
Note, however, that `is_protocol` returns `False` at runtime for specializations of generic
protocols. We still consider these to be "protocol classes" internally, regardless:
```py
class MyGenericProtocol[T](Protocol):
x: T
reveal_type(is_protocol(MyGenericProtocol)) # revealed: Literal[True]
# We still consider this a protocol class internally,
# but the inferred type of the call here reflects the result at runtime:
reveal_type(is_protocol(MyGenericProtocol[int])) # revealed: Literal[False]
```
A type checker should follow the typeshed stubs if a non-class is passed in, and typeshed's stubs
indicate that the argument passed in must be an instance of `type`.
@@ -411,38 +397,24 @@ To see the kinds and types of the protocol members, you can use the debugging ai
```py
from ty_extensions import reveal_protocol_interface
from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator
from typing import SupportsIndex, SupportsAbs, ClassVar
# error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`"
reveal_protocol_interface(Foo)
# error: [revealed-type] "Revealed protocol interface: `{"__index__": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(SupportsIndex)
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> Unknown`)}`"
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> _T_co@SupportsAbs`)}`"
reveal_protocol_interface(SupportsAbs)
# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)}`"
reveal_protocol_interface(Iterator)
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
reveal_protocol_interface(int)
# error: [invalid-argument-type] "Argument to function `reveal_protocol_interface` is incorrect: Expected `type`, found `Literal["foo"]`"
reveal_protocol_interface("foo")
```
Similar to the way that `typing.is_protocol` returns `False` at runtime for all generic aliases,
`typing.get_protocol_members` raises an exception at runtime if you pass it a generic alias, so we
do not implement any special handling for generic aliases passed to the function.
`ty_extensions.reveal_protocol_interface` can be used on both, however:
```py
# TODO: these fail at runtime, but we don't emit `[invalid-argument-type]` diagnostics
# currently due to https://github.com/astral-sh/ty/issues/116
reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str]
reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str]
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> int`)}`"
# TODO: this should be a `revealed-type` diagnostic rather than `invalid-argument-type`, and it should reveal `{"__abs__": MethodMember(`(self) -> int`)}` for the protocol interface
#
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
reveal_protocol_interface(SupportsAbs[int])
# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)}`"
reveal_protocol_interface(Iterator[int])
class BaseProto(Protocol):
def member(self) -> int: ...
@@ -1060,11 +1032,6 @@ class A(Protocol):
## Equivalence of protocols
```toml
[environment]
python-version = "3.12"
```
Two protocols are considered equivalent types if they specify the same interface, even if they have
different names:
@@ -1113,46 +1080,6 @@ static_assert(is_equivalent_to(UnionProto1, UnionProto2))
static_assert(is_equivalent_to(UnionProto1 | A | B, B | UnionProto2 | A))
```
Different generic protocols with equivalent specializations can be equivalent, but generic protocols
with different specializations are not considered equivalent:
```py
from typing import TypeVar
S = TypeVar("S")
class NonGenericProto1(Protocol):
x: int
y: str
class NonGenericProto2(Protocol):
y: str
x: int
class Nominal1: ...
class Nominal2: ...
class GenericProto[T](Protocol):
x: T
class LegacyGenericProto(Protocol[S]):
x: S
static_assert(is_equivalent_to(GenericProto[int], LegacyGenericProto[int]))
static_assert(is_equivalent_to(GenericProto[NonGenericProto1], LegacyGenericProto[NonGenericProto2]))
static_assert(
is_equivalent_to(
GenericProto[NonGenericProto1 | Nominal1 | Nominal2], LegacyGenericProto[Nominal2 | Nominal1 | NonGenericProto2]
)
)
static_assert(not is_equivalent_to(GenericProto[str], GenericProto[int]))
static_assert(not is_equivalent_to(GenericProto[str], LegacyGenericProto[int]))
static_assert(not is_equivalent_to(GenericProto, GenericProto[int]))
static_assert(not is_equivalent_to(LegacyGenericProto, LegacyGenericProto[int]))
```
## Intersections of protocols
An intersection of two protocol types `X` and `Y` is equivalent to a protocol type `Z` that inherits
@@ -1465,16 +1392,10 @@ static_assert(is_subtype_of(XClassVar, HasXProperty))
static_assert(is_assignable_to(XClassVar, HasXProperty))
class XFinal:
x: Final[int] = 42
x: Final = 42
static_assert(is_subtype_of(XFinal, HasXProperty))
static_assert(is_assignable_to(XFinal, HasXProperty))
class XImplicitFinal:
x: Final = 42
static_assert(is_subtype_of(XImplicitFinal, HasXProperty))
static_assert(is_assignable_to(XImplicitFinal, HasXProperty))
```
A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below
@@ -1530,8 +1451,9 @@ static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
class XSub:
x: MyInt
static_assert(not is_subtype_of(XSub, XReadWriteProperty))
static_assert(not is_assignable_to(XSub, XReadWriteProperty))
# TODO: should pass
static_assert(not is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error]
```
A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable
@@ -1627,7 +1549,7 @@ class Descriptor:
def __get__(self, instance, owner) -> MyInt:
return MyInt(0)
def __set__(self, instance, value: int) -> None: ...
def __set__(self, value: int) -> None: ...
class XCustomDescriptor:
x: Descriptor = Descriptor()
@@ -1673,16 +1595,6 @@ static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error]
class HasSetAttrWithUnsuitableInput:
def __getattr__(self, attr: str) -> int:
return 1
def __setattr__(self, attr: str, value: str) -> None: ...
# TODO: these should pass
static_assert(not is_subtype_of(HasSetAttrWithUnsuitableInput, HasMutableXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasSetAttrWithUnsuitableInput, HasMutableXProperty)) # error: [static-assert-error]
```
## Subtyping of protocols with method members
@@ -1772,12 +1684,11 @@ class Bar:
f(Bar()) # error: [invalid-argument-type]
```
## Equivalence of protocols with method or property members
## Equivalence of protocols with method members
Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the
signature of `P1.x` is equivalent to the signature of `P2.x`, even though ty would normally model
any two function definitions as inhabiting distinct function-literal types. The same is also true
for property members.
any two function definitions as inhabiting distinct function-literal types.
```py
from typing import Protocol
@@ -1789,26 +1700,7 @@ class P1(Protocol):
class P2(Protocol):
def x(self, y: int) -> None: ...
class P3(Protocol):
@property
def y(self) -> str: ...
@property
def z(self) -> bytes: ...
@z.setter
def z(self, value: int) -> None: ...
class P4(Protocol):
@property
def y(self) -> str: ...
@property
def z(self) -> bytes: ...
@z.setter
def z(self, value: int) -> None: ...
static_assert(is_equivalent_to(P1, P2))
# TODO: should pass
static_assert(is_equivalent_to(P3, P4)) # error: [static-assert-error]
```
As with protocols that only have non-method members, this also holds true when they appear in
@@ -1819,9 +1711,6 @@ class A: ...
class B: ...
static_assert(is_equivalent_to(A | B | P1, P2 | B | A))
# TODO: should pass
static_assert(is_equivalent_to(A | B | P3, P4 | B | A)) # error: [static-assert-error]
```
## Narrowing of protocols
@@ -2309,69 +2198,6 @@ def f(value: Iterator):
cast(Iterator, value) # error: [redundant-cast]
```
### Recursive generic protocols
This snippet caused us to stack overflow on an early version of
<https://github.com/astral-sh/ruff/pull/19866>:
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol, TypeVar
class A: ...
class Foo[T](Protocol):
def x(self) -> "T | Foo[T]": ...
y: A | Foo[A]
# The same thing, but using the legacy syntax:
S = TypeVar("S")
class Bar(Protocol[S]):
def x(self) -> "S | Bar[S]": ...
z: S | Bar[S]
```
### Recursive generic protocols with property members
An early version of <https://github.com/astral-sh/ruff/pull/19936> caused stack overflows on this
snippet:
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Protocol
class Foo[T]: ...
class A(Protocol):
@property
def _(self: "A") -> Foo: ...
class B(Protocol):
@property
def b(self) -> Foo[A]: ...
class C(Undefined): ... # error: "Name `Undefined` used when not defined"
class D:
b: Foo[C]
class E[T: B](Protocol): ...
x: E[D]
```
## Meta-protocols
Where `P` is a protocol type, a class object `N` can be said to inhabit the type `type[P]` if:

View File

@@ -25,16 +25,22 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.
11 | assert_never(None) # error: [type-assertion-failure]
12 |
13 | def _():
14 | assert_never(()) # error: [type-assertion-failure]
14 | assert_never([]) # error: [type-assertion-failure]
15 |
16 | def _(flag: bool, never: Never):
17 | assert_never(1 if flag else never) # error: [type-assertion-failure]
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
18 |
19 | def _(any_: Any):
20 | assert_never(any_) # error: [type-assertion-failure]
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
21 |
22 | def _(unknown: Unknown):
23 | assert_never(unknown) # error: [type-assertion-failure]
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
24 |
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
27 |
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
```
# Diagnostics
@@ -95,14 +101,14 @@ error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:14:5
|
13 | def _():
14 | assert_never(()) # error: [type-assertion-failure]
14 | assert_never([]) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `tuple[()]`
| Inferred type of argument is `list[Unknown]`
15 |
16 | def _(flag: bool, never: Never):
16 | def _():
|
info: `Never` and `tuple[()]` are not equivalent types
info: `Never` and `list[Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
```
@@ -111,15 +117,15 @@ info: rule `type-assertion-failure` is enabled by default
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:17:5
|
16 | def _(flag: bool, never: Never):
17 | assert_never(1 if flag else never) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--------------------^
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `Literal[1]`
| Inferred type of argument is `dict[Unknown, Unknown]`
18 |
19 | def _(any_: Any):
19 | def _():
|
info: `Never` and `Literal[1]` are not equivalent types
info: `Never` and `dict[Unknown, Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
```
@@ -128,15 +134,15 @@ info: rule `type-assertion-failure` is enabled by default
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:20:5
|
19 | def _(any_: Any):
20 | assert_never(any_) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `Any`
| Inferred type of argument is `tuple[()]`
21 |
22 | def _(unknown: Unknown):
22 | def _(flag: bool, never: Never):
|
info: `Never` and `Any` are not equivalent types
info: `Never` and `tuple[()]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
```
@@ -145,8 +151,42 @@ info: rule `type-assertion-failure` is enabled by default
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:23:5
|
22 | def _(unknown: Unknown):
23 | assert_never(unknown) # error: [type-assertion-failure]
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--------------------^
| |
| Inferred type of argument is `Literal[1]`
24 |
25 | def _(any_: Any):
|
info: `Never` and `Literal[1]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
```
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:26:5
|
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
| |
| Inferred type of argument is `Any`
27 |
28 | def _(unknown: Unknown):
|
info: `Never` and `Any` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
```
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:29:5
|
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^-------^
| |
| Inferred type of argument is `Unknown`

View File

@@ -26,9 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `does_not_exist`
2 | from does_not_exist import foo, bar, baz
| ^^^^^^^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default

View File

@@ -24,9 +24,6 @@ error[unresolved-import]: Cannot resolve imported module `zqzqzqzqzqzqzq`
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve imported module `zqzqzqzqzqzqzq`"
| ^^^^^^^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default

View File

@@ -36,9 +36,6 @@ error[unresolved-import]: Cannot resolve imported module `a.foo`
3 |
4 | # Topmost component unresolvable:
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
@@ -52,9 +49,6 @@ error[unresolved-import]: Cannot resolve imported module `b.foo`
5 | import b.foo # error: [unresolved-import] "Cannot resolve imported module `b.foo`"
| ^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default

View File

@@ -91,15 +91,15 @@ 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:1000:28
|
998 | stacklevel: int
999 | def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
1000 | def __call__(self, arg: _T, /) -> _T: ...
| ^^^^^^^
1001 |
1002 | @final
|
--> stdlib/typing_extensions.pyi:973:28
|
971 | stacklevel: int
972 | def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ...
973 | def __call__(self, arg: _T, /) -> _T: ...
| ^^^^^^^
974 |
975 | @final
|
info: rule `missing-argument` is enabled by default
```

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