Skip to content

Commit

Permalink
add datetime.date related rules and advance kwargs check
Browse files Browse the repository at this point in the history
  • Loading branch information
pjknkda committed Apr 30, 2019
1 parent fbff17e commit 8dd66f9
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 69 deletions.
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
# flake8-datetimez

Check for python unsafe naive `datetime` module usages.
A plugin for flake8 to ban the usage of unsafe naive datetime class.


## List of warnings

**DTZ001**: The use of `datetime.datetime.utcnow()` is not allowed.
- **DTZ001** : The use of `datetime.datetime()` without `tzinfo` argument is not allowed.

**DTZ002**: The use of `datetime.datetime.utcfromtimestamp()` is not allowed.
- **DTZ002** : The use of `datetime.datetime.today()` is not allowed. Use `datetime.datetime.now(tz=)` instead.

**DTZ003**: The use of `datetime.datetime.now()` without `tz` argument is not allowed.
- **DTZ003** : The use of `datetime.datetime.utcnow()` is not allowed. Use `datetime.datetime.now(tz=)` instead.

**DTZ004**: The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed.
- **DTZ004** : The use of `datetime.datetime.utcfromtimestamp()` is not allowed. Use `datetime.datetime.fromtimestamp(, tz=)` instead.

**DTZ005**: The use of `datetime.datetime.strptime()` must be followed by `.replace(tzinfo=)`
- **DTZ005** : The use of `datetime.datetime.now()` without `tz` argument is not allowed.

- **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=)`.

- **DTZ011** : The use of `datetime.date.today()` is not allowed. Use `datetime.datetime.now(tz=).date()` instead.

- **DTZ012** : The use of `datetime.date.fromtimestamp()` is not allowed. Use `datetime.datetime.fromtimestamp(, tz=).date()` instead.


## Install
Expand Down
125 changes: 95 additions & 30 deletions flake8_datetimez.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '19.4.4.0'
__version__ = '19.4.5.0'

import ast
import logging
Expand All @@ -10,6 +10,12 @@
LOG = logging.getLogger('flake8.datetimez')


def _get_from_keywords(keywords, arg):
for keyword in keywords:
if keyword.arg == arg:
return keyword


class DateTimeZChecker:
name = 'flake8.datetimez'
version = __version__
Expand Down Expand Up @@ -57,12 +63,31 @@ def visit_Call(self, node):
and isinstance(node.func.value.value, ast.Name)
and node.func.value.value.id == 'datetime')

if is_datetime_class:
if node.func.attr == 'datetime':
# 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 == 'utcnow':
self.errors.append(DTZ001(node.lineno, node.col_offset))
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(DTZ002(node.lineno, node.col_offset))
self.errors.append(DTZ004(node.lineno, node.col_offset))

elif node.func.attr in 'now':
# ex: `datetime.now(UTC)`
Expand All @@ -72,14 +97,13 @@ def visit_Call(self, node):
and node.args[0].value is None))

# ex: `datetime.now(tz=UTC)`
is_case_2 = (len(node.args) == 0
and len(node.keywords) == 1
and node.keywords[0].arg == 'tz'
and not (isinstance(node.keywords[0].value, ast.NameConstant)
and node.keywords[0].value.value is None))
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(DTZ003(node.lineno, node.col_offset))
self.errors.append(DTZ005(node.lineno, node.col_offset))

elif node.func.attr == 'fromtimestamp':
# ex: `datetime.fromtimestamp(1234, UTC)`
Expand All @@ -89,53 +113,94 @@ def visit_Call(self, node):
and node.args[1].value is None))

# ex: `datetime.fromtimestamp(1234, tz=UTC)`
is_case_2 = (len(node.args) == 1
and len(node.keywords) == 1
and node.keywords[0].arg == 'tz'
and not (isinstance(node.keywords[0].value, ast.NameConstant)
and node.keywords[0].value.value is None))
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(DTZ004(node.lineno, node.col_offset))
self.errors.append(DTZ006(node.lineno, node.col_offset))

elif node.func.attr == 'strptime':
# ex: `datetime.strptime(...).replace(tzinfo=UTC)`
parent = getattr(node, '_flake8_datetimez_parent', None)
pparent = getattr(parent, '_flake8_datetimez_parent', None)
is_case_1 = (isinstance(parent, ast.Attribute)
and parent.attr == 'replace'
and isinstance(pparent, ast.Call)
and len(pparent.keywords) == 1
and pparent.keywords[0].arg == 'tzinfo'
and not (isinstance(pparent.keywords[0].value, ast.NameConstant)
and pparent.keywords[0].value.value is None))
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))

if not is_case_1:
self.errors.append(DTZ005(node.lineno, node.col_offset))
self.errors.append(DTZ007(node.lineno, node.col_offset))

# ex: `date.something()``
is_date_class = (isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.value.id == 'date')

# ex: `datetime.date.something()``
is_date_module_n_class = (isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Attribute)
and node.func.value.attr == 'date'
and isinstance(node.func.value.value, ast.Name)
and node.func.value.value.id == 'datetime')

if is_date_class or is_date_module_n_class:
if node.func.attr == 'today':
self.errors.append(DTZ011(node.lineno, node.col_offset))

elif node.func.attr == 'fromtimestamp':
self.errors.append(DTZ012(node.lineno, node.col_offset))

self.generic_visit(node)


error = namedtuple('error', ['lineno', 'col', 'message', 'type'])
Error = partial(partial, error, type=DateTimeZChecker)


DTZ001 = Error(
message='DTZ001 The use of `datetime.datetime.utcnow()` is not allowed.'
message='DTZ001 The use of `datetime.datetime()` without `tzinfo` argument is not allowed.'
)

DTZ002 = Error(
message='DTZ002 The use of `datetime.datetime.utcfromtimestamp()` is not allowed.'
message='DTZ002 The use of `datetime.datetime.today()` is not allowed. '
'Use `datetime.datetime.now(tz=)` instead.'
)

DTZ003 = Error(
message='DTZ003 The use of `datetime.datetime.now()` without `tz` argument is not allowed.'
message='DTZ003 The use of `datetime.datetime.utcnow()` is not allowed. '
'Use `datetime.datetime.now(tz=)` instead.'
)

DTZ004 = Error(
message='DTZ004 The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed.'
message='DTZ004 The use of `datetime.datetime.utcfromtimestamp()` is not allowed. '
'Use `datetime.datetime.fromtimestamp(, tz=)` instead.'
)

DTZ005 = Error(
message='DTZ005 The use of `datetime.datetime.strptime()` must be followed by `.replace(tzinfo=)`'
message='DTZ005 The use of `datetime.datetime.now()` without `tz` argument is not allowed.'
)

DTZ006 = Error(
message='DTZ006 The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed.'
)

DTZ007 = Error(
message='DTZ007 The use of `datetime.datetime.strptime()` must be followed by `.replace(tzinfo=)`.'
)

DTZ011 = Error(
message='DTZ011 The use of `datetime.date.today()` is not allowed. '
'Use `datetime.datetime.now(tz=).date()` instead.'
)

DTZ012 = Error(
message='DTZ012 The use of `datetime.date.fromtimestamp()` is not allowed. '
'Use `datetime.datetime.fromtimestamp(, tz=).date()` instead.'
)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
setup(
name='flake8-datetimez',
version=version,
description='A plugin for flake8 to ban naive datetime classes.',
description='A plugin for flake8 to ban the usage of unsafe naive datetime class.',
long_description=long_description,
keywords='flake8 datetime pyflakes pylint linter qa',
author='Jungkook Park',
Expand Down
Loading

0 comments on commit 8dd66f9

Please sign in to comment.