Switch to hacking 3.0.1

In hacking 2.0 or later, local-check-factory was removed as it is not
compatible with flake8 3.x and it is advised to use flake8's local
plugins [1]. neutron-lib provided a factory to register common hacking
rules, but it no longer works with hacking 2, so we need to define rules
defined in neutron-lib as flake8 local check plugin [2] explicitly.
This needs to be done in each neutron related project, so it is the
downside of the migration to hacking 2.x (I explored a way to continue
to use the factory but failed to find a good way to achieve this) but
I believe it is good to migrate the newer libraries.

* flake8ext decorator in neutron/hacking/checks.py is also replaced with
  hacking.core.flake8ext to avoid the copy-and-paste code.
* neutron-lib dependency is updated as neutron-lib 2.3 added hacking 3 support.
* Python modules related to coding style checks (listed in blacklist.txt in
  openstack/requirements repo) are dropped from lower-constraints.txt
  as they are not actually used in tests (other than pep8).
* HackingDocTestCase is now converted into normal test cases.
  HackingDocTestCase depends on the internal of hacking and pycodestyle
  so it looks better to use normal style of writing tests.

[1] https://docs.openstack.org/releasenotes/hacking/unreleased.html#relnotes-2-0-0
[2] https://flake8.pycqa.org/en/3.7.0/user/configuration.html#using-local-plugins

Change-Id: I92cf50a84bb587a0649a7cffee15cce4ce37d086
This commit is contained in:
Akihiro Motoki 2020-02-21 06:09:58 +09:00 committed by Brian Haley
parent 08e9ec1b56
commit 52e3fee5ef
6 changed files with 82 additions and 161 deletions

View File

@ -2,9 +2,7 @@ alabaster==0.7.10
alembic==0.8.10 alembic==0.8.10
amqp==2.1.1 amqp==2.1.1
appdirs==1.4.3 appdirs==1.4.3
astroid==2.1.0
Babel==2.3.4 Babel==2.3.4
bandit==1.1.0
bashate==0.5.1 bashate==0.5.1
beautifulsoup4==4.6.0 beautifulsoup4==4.6.0
cachetools==2.0.0 cachetools==2.0.0
@ -25,14 +23,11 @@ eventlet==0.18.2
extras==1.0.0 extras==1.0.0
fasteners==0.7.0 fasteners==0.7.0
fixtures==3.0.0 fixtures==3.0.0
flake8-import-order==0.12
flake8==2.6.2
future==0.16.0 future==0.16.0
futurist==1.2.0 futurist==1.2.0
gitdb==0.6.4 gitdb==0.6.4
GitPython==1.0.1 GitPython==1.0.1
greenlet==0.4.10 greenlet==0.4.10
hacking==1.1.0
httplib2==0.9.1 httplib2==0.9.1
imagesize==0.7.1 imagesize==0.7.1
iso8601==0.1.11 iso8601==0.1.11
@ -49,7 +44,6 @@ logilab-common==1.4.1
logutils==0.3.5 logutils==0.3.5
Mako==0.4.0 Mako==0.4.0
MarkupSafe==1.0 MarkupSafe==1.0
mccabe==0.2.1
mock==3.0.0 mock==3.0.0
monotonic==0.6;python_version<'3.3' monotonic==0.6;python_version<'3.3'
mox3==0.20.0 mox3==0.20.0
@ -57,7 +51,7 @@ msgpack-python==0.4.0
munch==2.1.0 munch==2.1.0
netaddr==0.7.18 netaddr==0.7.18
netifaces==0.10.4 netifaces==0.10.4
neutron-lib==2.2.0 neutron-lib==2.3.0
openstackdocstheme==1.30.0 openstackdocstheme==1.30.0
openstacksdk==0.31.2 openstacksdk==0.31.2
os-client-config==1.28.0 os-client-config==1.28.0
@ -92,19 +86,15 @@ Paste==2.0.2
PasteDeploy==1.5.0 PasteDeploy==1.5.0
pbr==4.0.0 pbr==4.0.0
pecan==1.3.2 pecan==1.3.2
pep8==1.5.7
pika-pool==0.1.3 pika-pool==0.1.3
pika==0.10.0 pika==0.10.0
positional==1.2.1 positional==1.2.1
prettytable==0.7.2 prettytable==0.7.2
psutil==3.2.2 psutil==3.2.2
pycadf==1.1.0 pycadf==1.1.0
pycodestyle==2.4.0
pycparser==2.18 pycparser==2.18
pyflakes==0.8.1
Pygments==2.2.0 Pygments==2.2.0
pyinotify==0.9.6 pyinotify==0.9.6
pylint==2.2.0
PyMySQL==0.7.6 PyMySQL==0.7.6
pyOpenSSL==17.1.0 pyOpenSSL==17.1.0
pyparsing==2.1.0 pyparsing==2.1.0

