-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[flake8-pyi
] Implement PYI063
#11699
Changes from 1 commit
079cb96
f3d403b
25bf53e
bb00f86
8456ee9
20662ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# See https://peps.python.org/pep-0484/#positional-only-arguments | ||
# for the full details on which arguments using the older syntax should/shouldn't | ||
# be considered positional-only arguments by type checkers. | ||
from typing import Self | ||
|
||
def bad(__x: int) -> None: ... # PYI063 | ||
def also_bad(__x: int, __y: str) -> None: ... # PYI063 | ||
def still_bad(__x_: int) -> None: ... # PYI063 | ||
|
||
def no_args() -> None: ... | ||
def okay(__x__: int) -> None: ... | ||
# The first argument isn't positional-only, so logically the second can't be either: | ||
def also_okay(x: int, __y: str) -> None: ... | ||
def fine(x: bytes, /) -> None: ... | ||
def no_idea_why_youd_do_this(__x: int, /, __y: str) -> None: ... | ||
def cool(_x__: int) -> None: ... | ||
def also_cool(x__: int) -> None: ... | ||
def unclear_from_pep_484_if_this_is_positional_or_not(__: str) -> None: ... | ||
def _(_: int) -> None: ... | ||
|
||
class Foo: | ||
def bad(__self) -> None: ... # PYI063 | ||
@staticmethod | ||
def bad2(__self) -> None: ... # PYI063 | ||
def bad3(__self, __x: int) -> None: ... # PYI063 | ||
def still_bad(self, __x_: int) -> None: ... # PYI063 | ||
@staticmethod | ||
def this_is_bad_too(__x: int) -> None: ... # PYI063 | ||
@classmethod | ||
def not_good(cls, __foo: int) -> None: ... # PYI063 | ||
|
||
# The first non-self argument isn't positional-only, so logically the second can't be either: | ||
def okay1(self, x: int, __y: int) -> None: ... | ||
# Same here: | ||
@staticmethod | ||
def okay2(x: int, __y_: int) -> None: ... | ||
@staticmethod | ||
def no_args() -> int: ... | ||
def okay3(__self__, __x__: int, __y: str) -> None: ... | ||
def okay4(self, /) -> None: ... | ||
def okay5(self, x: int, /) -> None: ... | ||
def okay6(__self, /) -> None: ... | ||
def cool(_self__: int) -> None: ... | ||
def also_cool(self__: int) -> None: ... | ||
def unclear_from_pep_484_if_this_is_positional_or_not(__: str) -> None: ... | ||
def _(_: int) -> None: ... | ||
@classmethod | ||
def fine(cls, foo: int, /) -> None: ... | ||
|
||
class Metaclass(type): | ||
@classmethod | ||
def __new__(mcls, __name: str, __bases: tuple[type, ...], __namespace: dict, **kwds) -> Self: ... # PYI063 | ||
|
||
class Metaclass2(type): | ||
@classmethod | ||
def __new__(metacls, __name: str, __bases: tuple[type, ...], __namespace: dict, **kwds) -> Self: ... # PYI063 | ||
|
||
class GoodMetaclass(type): | ||
@classmethod | ||
def __new__(mcls, name: str, bases: tuple[type, ...], namespace: dict, /, **kwds) -> Self: ... | ||
|
||
class GoodMetaclass2(type): | ||
@classmethod | ||
def __new__(metacls, name: str, bases: tuple[type, ...], namespace: dict, /, **kwds) -> Self: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# See https://peps.python.org/pep-0484/#positional-only-arguments | ||
# for the full details on which arguments using the older syntax should/shouldn't | ||
# be considered positional-only arguments by type checkers. | ||
from typing import Self | ||
|
||
def bad(__x: int) -> None: ... # PYI063 | ||
def also_bad(__x: int, __y: str) -> None: ... # PYI063 | ||
def still_bad(__x_: int) -> None: ... # PYI063 | ||
|
||
def no_args() -> None: ... | ||
def okay(__x__: int) -> None: ... | ||
# The first argument isn't positional-only, so logically the second can't be either: | ||
def also_okay(x: int, __y: str) -> None: ... | ||
def fine(x: bytes, /) -> None: ... | ||
def no_idea_why_youd_do_this(__x: int, /, __y: str) -> None: ... | ||
def cool(_x__: int) -> None: ... | ||
def also_cool(x__: int) -> None: ... | ||
def unclear_from_pep_484_if_this_is_positional_or_not(__: str) -> None: ... | ||
def _(_: int) -> None: ... | ||
|
||
class Foo: | ||
def bad(__self) -> None: ... # PYI063 | ||
@staticmethod | ||
def bad2(__self) -> None: ... # PYI063 | ||
def bad3(__self, __x: int) -> None: ... # PYI063 | ||
def still_bad(self, __x_: int) -> None: ... # PYI063 # TODO: doesn't get raised here | ||
@staticmethod | ||
def this_is_bad_too(__x: int) -> None: ... # PYI063 | ||
@classmethod | ||
def not_good(cls, __foo: int) -> None: ... # PYI063 | ||
|
||
# The first non-self argument isn't positional-only, so logically the second can't be either: | ||
def okay1(self, x: int, __y: int) -> None: ... | ||
# Same here: | ||
@staticmethod | ||
def okay2(x: int, __y_: int) -> None: ... | ||
@staticmethod | ||
def no_args() -> int: ... | ||
def okay3(__self__, __x__: int, __y: str) -> None: ... | ||
def okay4(self, /) -> None: ... | ||
def okay5(self, x: int, /) -> None: ... | ||
def okay6(__self, /) -> None: ... | ||
def cool(_self__: int) -> None: ... | ||
def also_cool(self__: int) -> None: ... | ||
def unclear_from_pep_484_if_this_is_positional_or_not(__: str) -> None: ... | ||
def _(_: int) -> None: ... | ||
@classmethod | ||
def fine(cls, foo: int, /) -> None: ... | ||
|
||
class Metaclass(type): | ||
@classmethod | ||
def __new__(mcls, __name: str, __bases: tuple[type, ...], __namespace: dict, **kwds) -> Self: ... # PYI063 | ||
|
||
class Metaclass2(type): | ||
@classmethod | ||
def __new__(metacls, __name: str, __bases: tuple[type, ...], __namespace: dict, **kwds) -> Self: ... # PYI063 | ||
|
||
class GoodMetaclass(type): | ||
@classmethod | ||
def __new__(mcls, name: str, bases: tuple[type, ...], namespace: dict, /, **kwds) -> Self: ... | ||
|
||
class GoodMetaclass2(type): | ||
@classmethod | ||
def __new__(metacls, name: str, bases: tuple[type, ...], namespace: dict, /, **kwds) -> Self: ... |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,7 +32,8 @@ pub(crate) fn definitions(checker: &mut Checker) { | |
Rule::MissingTypeKwargs, | ||
Rule::MissingTypeSelf, | ||
]); | ||
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub); | ||
let enforce_stubs = checker.source_type.is_stub() | ||
&& checker.any_enabled(&[Rule::DocstringInStub, Rule::OldStylePositionalOnlyArg]); | ||
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable); | ||
let enforce_dunder_method = checker.enabled(Rule::BadDunderMethodName); | ||
let enforce_docstrings = checker.any_enabled(&[ | ||
|
@@ -149,23 +150,22 @@ pub(crate) fn definitions(checker: &mut Checker) { | |
if checker.enabled(Rule::DocstringInStub) { | ||
flake8_pyi::rules::docstring_in_stubs(checker, docstring); | ||
} | ||
if checker.enabled(Rule::OldStylePositionalOnlyArg) { | ||
flake8_pyi::rules::old_style_positional_only_arg(checker, definition); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should make sure only to run this rule if the target version is set to Python 3.8 or greater (Ruff still supports Python 3.7), or we'll be recommending invalid syntax for Python 3.7 users |
||
} | ||
if enforce_stubs_and_runtime { | ||
if checker.enabled(Rule::IterMethodReturnIterable) { | ||
flake8_pyi::rules::iter_method_return_iterable(checker, definition); | ||
} | ||
flake8_pyi::rules::iter_method_return_iterable(checker, definition); | ||
} | ||
|
||
// pylint | ||
if enforce_dunder_method { | ||
if checker.enabled(Rule::BadDunderMethodName) { | ||
if let Definition::Member(Member { | ||
kind: MemberKind::Method(method), | ||
.. | ||
}) = definition | ||
{ | ||
pylint::rules::bad_dunder_method_name(checker, method); | ||
} | ||
if let Definition::Member(Member { | ||
kind: MemberKind::Method(method), | ||
.. | ||
}) = definition | ||
{ | ||
pylint::rules::bad_dunder_method_name(checker, method); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_ast::ParameterWithDefault; | ||
use ruff_python_semantic::{analyze::function_type, Definition}; | ||
use ruff_text_size::Ranged; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## What it does | ||
/// Checks for the presence of old-style positional-only arguments in stub files. | ||
/// | ||
/// ## Why is this bad? | ||
/// [PEP 570][1] defines new syntax for positional-only arguments, that should | ||
/// be preferred over the syntax defined in [PEP 484][2]. | ||
/// | ||
/// ## Example | ||
/// ```python | ||
/// def foo(__x: int) -> None: | ||
/// ``` | ||
/// | ||
/// Use instead: | ||
/// ```python | ||
/// def foo(x: int, /) -> None: ... | ||
/// ``` | ||
/// | ||
/// [1]: https://peps.python.org/pep-0570 | ||
/// [2]: https://peps.python.org/pep-0484/#positional-only-arguments | ||
#[violation] | ||
pub struct OldStylePositionalOnlyArg; | ||
|
||
impl Violation for OldStylePositionalOnlyArg { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
format!("Prefer PEP 570 syntax for positional-only arguments in stubs") | ||
} | ||
} | ||
|
||
/// PYI063 | ||
pub(crate) fn old_style_positional_only_arg(checker: &mut Checker, definition: &Definition) { | ||
let Some(function) = definition.as_function_def() else { | ||
return; | ||
}; | ||
if !function.parameters.posonlyargs.is_empty() { | ||
return; | ||
} | ||
|
||
let mut args = function.parameters.args.iter(); | ||
let Some(first_arg) = args.next() else { | ||
return; | ||
}; | ||
|
||
let semantic = checker.semantic(); | ||
// TODO: this scope is wrong. | ||
let scope = semantic.current_scope(); | ||
let function_type = function_type::classify( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This call currently doesn't correctly identify methods, which is why the issue is not correctly raised on line 26 of I didn't look super hard into it, but I don't see a very straightforward way to get the correct scope from a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fixed this by moving it out of the definition phase and into the standard statement checker. |
||
&function.name, | ||
&function.decorator_list, | ||
scope, | ||
semantic, | ||
&checker.settings.pep8_naming.classmethod_decorators, | ||
&checker.settings.pep8_naming.staticmethod_decorators, | ||
); | ||
if is_old_style_positional_only_arg(first_arg) { | ||
checker.diagnostics.push(Diagnostic::new( | ||
OldStylePositionalOnlyArg, | ||
first_arg.range(), | ||
)); | ||
} | ||
if matches!(function_type, function_type::FunctionType::Method) { | ||
if let Some(second_arg) = args.next() { | ||
if is_old_style_positional_only_arg(second_arg) { | ||
checker.diagnostics.push(Diagnostic::new( | ||
OldStylePositionalOnlyArg, | ||
second_arg.range(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the check limited to the first two arguments? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see, ok. |
||
)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn is_old_style_positional_only_arg(arg: &ParameterWithDefault) -> bool { | ||
let arg_name = &arg.parameter.name; | ||
arg_name.starts_with("__") && !arg_name.ends_with("__") | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason not to do this for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For regular python files, code like: def add(__a, __b):
return __a + __b It's not necessary that the user intended to make them positional only. Whereas in stub files it is intended. Raising it on python files can cause false positives. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although you certainly can do calls such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ended up enabling this based on Alex's suggestion. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs | ||
--- | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs | ||
--- | ||
PYI063.pyi:6:9: PYI063 Prefer PEP 570 syntax for positional-only arguments in stubs | ||
| | ||
4 | from typing import Self | ||
5 | | ||
6 | def bad(__x: int) -> None: ... # PYI063 | ||
| ^^^^^^^^ PYI063 | ||
7 | def also_bad(__x: int, __y: str) -> None: ... # PYI063 | ||
8 | def still_bad(__x_: int) -> None: ... # PYI063 | ||
| | ||
|
||
PYI063.pyi:7:14: PYI063 Prefer PEP 570 syntax for positional-only arguments in stubs | ||
| | ||
6 | def bad(__x: int) -> None: ... # PYI063 | ||
7 | def also_bad(__x: int, __y: str) -> None: ... # PYI063 | ||
| ^^^^^^^^ PYI063 | ||
8 | def still_bad(__x_: int) -> None: ... # PYI063 | ||
| | ||
|
||
PYI063.pyi:8:15: PYI063 Prefer PEP 570 syntax for positional-only arguments in stubs | ||
| | ||
6 | def bad(__x: int) -> None: ... # PYI063 | ||
7 | def also_bad(__x: int, __y: str) -> None: ... # PYI063 | ||
8 | def still_bad(__x_: int) -> None: ... # PYI063 | ||
| ^^^^^^^^^ PYI063 | ||
9 | | ||
10 | def no_args() -> None: ... | ||
| | ||
|
||
PYI063.pyi:22:13: PYI063 Prefer PEP 570 syntax for positional-only arguments in stubs | ||
| | ||
21 | class Foo: | ||
22 | def bad(__self) -> None: ... # PYI063 | ||
| ^^^^^^ PYI063 | ||
23 | @staticmethod | ||
24 | def bad2(__self) -> None: ... # PYI063 | ||
| | ||
|
||
PYI063.pyi:24:14: PYI063 Prefer PEP 570 syntax for positional-only arguments in stubs | ||
| | ||
22 | def bad(__self) -> None: ... # PYI063 | ||
23 | @staticmethod | ||
24 | def bad2(__self) -> None: ... # PYI063 | ||
| ^^^^^^ PYI063 | ||
25 | def bad3(__self, __x: int) -> None: ... # PYI063 | ||
26 | def still_bad(self, __x_: int) -> None: ... # PYI063 # TODO: doesn't get raised here | ||
| | ||
|
||
PYI063.pyi:25:14: PYI063 Prefer PEP 570 syntax for positional-only arguments in stubs | ||
| | ||
23 | @staticmethod | ||
24 | def bad2(__self) -> None: ... # PYI063 | ||
25 | def bad3(__self, __x: int) -> None: ... # PYI063 | ||
| ^^^^^^ PYI063 | ||
26 | def still_bad(self, __x_: int) -> None: ... # PYI063 # TODO: doesn't get raised here | ||
27 | @staticmethod | ||
| | ||
|
||
PYI063.pyi:28:25: PYI063 Prefer PEP 570 syntax for positional-only arguments in stubs | ||
| | ||
26 | def still_bad(self, __x_: int) -> None: ... # PYI063 # TODO: doesn't get raised here | ||
27 | @staticmethod | ||
28 | def this_is_bad_too(__x: int) -> None: ... # PYI063 | ||
| ^^^^^^^^ PYI063 | ||
29 | @classmethod | ||
30 | def not_good(cls, __foo: int) -> None: ... # PYI063 | ||
| |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can I suggest the test case
This should be fine right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems good, I'll add it.