Repository: pjknkda/flake8-datetimez

Issue:
datetime.datetime.strptime() triggers DTZ007 when using '%z'
AFAICT having `%z` in the format string always produces timezone aware datetime objects. Putting a `.replace(tz=)` after one would likely introduce a bug by switching to the wrong timezone.

Ideally DTZ007 should not be triggered when `%z` is present in the format string, and possibly add a new error for using `.replace(tz=)` after one that does.

Note that `%Z` does *not* produce timezone aware datetimes.

Comments:

Patch:
diff --git a/README.md b/README.md
index 9a5a3f6..4697b9f 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ A plugin for flake8 to ban the usage of unsafe naive datetime class.
 
 - **DTZ006** : The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed.
 
-- **DTZ007** : The use of `datetime.datetime.strptime()` must be followed by `.replace(tzinfo=)`.
+- **DTZ007** : The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)`.
 
 - **DTZ011** : The use of `datetime.date.today()` is not allowed. Use `datetime.datetime.now(tz=).date()` instead.
 
diff --git a/flake8_datetimez.py b/flake8_datetimez.py
index 2246bb9..33496ca 100644
--- a/flake8_datetimez.py
+++ b/flake8_datetimez.py
@@ -6,6 +6,11 @@ from functools import partial
 
 import pycodestyle
 
+try:
+    STRING_NODE = ast.Str
+except AttributeError:  # ast.Str is deprecated in Python3.8
+    STRING_NODE = ast.Constant
+
 
 def _get_from_keywords(keywords, arg):
     for keyword in keywords:
@@ -132,8 +137,11 @@ class DateTimeZVisitor(ast.NodeVisitor):
                     is_case_1 = (tzinfo_keyword is not None
                                  and not (isinstance(tzinfo_keyword.value, ast.NameConstant)
                                           and tzinfo_keyword.value.value is None))
+                # ex: `datetime.strptime(..., '...%z...')`
+                is_case_2 = ((1 < len(node.args)) and isinstance(node.args[1], STRING_NODE)
+                             and ('%z' in node.args[1].s))
 
-                if not is_case_1:
+                if not (is_case_1 or is_case_2):
                     self.errors.append(DTZ007(node.lineno, node.col_offset))
 
         # ex: `date.something()``
@@ -189,7 +197,7 @@ DTZ006 = Error(
 )
 
 DTZ007 = Error(
-    message='DTZ007 The use of `datetime.datetime.strptime()` must be followed by `.replace(tzinfo=)`.'
+    message='DTZ007 The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)`.'
 )
 
 DTZ011 = Error(


Test patch:
diff --git a/test_datetimez.py b/test_datetimez.py
index fd4559c..6c5fa43 100644
--- a/test_datetimez.py
+++ b/test_datetimez.py
@@ -163,6 +163,18 @@ class TestDateTimeZ(unittest.TestCase):
         )
         self.assert_codes(errors, [])
 
+    def test_DTZ007_good_format(self):
+        errors = self.write_file_and_run_checker(
+            'datetime.datetime.strptime(something, "%H:%M:%S%z")'
+        )
+        self.assert_codes(errors, [])
+
+    def test_DTZ007_bad_format(self):
+        errors = self.write_file_and_run_checker(
+            'datetime.datetime.strptime(something, "%H:%M:%S%Z")'
+        )
+        self.assert_codes(errors, ['DTZ007'])
+
     def test_DTZ007_no_replace(self):
         errors = self.write_file_and_run_checker(
             'datetime.datetime.strptime(something, something)'