From 570be3def8c7df5c2368ef3efd944c91ae80562e Mon Sep 17 00:00:00 2001 From: Sasan Jacob Rasti Date: Mon, 12 Dec 2022 14:46:32 +0100 Subject: [PATCH] Detect `.astimezone()` Fixes #9 --- flake8_datetimez.py | 175 +++++++++++++++++++++++--------------------- test_datetimez.py | 38 +++++++++- 2 files changed, 127 insertions(+), 86 deletions(-) diff --git a/flake8_datetimez.py b/flake8_datetimez.py index 4ccf540..8dad44f 100644 --- a/flake8_datetimez.py +++ b/flake8_datetimez.py @@ -18,6 +18,18 @@ def _get_from_keywords(keywords, arg): return keyword +def astimezone_called(node): + parent = getattr(node, '_flake8_datetimez_parent', None) + pparent = getattr(parent, '_flake8_datetimez_parent', None) + if not (isinstance(parent, ast.Attribute) + and parent.attr == 'astimezone'): + return False + elif not isinstance(pparent, ast.Call): + return False + else: + return True + + class DateTimeZChecker: name = 'flake8.datetimez' version = __version__ @@ -69,94 +81,87 @@ def visit_Call(self, node): and node.func.value.value.id == 'datetime') if (is_datetime_class and node.func.attr == 'datetime') or is_unqualified_datetime_class_call: - # ex `datetime(2000, 1, 1, 0, 0, 0, 0, datetime.timezone.utc)` - is_case_1 = (len(node.args) == 8 - and not (isinstance(node.args[7], ast.NameConstant) - and node.args[7].value is None)) - - # ex `datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)` - tzinfo_keyword = _get_from_keywords(node.keywords, 'tzinfo') - is_case_2 = (tzinfo_keyword is not None - and not (isinstance(tzinfo_keyword.value, ast.NameConstant) - and tzinfo_keyword.value.value is None)) - - if not (is_case_1 or is_case_2): - self.errors.append(DTZ001(node.lineno, node.col_offset)) - - if is_datetime_class or is_datetime_module_n_class: - if node.func.attr == 'today': - self.errors.append(DTZ002(node.lineno, node.col_offset)) - - elif node.func.attr == 'utcnow': - self.errors.append(DTZ003(node.lineno, node.col_offset)) - - elif node.func.attr == 'utcfromtimestamp': - self.errors.append(DTZ004(node.lineno, node.col_offset)) - - elif node.func.attr in 'now': - # ex: `datetime.now(UTC)` - is_case_1 = (len(node.args) == 1 - and len(node.keywords) == 0 - and not (isinstance(node.args[0], ast.NameConstant) - and node.args[0].value is None)) - - # ex: `datetime.now(tz=UTC)` - tz_keyword = _get_from_keywords(node.keywords, 'tz') - is_case_2 = (tz_keyword is not None - and not (isinstance(tz_keyword.value, ast.NameConstant) - and tz_keyword.value.value is None)) + if not astimezone_called(node): + # ex `datetime(2000, 1, 1, 0, 0, 0, 0, datetime.timezone.utc)` + is_case_1 = (len(node.args) == 8 + and not (isinstance(node.args[7], ast.NameConstant) + and node.args[7].value is None)) + + # ex `datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)` + tzinfo_keyword = _get_from_keywords(node.keywords, 'tzinfo') + is_case_2 = (tzinfo_keyword is not None + and not (isinstance(tzinfo_keyword.value, ast.NameConstant) + and tzinfo_keyword.value.value is None)) if not (is_case_1 or is_case_2): - self.errors.append(DTZ005(node.lineno, node.col_offset)) + self.errors.append(DTZ001(node.lineno, node.col_offset)) - elif node.func.attr == 'fromtimestamp': - # ex: `datetime.fromtimestamp(1234, UTC)` - is_case_1 = (len(node.args) == 2 - and len(node.keywords) == 0 - and not (isinstance(node.args[1], ast.NameConstant) - and node.args[1].value is None)) - - # ex: `datetime.fromtimestamp(1234, tz=UTC)` - tz_keyword = _get_from_keywords(node.keywords, 'tz') - is_case_2 = (tz_keyword is not None - and not (isinstance(tz_keyword.value, ast.NameConstant) - and tz_keyword.value.value is None)) - - if not (is_case_1 or is_case_2): - self.errors.append(DTZ006(node.lineno, node.col_offset)) - - elif node.func.attr == 'strptime': - parent = getattr(node, '_flake8_datetimez_parent', None) - pparent = getattr(parent, '_flake8_datetimez_parent', None) - - # ex: `datetime.strptime(...).replace(tzinfo=UTC)` - if not (isinstance(parent, ast.Attribute) - and parent.attr == 'replace'): - is_case_1 = False - elif not isinstance(pparent, ast.Call): - is_case_1 = False - else: - tzinfo_keyword = _get_from_keywords(pparent.keywords, 'tzinfo') - 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(...).astimezone()` - if not (isinstance(parent, ast.Attribute) - and parent.attr == 'astimezone'): - is_case_2 = False - elif not isinstance(pparent, ast.Call): - is_case_2 = False - else: - is_case_2 = True - - # ex: `datetime.strptime(..., '...%z...')` - is_case_3 = (1 < len(node.args) - and isinstance(node.args[1], STRING_NODE) - and '%z' in node.args[1].s) - - if not (is_case_1 or is_case_2 or is_case_3): - self.errors.append(DTZ007(node.lineno, node.col_offset)) + if is_datetime_class or is_datetime_module_n_class: + if not astimezone_called(node): + if node.func.attr == 'today': + self.errors.append(DTZ002(node.lineno, node.col_offset)) + + elif node.func.attr == 'utcnow': + self.errors.append(DTZ003(node.lineno, node.col_offset)) + + elif node.func.attr == 'utcfromtimestamp': + self.errors.append(DTZ004(node.lineno, node.col_offset)) + + elif node.func.attr in 'now': + # ex: `datetime.now(UTC)` + is_case_1 = (len(node.args) == 1 + and len(node.keywords) == 0 + and not (isinstance(node.args[0], ast.NameConstant) + and node.args[0].value is None)) + + # ex: `datetime.now(tz=UTC)` + tz_keyword = _get_from_keywords(node.keywords, 'tz') + is_case_2 = (tz_keyword is not None + and not (isinstance(tz_keyword.value, ast.NameConstant) + and tz_keyword.value.value is None)) + + if not (is_case_1 or is_case_2): + self.errors.append(DTZ005(node.lineno, node.col_offset)) + + elif node.func.attr == 'fromtimestamp': + # ex: `datetime.fromtimestamp(1234, UTC)` + is_case_1 = (len(node.args) == 2 + and len(node.keywords) == 0 + and not (isinstance(node.args[1], ast.NameConstant) + and node.args[1].value is None)) + + # ex: `datetime.fromtimestamp(1234, tz=UTC)` + tz_keyword = _get_from_keywords(node.keywords, 'tz') + is_case_2 = (tz_keyword is not None + and not (isinstance(tz_keyword.value, ast.NameConstant) + and tz_keyword.value.value is None)) + + if not (is_case_1 or is_case_2): + self.errors.append(DTZ006(node.lineno, node.col_offset)) + + elif node.func.attr == 'strptime': + parent = getattr(node, '_flake8_datetimez_parent', None) + pparent = getattr(parent, '_flake8_datetimez_parent', None) + + # ex: `datetime.strptime(...).replace(tzinfo=UTC)` + if not (isinstance(parent, ast.Attribute) + and parent.attr == 'replace'): + is_case_1 = False + elif not isinstance(pparent, ast.Call): + is_case_1 = False + else: + tzinfo_keyword = _get_from_keywords(pparent.keywords, 'tzinfo') + 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 or is_case_2): + self.errors.append(DTZ007(node.lineno, node.col_offset)) # ex: `date.something()`` is_date_class = (isinstance(node.func, ast.Attribute) diff --git a/test_datetimez.py b/test_datetimez.py index 2975545..0c8d21b 100644 --- a/test_datetimez.py +++ b/test_datetimez.py @@ -11,7 +11,7 @@ def assert_codes(self, errors, codes): self.assertTrue(error.message.startswith(code + ' ')) def write_file_and_run_checker(self, content): - with tempfile.NamedTemporaryFile('w') as f: + with tempfile.NamedTemporaryFile('w', delete=False) as f: f.write(content) f.flush() checker = DateTimeZChecker(None, f.name) @@ -61,6 +61,12 @@ def test_DTZ001_none_kwargs(self): ) self.assert_codes(errors, ['DTZ001']) + def test_DTZ001_good_astimezone(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime(2000, 1, 1, tzinfo=None).astimezone()' + ) + self.assert_codes(errors, []) + # DTZ002 def test_DTZ002(self): @@ -75,6 +81,12 @@ def test_DTZ002_unqualified(self): ) self.assert_codes(errors, ['DTZ002']) + def test_DTZ002_good_astimezone(self): + errors = self.write_file_and_run_checker( + 'datetime.today().astimezone()' + ) + self.assert_codes(errors, []) + # DTZ003 def test_DTZ003(self): @@ -89,6 +101,12 @@ def test_DTZ003_unqualified(self): ) self.assert_codes(errors, ['DTZ003']) + def test_DTZ003_good_astimezone(self): + errors = self.write_file_and_run_checker( + 'datetime.utcnow().astimezone()' + ) + self.assert_codes(errors, []) + # DTZ004 def test_DTZ004(self): @@ -103,6 +121,12 @@ def test_DTZ004_unqualified(self): ) self.assert_codes(errors, ['DTZ004']) + def test_DTZ004_good_astimezone(self): + errors = self.write_file_and_run_checker( + 'datetime.utcfromtimestamp(1234).astimezone()' + ) + self.assert_codes(errors, []) + # DTZ005 def test_DTZ005_args_good(self): @@ -147,6 +171,12 @@ def test_DTZ005_none_keywords(self): ) self.assert_codes(errors, ['DTZ005']) + def test_DTZ005_good_astimezone(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime.now(tz=None).astimezone()' + ) + self.assert_codes(errors, []) + # DTZ006 def test_DTZ006_args_good(self): @@ -191,6 +221,12 @@ def test_DTZ006_none_keywords(self): ) self.assert_codes(errors, ['DTZ006']) + def test_DTZ006_good_astimezone(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime.fromtimestamp(1234, tz=None).astimezone()' + ) + self.assert_codes(errors, []) + # DTZ007 def test_DTZ007_good_replace(self):