diff --git a/crates/ruff_linter/resources/mdtest/flake8-datetimez/call-datetime-strptime-without-zone.md b/crates/ruff_linter/resources/mdtest/flake8-datetimez/call-datetime-strptime-without-zone.md new file mode 100644 index 0000000000000..ac951544dec36 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/flake8-datetimez/call-datetime-strptime-without-zone.md @@ -0,0 +1,39 @@ +# `call-datetime-strptime-without-zone` (`DTZ007`) + +```toml +lint.select = ["DTZ007"] +``` + +## Replacing fields before converting the timezone + +Calling `astimezone` produces an aware datetime even when one or more `replace` calls precede it. + +```py +from datetime import datetime, timezone + +datetime.strptime("2026", "%Y").replace(microsecond=0).astimezone() +datetime.strptime("2026", "%Y").replace(hour=12).replace(minute=30).astimezone(timezone.utc) +datetime.strptime("2026", "%Y").replace(tzinfo=None).astimezone() +``` + +## Replacements that leave a naive datetime + +A replacement without a timezone conversion still produces a naive datetime. + +```py +from datetime import datetime + +datetime.strptime("2026", "%Y").replace(microsecond=0) # error: [call-datetime-strptime-without-zone] +datetime.strptime("2026", "%Y").replace(tzinfo=None) # error: [call-datetime-strptime-without-zone] +``` + +## Passing a method to another call + +Passing the `replace` method to another object's method does not convert the parsed datetime. + +```py +from datetime import datetime + +def convert(converter): + converter.replace(datetime.strptime("2026", "%Y").replace).astimezone() # error: [call-datetime-strptime-without-zone] +``` diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index 384e3ea58d840..30227b06470b8 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -6,7 +6,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; -use crate::rules::flake8_datetimez::helpers::DatetimeModuleAntipattern; +use crate::rules::flake8_datetimez::helpers::{self, DatetimeModuleAntipattern}; /// ## What it does /// Checks for uses of `datetime.datetime.strptime()` that lead to naive @@ -104,6 +104,10 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &Checker, call: &ast: return; } + if helpers::followed_by_astimezone(checker) { + return; + } + // Does the `strptime` call contain a format string with a timezone specifier? if let Some(expr) = call.arguments.args.get(1) { match expr { @@ -155,10 +159,6 @@ fn find_antipattern( let Some(Expr::Attribute(ast::ExprAttribute { attr, .. })) = parent else { return Some(DatetimeModuleAntipattern::NoTzArgumentPassed); }; - // Ex) `datetime.strptime(...).astimezone()` - if attr == "astimezone" { - return None; - } if attr != "replace" { return Some(DatetimeModuleAntipattern::NoTzArgumentPassed); }