-
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
Use ast::PythonVersion
internally in the formatter and linter
#16170
Conversation
Apparently I also messed up some cargo features here, like those that caused #16169. Sorry Micha! I guess these pass locally because I always run clippy and nextest on the whole workspace. |
crates/ruff/tests/snapshots/show_settings__display_default_settings.snap
Outdated
Show resolved
Hide resolved
#[serde(deserialize_with = "adapter::deserialize::<_, PythonVersion>")] | ||
#[serde(serialize_with = "adapter::serialize::<_, PythonVersion>")] |
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.
Is this type used anywhere in a user facing facade? If not, I then think it's fine if the representation changes.
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.
It comes out in ruff check --show-setttings
, but the version that's printed now (Py39
) can't be directly pasted back into a config file anyway, so I think it should be fine, like you said.
|
the python version is only used internally, so we can just switch to the dotted representation
this Default impl is only used in one test, so we could possibly get rid of it as well if we updated the test
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.
This is fantastic!
Thank you all for the reviews! I think I took care of all the comments. If you review again, you may want to skip the last commit. I made |
Oh, maybe it won't be the last commit anymore. Let me merge main. |
@@ -523,7 +523,7 @@ fn check_dynamically_typed<F>( | |||
if type_hint_resolves_to_any( | |||
parsed_annotation.expression(), | |||
checker, | |||
checker.settings.target_version.minor(), | |||
checker.settings.target_version.minor, |
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.
type_hint_resolves_to_any()
should probably take a version: ast::PythonVersion
rather than a minor_version: u8
argument, looking at this. It would make the method more strongly typed.
Patch
diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs
index 692ed478d..e637f0ea1 100644
--- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs
+++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs
@@ -523,7 +523,7 @@ fn check_dynamically_typed<F>(
if type_hint_resolves_to_any(
parsed_annotation.expression(),
checker,
- checker.settings.target_version.minor,
+ checker.settings.target_version,
) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
@@ -532,7 +532,7 @@ fn check_dynamically_typed<F>(
}
}
} else {
- if type_hint_resolves_to_any(annotation, checker, checker.settings.target_version.minor) {
+ if type_hint_resolves_to_any(annotation, checker, checker.settings.target_version) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs
index 6c10ae503..de90e3b6e 100644
--- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs
+++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs
@@ -177,7 +177,7 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
let Some(expr) = type_hint_explicitly_allows_none(
parsed_annotation.expression(),
checker,
- checker.settings.target_version.minor,
+ checker.settings.target_version,
) else {
continue;
};
@@ -195,7 +195,7 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
let Some(expr) = type_hint_explicitly_allows_none(
annotation,
checker,
- checker.settings.target_version.minor,
+ checker.settings.target_version,
) else {
continue;
};
diff --git a/crates/ruff_linter/src/rules/ruff/typing.rs b/crates/ruff_linter/src/rules/ruff/typing.rs
index 3faea2fa8..a12ab9b6d 100644
--- a/crates/ruff_linter/src/rules/ruff/typing.rs
+++ b/crates/ruff_linter/src/rules/ruff/typing.rs
@@ -10,10 +10,10 @@ use crate::checkers::ast::Checker;
///
/// A known type is either a builtin type, any object from the standard library,
/// or a type from the `typing_extensions` module.
-fn is_known_type(qualified_name: &QualifiedName, minor_version: u8) -> bool {
+fn is_known_type(qualified_name: &QualifiedName, version: ast::PythonVersion) -> bool {
match qualified_name.segments() {
["" | "typing_extensions", ..] => true,
- [module, ..] => is_known_standard_library(minor_version, module),
+ [module, ..] => is_known_standard_library(version.minor, module),
_ => false,
}
}
@@ -70,7 +70,11 @@ enum TypingTarget<'a> {
}
impl<'a> TypingTarget<'a> {
- fn try_from_expr(expr: &'a Expr, checker: &'a Checker, minor_version: u8) -> Option<Self> {
+ fn try_from_expr(
+ expr: &'a Expr,
+ checker: &'a Checker,
+ version: ast::PythonVersion,
+ ) -> Option<Self> {
let semantic = checker.semantic();
match expr {
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
@@ -91,7 +95,7 @@ impl<'a> TypingTarget<'a> {
resolve_slice_value(slice.as_ref()).next(),
))
} else {
- if is_known_type(&qualified_name, minor_version) {
+ if is_known_type(&qualified_name, version) {
Some(TypingTarget::Known)
} else {
Some(TypingTarget::Unknown)
@@ -137,7 +141,7 @@ impl<'a> TypingTarget<'a> {
)
{
Some(TypingTarget::Hashable)
- } else if !is_known_type(&qualified_name, minor_version) {
+ } else if !is_known_type(&qualified_name, version) {
// If it's not a known type, we assume it's `Any`.
Some(TypingTarget::Unknown)
} else {
@@ -149,7 +153,7 @@ impl<'a> TypingTarget<'a> {
}
/// Check if the [`TypingTarget`] explicitly allows `None`.
- fn contains_none(&self, checker: &Checker, minor_version: u8) -> bool {
+ fn contains_none(&self, checker: &Checker, version: ast::PythonVersion) -> bool {
match self {
TypingTarget::None
| TypingTarget::Optional(_)
@@ -162,10 +166,10 @@ impl<'a> TypingTarget<'a> {
resolve_slice_value(slice).any(|element| {
// Literal can only contain `None`, a literal value, other `Literal`
// or an enum value.
- match TypingTarget::try_from_expr(element, checker, minor_version) {
+ match TypingTarget::try_from_expr(element, checker, version) {
None | Some(TypingTarget::None) => true,
Some(new_target @ TypingTarget::Literal(_)) => {
- new_target.contains_none(checker, minor_version)
+ new_target.contains_none(checker, version)
}
_ => false,
}
@@ -173,35 +177,32 @@ impl<'a> TypingTarget<'a> {
}),
TypingTarget::Union(slice) => slice.is_some_and(|slice| {
resolve_slice_value(slice).any(|element| {
- TypingTarget::try_from_expr(element, checker, minor_version)
+ TypingTarget::try_from_expr(element, checker, version)
.map_or(true, |new_target| {
- new_target.contains_none(checker, minor_version)
+ new_target.contains_none(checker, version)
})
})
}),
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
- TypingTarget::try_from_expr(element, checker, minor_version)
- .map_or(true, |new_target| {
- new_target.contains_none(checker, minor_version)
- })
+ TypingTarget::try_from_expr(element, checker, version).map_or(true, |new_target| {
+ new_target.contains_none(checker, version)
+ })
}),
TypingTarget::Annotated(expr) => expr.is_some_and(|expr| {
- TypingTarget::try_from_expr(expr, checker, minor_version)
- .map_or(true, |new_target| {
- new_target.contains_none(checker, minor_version)
- })
+ TypingTarget::try_from_expr(expr, checker, version).map_or(true, |new_target| {
+ new_target.contains_none(checker, version)
+ })
}),
TypingTarget::ForwardReference(expr) => {
- TypingTarget::try_from_expr(expr, checker, minor_version)
- .map_or(true, |new_target| {
- new_target.contains_none(checker, minor_version)
- })
+ TypingTarget::try_from_expr(expr, checker, version).map_or(true, |new_target| {
+ new_target.contains_none(checker, version)
+ })
}
}
}
/// Check if the [`TypingTarget`] explicitly allows `Any`.
- fn contains_any(&self, checker: &Checker, minor_version: u8) -> bool {
+ fn contains_any(&self, checker: &Checker, version: ast::PythonVersion) -> bool {
match self {
TypingTarget::Any => true,
// `Literal` cannot contain `Any` as it's a dynamic value.
@@ -213,31 +214,23 @@ impl<'a> TypingTarget<'a> {
| TypingTarget::Unknown => false,
TypingTarget::Union(slice) => slice.is_some_and(|slice| {
resolve_slice_value(slice).any(|element| {
- TypingTarget::try_from_expr(element, checker, minor_version)
- .map_or(true, |new_target| {
- new_target.contains_any(checker, minor_version)
- })
+ TypingTarget::try_from_expr(element, checker, version)
+ .map_or(true, |new_target| new_target.contains_any(checker, version))
})
}),
TypingTarget::PEP604Union(left, right) => [left, right].iter().any(|element| {
- TypingTarget::try_from_expr(element, checker, minor_version)
- .map_or(true, |new_target| {
- new_target.contains_any(checker, minor_version)
- })
+ TypingTarget::try_from_expr(element, checker, version)
+ .map_or(true, |new_target| new_target.contains_any(checker, version))
}),
TypingTarget::Annotated(expr) | TypingTarget::Optional(expr) => {
expr.is_some_and(|expr| {
- TypingTarget::try_from_expr(expr, checker, minor_version)
- .map_or(true, |new_target| {
- new_target.contains_any(checker, minor_version)
- })
+ TypingTarget::try_from_expr(expr, checker, version)
+ .map_or(true, |new_target| new_target.contains_any(checker, version))
})
}
TypingTarget::ForwardReference(expr) => {
- TypingTarget::try_from_expr(expr, checker, minor_version)
- .map_or(true, |new_target| {
- new_target.contains_any(checker, minor_version)
- })
+ TypingTarget::try_from_expr(expr, checker, version)
+ .map_or(true, |new_target| new_target.contains_any(checker, version))
}
}
}
@@ -253,9 +246,9 @@ impl<'a> TypingTarget<'a> {
pub(crate) fn type_hint_explicitly_allows_none<'a>(
annotation: &'a Expr,
checker: &'a Checker,
- minor_version: u8,
+ version: ast::PythonVersion,
) -> Option<&'a Expr> {
- match TypingTarget::try_from_expr(annotation, checker, minor_version) {
+ match TypingTarget::try_from_expr(annotation, checker, version) {
None |
// Short circuit on top level `None`, `Any` or `Optional`
Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None,
@@ -264,10 +257,10 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
// is found nested inside another type, then the outer type should
// be returned.
Some(TypingTarget::Annotated(expr)) => {
- expr.and_then(|expr| type_hint_explicitly_allows_none(expr, checker, minor_version))
+ expr.and_then(|expr| type_hint_explicitly_allows_none(expr, checker, version))
}
Some(target) => {
- if target.contains_none(checker, minor_version) {
+ if target.contains_none(checker, version) {
return None;
}
Some(annotation)
@@ -281,9 +274,9 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>(
pub(crate) fn type_hint_resolves_to_any(
annotation: &Expr,
checker: &Checker,
- minor_version: u8,
+ version: ast::PythonVersion,
) -> bool {
- match TypingTarget::try_from_expr(annotation, checker, minor_version) {
+ match TypingTarget::try_from_expr(annotation, checker, version) {
// Short circuit on top level `Any`
None | Some(TypingTarget::Any) => true,
// `Optional` is `Optional[Any]` which is `Any | None`.
@@ -291,39 +284,43 @@ pub(crate) fn type_hint_resolves_to_any(
// Top-level `Annotated` node should check if the inner type resolves
// to `Any`.
Some(TypingTarget::Annotated(expr)) => {
- expr.is_some_and(|expr| type_hint_resolves_to_any(expr, checker, minor_version))
+ expr.is_some_and(|expr| type_hint_resolves_to_any(expr, checker, version))
}
- Some(target) => target.contains_any(checker, minor_version),
+ Some(target) => target.contains_any(checker, version),
}
}
#[cfg(test)]
mod tests {
use super::is_known_type;
+ use ruff_python_ast as ast;
use ruff_python_ast::name::QualifiedName;
#[test]
fn test_is_known_type() {
- assert!(is_known_type(&QualifiedName::builtin("int"), 11));
+ assert!(is_known_type(
+ &QualifiedName::builtin("int"),
+ ast::PythonVersion::PY311
+ ));
assert!(is_known_type(
&QualifiedName::from_iter(["builtins", "int"]),
- 11
+ ast::PythonVersion::PY311
));
assert!(is_known_type(
&QualifiedName::from_iter(["typing", "Optional"]),
- 11
+ ast::PythonVersion::PY311
));
assert!(is_known_type(
&QualifiedName::from_iter(["typing_extensions", "Literal"]),
- 11
+ ast::PythonVersion::PY311
));
assert!(is_known_type(
&QualifiedName::from_iter(["zoneinfo", "ZoneInfo"]),
- 11
+ ast::PythonVersion::PY311
));
assert!(!is_known_type(
&QualifiedName::from_iter(["zoneinfo", "ZoneInfo"]),
- 8
+ ast::PythonVersion::PY38
));
}
}
Should probably be a followup, though; this is already a big PR and the change isn't directly related
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.
Ooh good idea. I'll save this as a fast follow-up.
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> This is a follow-up to #16170 to use `ast::PythonVersion` in the `type_hint_resolves_to_any` call chain, as suggested (and implemented!) by Alex [here](#16170 (comment)).
This is a follow-up to #16170 to use `ast::PythonVersion` in the `type_hint_resolves_to_any` call chain, as suggested (and implemented!) by Alex [here](#16170 (comment)). Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This is a follow-up to #16170 to use `ast::PythonVersion` in the `type_hint_resolves_to_any` call chain, as suggested (and implemented!) by Alex [here](#16170 (comment)). Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
* main: [red-knot] Allow any `Ranged` argument for `report_lint` and `report_diagnostic` (#16252) [pycodestyle] Exempt `site.addsitedir(...)` calls (E402) (#16251) red_knot_python_semantic: improve diagnostic message for "invalid argument type" ruff_db: add "secondary" messages to `Diagnostic` trait ruff_db: refactor snippet rendering red_knot_python_semantic: remove `Ranged` impl for `TypeCheckDiagnostic` [red-knot] Refactor `infer_chained_boolean_types` to have access to `TypeInferenceBuilder` (#16222) Add `red_knot/README.md` (#16230) [airflow] move class attributed related cases to AIR302_class_attribute (AIR302) (#16226) [red-knot] Update tests for attributes inferred from parameters (#16208) [red-knot] update TODO comment in mdtest (#16242) [`refurb`] Correctly handle lengths of literal strings in `slice-to-remove-prefix-or-suffix` (`FURB188`) (#16237) Pass `ast::PythonVersion` to `type_hint_resolves_to_any` (#16236) Use `ast::PythonVersion` internally in the formatter and linter (#16170) Add `SECURITY.md` (#16224)
Summary
This PR updates the formatter and linter to use the
PythonVersion
struct from theruff_python_ast
crate internally. While this doesn't remove the need for thelinter::PythonVersion
enum, it does remove theformatter::PythonVersion
enum and limits the use in the linter to deserializing from CLI arguments and config files and moves most of the remaining methods to theast::PythonVersion
struct.Test Plan
Existing tests, with some inputs and outputs updated to reflect the new (de)serialization format. I think these are test-specific and shouldn't affect any external (de)serialization.