View File

@ -15,17 +15,7 @@
import os import os
import re import re
from neutron_lib.hacking import checks from hacking import core
def flake8ext(f):
"""Decorator to indicate flake8 extension.
This is borrowed from hacking.core.flake8ext(), but at now it is used
only for unit tests to know which are neutron flake8 extensions.
"""
f.name = __name__
return f
# Guidelines for writing new hacking checks # Guidelines for writing new hacking checks
@ -50,7 +40,7 @@ tests_imports_from2 = re.compile(r"\bfrom[\s]+neutron[\s]+import[\s]+tests\b")
import_mock = re.compile(r"\bimport[\s]+mock\b") import_mock = re.compile(r"\bimport[\s]+mock\b")
@flake8ext @core.flake8ext
def check_assert_called_once_with(logical_line, filename): def check_assert_called_once_with(logical_line, filename):
"""N322 - Try to detect unintended calls of nonexistent mock methods like: """N322 - Try to detect unintended calls of nonexistent mock methods like:
assertCalledOnceWith assertCalledOnceWith
@ -74,7 +64,7 @@ def check_assert_called_once_with(logical_line, filename):
yield (0, msg) yield (0, msg)
@flake8ext @core.flake8ext
def check_asserttruefalse(logical_line, filename): def check_asserttruefalse(logical_line, filename):
"""N328 - Don't use assertEqual(True/False, observed).""" """N328 - Don't use assertEqual(True/False, observed)."""
if 'neutron/tests/' in filename: if 'neutron/tests/' in filename:
@ -96,7 +86,7 @@ def check_asserttruefalse(logical_line, filename):
yield (0, msg) yield (0, msg)
@flake8ext @core.flake8ext
def check_assertempty(logical_line, filename): def check_assertempty(logical_line, filename):
"""N330 - Enforce using assertEqual parameter ordering in case of empty """N330 - Enforce using assertEqual parameter ordering in case of empty
objects. objects.
@ -111,7 +101,7 @@ def check_assertempty(logical_line, filename):
yield (0, msg) yield (0, msg)
@flake8ext @core.flake8ext
def check_assertisinstance(logical_line, filename): def check_assertisinstance(logical_line, filename):
"""N331 - Enforce using assertIsInstance.""" """N331 - Enforce using assertIsInstance."""
if 'neutron/tests/' in filename: if 'neutron/tests/' in filename:
@ -122,7 +112,7 @@ def check_assertisinstance(logical_line, filename):
yield (0, msg) yield (0, msg)
@flake8ext @core.flake8ext
def check_assertequal_for_httpcode(logical_line, filename): def check_assertequal_for_httpcode(logical_line, filename):
"""N332 - Enforce correct oredering for httpcode in assertEqual.""" """N332 - Enforce correct oredering for httpcode in assertEqual."""
msg = ("N332: Use assertEqual(expected_http_code, observed_http_code) " msg = ("N332: Use assertEqual(expected_http_code, observed_http_code) "
@ -133,18 +123,9 @@ def check_assertequal_for_httpcode(logical_line, filename):
yield (0, msg) yield (0, msg)
@flake8ext @core.flake8ext
def check_oslo_i18n_wrapper(logical_line, filename, noqa): def check_oslo_i18n_wrapper(logical_line, filename, noqa):
"""N340 - Check for neutron.i18n usage. """N340 - Check for neutron.i18n usage."""
Okay(neutron/foo/bar.py): from neutron._i18n import _
Okay(neutron_fwaas/foo/bar.py): from neutron_fwaas._i18n import _
N340(neutron/foo/bar.py): from neutron.i18n import _
N340(neutron_fwaas/foo/bar.py): from neutron_fwaas.i18n import _
N340(neutron_fwaas/foo/bar.py): from neutron.i18n import _
N340(neutron_fwaas/foo/bar.py): from neutron._i18n import _
Okay(neutron/foo/bar.py): from neutron.i18n import _ # noqa
"""
if noqa: if noqa:
return return
@ -162,16 +143,9 @@ def check_oslo_i18n_wrapper(logical_line, filename, noqa):
yield (0, msg) yield (0, msg)
@flake8ext @core.flake8ext
def check_builtins_gettext(logical_line, tokens, filename, lines, noqa): def check_builtins_gettext(logical_line, tokens, filename, lines, noqa):
"""N341 - Check usage of builtins gettext _(). """N341 - Check usage of builtins gettext _()."""
Okay(neutron/foo.py): from neutron._i18n import _\n_('foo')
N341(neutron/foo.py): _('foo')
Okay(neutron/_i18n.py): _('foo')
Okay(neutron/i18n.py): _('foo')
Okay(neutron/foo.py): _('foo') # noqa
"""
if noqa: if noqa:
return return
@ -202,7 +176,7 @@ def check_builtins_gettext(logical_line, tokens, filename, lines, noqa):
yield (0, msg) yield (0, msg)
@flake8ext @core.flake8ext
def check_no_imports_from_tests(logical_line, filename, noqa): def check_no_imports_from_tests(logical_line, filename, noqa):
"""N343 - Production code must not import from neutron.tests.* """N343 - Production code must not import from neutron.tests.*
""" """
@ -219,7 +193,7 @@ def check_no_imports_from_tests(logical_line, filename, noqa):
yield(0, msg) yield(0, msg)
@flake8ext @core.flake8ext
def check_python3_no_filter(logical_line): def check_python3_no_filter(logical_line):
"""N344 - Use list comprehension instead of filter(lambda).""" """N344 - Use list comprehension instead of filter(lambda)."""
@ -231,7 +205,7 @@ def check_python3_no_filter(logical_line):
# TODO(boden): rehome this check to neutron-lib # TODO(boden): rehome this check to neutron-lib
@flake8ext @core.flake8ext
def check_no_sqlalchemy_event_import(logical_line, filename, noqa): def check_no_sqlalchemy_event_import(logical_line, filename, noqa):
"""N346 - Use neutron_lib.db.api.sqla_listen rather than sqlalchemy.""" """N346 - Use neutron_lib.db.api.sqla_listen rather than sqlalchemy."""
if noqa: if noqa:
@ -248,7 +222,7 @@ def check_no_sqlalchemy_event_import(logical_line, filename, noqa):
"between unit tests") "between unit tests")
@flake8ext @core.flake8ext
def check_no_import_mock(logical_line, filename, noqa): def check_no_import_mock(logical_line, filename, noqa):
"""N347 - Test code must not import mock library """N347 - Test code must not import mock library
""" """
@ -262,18 +236,3 @@ def check_no_import_mock(logical_line, filename, noqa):
if re.match(import_mock, logical_line): if re.match(import_mock, logical_line):
yield(0, msg) yield(0, msg)
def factory(register):
checks.factory(register)
register(check_assert_called_once_with)
register(check_asserttruefalse)
register(check_assertempty)
register(check_assertisinstance)
register(check_assertequal_for_httpcode)
register(check_oslo_i18n_wrapper)
register(check_builtins_gettext)
register(check_no_imports_from_tests)
register(check_python3_no_filter)
register(check_no_sqlalchemy_event_import)
register(check_no_import_mock)

