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
This commit is contained in:
Ian Cordasco 2016-08-06 14:16:08 -05:00
parent f768ed6fd0
commit f67f481bee
No known key found for this signature in database
GPG key ID: 656D3395E4A9791A
7 changed files with 215 additions and 2 deletions

View file

@ -0,0 +1,4 @@
3.1.0 -- 2016-yy-xx
-------------------
- Add ``--bug-report`` flag to make issue reporters' lives easier.

View file

@ -6,6 +6,7 @@ All of the release notes that have been recorded for Flake8 are organized here
with the newest releases first. with the newest releases first.
.. toctree:: .. toctree::
3.1.0
3.0.4 3.0.4
3.0.3 3.0.3
3.0.2 3.0.2

View file

@ -728,3 +728,51 @@
flake8 --benchmark dir/ flake8 --benchmark dir/
This **can not** be specified in config files. 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.

62
src/flake8/main/debug.py Normal file
View file

@ -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__}]

View file

@ -1,5 +1,6 @@
"""Contains the logic for all of the default options for Flake8.""" """Contains the logic for all of the default options for Flake8."""
from flake8 import defaults from flake8 import defaults
from flake8.main import debug
from flake8.main import vcs from flake8.main import vcs
@ -29,6 +30,8 @@ def register_default_options(option_manager):
- ``--append-config`` - ``--append-config``
- ``--config`` - ``--config``
- ``--isolated`` - ``--isolated``
- ``--benchmark``
- ``--bug-report``
""" """
add_option = option_manager.add_option add_option = option_manager.add_option
@ -199,3 +202,11 @@ def register_default_options(option_manager):
'--benchmark', default=False, action='store_true', '--benchmark', default=False, action='store_true',
help='Print benchmark information about this run of Flake8', 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',
)

View file

@ -240,9 +240,10 @@ class OptionManager(object):
LOG.debug('Extending default select list with %r', error_codes) LOG.debug('Extending default select list with %r', error_codes)
self.extended_default_select.update(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.""" """Generate a comma-separated list of versions of plugins."""
return ', '.join( return join_on.join(
format_str % self.format_plugin(plugin) format_str % self.format_plugin(plugin)
for plugin in self.registered_plugins for plugin in self.registered_plugins
) )

86
tests/unit/test_debug.py Normal file
View file

@ -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)