diff --git a/flake8/engine.py b/flake8/engine.py index 1ed06f2..0711f8d 100644 --- a/flake8/engine.py +++ b/flake8/engine.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import re import platform +import warnings import pep8 @@ -8,7 +9,7 @@ from flake8 import __version__ from flake8 import callbacks from flake8.reporter import (multiprocessing, BaseQReport, FileQReport, QueueReport) -from flake8.util import OrderedSet, is_windows, is_using_stdin +from flake8 import util _flake8_noqa = re.compile(r'\s*# flake8[:=]\s*noqa', re.I).search @@ -17,7 +18,7 @@ EXTRA_EXCLUDE = ['.tox', '.eggs', '*.egg'] def _register_extensions(): """Register all the extensions.""" - extensions = OrderedSet() + extensions = util.OrderedSet() extensions.add(('pep8', pep8.__version__)) parser_hooks = [] options_hooks = [] @@ -123,10 +124,25 @@ def get_style_guide(**kwargs): for options_hook in options_hooks: options_hook(options) + if util.warn_when_using_jobs(options): + if not multiprocessing: + warnings.warn("The multiprocessing module is not available. " + "Ignoring --jobs arguments.") + if util.is_windows(): + warnings.warn("The --jobs option is not available on Windows. " + "Ignoring --jobs arguments.") + if util.is_using_stdin(styleguide.paths): + warnings.warn("The --jobs option is not compatible with supplying " + "input using - . Ignoring --jobs arguments.") + if options.diff: + warnings.warn("The --diff option was specified with --jobs but " + "they are not compatible. Ignoring --jobs arguments." + ) + if options.diff: options.jobs = None - force_disable_jobs = is_windows() or is_using_stdin(styleguide.paths) + force_disable_jobs = util.force_disable_jobs(styleguide) if multiprocessing and options.jobs and not force_disable_jobs: if options.jobs.isdigit(): diff --git a/flake8/tests/test_engine.py b/flake8/tests/test_engine.py index b7f7f5f..06fe62a 100644 --- a/flake8/tests/test_engine.py +++ b/flake8/tests/test_engine.py @@ -39,6 +39,7 @@ class TestEngine(unittest.TestCase): with mock.patch('flake8.engine.get_parser') as get_parser: m.ignored_extensions = [] StyleGuide.return_value.options.jobs = '42' + StyleGuide.return_value.options.diff = False get_parser.return_value = (m, []) engine.get_style_guide(foo='bar') get_parser.assert_called_once_with() @@ -85,13 +86,13 @@ class TestEngine(unittest.TestCase): # ourselves) what system we may be testing on. def test_windows_disables_jobs(self): - with mock.patch('flake8.engine.is_windows') as is_windows: + with mock.patch('flake8.util.is_windows') as is_windows: is_windows.return_value = True guide = engine.get_style_guide() assert isinstance(guide, reporter.BaseQReport) is False def test_stdin_disables_jobs(self): - with mock.patch('flake8.engine.is_using_stdin') as is_using_stdin: + with mock.patch('flake8.util.is_using_stdin') as is_using_stdin: is_using_stdin.return_value = True guide = engine.get_style_guide() assert isinstance(guide, reporter.BaseQReport) is False diff --git a/flake8/tests/test_integration.py b/flake8/tests/test_integration.py index 55ace48..d1417c6 100644 --- a/flake8/tests/test_integration.py +++ b/flake8/tests/test_integration.py @@ -8,6 +8,7 @@ except ImportError: import mock # < PY33 from flake8 import engine +from flake8.util import is_windows class IntegrationTestCase(unittest.TestCase): @@ -41,8 +42,15 @@ class IntegrationTestCase(unittest.TestCase): # mock stdout.flush so we can count the number of jobs created with mock.patch('sys.stdout.flush') as mocked: guide, report = self.check_files(arglist=['--jobs=%s' % jobs]) - self.assertEqual(guide.options.jobs, jobs) - self.assertEqual(mocked.call_count, jobs) + if is_windows(): + # The code path where guide.options.jobs gets converted to an + # int is not run on windows. So, do the int conversion here. + self.assertEqual(int(guide.options.jobs), jobs) + # On windows, call count is always zero. + self.assertEqual(mocked.call_count, 0) + else: + self.assertEqual(guide.options.jobs, jobs) + self.assertEqual(mocked.call_count, jobs) def test_jobs(self): self._job_tester(2) diff --git a/flake8/tests/test_warnings.py b/flake8/tests/test_warnings.py new file mode 100644 index 0000000..9dca44b --- /dev/null +++ b/flake8/tests/test_warnings.py @@ -0,0 +1,200 @@ +""" + _test_warnings.py + + Tests for the warnings that are emitted by flake8. + + This module is named _test_warnings instead of test_warnings so that a + normal nosetests run does not collect it. The tests in this module pass + when they are run alone, but they fail when they are run along with other + tests (nosetests --with-isolation doesn't help). + + In tox.ini, these tests are run separately. +""" + +from __future__ import with_statement + +import os +import warnings +import unittest +try: + from unittest import mock +except ImportError: + import mock # < PY33 + +from flake8 import engine +from flake8.util import is_windows + + +class IntegrationTestCaseWarnings(unittest.TestCase): + """Integration style tests to check that warnings are issued properly for + different command line options.""" + + windows_warning_text = ("The --jobs option is not available on Windows." + " Ignoring --jobs arguments.") + stdin_warning_text = ("The --jobs option is not compatible with" + " supplying input using - . Ignoring --jobs" + " arguments.") + + def this_file(self): + """Return the real path of this file.""" + this_file = os.path.realpath(__file__) + if this_file.endswith("pyc"): + this_file = this_file[:-1] + return this_file + + @staticmethod + def get_style_guide_with_warnings(engine, *args, **kwargs): + """ + Return a style guide object (obtained by calling + engine.get_style_guide) and a list of the warnings that were raised in + the process. + + Note: not threadsafe + """ + + # Note + # https://docs.python.org/2/library/warnings.html + # + # The catch_warnings manager works by replacing and then later + # restoring the module's showwarning() function and internal list of + # filter specifications. This means the context manager is modifying + # global state and therefore is not thread-safe + + with warnings.catch_warnings(record=True) as collected_warnings: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + # Get the style guide + style_guide = engine.get_style_guide(*args, **kwargs) + + # Now that the warnings have been collected, return the style guide and + # the + # warnings. + return (style_guide, collected_warnings) + + def verify_warnings(self, collected_warnings, expected_warnings): + """ + Verifies that collected_warnings is a sequence that contains user + warnings that match the sequence of string values passed in as + expected_warnings. + """ + if expected_warnings is None: + expected_warnings = [] + + collected_user_warnings = [w for w in collected_warnings + if issubclass(w.category, UserWarning)] + + self.assertEqual(len(collected_user_warnings), + len(expected_warnings)) + + collected_warnings_set = set(str(warning.message) + for warning + in collected_user_warnings) + expected_warnings_set = set(expected_warnings) + self.assertEqual(collected_warnings_set, expected_warnings_set) + + def check_files_collect_warnings(self, + arglist=[], + explicit_stdin=False, + count=0, + verbose=False): + """Call check_files and collect any warnings that are issued.""" + if verbose: + arglist.append('--verbose') + if explicit_stdin: + target_file = "-" + else: + target_file = self.this_file() + argv = ['flake8'] + arglist + [target_file] + with mock.patch("sys.argv", argv): + (style_guide, + collected_warnings, + ) = self.get_style_guide_with_warnings(engine, + parse_argv=True) + report = style_guide.check_files() + self.assertEqual(report.total_errors, count) + return style_guide, report, collected_warnings + + def check_files_no_warnings_allowed(self, + arglist=[], + explicit_stdin=False, + count=0, + verbose=False): + """Call check_files, and assert that there were no warnings issued.""" + (style_guide, + report, + collected_warnings, + ) = self.check_files_collect_warnings(arglist=arglist, + explicit_stdin=explicit_stdin, + count=count, + verbose=verbose) + self.verify_warnings(collected_warnings, expected_warnings=None) + return style_guide, report + + def _job_tester(self, jobs, verbose=False): + # mock stdout.flush so we can count the number of jobs created + with mock.patch('sys.stdout.flush') as mocked: + (guide, + report, + collected_warnings, + ) = self.check_files_collect_warnings( + arglist=['--jobs=%s' % jobs], + verbose=verbose) + + if is_windows(): + # The code path where guide.options.jobs gets converted to an + # int is not run on windows. So, do the int conversion here. + self.assertEqual(int(guide.options.jobs), jobs) + # On windows, call count is always zero. + self.assertEqual(mocked.call_count, 0) + else: + self.assertEqual(guide.options.jobs, jobs) + self.assertEqual(mocked.call_count, jobs) + + expected_warings = [] + if verbose and is_windows(): + expected_warings.append(self.windows_warning_text) + self.verify_warnings(collected_warnings, expected_warings) + + def test_jobs(self, verbose=False): + self._job_tester(2, verbose=verbose) + self._job_tester(10, verbose=verbose) + + def test_no_args_no_warnings(self, verbose=False): + self.check_files_no_warnings_allowed(verbose=verbose) + + def test_stdin_jobs_warning(self, verbose=False): + self.count = 0 + + def fake_stdin(): + self.count += 1 + with open(self.this_file(), "r") as f: + return f.read() + + with mock.patch("pep8.stdin_get_value", fake_stdin): + (style_guide, + report, + collected_warnings, + ) = self.check_files_collect_warnings(arglist=['--jobs=4'], + explicit_stdin=True, + verbose=verbose) + expected_warings = [] + if verbose: + expected_warings.append(self.stdin_warning_text) + if is_windows(): + expected_warings.append(self.windows_warning_text) + self.verify_warnings(collected_warnings, expected_warings) + self.assertEqual(self.count, 1) + + def test_jobs_verbose(self): + self.test_jobs(verbose=True) + + def test_no_args_no_warnings_verbose(self): + self.test_no_args_no_warnings(verbose=True) + + def test_stdin_jobs_warning_verbose(self): + self.test_stdin_jobs_warning(verbose=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/flake8/util.py b/flake8/util.py index 5a954f8..fda6331 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -54,6 +54,15 @@ def is_using_stdin(paths): return '-' in paths +def warn_when_using_jobs(options): + return (options.verbose and options.jobs and options.jobs.isdigit() and + int(options.jobs) > 1) + + +def force_disable_jobs(styleguide): + return is_windows() or is_using_stdin(styleguide.paths) + + def flag_on(val): """Return true if flag is on""" return str(val).upper() in ('1', 'T', 'TRUE', 'ON') diff --git a/tox.ini b/tox.ini index 2170c62..b21e44c 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = [testenv] usedevelop = True deps = + mock commands = python setup.py test -q python setup.py flake8