View File

@ -10,16 +10,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import io
import re import re
import tokenize
from flake8 import engine
from hacking.tests import test_doctest as hacking_doctest
import pkg_resources
import pycodestyle
import testscenarios
import testtools import testtools
from testtools import content
from testtools import matchers
from neutron.hacking import checks from neutron.hacking import checks
from neutron.tests import base from neutron.tests import base
@ -30,12 +25,14 @@ CREATE_DUMMY_MATCH_OBJECT = re.compile('a')
class HackingTestCase(base.BaseTestCase): class HackingTestCase(base.BaseTestCase):
def assertLinePasses(self, func, line): def assertLinePasses(self, func, line, *args, **kwargs):
with testtools.ExpectedException(StopIteration): with testtools.ExpectedException(StopIteration):
next(func(line)) next(func(line, *args, **kwargs))
def assertLineFails(self, func, line): def assertLineFails(self, expected_code, func, line, *args, **kwargs):
self.assertIsInstance(next(func(line)), tuple) value = next(func(line, *args, **kwargs))
self.assertIsInstance(value, tuple)
self.assertIn(expected_code, value[1])
def test_assert_called_once_with(self): def test_assert_called_once_with(self):
fail_code2 = """ fail_code2 = """
@ -201,9 +198,9 @@ class HackingTestCase(base.BaseTestCase):
checks.check_no_imports_from_tests( checks.check_no_imports_from_tests(
fail_code, "neutron/tests/test_fake.py", None)))) fail_code, "neutron/tests/test_fake.py", None))))
def test_check_python3_filter(self): def test_check_python3_no_filter(self):
f = checks.check_python3_no_filter f = checks.check_python3_no_filter
self.assertLineFails(f, "filter(lambda obj: test(obj), data)") self.assertLineFails('N344', f, "filter(lambda obj: test(obj), data)")
self.assertLinePasses(f, "[obj for obj in data if test(obj)]") self.assertLinePasses(f, "[obj for obj in data if test(obj)]")
self.assertLinePasses(f, "filter(function, range(0,10))") self.assertLinePasses(f, "filter(function, range(0,10))")
self.assertLinePasses(f, "lambda x, y: x+y") self.assertLinePasses(f, "lambda x, y: x+y")
@ -226,88 +223,50 @@ class HackingTestCase(base.BaseTestCase):
checks.check_no_import_mock( checks.check_no_import_mock(
fail_line, "neutron/tests/test_fake.py", None)))) fail_line, "neutron/tests/test_fake.py", None))))
def test_check_oslo_i18n_wrapper(self):
def _pass(line, filename, noqa=False):
self.assertLinePasses(
checks.check_oslo_i18n_wrapper,
line, filename, noqa)
# The following is borrowed from hacking/tests/test_doctest.py. def _fail(line, filename):
# Tests defined in docstring is easier to understand self.assertLineFails(
# in some cases, for example, hacking rules which take tokens as argument. "N340", checks.check_oslo_i18n_wrapper,
line, filename, noqa=False)
# TODO(amotoki): Migrate existing unit tests above to docstring tests. _pass("from neutron._i18n import _", "neutron/foo/bar.py")
# NOTE(amotoki): Is it better to enhance HackingDocTestCase in hacking repo to _pass("from neutron_fwaas._i18n import _", "neutron_fwaas/foo/bar.py")
# pass filename to pycodestyle.Checker so that we can reuse it in this test. _fail("from neutron.i18n import _", "neutron/foo/bar.py")
# I am not sure whether unit test class is public. _fail("from neutron_fwaas.i18n import _", "neutron_fwaas/foo/bar.py")
_fail("from neutron.i18n import _", "neutron_fwaas/foo/bar.py")
_fail("from neutron._i18n import _", "neutron_fwaas/foo/bar.py")
_pass("from neutron.i18n import _", "neutron/foo/bar.py", noqa=True)
SELFTEST_REGEX = re.compile(r'\b(Okay|N\d{3})(\((\S+)\))?:\s(.*)') def test_check_builtins_gettext(self):
# NOTE: check_builtins_gettext() takes two additional arguments,
# "tokens" and "lines". "tokens" is a list of tokens from the target
# logical line, and "lines" is a list of lines of the input file.
# Considering this, test functions (_pass and _fail) take "lines"
# as an argument and calls the hacking check function line by line
# after generating tokens from the target line.
def _get_tokens(line):
return tokenize.tokenize(io.BytesIO(line.encode('utf-8')).readline)
# Each scenario is (name, dict(filename=..., lines=.., options=..., code=...)) def _pass(lines, filename, noqa=False):
file_cases = [] for line in lines:
self.assertLinePasses(
checks.check_builtins_gettext,
line, _get_tokens(line), filename, lines, noqa)
def _fail(lines, filename):
for line in lines:
self.assertLineFails(
"N341", checks.check_builtins_gettext,
line, _get_tokens(line), filename, lines, noqa=False)
class HackingDocTestCase(hacking_doctest.HackingTestCase): _pass(["from neutron._i18n import _", "_('foo')"], "neutron/foo.py")
_fail(["_('foo')"], "neutron/foo.py")
scenarios = file_cases _pass(["_('foo')"], "neutron/_i18n.py")
_pass(["_('foo')"], "neutron/i18n.py")
def test_pycodestyle(self): _pass(["_('foo')"], "neutron/foo.py", noqa=True)
# NOTE(jecarey): Add tests marked as off_by_default to enable testing
turn_on = set(['H106'])
if self.options.select:
turn_on.update(self.options.select)
self.options.select = tuple(turn_on)
self.options.ignore = ('N530',)
report = pycodestyle.BaseReport(self.options)
checker = pycodestyle.Checker(filename=self.filename, lines=self.lines,
options=self.options, report=report)
checker.check_all()
self.addDetail('doctest', content.text_content(self.raw))
if self.code == 'Okay':
self.assertThat(
len(report.counters),
matchers.Not(matchers.GreaterThan(
len(self.options.benchmark_keys))),
"incorrectly found %s" % ', '.join(
[key for key in report.counters
if key not in self.options.benchmark_keys]))
else:
self.addDetail('reason',
content.text_content("Failed to trigger rule %s" %
self.code))
self.assertIn(self.code, report.counters)
def _get_lines(check):
for line in check.__doc__.splitlines():
line = line.lstrip()
match = SELFTEST_REGEX.match(line)
if match is None:
continue
yield (line, match.groups())
def load_tests(loader, tests, pattern):
default_checks = [e.name for e
in pkg_resources.iter_entry_points('flake8.extension')]
flake8_style = engine.get_style_guide(
parse_argv=False,
# We are testing neutron-specific hacking rules, so there is no need
# to run the checks registered by hacking or other flake8 extensions.
ignore=default_checks)
options = flake8_style.options
for name, check in checks.__dict__.items():
if not hasattr(check, 'name'):
continue
if check.name != checks.__name__:
continue
if not check.__doc__:
continue
for (lineno, (raw, line)) in enumerate(_get_lines(check)):
code, __, filename, source = line
lines = [part.replace(r'\t', '\t') + '\n'
for part in source.split(r'\n')]
file_cases.append(("%s-line-%s" % (name, lineno),
dict(lines=lines, raw=raw, options=options,
code=code, filename=filename)))
return testscenarios.load_tests_apply_scenarios(loader, tests, pattern)

