From f67f481beea1f32e503d346e9d4d3f2f2a8b2ebe Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sat, 6 Aug 2016 14:16:08 -0500 Subject: [PATCH] Add --bug-report flag to help bug reporters When invoked it will print out JSON that has all of the debugging information needed by the maintainers to diagnose or reproduce a bug. Closes #207 --- docs/source/release-notes/3.1.0.rst | 4 ++ docs/source/release-notes/index.rst | 1 + docs/source/user/options.rst | 48 ++++++++++++++++ src/flake8/main/debug.py | 62 +++++++++++++++++++++ src/flake8/main/options.py | 11 ++++ src/flake8/options/manager.py | 5 +- tests/unit/test_debug.py | 86 +++++++++++++++++++++++++++++ 7 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 docs/source/release-notes/3.1.0.rst create mode 100644 src/flake8/main/debug.py create mode 100644 tests/unit/test_debug.py diff --git a/docs/source/release-notes/3.1.0.rst b/docs/source/release-notes/3.1.0.rst new file mode 100644 index 0000000..7df073a --- /dev/null +++ b/docs/source/release-notes/3.1.0.rst @@ -0,0 +1,4 @@ +3.1.0 -- 2016-yy-xx +------------------- + +- Add ``--bug-report`` flag to make issue reporters' lives easier. diff --git a/docs/source/release-notes/index.rst b/docs/source/release-notes/index.rst index 07b9cd6..c59a64f 100644 --- a/docs/source/release-notes/index.rst +++ b/docs/source/release-notes/index.rst @@ -6,6 +6,7 @@ All of the release notes that have been recorded for Flake8 are organized here with the newest releases first. .. toctree:: + 3.1.0 3.0.4 3.0.3 3.0.2 diff --git a/docs/source/user/options.rst b/docs/source/user/options.rst index acaa67c..c139f69 100644 --- a/docs/source/user/options.rst +++ b/docs/source/user/options.rst @@ -728,3 +728,51 @@ flake8 --benchmark dir/ This **can not** be specified in config files. + + +.. option:: --bug-report + + Generate information necessary to file a complete bug report for Flake8. + This will pretty-print a JSON blob that should be copied and pasted into a + bug report for Flake8. + + Command-line usage: + + .. prompt:: bash + + flake8 --bug-report + + The output should look vaguely like: + + .. code-block:: js + + { + "dependencies": [ + { + "dependency": "setuptools", + "version": "25.1.1" + } + ], + "platform": { + "python_implementation": "CPython", + "python_version": "2.7.12", + "system": "Darwin" + }, + "plugins": [ + { + "plugin": "mccabe", + "version": "0.5.1" + }, + { + "plugin": "pycodestyle", + "version": "2.0.0" + }, + { + "plugin": "pyflakes", + "version": "1.2.3" + } + ], + "version": "3.1.0.dev0" + } + + This **can not** be specified in config files. diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py new file mode 100644 index 0000000..e6ea141 --- /dev/null +++ b/src/flake8/main/debug.py @@ -0,0 +1,62 @@ +"""Module containing the logic for our debugging logic.""" +from __future__ import print_function + +import json +import platform + +import setuptools + + +def print_information(option, option_string, value, parser, + option_manager=None): + """Print debugging information used in bug reports. + + :param option: + The optparse Option instance. + :type option: + optparse.Option + :param str option_string: + The option name + :param value: + The value passed to the callback parsed from the command-line + :param parser: + The optparse OptionParser instance + :type parser: + optparse.OptionParser + :param option_manager: + The Flake8 OptionManager instance. + :type option_manager: + flake8.options.manager.OptionManager + """ + if not option_manager.registered_plugins: + # NOTE(sigmavirus24): Flake8 parses options twice. The first time, we + # will not have any registered plugins. We can skip this one and only + # take action on the second time we're called. + return + print(json.dumps(information(option_manager), indent=2, sort_keys=True)) + raise SystemExit(False) + + +def information(option_manager): + """Generate the information to be printed for the bug report.""" + return { + 'version': option_manager.version, + 'plugins': plugins_from(option_manager), + 'dependencies': dependencies(), + 'platform': { + 'python_implementation': platform.python_implementation(), + 'python_version': platform.python_version(), + 'system': platform.system(), + }, + } + + +def plugins_from(option_manager): + """Generate the list of plugins installed.""" + return [{'plugin': plugin, 'version': version} + for (plugin, version) in sorted(option_manager.registered_plugins)] + + +def dependencies(): + """Generate the list of dependencies we care about.""" + return [{'dependency': 'setuptools', 'version': setuptools.__version__}] diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index c725c38..47fb30c 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -1,5 +1,6 @@ """Contains the logic for all of the default options for Flake8.""" from flake8 import defaults +from flake8.main import debug from flake8.main import vcs @@ -29,6 +30,8 @@ def register_default_options(option_manager): - ``--append-config`` - ``--config`` - ``--isolated`` + - ``--benchmark`` + - ``--bug-report`` """ add_option = option_manager.add_option @@ -199,3 +202,11 @@ def register_default_options(option_manager): '--benchmark', default=False, action='store_true', help='Print benchmark information about this run of Flake8', ) + + # Debugging + + add_option( + '--bug-report', action='callback', callback=debug.print_information, + callback_kwargs={'option_manager': option_manager}, + help='Print information necessary when preparing a bug report', + ) diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index de9356e..b536907 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -240,9 +240,10 @@ class OptionManager(object): LOG.debug('Extending default select list with %r', error_codes) self.extended_default_select.update(error_codes) - def generate_versions(self, format_str='%(name)s: %(version)s'): + def generate_versions(self, format_str='%(name)s: %(version)s', + join_on=', '): """Generate a comma-separated list of versions of plugins.""" - return ', '.join( + return join_on.join( format_str % self.format_plugin(plugin) for plugin in self.registered_plugins ) diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py new file mode 100644 index 0000000..dbafe2e --- /dev/null +++ b/tests/unit/test_debug.py @@ -0,0 +1,86 @@ +"""Tests for our debugging module.""" +import mock +import pytest +import setuptools + +from flake8.main import debug + + +def test_dependencies(): + """Verify that we format our dependencies appropriately.""" + expected = [{'dependency': 'setuptools', + 'version': setuptools.__version__}] + assert expected == debug.dependencies() + + +@pytest.mark.parametrize('plugins, expected', [ + ([], []), + ([('pycodestyle', '2.0.0')], [{'plugin': 'pycodestyle', + 'version': '2.0.0'}]), + ([('pycodestyle', '2.0.0'), ('mccabe', '0.5.9')], + [{'plugin': 'mccabe', 'version': '0.5.9'}, + {'plugin': 'pycodestyle', 'version': '2.0.0'}]), +]) +def test_plugins_from(plugins, expected): + """Test that we format plugins appropriately.""" + option_manager = mock.Mock(registered_plugins=set(plugins)) + assert expected == debug.plugins_from(option_manager) + + +@mock.patch('platform.python_implementation', return_value='CPython') +@mock.patch('platform.python_version', return_value='3.5.3') +@mock.patch('platform.system', return_value='Linux') +def test_information(system, pyversion, pyimpl): + """Verify that we return all the information we care about.""" + expected = { + 'version': '3.1.0', + 'plugins': [{'plugin': 'mccabe', 'version': '0.5.9'}, + {'plugin': 'pycodestyle', 'version': '2.0.0'}], + 'dependencies': [{'dependency': 'setuptools', + 'version': setuptools.__version__}], + 'platform': { + 'python_implementation': 'CPython', + 'python_version': '3.5.3', + 'system': 'Linux', + }, + } + option_manager = mock.Mock( + registered_plugins=set([('pycodestyle', '2.0.0'), + ('mccabe', '0.5.9')]), + version='3.1.0', + ) + assert expected == debug.information(option_manager) + pyimpl.assert_called_once_with() + pyversion.assert_called_once_with() + system.assert_called_once_with() + + +@mock.patch('flake8.main.debug.print') +@mock.patch('flake8.main.debug.information', return_value={}) +@mock.patch('json.dumps', return_value='{}') +def test_print_information_no_plugins(dumps, information, print_mock): + """Verify we print and exit only when we have plugins.""" + plugins = [] + option_manager = mock.Mock(registered_plugins=set(plugins)) + assert debug.print_information( + None, None, None, None, option_manager=option_manager, + ) is None + assert dumps.called is False + assert information.called is False + assert print_mock.called is False + + +@mock.patch('flake8.main.debug.print') +@mock.patch('flake8.main.debug.information', return_value={}) +@mock.patch('json.dumps', return_value='{}') +def test_print_information(dumps, information, print_mock): + """Verify we print and exit only when we have plugins.""" + plugins = [('pycodestyle', '2.0.0'), ('mccabe', '0.5.9')] + option_manager = mock.Mock(registered_plugins=set(plugins)) + with pytest.raises(SystemExit): + debug.print_information( + None, None, None, None, option_manager=option_manager, + ) + print_mock.assert_called_once_with('{}') + dumps.assert_called_once_with({}, indent=2, sort_keys=True) + information.assert_called_once_with(option_manager)