Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 90 additions & 85 deletions flake8_datetimez.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down Expand Up @@ -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)
Expand Down
38 changes: 37 additions & 1 deletion test_datetimez.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down