View File

@ -16,7 +16,7 @@ Jinja2>=2.10 # BSD License (3 clause)
keystonemiddleware>=4.17.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0
netaddr>=0.7.18 # BSD netaddr>=0.7.18 # BSD
netifaces>=0.10.4 # MIT netifaces>=0.10.4 # MIT
neutron-lib>=2.2.0 # Apache-2.0 neutron-lib>=2.3.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0
tenacity>=4.4.0 # Apache-2.0 tenacity>=4.4.0 # Apache-2.0
SQLAlchemy>=1.2.0 # MIT SQLAlchemy>=1.2.0 # MIT

View File

@ -1,12 +1,11 @@
# The order of packages is significant, because pip processes them in the order # The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration # of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
hacking>=1.1.0,<1.2.0 # Apache-2.0 hacking>=3.0.1,<3.1.0 # Apache-2.0
bandit!=1.6.0,>=1.1.0 # Apache-2.0 bandit!=1.6.0,>=1.1.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD fixtures>=3.0.0 # Apache-2.0/BSD
flake8-import-order==0.12 # LGPLv3 flake8-import-order==0.12 # LGPLv3
pycodestyle>=2.0.0 # MIT
python-subunit>=1.0.0 # Apache-2.0/BSD python-subunit>=1.0.0 # Apache-2.0/BSD
testtools>=2.2.0 # MIT testtools>=2.2.0 # MIT
testresources>=2.0.0 # Apache-2.0/BSD testresources>=2.0.0 # Apache-2.0/BSD

