diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 5d2d272..0d7298a 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -105,6 +105,12 @@ entry: detect-private-key language: python types: [text] +- id: detect-datetime-raw-manipulation + name: Detect Raw datetime manipulation + description: Detects the raw manipulation of datetime. + entry: detect-datetime-raw-manipulation + language: python + types: [text] - id: double-quote-string-fixer name: Fix double quoted strings description: This hook replaces double quoted strings with single quoted strings diff --git a/README.md b/README.md index 43b9887..d0e8773 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Add this to your `.pre-commit-config.yaml` - `--allow-missing-credentials` - Allow hook to pass when no credentials are detected. - `detect-private-key` - Checks for the existence of private keys. +- `detect-datetime-raw-manipulation` - Check for raw manipulation of datetime. - `double-quote-string-fixer` - This hook replaces double quoted strings with single quoted strings. - `end-of-file-fixer` - Makes sure files end in a newline and only a newline. diff --git a/pre_commit_hooks/detect_datetime_raw_manipulation.py b/pre_commit_hooks/detect_datetime_raw_manipulation.py new file mode 100644 index 0000000..e6f1d2c --- /dev/null +++ b/pre_commit_hooks/detect_datetime_raw_manipulation.py @@ -0,0 +1,43 @@ +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import re +import sys + +DT_MANIPULATION_RE = re.compile( + r'[+-]=?\s?datetime.timedelta\(|[+-]=?\s?timedelta\(|.replace\(.*tzinfo=|datetime\(.*tzinfo=', +) + + +def _check_file(filename): + return_value = 0 + with open(filename, 'r') as f: + for i, line in enumerate(f, 1): + if DT_MANIPULATION_RE.search(line): + if line.strip().endswith(' # safe_dt_op') or line.strip().startswith('#'): + continue + + sys.stdout.write('{}:{}: {}'.format(filename, i, line)) + sys.stdout.flush() + + return_value += 1 + + return return_value + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*') + args = parser.parse_args(argv) + + return_value = 0 + for filename in args.filenames: + result = _check_file(filename) + return_value |= 1 if result > 0 else 0 + + return return_value + + +if __name__ == '__main__': + exit(main()) diff --git a/setup.py b/setup.py index 4bca2b0..4fcf4a6 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='pre_commit_hooks', description='Some out-of-the-box hooks for pre-commit.', url='https://github.com/pre-commit/pre-commit-hooks', - version='2.0.0', + version='2.1.0', author='Anthony Sottile', author_email='asottile@umich.edu', @@ -47,6 +47,8 @@ setup( 'debug-statement-hook = pre_commit_hooks.debug_statement_hook:main', 'detect-aws-credentials = pre_commit_hooks.detect_aws_credentials:main', 'detect-private-key = pre_commit_hooks.detect_private_key:detect_private_key', + 'detect-datetime-raw-manipulation = ' + 'pre_commit_hooks.detect_datetime_raw_manipulation:detect_datetime_raw_manipulation', 'double-quote-string-fixer = pre_commit_hooks.string_fixer:main', 'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer', 'file-contents-sorter = pre_commit_hooks.file_contents_sorter:main', diff --git a/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__clean.py b/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__clean.py new file mode 100644 index 0000000..1a7eadf --- /dev/null +++ b/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__clean.py @@ -0,0 +1,27 @@ +import datetime +from datetime import timedelta + + +def dummy_func(inp): + print(inp) + + +def move_time_forward(): + now = datetime.datetime.now() + + # Check commented out lines + # now += timedelta(days=1) + # now += datetime.timedelta(weeks=1) + # + # now -= timedelta(weeks=1) + # now -= timedelta(weeks=2) + # + # now = now.replace(hour=2) + # now = now.replace(tzinfo=None) + # now = now.replace(hour=1, tzinfo=pytz.utc) + + # Test regular usage of timedelta & datetime + dummy_func(timedelta(days=1)) + dummy_func(datetime.datetime(2018, 10, 11, 2, 3, 4, 450000)) + + print(now) diff --git a/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__flag.py b/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__flag.py new file mode 100644 index 0000000..6834882 --- /dev/null +++ b/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__flag.py @@ -0,0 +1,37 @@ +import datetime +from datetime import timedelta + +import pytz + + +def move_time_forward(): + now = datetime.datetime.now() + timezone = pytz.utc + + # 1 + now = datetime.datetime(now.year, now.month, now.day, hour=8, minute=0, tzinfo=timezone) + + # 2 + now += timedelta(days=1) + # 3 + now += datetime.timedelta(weeks=1) + # 4 + now = now + timedelta(seconds=1) + + # 5 + now -= datetime.timedelta(weeks=1) + # 6 + now -= timedelta(weeks=2) + # 7 + now = now - timedelta(seconds=1) + + now = now.replace(hour=2) + # 8 + now = now.replace(tzinfo=None) + # 9 + now = now.replace(hour=1, tzinfo=pytz.utc) + + # 10 + now = datetime.datetime(2018, 10, 11, 2, 3, 4, 450000, tzinfo=pytz.utc) + + print(now) diff --git a/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__ignore.py b/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__ignore.py new file mode 100644 index 0000000..72a05ec --- /dev/null +++ b/testing/resources/detect_datetime_raw_manipulation/raw_timedelta_usage__ignore.py @@ -0,0 +1,20 @@ +import datetime +from datetime import timedelta + +import pytz + + +def move_time_forward(): + now = datetime.datetime.now() + + now += timedelta(days=1) # safe_dt_op + now += datetime.timedelta(weeks=1) # safe_dt_op + + now -= timedelta(weeks=1) # safe_dt_op + now -= timedelta(weeks=2) # safe_dt_op + + now = now.replace(hour=2) + now = now.replace(tzinfo=None) # safe_dt_op + now = now.replace(hour=1, tzinfo=pytz.utc) # safe_dt_op + + print(now) diff --git a/tests/detect_raw_datetime_manipulation_test.py b/tests/detect_raw_datetime_manipulation_test.py new file mode 100644 index 0000000..b8a0cfc --- /dev/null +++ b/tests/detect_raw_datetime_manipulation_test.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit_hooks.detect_datetime_raw_manipulation import _check_file +from pre_commit_hooks.detect_datetime_raw_manipulation import main +from testing.util import get_resource_path + + +def test_flagged_file(): + rc = main([get_resource_path('detect_datetime_raw_manipulation/raw_timedelta_usage__flag.py')]) + assert rc == 1 + + result = _check_file(get_resource_path('detect_datetime_raw_manipulation/raw_timedelta_usage__flag.py')) + assert result == 10 + + +def test_flagged_ignore(): + rc = main([get_resource_path('detect_datetime_raw_manipulation/raw_timedelta_usage__ignore.py')]) + assert rc == 0 + + +def test_flagged_clean(): + rc = main([get_resource_path('detect_datetime_raw_manipulation/raw_timedelta_usage__clean.py')]) + assert rc == 0