ruff icon indicating copy to clipboard operation
ruff copied to clipboard

False-positive `DTZ007`: The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)`

Open actionless opened this issue 2 years ago • 4 comments
trafficstars

import datetime

DT_FORMAT = "%a, %d %b %Y %H:%M:%S %z"
file_data = "Fri, 23 Sep 2022 08:42:36 +0000"


parsed_date = datetime.datetime.strptime(
    file_data, DT_FORMAT
)
$ ruff test_ruff_DTZ007.py
Found 1 error(s).
test_ruff_DTZ007.py:7:15: DTZ007 The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)`

actionless avatar Dec 20 '22 22:12 actionless

Probably a stretch to fix this right now since it relies on static analysis that's beyond Ruff's current capabilities (understanding that DT_FORMAT is assigned to a string containing %z).

charliermarsh avatar Dec 20 '22 22:12 charliermarsh

yeah i was actually wondering how you going to address that [advanced static analysis] in the feature, by reusing parts of RustPython to actually run some parts?

actionless avatar Dec 20 '22 22:12 actionless

Just to dream a little bit here, I think there could be a future where we integrate with mypy or another type checker to enable type-aware lints.

In that world, one could write

from typing import Final

DT_FORMAT: Final = "%a, %d %b %Y %H:%M:%S %z"
file_data = "Fri, 23 Sep 2022 08:42:36 +0000"

parsed_date = datetime.datetime.strptime(
    file_data, DT_FORMAT
)

The Final annotation (PEP 591) promises that the value won’t be reassigned, and allows the type checker to infer a type Literal["%a, %d %b %Y %H:%M:%S %z"] (PEP 586) for DT_FORMAT, which could be used in the Ruff rule.

This kind of integration would enable us to improve the accuracy of many rules and enable a variety of new ones.

andersk avatar Dec 26 '22 02:12 andersk

As a starting point... we could support Final specifically. Maybe that's slightly unprincipled though.

charliermarsh avatar Dec 26 '22 02:12 charliermarsh

A similar case I ran across today was that grabbing only date-related items from a string, e.g.:

from datetime import datetime
datetime.strptime("2022-01-01", "%Y-%m-%d").date()

...also results in a DTZ007 false-positive:

$ ruff check --isolated --select DTZ strptime_test.py 
strptime_test.py:2:1: DTZ007 The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)` or `.astimezone()`
Found 1 error.

In a case like this where not dealing with time, the resulting date value is the same and is not dependent on a timezone. To detect this case, though, it seems that we'd have to recognize both that the format string contains no time-related items and that there's a following conversion with .date().

gdub avatar Feb 16 '23 05:02 gdub

@charliermarsh Does RustPython need to expose features out of core? symboltable.rs looks like related to this one. Not sure it is enough though.

youknowone avatar Apr 20 '23 15:04 youknowone

@youknowone - We do have some of our abstractions in Ruff for implementing a semantic model -- we track scopes, bindings, etc. I want to spend some time improving the semantic model, so I'll take a look at what you have in RustPython as part of that exercise -- maybe there are things we can leverage!

charliermarsh avatar Apr 21 '23 02:04 charliermarsh

Don't know if this is the same issue, but there is also a false-positive if the format string is an f-string.

For example:

from datetime import datetime
def parse_iso(iso_str,millis=True):
    return datetime.strptime(iso_str, f"%Y-%m-%dT%H:%M:%S{('.%f' if millis else '')}%z")

Even without a condition inside the f-string, it immediately triggers the lint error.

For example:

from datetime import datetime
dt = datetime.strptime("","%Y-%m-%d %H:%M:%S%z") # everything okay

dt = datetime.strptime("", f"%Y-%m-%d %H:%M:%S%z") # throws DTZ007

jonas-w avatar Mar 25 '24 21:03 jonas-w

@jonas-w I think that's unrelated. I created a new issue to track the f-string format specifier handling.

MichaReiser avatar Mar 26 '24 07:03 MichaReiser