Merge branch 'feature/207' into 'master'

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

See merge request !117
This commit is contained in:
Ian Cordasco 2016-08-11 00:19:58 +00:00
commit e8cb26895e
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.
.. toctree::
3.1.0
3.0.4
3.0.3
3.0.2

View file

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

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."""
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',
)

View file

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

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)