From d810fbbb4686e9a6efeaabd35f664bc7eb4f91f1 Mon Sep 17 00:00:00 2001 From: David Stensland Date: Mon, 27 Mar 2017 20:48:09 -0700 Subject: [PATCH 1/3] download latest 3.6, for more stable tests --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1f03dd3..5a89486 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ notifications: before_install: - pip install codecov - mkdir bad_tests - - wget https://hg.python.org/cpython/archive/tip.tar.bz2/Lib/test/ -O bad_tests/test.tar.bz2 + - wget https://hg.python.org/cpython/archive/3.6.tar.bz2/Lib/test/ -O bad_tests/test.tar.bz2 - tar -xjf bad_tests/test.tar.bz2 -C bad_tests/ - mv bad_tests/cpython-*/Lib/test/badsyntax_future* . -v - rm -r bad_tests/ From 43eac27c69c5e524ebc410153a5ef0fd830c3f42 Mon Sep 17 00:00:00 2001 From: David Stensland Date: Mon, 27 Mar 2017 20:49:15 -0700 Subject: [PATCH 2/3] Revert "Make offset consistent with flake8 version 3" This reverts commit eaf2799f41ca3a5692fed2df4dfbc06b0627dcfe. --- flake8_future_import.py | 2 +- test_flake8_future_import.py | 26 ++++++-------------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/flake8_future_import.py b/flake8_future_import.py index 99d7aee..c2a22ef 100755 --- a/flake8_future_import.py +++ b/flake8_future_import.py @@ -204,7 +204,7 @@ def main(args): filename).run(): if msg[:4] not in ignored: has_errors = True - print('{0}:{1}:{2}: {3}'.format(filename, line, char, msg)) + print('{0}:{1}:{2}: {3}'.format(filename, line, char + 1, msg)) return has_errors diff --git a/test_flake8_future_import.py b/test_flake8_future_import.py index 80abb13..64aed86 100644 --- a/test_flake8_future_import.py +++ b/test_flake8_future_import.py @@ -11,8 +11,6 @@ import sys import tempfile -from distutils.version import StrictVersion - if sys.version_info < (2, 7): import unittest2 as unittest else: @@ -87,13 +85,12 @@ def iterator(self, checker): self.assertEqual(char, 0) self.assertIs(origin, flake8_future_import.FutureImportChecker) - def reverse_parse(self, lines, expected_offset, tmp_file=None): + def reverse_parse(self, lines, tmp_file=None): for line in lines: - match = re.match(r'([^:]+):(\d+):(\d+): (.*)', line) - yield int(match.group(2)), match.group(4) + match = re.match(r'([^:]+):(\d+):1: (.*)', line) + yield int(match.group(2)), match.group(3) if tmp_file is not None: self.assertEqual(match.group(1), tmp_file) - self.assertEqual(int(match.group(3)), expected_offset) class SimpleImportTestCase(TestCaseBase): @@ -173,7 +170,7 @@ def run_main(self, *imported): flake8_future_import.main([tmp_file]) finally: os.remove(tmp_file) - self.run_test(self.reverse_parse(self.messages, 0), imported) + self.run_test(self.reverse_parse(self.messages), imported) def test_main(self): self.run_main() @@ -264,14 +261,6 @@ def setUpClass(cls): else: raise unittest.SkipTest('The plugin is not installed and ' 'TEST_FLAKE8_INSTALL not set') - # Determine version of installed flake8 package - for dist in pip.utils.get_installed_distributions(False): - if dist.key == 'flake8': - version = StrictVersion(dist.version) - cls.expected_offset = 0 if version.version[0] >= 3 else 1 - break - else: - raise ValueError('Unable to find Flake8 installation') super(Flake8TestCase, cls).setUpClass() @classmethod @@ -300,11 +289,8 @@ def run_flake8(self, *imported): os.close(handle) os.remove(tmp_file) self.assertFalse(data_err) - self.run_test( - self.reverse_parse(data_out.decode('utf8').splitlines(), - self.expected_offset, - tmp_file), - imported) + self.run_test(self.reverse_parse(data_out.decode('utf8').splitlines(), tmp_file), + imported) self.assertEqual(p.returncode, 1) def test_flake8(self): From 597e20e8158b8963c71c43e6ecebc4aedfa0d9b3 Mon Sep 17 00:00:00 2001 From: David Stensland Date: Fri, 8 Jan 2016 13:22:57 -0800 Subject: [PATCH 3/3] intelligently complain about python 3 imports Don't complain all the time about print, division, etc. Instead, just complain when the module in question actually uses a language feature that would be affected. travis rebuild --- flake8_future_import.py | 81 ++++++++++++++++++++++++++++++------ test_flake8_future_import.py | 59 ++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 12 deletions(-) diff --git a/flake8_future_import.py b/flake8_future_import.py index c2a22ef..481d809 100755 --- a/flake8_future_import.py +++ b/flake8_future_import.py @@ -13,29 +13,61 @@ except ImportError as e: argparse = e -from ast import NodeVisitor, Str, Module, parse +import ast __version__ = '0.4.3' -class FutureImportVisitor(NodeVisitor): +class FutureImportVisitor(ast.NodeVisitor): def __init__(self): super(FutureImportVisitor, self).__init__() self.future_imports = [] + self._uses_code = False + self._uses_print = False + self._uses_division = False + self._uses_import = False + self._uses_str_literals = False + self._uses_generators = False + self._uses_with = False + + def _is_print(self, node): + # python 2 + if hasattr(ast, 'Print') and isinstance(node, ast.Print): + return True + + # python 3 + if isinstance(node, ast.Call) and \ + isinstance(node.func, ast.Name) and \ + node.func.id == 'print': + return True + + return False def visit_ImportFrom(self, node): if node.module == '__future__': self.future_imports += [node] - - def visit_Expr(self, node): - if not isinstance(node.value, Str) or node.value.col_offset != 0: - self._uses_code = True + else: + self._uses_import = True def generic_visit(self, node): - if not isinstance(node, Module): + if not isinstance(node, ast.Module): self._uses_code = True + + if isinstance(node, ast.Str): + self._uses_str_literals = True + elif self._is_print(node): + self._uses_print = True + elif isinstance(node, ast.Div): + self._uses_division = True + elif isinstance(node, ast.Import): + self._uses_import = True + elif isinstance(node, ast.With): + self._uses_with = True + elif isinstance(node, ast.Yield): + self._uses_generators = True + super(FutureImportVisitor, self).generic_visit(node) @property @@ -94,6 +126,7 @@ class FutureImportChecker(Flake8Argparse): name = 'flake8-future-import' require_code = True min_version = False + require_used = False def __init__(self, tree, filename): self.tree = tree @@ -106,6 +139,8 @@ def add_arguments(cls, parser): parser.add_argument('--min-version', default=False, help='The minimum version supported so that it can ' 'ignore mandatory and non-existent features') + parser.add_argument('--require-used', action='store_true', + help='Only alert when relevant features are used') @classmethod def parse_options(cls, options): @@ -122,6 +157,7 @@ def parse_options(cls, options): 'like "A.B.C"'.format(options.min_version)) min_version += (0, ) * (max(3 - len(min_version), 0)) cls.min_version = min_version + cls.require_used = options.require_used def _generate_error(self, future_import, lineno, present): feature = FEATURES.get(future_import) @@ -156,10 +192,31 @@ def run(self): yield err present.add(alias.name) for name in FEATURES: - if name not in present: - err = self._generate_error(name, 1, False) - if err: - yield err + if name in present: + continue + + if self.require_used: + if name == 'print_function' and not visitor._uses_print: + continue + + if name == 'division' and not visitor._uses_division: + continue + + if name == 'absolute_import' and not visitor._uses_import: + continue + + if name == 'unicode_literals' and not visitor._uses_str_literals: + continue + + if name == 'generators' and not visitor._uses_generators: + continue + + if name == 'with_statement' and not visitor._uses_with: + continue + + err = self._generate_error(name, 1, False) + if err: + yield err def main(args): @@ -199,7 +256,7 @@ def main(args): has_errors = False for filename in args.files: with open(filename, 'rb') as f: - tree = parse(f.read(), filename=filename, mode='exec') + tree = ast.parse(f.read(), filename=filename, mode='exec') for line, char, msg, checker in FutureImportChecker(tree, filename).run(): if msg[:4] not in ignored: diff --git a/test_flake8_future_import.py b/test_flake8_future_import.py index 64aed86..3da9c11 100644 --- a/test_flake8_future_import.py +++ b/test_flake8_future_import.py @@ -349,5 +349,64 @@ class TestFeatures(TestCaseBase): """Verify that the features are up to date.""" +class FeatureDetectionTestCase(TestCaseBase): + + ALWAYS_MISSING = frozenset(('generator_stop', 'nested_scopes')) + + def check_code(self, code): + tree = ast.parse(code) + checker = flake8_future_import.FutureImportChecker(tree, 'fn') + checker.require_used = True + iterator = self.iterator(checker) + return self.check_result(iterator) + + def assert_errors(self, code, missing=None, forbidden=None): + missing = missing or set() + forbidden = forbidden or set() + + found_missing, found_forbidden, _ = self.check_code(code) + + self.assertEqual(missing, found_missing) + self.assertEqual(forbidden, found_forbidden) + + def test_no_code(self): + self.assert_errors('') + self.assert_errors('# comment only') + + def test_simple_statement(self): + self.assert_errors('1+1', missing=self.ALWAYS_MISSING) + + def test_print_function(self): + self.assert_errors('print(foo)', self.ALWAYS_MISSING | set(['print_function'])) + + def test_unicode_literals(self): + expected_missing = self.ALWAYS_MISSING | set(['unicode_literals']) + self.assert_errors('"foo"', expected_missing) + self.assert_errors('u"foo"', expected_missing) + self.assert_errors('r"foo"', expected_missing) + self.assert_errors('fn("foo")', expected_missing) + + def test_division(self): + # not division + self.assert_errors('a % b', self.ALWAYS_MISSING) + + expected_missing = self.ALWAYS_MISSING | set(['division']) + self.assert_errors('1 / 0', expected_missing) + self.assert_errors('1 / 2 / 1', expected_missing) + self.assert_errors('a /= b', expected_missing) + self.assert_errors('fn(3 / 2)', expected_missing) + + def test_absolute_import(self): + expected_missing = self.ALWAYS_MISSING | set(['absolute_import']) + self.assert_errors('import foo\npass', expected_missing) + self.assert_errors('from foo import bar\npass', expected_missing) + + def test_with_statement(self): + self.assert_errors('with foo: foo()', self.ALWAYS_MISSING | set(['with_statement'])) + + def test_generators(self): + self.assert_errors('def foo(): yield', self.ALWAYS_MISSING | set(['generators'])) + + if __name__ == '__main__': unittest.main()