diff --git a/Tools/PyUtils/CMakeLists.txt b/Tools/PyUtils/CMakeLists.txt index cdeb97162f24e768bd38d5d82bd509a0cdc96b29..48f93dfc0068f0755a0c989f6b1fb60232e740a7 100644 --- a/Tools/PyUtils/CMakeLists.txt +++ b/Tools/PyUtils/CMakeLists.txt @@ -60,5 +60,9 @@ atlas_add_test( RootUtils EXTRA_PATTERNS "Ran 1 test in |CheckABICompatibility|standard library" ) atlas_add_test( flake8_OutputLevel - SCRIPT flake8 --enable-extensions=ATL100 --ignore=E,F,W --stdin-display-name=flake8_OutputLevel.py - --exit-zero --isolated - < ${CMAKE_CURRENT_SOURCE_DIR}/test/flake8_OutputLevel.py ) + SCRIPT flake8 --enable-extensions=ATL900 --select=ATL --stdin-display-name=flake8_OutputLevel.py + --exit-zero --isolated - < ${CMAKE_CURRENT_SOURCE_DIR}/python/flake8_atlas/test/flake8_OutputLevel.py ) + +atlas_add_test( flake8_logging + SCRIPT flake8 --enable-extensions=ATL901 --select=ATL --stdin-display-name=flake8_logging.py + --exit-zero --isolated - < ${CMAKE_CURRENT_SOURCE_DIR}/python/flake8_atlas/test/flake8_logging.py ) diff --git a/Tools/PyUtils/python/flake8_atlas/README.md b/Tools/PyUtils/python/flake8_atlas/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1baf12ccce746328e6493063687581ec86e7f36a --- /dev/null +++ b/Tools/PyUtils/python/flake8_atlas/README.md @@ -0,0 +1,17 @@ +# ATLAS plugins for flake8 + +This module contains ATLAS-specific `flake8_atlas` plugins. It is built by default in the +athena release and available after running `asetup`. To verify if the plugin is found +run: +``` +> flake8 --version +3.6.0 (flake8_atlas: 1.0, mccabe: 0.6.1, pycodestyle: 2.4.0, pyflakes: 2.0.0) CPython 2.7.15 on Linux +``` + +Plugins with codes `>=ATL900` are disabled by default. To enable them use +``` +flake8 --enable-extension=ATL901 +``` +In addition to ATLAS specific plugins most of the `python23.py` plugins from +the [hacking](https://github.com/openstack-dev/hacking) plugin by OpenStack +were imported (and some modified). \ No newline at end of file diff --git a/Tools/PyUtils/python/flake8_atlas/checks.py b/Tools/PyUtils/python/flake8_atlas/checks.py index 6d5a7a0df6f808cdc17937c9efffd28bf60cfdfb..49a26cd83c60903d6703964e78ffccc1f9654a14 100644 --- a/Tools/PyUtils/python/flake8_atlas/checks.py +++ b/Tools/PyUtils/python/flake8_atlas/checks.py @@ -4,15 +4,38 @@ Documentation: http://flake8.pycqa.org/en/latest/plugin-development """ -from utils import flake8_atlas, off_by_default +from PyUtils.flake8_atlas import utils import ast +import re -@flake8_atlas -@off_by_default -class OutputLevel(object): - """Check if an explicit OutputLevel is set""" +# Inspired by: https://github.com/openstack-dev/hacking/blob/master/hacking/checks/other.py +RE_LOGGING = re.compile(r".*\.(?:error|warn|warning|info|debug)" + r"\([^,]*(%)[^,]*[,)]") +@utils.flake8_atlas +def delayed_string_interpolation(logical_line): + r"""String interpolation should be delayed at logging calls. + ATL101: log.debug('Example: %s' % 'bad') + Okay: log.debug('Example: %s', 'good') + """ + msg = ("ATL100: use lazy string formatting in logging calls (',' instead of '%')") + + m = RE_LOGGING.match(logical_line) + if m is not None: + col = m.start(1) + yield (col-1, msg) + + +###################################################################### +# ATL9xy: Specialized plugins (disabled by default) +###################################################################### - code = ('ATL100: Do not assign an explicit OutputLevel', 'ATL100') +@utils.flake8_atlas +@utils.off_by_default +class OutputLevel(object): + """Check if an explicit OutputLevel is set + ATL900: myalg.OutputLevel = DEBUG + """ + msg = ('ATL900: Do not assign an explicit OutputLevel', 'ATL900') def __init__(self, tree): self.tree = tree @@ -24,10 +47,21 @@ class OutputLevel(object): if isinstance(node, ast.Assign): for t in node.targets: if isinstance(t,ast.Attribute) and t.attr=='OutputLevel': - yield (node.lineno, node.col_offset) + self.code + yield (node.lineno, node.col_offset) + self.msg # Find: setattr(c,'OutputLevel',DEBUG) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id=='setattr': a = node.args[1] if isinstance(a, ast.Str) and a.s=='OutputLevel': - yield (node.lineno, node.col_offset) + self.code + yield (node.lineno, node.col_offset) + self.msg + + +RE_PRINT = re.compile(r"\bprint") +@utils.flake8_atlas +@utils.off_by_default +def print_for_logging(logical_line): + """Check for occurences of plain 'print'""" + + for match in RE_PRINT.finditer(logical_line): + yield match.start(0), ( + "ATL901: use 'AthenaCommon.Logging' instead of 'print'") diff --git a/Tools/PyUtils/python/flake8_atlas/python23.py b/Tools/PyUtils/python/flake8_atlas/python23.py new file mode 100644 index 0000000000000000000000000000000000000000..42d804630816297fe90ea207fe363a85aa39156f --- /dev/null +++ b/Tools/PyUtils/python/flake8_atlas/python23.py @@ -0,0 +1,224 @@ +# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration +# +# This is a modified version of the 'hacking' flake8 plugin: +# https://github.com/openstack-dev/hacking +# +# The unmodified original checks are kept with the "hacking_" prefix. +# For modified versions (including bugfixes) the prefix was removed. +# + +# Original licence: +# ----------------- +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from PyUtils.flake8_atlas import utils +import re +import tokenize + + +def import_normalize(line): + # convert "from x import y" to "import x.y" + # handle "from x import y as z" to "import x.y as z" + split_line = line.split() + if ("import" in line and line.startswith("from ") and "," not in line and + split_line[2] == "import" and split_line[3] != "*" and + split_line[1] != "__future__" and + (len(split_line) == 4 or + (len(split_line) == 6 and split_line[4] == "as"))): + return "import %s.%s" % (split_line[1], split_line[3]) + else: + return line + + +#def hacking_python3x_except_compatible(logical_line, noqa): +#Deleted as this is covered by pycodestyle E722 + +RE_OCTAL = re.compile(r"0+([1-9]\d*)") + +@utils.flake8_atlas +def hacking_python3x_octal_literals(logical_line, tokens, noqa): + r"""Check for octal literals in Python 3.x compatible form. + + As of Python 3.x, the construct "0755" has been removed. + Use "0o755" instead". + + Okay: f(0o755) + Okay: f(0755) + Okay: f(755) + Okay: f(0) + Okay: f(000) + Okay: MiB = 1.0415 + ATL232: f(0755) + Okay: f(0755) # noqa + """ + if noqa: + return + + for token_type, text, _, _, _ in tokens: + if token_type == tokenize.NUMBER: + match = RE_OCTAL.match(text) + if match: + yield 0, ("ATL232: Python 3.x incompatible octal %s should be " + "written as 0o%s " % + (match.group(0)[1:], match.group(1))) + + +RE_PRINT = re.compile(r"\bprint(?:$|\s+[^\(])") + +@utils.flake8_atlas +def hacking_python3x_print_function(logical_line, noqa): + r"""Check that all print occurrences look like print functions. + + Check that all occurrences of print look like functions, not + print operator. As of Python 3.x, the print operator has + been removed. + + Okay: print(msg) + Okay: print (msg) + Okay: print msg # noqa + Okay: print() + ATL233: print msg + ATL233: print >>sys.stderr, "hello" + ATL233: print msg, + ATL233: print + """ + if noqa: + return + for match in RE_PRINT.finditer(logical_line): + yield match.start(0), ( + "ATL233: Python 3.x incompatible use of print operator") + + +@utils.flake8_atlas +def hacking_no_assert_equals(logical_line, tokens, noqa): + r"""assert(Not)Equals() is deprecated, use assert(Not)Equal instead. + + Okay: self.assertEqual(0, 0) + Okay: self.assertNotEqual(0, 1) + ATL234: self.assertEquals(0, 0) + ATL234: self.assertNotEquals(0, 1) + Okay: self.assertEquals(0, 0) # noqa + Okay: self.assertNotEquals(0, 1) # noqa + """ + + if noqa: + return + for token_type, text, start_index, _, _ in tokens: + + if token_type == tokenize.NAME: + if text == "assertEquals" or text == "assertNotEquals": + yield (start_index[1], + "ATL234: %s is deprecated, use %s" % (text, text[:-1])) + + +@utils.flake8_atlas +def hacking_no_assert_underscore(logical_line, tokens, noqa): + r"""assert_() is deprecated, use assertTrue instead. + + Okay: self.assertTrue(foo) + ATL235: self.assert_(foo) + Okay: self.assert_(foo) # noqa + """ + if noqa: + return + for token_type, text, start_index, _, _ in tokens: + + if token_type == tokenize.NAME and text == "assert_": + yield ( + start_index[1], + "ATL235: assert_ is deprecated, use assertTrue") + + +@utils.flake8_atlas +def hacking_python3x_metaclass(logical_line, noqa): + r"""Check for metaclass to be Python 3.x compatible. + + Okay: @six.add_metaclass(Meta)\nclass Foo(object):\n pass + Okay: @six.with_metaclass(Meta)\nclass Foo(object):\n pass + Okay: class Foo(object):\n '''docstring\n\n __metaclass__ = Meta\n''' + ATL236: class Foo(object):\n __metaclass__ = Meta + ATL236: class Foo(object):\n foo=bar\n __metaclass__ = Meta + ATL236: class Foo(object):\n '''docstr.'''\n __metaclass__ = Meta + ATL236: class Foo(object):\n __metaclass__ = \\\n Meta + Okay: class Foo(object):\n __metaclass__ = Meta # noqa + """ + if noqa: + return + split_line = logical_line.split() + if(len(split_line) > 2 and split_line[0] == '__metaclass__' and + split_line[1] == '='): + yield (logical_line.find('__metaclass__'), + "ATL236: Python 3.x incompatible __metaclass__, " + "use six.add_metaclass()") + + +# NOTE(guochbo): This is removed module list: +# http://python3porting.com/stdlib.html#removed-modules +removed_modules = [ + 'audiodev', 'Bastion', 'bsddb185', 'bsddb3', + 'Canvas', 'cfmfile', 'cl', 'commands', 'compiler' + 'dircache', 'dl', 'exception', 'fpformat', + 'htmllib', 'ihooks', 'imageop', 'imputil' + 'linuxaudiodev', 'md5', 'mhlib', 'mimetools' + 'MimeWriter', 'mimify', 'multifile', 'mutex', + 'new', 'popen2', 'posixfile', 'pure', 'rexec' + 'rfc822', 'sha', 'sgmllib', 'sre', 'stat' + 'stringold', 'sunaudio' 'sv', 'test.testall', + 'thread', 'timing', 'toaiff', 'user' +] + + +@utils.flake8_atlas +def hacking_no_removed_module(logical_line, noqa): + r"""Check for removed modules in Python 3. + + Examples: + Okay: from os import path + Okay: from os import path as p + Okay: from os import (path as p) + Okay: import os.path + ATL237: import thread + Okay: import thread # noqa + ATL237: import commands + ATL237: import md5 as std_md5 + """ + if noqa: + return + line = import_normalize(logical_line.strip()) + if line and line.split()[0] == 'import': + module_name = line.split()[1].split('.')[0] + if module_name in removed_modules: + yield 0, ("ATL237: module %s is " + "removed in Python 3" % module_name) + + +RE_NEW_STYLE_CLASS = re.compile(r"^class[^(]+\(.+\):") + +@utils.flake8_atlas +def no_old_style_class(logical_line, noqa): + r"""Check for old style classes. + + Examples: + Okay: class Foo(object):\n pass + Okay: class Foo(Bar, Baz):\n pass + Okay: class Foo(object, Baz):\n pass + Okay: class Foo(somefunc()):\n pass + ATL238: class Bar:\n pass + ATL238: class Bar():\n pass + """ + if noqa: + return + line = logical_line.replace(' ','') + if line.startswith("class") and not RE_NEW_STYLE_CLASS.match(line): + yield (0, "ATL238: old style class declaration, " + "use new style (inherit from `object`)") diff --git a/Tools/PyUtils/python/flake8_atlas/setup.py b/Tools/PyUtils/python/flake8_atlas/setup.py index 51847beed3c4c879e84115a70cfa469c0515982d..ad4c4bb09b593d493c3ecf3ced55617e7cbe3feb 100644 --- a/Tools/PyUtils/python/flake8_atlas/setup.py +++ b/Tools/PyUtils/python/flake8_atlas/setup.py @@ -14,7 +14,16 @@ setuptools.setup( description="ATLAS plugins for flake8", entry_points={ 'flake8.extension': [ - 'ATL100 = PyUtils.flake8_atlas.checks:OutputLevel' + 'ATL100 = PyUtils.flake8_atlas.checks:delayed_string_interpolation', + 'ATL232 = PyUtils.flake8_atlas.python23:hacking_python3x_octal_literals', + 'ATL233 = PyUtils.flake8_atlas.python23:hacking_python3x_print_function', + 'ATL234 = PyUtils.flake8_atlas.python23:hacking_no_assert_equals', + 'ATL235 = PyUtils.flake8_atlas.python23:hacking_no_assert_underscore', + 'ATL236 = PyUtils.flake8_atlas.python23:hacking_python3x_metaclass', + 'ATL237 = PyUtils.flake8_atlas.python23:hacking_no_removed_module', + 'ATL238 = PyUtils.flake8_atlas.python23:no_old_style_class', + 'ATL900 = PyUtils.flake8_atlas.checks:OutputLevel', + 'ATL901 = PyUtils.flake8_atlas.checks:print_for_logging', ], } ) diff --git a/Tools/PyUtils/test/flake8_OutputLevel.py b/Tools/PyUtils/python/flake8_atlas/test/flake8_OutputLevel.py similarity index 100% rename from Tools/PyUtils/test/flake8_OutputLevel.py rename to Tools/PyUtils/python/flake8_atlas/test/flake8_OutputLevel.py diff --git a/Tools/PyUtils/python/flake8_atlas/test/flake8_logging.py b/Tools/PyUtils/python/flake8_atlas/test/flake8_logging.py new file mode 100644 index 0000000000000000000000000000000000000000..e4f5fe5d425d0be89b1f88c1aac94f72b20299c7 --- /dev/null +++ b/Tools/PyUtils/python/flake8_atlas/test/flake8_logging.py @@ -0,0 +1,24 @@ +# +# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration +# +# Test for logging related plugins + +import logging +logging.basicConfig(level=logging.INFO) +log = logging.getLogger() +myfunnyloggername = log + +log.info('This is %s logging practice' % 'bad') +log.info('This is %s logging practice', 'good') +myfunnyloggername.warning('Hello %s' % 'world') + +log.info('This is %s logging practice: %d' % ('bad',42)) +log.info('This is %s logging practice: %d' % ('bad',42)) # noqa +log.info('This is %s logging practice: %d', 'good', 42) + +print("Hello world") +print("Hello world") # noqa +print "Hello world" + +def myprint(s): pass +myprint("Function that ends with print is OK") diff --git a/Tools/PyUtils/python/flake8_atlas/utils.py b/Tools/PyUtils/python/flake8_atlas/utils.py index 730c2a24edc93f4653122252bc25a13e10e3c9b0..a914d2a4669030dd27ab9a77ef9d4576489401e8 100644 --- a/Tools/PyUtils/python/flake8_atlas/utils.py +++ b/Tools/PyUtils/python/flake8_atlas/utils.py @@ -5,6 +5,8 @@ def flake8_atlas(f): """Default decorator for flake8 plugins""" f.name = 'flake8_atlas' f.version = '1.0' + if not hasattr(f, 'off_by_default'): + f.off_by_default = False return f def off_by_default(f): diff --git a/Tools/PyUtils/share/flake8_OutputLevel.ref b/Tools/PyUtils/share/flake8_OutputLevel.ref index b5aafa3f51436d8e2003dbc83d62be44a395e95c..e48d4980e4e4795ae1911a6ee313887ccc7d6366 100644 --- a/Tools/PyUtils/share/flake8_OutputLevel.ref +++ b/Tools/PyUtils/share/flake8_OutputLevel.ref @@ -1,3 +1,3 @@ -flake8_OutputLevel.py:10:7: ATL100: Do not assign an explicit OutputLevel -flake8_OutputLevel.py:14:1: ATL100: Do not assign an explicit OutputLevel -flake8_OutputLevel.py:16:1: ATL100: Do not assign an explicit OutputLevel +flake8_OutputLevel.py:10:7: ATL900: Do not assign an explicit OutputLevel +flake8_OutputLevel.py:14:1: ATL900: Do not assign an explicit OutputLevel +flake8_OutputLevel.py:16:1: ATL900: Do not assign an explicit OutputLevel diff --git a/Tools/PyUtils/share/flake8_logging.ref b/Tools/PyUtils/share/flake8_logging.ref new file mode 100644 index 0000000000000000000000000000000000000000..7cc15218e18c06ffb3d3f0bf101120b680ffa968 --- /dev/null +++ b/Tools/PyUtils/share/flake8_logging.ref @@ -0,0 +1,6 @@ +flake8_logging.py:11:39: ATL100: use lazy string formatting in logging calls (',' instead of '%') +flake8_logging.py:13:37: ATL100: use lazy string formatting in logging calls (',' instead of '%') +flake8_logging.py:15:43: ATL100: use lazy string formatting in logging calls (',' instead of '%') +flake8_logging.py:19:1: ATL901: use 'AthenaCommon.Logging' instead of 'print' +flake8_logging.py:21:1: ATL233: Python 3.x incompatible use of print operator +flake8_logging.py:21:1: ATL901: use 'AthenaCommon.Logging' instead of 'print'