From 8dd66f935e44e8a7f0ef76f4c93ed263a5e12a33 Mon Sep 17 00:00:00 2001 From: Jungkook Park Date: Tue, 30 Apr 2019 10:26:58 +0000 Subject: [PATCH] add datetime.date related rules and advance kwargs check --- README.md | 20 ++++--- flake8_datetimez.py | 125 ++++++++++++++++++++++++++++++++----------- setup.py | 2 +- test_datetimez.py | 126 +++++++++++++++++++++++++++++++++----------- 4 files changed, 204 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 2f94267..9a5a3f6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/flake8_datetimez.py b/flake8_datetimez.py index 2366963..173ecc3 100644 --- a/flake8_datetimez.py +++ b/flake8_datetimez.py @@ -1,4 +1,4 @@ -__version__ = '19.4.4.0' +__version__ = '19.4.5.0' import ast import logging @@ -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__ @@ -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)` @@ -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)` @@ -89,29 +113,50 @@ 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) @@ -119,23 +164,43 @@ def visit_Call(self, 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.' ) diff --git a/setup.py b/setup.py index 58c4ffc..99c21d8 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/test_datetimez.py b/test_datetimez.py index 63ef632..fd4559c 100644 --- a/test_datetimez.py +++ b/test_datetimez.py @@ -19,9 +19,39 @@ def write_file_and_run_checker(self, content): # DTZ001 - def test_DTZ001(self): + def test_DTZ001_args_good(self): errors = self.write_file_and_run_checker( - 'datetime.datetime.utcnow()' + 'datetime.datetime(2000, 1, 1, 0, 0, 0, 0, datetime.timezone.utc)' + ) + self.assert_codes(errors, []) + + def test_DTZ001_kwargs_good(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)' + ) + self.assert_codes(errors, []) + + def test_DTZ001_no_args(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime(2000, 1, 1, 0, 0, 0)' + ) + self.assert_codes(errors, ['DTZ001']) + + def test_DTZ001_none_args(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime(2000, 1, 1, 0, 0, 0, 0, None)' + ) + self.assert_codes(errors, ['DTZ001']) + + def test_DTZ001_no_kwargs(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime(2000, 1, 1, fold=1)' + ) + self.assert_codes(errors, ['DTZ001']) + + def test_DTZ001_none_kwargs(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime(2000, 1, 1, tzinfo=None)' ) self.assert_codes(errors, ['DTZ001']) @@ -29,108 +59,140 @@ def test_DTZ001(self): def test_DTZ002(self): errors = self.write_file_and_run_checker( - 'datetime.datetime.utcfromtimestamp(1234)' + 'datetime.datetime.today()' ) self.assert_codes(errors, ['DTZ002']) # DTZ003 - def test_DTZ003_args_good(self): + def test_DTZ003(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime.utcnow()' + ) + self.assert_codes(errors, ['DTZ003']) + + # DTZ004 + + def test_DTZ004(self): + errors = self.write_file_and_run_checker( + 'datetime.datetime.utcfromtimestamp(1234)' + ) + self.assert_codes(errors, ['DTZ004']) + + # DTZ005 + + def test_DTZ005_args_good(self): errors = self.write_file_and_run_checker( 'datetime.datetime.now(datetime.timezone.utc)' ) self.assert_codes(errors, []) - def test_DTZ003_keywords_good(self): + def test_DTZ005_keywords_good(self): errors = self.write_file_and_run_checker( 'datetime.datetime.now(tz=datetime.timezone.utc)' ) self.assert_codes(errors, []) - def test_DTZ003_no_args(self): + def test_DTZ005_no_args(self): errors = self.write_file_and_run_checker( 'datetime.datetime.now()' ) - self.assert_codes(errors, ['DTZ003']) + self.assert_codes(errors, ['DTZ005']) - def test_DTZ003_wrong_keywords(self): + def test_DTZ005_wrong_keywords(self): errors = self.write_file_and_run_checker( 'datetime.datetime.now(bad=datetime.timezone.utc)' ) - self.assert_codes(errors, ['DTZ003']) + self.assert_codes(errors, ['DTZ005']) - def test_DTZ003_none_arg(self): + def test_DTZ005_none_args(self): errors = self.write_file_and_run_checker( 'datetime.datetime.now(None)' ) - self.assert_codes(errors, ['DTZ003']) + self.assert_codes(errors, ['DTZ005']) - def test_DTZ003_none_keywords(self): + def test_DTZ005_none_keywords(self): errors = self.write_file_and_run_checker( 'datetime.datetime.now(tz=None)' ) - self.assert_codes(errors, ['DTZ003']) + self.assert_codes(errors, ['DTZ005']) - # DTZ004 + # DTZ006 - def test_DTZ004_args_good(self): + def test_DTZ006_args_good(self): errors = self.write_file_and_run_checker( 'datetime.datetime.fromtimestamp(1234, datetime.timezone.utc)' ) self.assert_codes(errors, []) - def test_DTZ004_keywords_good(self): + def test_DTZ006_keywords_good(self): errors = self.write_file_and_run_checker( 'datetime.datetime.fromtimestamp(1234, tz=datetime.timezone.utc)' ) self.assert_codes(errors, []) - def test_DTZ004_no_args(self): + def test_DTZ006_no_args(self): errors = self.write_file_and_run_checker( 'datetime.datetime.fromtimestamp(1234)' ) - self.assert_codes(errors, ['DTZ004']) + self.assert_codes(errors, ['DTZ006']) - def test_DTZ004_wrong_keywords(self): + def test_DTZ006_wrong_keywords(self): errors = self.write_file_and_run_checker( 'datetime.datetime.fromtimestamp(1234, bad=datetime.timezone.utc)' ) - self.assert_codes(errors, ['DTZ004']) + self.assert_codes(errors, ['DTZ006']) - def test_DTZ004_none_arg(self): + def test_DTZ006_none_args(self): errors = self.write_file_and_run_checker( 'datetime.datetime.fromtimestamp(1234, None)' ) - self.assert_codes(errors, ['DTZ004']) + self.assert_codes(errors, ['DTZ006']) - def test_DTZ004_none_keywords(self): + def test_DTZ006_none_keywords(self): errors = self.write_file_and_run_checker( 'datetime.datetime.fromtimestamp(1234, tz=None)' ) - self.assert_codes(errors, ['DTZ004']) + self.assert_codes(errors, ['DTZ006']) - # DTZ005 + # DTZ007 - def test_DTZ005_good(self): + def test_DTZ007_good(self): errors = self.write_file_and_run_checker( 'datetime.datetime.strptime(something, something).replace(tzinfo=datetime.timezone.utc)' ) self.assert_codes(errors, []) - def test_DTZ005_no_replace(self): + def test_DTZ007_no_replace(self): errors = self.write_file_and_run_checker( 'datetime.datetime.strptime(something, something)' ) - self.assert_codes(errors, ['DTZ005']) + self.assert_codes(errors, ['DTZ007']) - def test_DTZ005_wrong_replace(self): + def test_DTZ007_wrong_replace(self): errors = self.write_file_and_run_checker( 'datetime.datetime.strptime(something, something).replace(hour=1)' ) - self.assert_codes(errors, ['DTZ005']) + self.assert_codes(errors, ['DTZ007']) - def test_DTZ005_none_replace(self): + def test_DTZ007_none_replace(self): errors = self.write_file_and_run_checker( 'datetime.datetime.strptime(something, something).replace(tzinfo=None)' ) - self.assert_codes(errors, ['DTZ005']) + self.assert_codes(errors, ['DTZ007']) + + # DTZ011 + + def test_DTZ011(self): + errors = self.write_file_and_run_checker( + 'datetime.date.today()' + ) + self.assert_codes(errors, ['DTZ011']) + + # DTZ012 + + def test_DTZ012(self): + errors = self.write_file_and_run_checker( + 'datetime.date.fromtimestamp(1234)' + ) + self.assert_codes(errors, ['DTZ012'])