[initial-setup 1/2] Add pylint testing infrastructure.

mulhern amulhern at redhat.com
Thu Dec 4 14:32:43 UTC 2014


Signed-off-by: mulhern <amulhern at redhat.com>
---
 .gitignore                          |   1 +
 tests/lib/testlib.sh                |  37 ++++++
 tests/pylint/intl.py                |  63 +++++++++
 tests/pylint/pointless-override.py  | 252 ++++++++++++++++++++++++++++++++++++
 tests/pylint/pylint-false-positives |   0
 tests/pylint/pylint-one.sh          |  33 +++++
 tests/pylint/runpylint.sh           | 125 ++++++++++++++++++
 tests/testenv.sh                    |  13 ++
 8 files changed, 524 insertions(+)
 create mode 100644 tests/lib/testlib.sh
 create mode 100644 tests/pylint/intl.py
 create mode 100644 tests/pylint/pointless-override.py
 create mode 100644 tests/pylint/pylint-false-positives
 create mode 100755 tests/pylint/pylint-one.sh
 create mode 100755 tests/pylint/runpylint.sh
 create mode 100644 tests/testenv.sh

diff --git a/.gitignore b/.gitignore
index 9bb588d..839c364 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
 po/*.po
 po/*.gmo
 po/tmp
+tests/pylint/.pylint.d
diff --git a/tests/lib/testlib.sh b/tests/lib/testlib.sh
new file mode 100644
index 0000000..896d884
--- /dev/null
+++ b/tests/lib/testlib.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# Shell functions for use by anaconda tests
+#
+# Copyright (C) 2014  Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: David Shea <dshea at redhat.com>
+
+# Print a list of files to test on stdout
+# Takes filter arguments identical to the find utility, for example
+# findtestfiles -name '*.py'. Note that pruning directories will not
+# work since find is passed a list of filenames as the path arguments.
+findtestfiles()
+{
+    # If the test is being run from a git work tree, use a list of all files
+    # known to git
+    if [ -d "${top_srcdir}/.git" ]; then
+        findpath=$(git ls-files -c "${top_srcdir}")
+    # Otherwise list everything under $top_srcdir
+    else
+        findpath="${top_srcdir} -type f"
+    fi
+
+    find $findpath "$@"
+}
diff --git a/tests/pylint/intl.py b/tests/pylint/intl.py
new file mode 100644
index 0000000..3052680
--- /dev/null
+++ b/tests/pylint/intl.py
@@ -0,0 +1,63 @@
+# I18N-related pylint module
+#
+# Copyright (C) 2013  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.  You should have received a copy of the
+# GNU General Public License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): Chris Lumens <clumens at redhat.com>
+#
+
+import astroid
+
+from pylint.checkers import BaseChecker
+from pylint.checkers.utils import check_messages
+from pylint.interfaces import IAstroidChecker
+
+translationMethods = ["_", "N_", "P_", "C_", "CN_", "CP_"]
+
+class IntlChecker(BaseChecker):
+    __implements__ = (IAstroidChecker, )
+    name = "internationalization"
+    msgs = {"W9901": ("Found % in a call to a _() method",
+                      "found-percent-in-_",
+                      "% in a call to one of the _() methods results in incorrect translations"),
+            "W9902": ("Found _ call at module/class level",
+                      "found-_-in-module-class",
+                      "Calling _ at the module or class level results in translations to the wrong language")
+           }
+
+    @check_messages("W9901")
+    def visit_binop(self, node):
+        if node.op != "%":
+            return
+
+        curr = node
+        while curr.parent:
+            if isinstance(curr.parent, astroid.CallFunc) and getattr(curr.parent.func, "name", "") in translationMethods:
+                self.add_message("W9901", node=node)
+                break
+
+            curr = curr.parent
+
+    @check_messages("W9902")
+    def visit_callfunc(self, node):
+        # The first test skips internal functions like getattr.
+        if isinstance(node.func, astroid.Name) and node.func.name == "_":
+            if isinstance(node.scope(), astroid.Module) or isinstance(node.scope(), astroid.Class):
+                self.add_message("W9902", node=node)
+
+def register(linter):
+    """required method to auto register this checker """
+    linter.register_checker(IntlChecker(linter))
diff --git a/tests/pylint/pointless-override.py b/tests/pylint/pointless-override.py
new file mode 100644
index 0000000..c8d4776
--- /dev/null
+++ b/tests/pylint/pointless-override.py
@@ -0,0 +1,252 @@
+# Pylint checker for pointless class attributes overrides.
+#
+# Copyright (C) 2014  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.  You should have received a copy of the
+# GNU General Public License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): Anne Mulhern <amulhern at redhat.com>
+#
+
+import abc
+
+from six import add_metaclass
+
+import astroid
+
+from pylint.checkers import BaseChecker
+from pylint.checkers.utils import check_messages
+from pylint.interfaces import IAstroidChecker
+
+ at add_metaclass(abc.ABCMeta)
+class PointlessData(object):
+
+    _DEF_CLASS = abc.abstractproperty(doc="Class of interesting definitions.")
+    message_id = abc.abstractproperty(doc="Pylint message identifier.")
+
+    @classmethod
+    @abc.abstractmethod
+    def _retain_node(cls, node, restrict=True):
+        """ Determines whether to retain a node for the analysis.
+
+            :param node: an AST node
+            :type node: astroid.Class
+            :param restrict bool: True if results returned should be restricted
+            :returns: True if the node should be kept, otherwise False
+            :rtype: bool
+
+            Restricted nodes are candidates for being marked as overridden.
+            Only restricted nodes are put into the initial pool of candidates.
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    @abc.abstractmethod
+    def _extract_value(node):
+        """ Return the node that contains the assignment's value.
+
+            :param node: an AST node
+            :type node: astroid.Class
+            :returns: the node corresponding to the value
+            :rtype: bool
+        """
+        raise NotImplementedError()
+
+    @staticmethod
+    @abc.abstractmethod
+    def _extract_targets(node):
+        """ Generates the names being assigned to.
+
+            :param node: an AST node
+            :type node: astroid.Class
+            :returns: a list of assignment target names
+            :rtype: generator of str
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def get_data(cls, node, restrict=True):
+        """ Find relevant nodes for this analysis.
+
+            :param node: an AST node
+            :type node: astroid.Class
+            :param restrict bool: True if results returned should be restricted
+
+            :rtype: generator of astroid.Class
+            :returns: a generator of interesting nodes.
+
+            Note that all nodes returned are guaranteed to be instances of
+            some class in self._DEF_CLASS.
+        """
+        nodes = (n for n in node.body if isinstance(n, cls._DEF_CLASS))
+        for n in nodes:
+            if cls._retain_node(n, restrict):
+                for name in cls._extract_targets(n):
+                    yield (name, cls._extract_value(n))
+
+    @classmethod
+    @abc.abstractmethod
+    def check_equal(cls, node, other):
+        """ Check whether the two nodes are considered equal.
+
+            :param node: some ast node
+            :param other: some ast node
+
+            :rtype: bool
+            :returns: True if the nodes are considered equal, otherwise False
+
+            If the method returns True, the nodes are actually equal, but it
+            may return False when the nodes are equal.
+        """
+        raise NotImplementedError()
+
+class PointlessFunctionDefinition(PointlessData):
+    """ Looking for pointless function definitions. """
+
+    _DEF_CLASS = astroid.Function
+    message_id = "W9952"
+
+    @classmethod
+    def _retain_node(cls, node, restrict=True):
+        return not restrict or \
+           (len(node.body) == 1 and isinstance(node.body[0], astroid.Pass))
+
+    @classmethod
+    def check_equal(cls, node, other):
+        return len(node.body) == 1 and isinstance(node.body[0], astroid.Pass) and \
+           len(other.body) == 1 and isinstance(other.body[0], astroid.Pass)
+
+    @staticmethod
+    def _extract_value(node):
+        return node
+
+    @staticmethod
+    def _extract_targets(node):
+        yield node.name
+
+class PointlessAssignment(PointlessData):
+
+    _DEF_CLASS = astroid.Assign
+    message_id = "W9951"
+
+    _VALUE_CLASSES = (
+        astroid.Const,
+        astroid.Dict,
+        astroid.List,
+        astroid.Tuple
+    )
+
+    @classmethod
+    def _retain_node(cls, node, restrict=True):
+        return not restrict or isinstance(node.value, cls._VALUE_CLASSES)
+
+    @classmethod
+    def check_equal(cls, node, other):
+        if type(node) != type(other):
+            return False
+        if isinstance(node, astroid.Const):
+            return node.value == other.value
+        if isinstance(node, (astroid.List, astroid.Tuple)):
+            return len(node.elts) == len(other.elts) and \
+               all(cls.check_equal(n, o) for (n, o) in zip(node.elts, other.elts))
+        if isinstance(node, astroid.Dict):
+            return len(node.items) == len(other.items)
+        return False
+
+    @staticmethod
+    def _extract_value(node):
+        return node.value
+
+    @staticmethod
+    def _extract_targets(node):
+        for target in node.targets:
+            yield target.name
+
+class PointlessClassAttributeOverrideChecker(BaseChecker):
+    """ If the nearest definition of the class attribute in the MRO assigns
+        it the same value, then the overriding definition is said to be
+        pointless.
+
+        The algorithm for detecting a pointless attribute override is the following.
+
+        * For each class, C:
+           - For each attribute assignment,
+               name_1 = name_2 ... name_n = l (where l is a literal):
+              * For each n in (n_1, n_2):
+                - Traverse the linearization of the MRO until the first
+                   matching assignment n = l' is identified. If l is equal to l',
+                   then consider that the assignment to l in C is a
+                   pointless override.
+
+        The algorithm for detecting a pointless method override has the same
+        general structure, and the same defects discussed below.
+
+        Note that this analysis is neither sound nor complete. It is unsound
+        under multiple inheritance. Consider the following class hierarchy::
+
+            class A(object):
+                _attrib = False
+
+            class B(A):
+                _attrib = False
+
+            class C(A):
+                _attrib = True
+
+            class D(B,C):
+                pass
+
+        In this  case, starting from B, B._attrib = False would be considered
+        pointless. However, for D the MRO is B, C, A, and removing the assignment
+        B._attrib = False would change the inherited value of D._attrib from
+        False to True.
+
+        The analysis is incomplete because it will find some values unequal when
+        actually they are equal.
+
+        The analysis is both incomplete and unsound because it expects that
+        assignments will always be made by means of the same syntax.
+    """
+
+    __implements__ = (IAstroidChecker,)
+
+    name = "pointless class attribute override checker"
+    msgs = {
+       "W9951":
+       (
+          "Assignment to class attribute %s overrides identical assignment in ancestor.",
+          "pointless-class-attribute-override",
+          "Assignment to class attribute  that overrides assignment in ancestor that assigns identical value has no effect."
+       ),
+       "W9952":
+       (
+          "definition of %s method overrides identical method definition in ancestor",
+          "pointless-method-definition-override",
+          "Overriding empty method definition with another empty method definition has no effect."
+       )
+    }
+
+    @check_messages("W9951", "W9952")
+    def visit_class(self, node):
+        for checker in (PointlessAssignment, PointlessFunctionDefinition):
+            for (name, value) in checker.get_data(node):
+                for a in node.ancestors():
+                    match = next((v for (n, v) in checker.get_data(a, False) if n == name), None)
+                    if match is not None:
+                        if checker.check_equal(value, match):
+                            self.add_message(checker.message_id, node=value, args=(name,))
+                        break
+
+def register(linter):
+    linter.register_checker(PointlessClassAttributeOverrideChecker(linter))
diff --git a/tests/pylint/pylint-false-positives b/tests/pylint/pylint-false-positives
new file mode 100644
index 0000000..e69de29
diff --git a/tests/pylint/pylint-one.sh b/tests/pylint/pylint-one.sh
new file mode 100755
index 0000000..9b1e79b
--- /dev/null
+++ b/tests/pylint/pylint-one.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# $1 -- python source to run pylint on
+#
+
+if [ $# -lt 1 ]; then
+    # no source, just exit
+    exit 1
+fi
+
+file_suffix="$(eval echo \$$#|sed s?/?_?g)"
+
+pylint_output="$(pylint \
+    --msg-template='{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}' \
+    -r n --disable=C,R --rcfile=/dev/null \
+    --dummy-variables-rgx=_ \
+    --ignored-classes=Popen,TransactionSet \
+    --defining-attr-methods=__init__,_grabObjects,initialize,reset,start,setUp \
+    --load-plugins=intl,pointless-override \
+    $DISABLED_WARN_OPTIONS \
+    $DISABLED_ERR_OPTIONS \
+    $NON_STRICT_OPTIONS "$@" 2>&1 | \
+    egrep -v -f "$FALSE_POSITIVES" \
+    )"
+
+# I0011 is the informational "Locally disabling ...." message
+if [ -n "$(echo "$pylint_output" | fgrep -v '************* Module ' |\
+          grep -v '^I0011:')" ]; then
+    # Replace the Module line with the actual filename
+    pylint_output="$(echo "$pylint_output" | sed "s|\* Module .*|* Module $(eval echo \$$#)|")"
+    echo "$pylint_output" > pylint-out_$file_suffix
+    touch "pylint-$file_suffix-failed"
+fi
diff --git a/tests/pylint/runpylint.sh b/tests/pylint/runpylint.sh
new file mode 100755
index 0000000..18200bf
--- /dev/null
+++ b/tests/pylint/runpylint.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+
+# This script will check for any pylint warning and errors using a set
+# of options minimizing false positives, in combination with filtering of any
+# warning regularexpressions listed in pylint-false-positives.
+#
+# If any warnings are found they will be stored in pylint-log and printed
+# to stdout and this script will exit with a status of 1, if no (non filtered)
+# warnings are found it exits with a status of 0
+
+# If $top_srcdir is set, assume this is being run from automake and we don't
+# need to keep a separate log
+export pylint_log=0
+if [ -z "$top_srcdir" ]; then
+    export pylint_log=1
+fi
+
+# Unset TERM so that things that use readline don't output terminal garbage
+unset TERM
+
+# Don't try to connect to the accessibility socket
+export NO_AT_BRIDGE=1
+
+# If $top_srcdir has not been set by automake, import the test environment
+if [ -z "$top_srcdir" ]; then
+    top_srcdir="$(dirname "$0")/../.."
+    . ${top_srcdir}/tests/testenv.sh
+fi
+
+. ${top_srcdir}/tests/lib/testlib.sh
+
+srcdir="${top_srcdir}/tests/pylint"
+builddir="${top_builddir}/tests/pylint"
+
+# Need to add the pylint module directory to PYTHONPATH as well.
+export PYTHONPATH="${PYTHONPATH}:${srcdir}"
+
+# Save analysis data in the pylint directory
+export PYLINTHOME="${builddir}/.pylint.d"
+[ -d "$PYLINTHOME" ] || mkdir "$PYLINTHOME"
+
+export FALSE_POSITIVES="${srcdir}"/pylint-false-positives
+
+# W0212 - Access to a protected member %s of a client class
+export NON_STRICT_OPTIONS="--disable=W0212"
+
+# E1103 - %s %r has no %r member (but some types could not be inferred)
+export DISABLED_ERR_OPTIONS="--disable=E1103"
+
+# W0105 - String statement has no effect
+# W0110 - map/filter on lambda could be replaced by comprehension
+# W0141 - Used builtin function %r
+# W0142 - Used * or ** magic
+# W0511 - Used when a warning note as FIXME or XXX is detected.
+# W0603 - Using the global statement
+# W0614 - Unused import %s from wildcard import
+# I0011 - Locally disabling %s (i.e., pylint: disable)
+export DISABLED_WARN_OPTIONS="--disable=W0105,W0110,W0141,W0142,W0511,W0603,W0614,I0011"
+
+usage () {
+  echo "usage: `basename $0` [--strict] [--help] [files...]"
+  exit $1
+}
+
+# Separate the module parameters from the files list
+ARGS=
+FILES=
+while [ $# -gt 0 ]; do
+  case $1 in
+    --strict)
+      export NON_STRICT_OPTIONS=
+      ;;
+    --help)
+      usage 0
+      ;;
+    -*)
+      ARGS="$ARGS $1"
+      ;;
+    *)
+      FILES=$@
+      break
+  esac
+  shift
+done
+
+exit_status=0
+
+if [ -s pylint-log ]; then
+    rm pylint-log
+fi
+
+# run pylint one file / module at a time, otherwise it sometimes gets
+# confused
+if [ -z "$FILES" ]; then
+    # Test any file that either ends in .py or contains #!/usr/bin/python in
+    # the first line.  Scan everything except old_tests
+    FILES=$(findtestfiles \( -name '*.py' -o \
+                -exec /bin/sh -c "head -1 {} | grep -q '#!/usr/bin/python'" \; \) -print | \
+            egrep -v '(|/)doc/conf.py')
+fi
+
+num_cpus=$(getconf _NPROCESSORS_ONLN)
+# run pylint in paralel
+echo $FILES | xargs --max-procs=$num_cpus -n 1 "$srcdir"/pylint-one.sh $ARGS || exit 1
+
+for file in $(find -name 'pylint-out*'); do
+    cat "$file" >> pylint-log
+    rm "$file"
+done
+
+fails=$(find -name 'pylint*failed' -print -exec rm '{}' \;)
+if [ -z "$fails" ]; then
+    exit_status=0
+else
+    exit_status=1
+fi
+
+if [ -s pylint-log ]; then
+    echo "pylint reports the following issues:"
+    cat pylint-log
+elif [ -e pylint-log ]; then
+    rm pylint-log
+fi
+
+exit "$exit_status"
diff --git a/tests/testenv.sh b/tests/testenv.sh
new file mode 100644
index 0000000..fca48db
--- /dev/null
+++ b/tests/testenv.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+if [ -z "$top_srcdir" ]; then
+    echo "*** top_srcdir must be set"
+    exit 1
+fi
+
+# If no top_builddir is set, use top_srcdir
+: "${top_builddir:=$top_srcdir}"
+
+export PYTHONPATH
+export top_srcdir
+export top_builddir
-- 
1.9.3



More information about the anaconda-patches mailing list