16
tox.ini
View File

@ -169,9 +169,23 @@ show-source = true
exclude = ./.*,build,dist,doc exclude = ./.*,build,dist,doc
import-order-style = pep8 import-order-style = pep8
[flake8:local-plugins]
extension =
# Checks specific to neutron repo
N340 = neutron.hacking.checks:check_oslo_i18n_wrapper
N341 = neutron.hacking.checks:check_builtins_gettext
# Checks from neutron-lib
N521 = neutron_lib.hacking.checks:use_jsonutils
N524 = neutron_lib.hacking.checks:check_no_contextlib_nested
N529 = neutron_lib.hacking.checks:no_mutable_default_args
N530 = neutron_lib.hacking.checks:check_neutron_namespace_imports
N532 = neutron_lib.hacking.translation_checks:check_log_warn_deprecated
N534 = neutron_lib.hacking.translation_checks:check_raised_localized_exceptions
N536 = neutron_lib.hacking.checks:assert_equal_none
N537 = neutron_lib.hacking.translation_checks:no_translate_logs
[hacking] [hacking]
import_exceptions = neutron._i18n import_exceptions = neutron._i18n
local-check-factory = neutron.hacking.checks.factory
[testenv:bandit] [testenv:bandit]
envdir = {toxworkdir}/shared envdir = {toxworkdir}/shared