#! /usr/bin/python

from os import F_OK, access
from re import search
from string import ascii_letters, ascii_uppercase
import sys


class Validate:
    def __init__(self):
        """Script for checking if a Python file meets relax's coding conventions."""

        # Test the arguments.
        self.test_args()

        # Open the file, read the lines, then close it.
        file = open(sys.argv[1], 'r')
        self.lines = file.readlines()
        file.close()

        # Header.
        sys.stdout.write("# Script for testing if a Python file meets relax's coding conventions.\n")
        sys.stdout.write("#\n")
        sys.stdout.write("# As the relax manual states, the 'conventions must be followed at all times for any code to be accepted into the relax repository.'\n")
        sys.stdout.write("# However, false positives do occur.\n")
        sys.stdout.write("# General code cleanliness is also tested.\n")
        sys.stdout.write("# If a test passes, the text '[ OK ]' is printed.\n")
        sys.stdout.write("# If a test fails, the line number is printed.\n\n\n")


        # Indentation.
        ##############

        sys.stdout.write("Indentation.\n")
        self.indent = '    '

        # Code indentation (4 spaces!).
        self.indentation()

        # Search for tabs.
        self.tabs()


        # Docstring tests.
        ##################

        sys.stdout.write("Docstring tests.\n")

        # Missing docstring.
        self.missing_docstring()

        # Docstring header line.
        self.docstring_header()

        # Docstring indentation.
        self.docstring_indentation()

        # Empty line after docstring.
        self.docstring_clearance()


        # Variable, function, and class names.
        ######################################

        sys.stdout.write("Variable, function, and class name tests.\n")

        # Class naming.
        self.class_naming()

        # Function naming.
        self.function_naming()


        # Whitespace.
        #############

        sys.stdout.write("Whitespace.\n")

        # Test for trailing whitespace.
        self.trailing_whitespace()

        # Function spacing.
        self.function_spacing()

        # Function argument spacing.
        self.function_args()

        # Single spacing.
        self.single_spacing()

        # Assignment.
        self.assignment()


        # Comment density.
        ##################

        self.comment_density()


    def assignment(self):
        """Test for proper spacing around the assignment."""

        # Heading.
        sys.stdout.write(self.indent + "Assignment spacing (should be ' = ').\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Alias the line.
            line = self.lines[i]

            # Skip functions.
            if search("^ *def ", line):
                continue

            # Flags.
            proper_spacing = 1
            in_args = 0

            # Loop over the characters of the line.
            for j in range(len(line)):
                # Start of the arguments.
                if line[j] == '(':
                    in_args = in_args + 1
                    continue

                # End of the arguments.
                if line[j] == ')':
                    in_args = in_args - 1
                    continue

                # Within function arguments.
                if in_args:
                    continue

                # Find an equal sign.
                if line[j] == '=':
                    # Space before.
                    if line[j-1] in ascii_letters:
                        proper_spacing = 0
                        break

                    # Space after.
                    if line[j+1] in ascii_letters:
                        proper_spacing = 0
                        break

            # Spacing is ok.
            if proper_spacing:
                continue

            # Initial printout.
            if ok:
                sys.stdout.write(self.indent + "    Line num: ")
                sys.stdout.write(`i+1`)
                ok = 0

            # Print the line number.
            else:
                sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def class_naming(self):
        """Test for proper class naming."""

        # Heading.
        sys.stdout.write(self.indent + "Class naming (should start with a capital).\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the classes.
            if search("^ *class ", self.lines[i]):
                # Split the line.
                row = self.lines[i].split()

                # Test if the name starts with a capital.
                if row[1][0] in ascii_uppercase:
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def comment_density(self):
        """Calculate the comment density."""

        # Heading.
        sys.stdout.write("Comment density (should be between 15% to 30%).  This only includes lines with the '#' character and docstring lines are ignored.\n")

        # Flags and counters.
        comment_count = 0
        in_header = 1
        in_docstring = 0
        docstring_count = 0
        empty_lines = 0

        # Loop over the file.
        for line in self.lines:
            # Docstrings.
            if search("\"\"\"", line) or search("\'\'\'", line):
                # Split the docstring line.
                if search("\"\"\"", line):
                    row = line.split("\"\"\"")
                else:
                    row = line.split("\'\'\'")

                # Single line docstring.
                if len(row) == 3:
                    docstring_count = docstring_count + 1
                    continue

                # In or out of the docstring.
                if not in_docstring:
                    in_docstring = 1
                else:
                    in_docstring = 0

            # Count the docstrings.
            if in_docstring:
                docstring_count = docstring_count + 1
                continue

            # Still in the header.
            if search("#", line) and in_header:
                continue

            # Count the empty lines.
            row = line.split()
            if len(row) == 0:
                # Out of the header.
                if in_header:
                    in_header = 0

                # Increment the empty line counter.
                empty_lines = empty_lines + 1
                continue

            # Find the comments.
            if search("#", line):
                comment_count = comment_count + 1

            # Out of the header.
            else:
                in_header = 0

        # Percentage.
        sys.stdout.write("    %.3f%s\n\n" % (100 * comment_count / (float(len(self.lines)) - docstring_count - empty_lines), '%'))


    def docstring_clearance(self):
        """Test that there is an empty line after the docstring."""

        # Heading.
        sys.stdout.write(self.indent + "Missing empty line after the docstring.\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the functions.
            if search("^ *def ", self.lines[i]):
                # Skip functions without docstrings!
                if not search("^ *\"\"\"", self.lines[i+1]) and not search("^ *\'\'\'", self.lines[i+1]):
                    continue

                # Split the docstring line.
                if search("^ *\"\"\"", self.lines[i+1]):
                    row = self.lines[i+1].split("\"\"\"")
                else:
                    row = self.lines[i+1].split("\'\'\'")

                # Loop over the docstring.
                j = 0
                while 1:
                    # Single line docstring.
                    if len(row) == 3:
                        break

                    # Increment the line counter.
                    j = j + 1

                    # End of the docstring.
                    if search("\"\"\"", self.lines[i+1+j]) or search("\'\'\'", self.lines[i+1+j]):
                        break

                # End of the file.
                if i+1+j+1 >= len(self.lines):
                    continue

                # Empty line.
                row = self.lines[i+1+j+1].split()
                if not row:
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1+j+2`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1+j+2`)

        # Termination.
        self.terminate(ok)


    def docstring_header(self):
        """Test the docstring header line."""

        # Heading.
        sys.stdout.write(self.indent + "Docstring header line.  Must start with a capital and end in a period (required for certain docstring parsers, see the Python docs).\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the classes.
            if search("^ *def ", self.lines[i]):
                # Skip functions without docstrings!
                if not search("^ *\"\"\"", self.lines[i+1]) and not search("^ *\'\'\'", self.lines[i+1]):
                    continue

                # Split the docstring line.
                if search("^ *\"\"\"", self.lines[i+1]):
                    row = self.lines[i+1].split("\"\"\"")
                else:
                    row = self.lines[i+1].split("\'\'\'")

                # Test for text.
                if row[1] == '\n':
                    continue

                # Start with a capital.
                if row[1][0] in ascii_uppercase:
                    continue

                # End in a period.
                if row[1][-2] == '.':
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+2`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+2`)

        # Termination.
        self.terminate(ok)


    def docstring_indentation(self):
        """Test the docstring indentation."""

        # Heading.
        sys.stdout.write(self.indent + "Docstring indentation (should be the same indentation as the first line of code).\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the classes.
            if search("^ *def ", self.lines[i]):
                # Skip functions without docstrings!
                if not search("^ *\"\"\"", self.lines[i+1]) and not search("^ *\'\'\'", self.lines[i+1]):
                    continue

                # Split the docstring line.
                if search("^ *\"\"\"", self.lines[i+1]):
                    row = self.lines[i+1].split("\"\"\"")
                else:
                    row = self.lines[i+1].split("\'\'\'")

                # Single line docstring.
                if len(row) == 3:
                    continue

                # Indentation length.
                indent = len(row[0])

                # Loop over the docstring.
                indent_ok = 0
                j = 0
                while 1:
                    # Increment the line counter.
                    j = j + 1

                    # Empty row.
                    row = self.lines[i+1+j].split()
                    if not row:
                        continue

                    # End of the docstring.
                    if search("\"\"\"", self.lines[i+1+j]) or search("\'\'\'", self.lines[i+1+j]):
                        break

                    # Split by whitespace.
                    row = self.lines[i+1+j].split(' ')

                    # Indentation length.
                    length = 0
                    for element in row:
                        if element == '':
                            length = length + 1
                        elif element == '\n':
                            length = 0
                            break
                        else:
                            break

                    # Equal length.
                    if length == indent:
                        indent_ok = 1
                        break

                # Indentation is ok.
                if indent_ok:
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+2`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+2`)

        # Termination.
        self.terminate(ok)


    def function_args(self):
        """Test for proper function argment spacing."""

        # Heading.
        sys.stdout.write(self.indent + "Function argment spacing (should be a space after the comma).  Ignore tuples, these produce false positives.\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Alias the line.
            line = self.lines[i]

            # Find the classes.
            if line:
                # Flags.
                in_args = 0
                bad_spacing = 0

                # Loop over the characters of the line.
                for j in range(len(line)):
                    # Start of the arguments.
                    if line[j] == '(':
                        in_args = in_args + 1
                        continue

                    # Not within the arguments yet.
                    if not in_args:
                        continue

                    # End of the arguments.
                    if line[j] == ')':
                        in_args = in_args - 1
                        if not in_args:
                            break

                    # No spacing between arguments!
                    if line[j] == ',' and (line[j+1] != ' ' and line[j+1] != '\n'):
                        bad_spacing = 1

                # Spacing is ok.
                if not bad_spacing:
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def function_naming(self):
        """Test for proper function naming."""

        # Heading.
        sys.stdout.write(self.indent + "Function naming (no capitals).  Ignore this for module method names.\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the classes.
            if search("^ *def ", self.lines[i]):
                # Split the line.
                row = self.lines[i].split(' ')

                # Isolate the function name.
                name = row[1].split('(')

                # Test if the name starts with a capital.
                if not search('[' + ascii_uppercase + ']', name[0]):
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def function_spacing(self):
        """Test for proper function spacing."""

        # Heading.
        sys.stdout.write(self.indent + "Function spacing (2 preceding empty lines).  Ignore for the first method in a class.\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the functions.
            if search("^ *def ", self.lines[i]) and search("\(", self.lines[i]):
                # Count the number of blank lines before it.
                blanks = 0
                j = 1
                while 1:
                    if self.lines[i-j] == "\n":
                        blanks = blanks + 1
                        j = j + 1
                    else:
                        break

                # Two empty lines (go to the next line).
                if blanks == 2:
                    continue

                # The function straight after a class.
                if search("class", self.lines[i-1]):
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def indentation(self):
        """Test for proper indentation."""

        # Heading.
        sys.stdout.write(self.indent + "Indentation (must be 4 spaces).  This should always say OK without exception!\n")

        # Flags.
        ok = 1
        in_docstring = 0
        in_array = 0

        # Loop over the file.
        for i in range(len(self.lines)):
            # Alias the line.
            line = self.lines[i]

            # Flags.
            spacing = 0

            # Docstrings.
            if search("\"\"\"", line) or search("\'\'\'", line):
                # Split the docstring line.
                if search("\"\"\"", line):
                    row = line.split("\"\"\"")
                else:
                    row = line.split("\'\'\'")

                # Single line docstring.
                if len(row) == 3:
                    continue

                # In or out of the docstring.
                if not in_docstring:
                    in_docstring = 1
                else:
                    in_docstring = 0

            # Skip docstrings.
            if in_docstring:
                continue

            # Skip empty lines.
            row = line.split()
            if not row:
                continue

            # Count the '[' and ']' characters.
            for char in line:
                # In.
                if char == '[':
                    in_array = in_array + 1

                # Out.
                elif char == ']':
                    in_array = in_array - 1

            # Go to the next line if still within an array.
            if in_array > 0:
                continue
            elif in_array < 0:
                print("Bug detected at line %i.  Unpaired terminating ] encountered.")
                in_array = 0

            # Loop over the characters of the line.
            for char in line:
                # Count the spacing.
                if char == ' ':
                    spacing = spacing + 1

                # No more spacing.
                else:
                    break

            # Spacing is ok.
            if spacing % 4 == 0:
                continue

            # Initial printout.
            if ok:
                sys.stdout.write(self.indent + "    Line num: ")
                sys.stdout.write(`i+1`)
                ok = 0

            # Print the line number.
            else:
                sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def missing_docstring(self):
        """Test for a docstring."""

        # Heading.
        sys.stdout.write(self.indent + "Missing docstring.\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the functions.
            if search("^ *def ", self.lines[i]) and search("\(", self.lines[i]):
                # Test that the next line has the text """ or ''' (and split the docstring line).
                if search("^ *\"\"\"", self.lines[i+1]):
                    continue
                elif search("^ *\'\'\'", self.lines[i+1]):
                    continue

                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def single_spacing(self):
        """Test for proper spacing."""

        # Heading.
        sys.stdout.write(self.indent + "Spacing (2 or more spaces).  For certain formatting, e.g. lists, strings written to files, etc., multiple spacing is ok.\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Alias the line.
            line = self.lines[i]

            # Flags.
            double_space = 0
            j = 0

            # Skip empty lines.
            row = line.split()
            if not row:
                continue

            # Skip leading whitespace.
            while 1:
                if line[j] == ' ':
                    j = j + 1
                else:
                    break

            # Loop over the characters of the line.
            while 1:
                # End of the line.
                if len(line) <= j:
                    break

                # Break if a comment is encountered.
                if line[j] == '#':
                    break

                # Tables (start with '|').
                if line[j] == '|':
                    break

                # Double whitespace.
                if line[j] == ' ':
                    # Terminal whitespace.
                    end_row = line[j:-1].split()
                    if len(end_row) == 0:
                        break

                    # End of the line.
                    if len(line) <= j+1:
                        break

                    # Double spacing after a period.
                    if line[j-1] == '.' and line[j+1] == ' ':
                        break

                    # Double spacing after a colon.
                    if line[j-1] == ':' and line[j+1] == ' ':
                        break

                    # Double spacing.
                    elif line[j+1] == ' ':
                        double_space = 1
                        break

                # Increment the line counter.
                j = j + 1

            # Spacing is ok.
            if not double_space:
                continue

            # Initial printout.
            if ok:
                sys.stdout.write(self.indent + "    Line num: ")
                sys.stdout.write(`i+1`)
                ok = 0

            # Print the line number.
            else:
                sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)


    def tabs(self):
        """Find tabs."""

        # Heading.
        sys.stdout.write(self.indent + "Tab characters (should not be used).\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the whitespace.
            if search("\t", self.lines[i]):
                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)

    def terminate(self, ok):
        """Print out at the end of each test."""

        if ok:
            sys.stdout.write(self.indent + "    [ OK ]\n\n")
        else:
            sys.stdout.write("\n\n")


    def test_args(self):
        """Test that a single argument is given and that it is a readable file."""

        # Test for a single argument.
        if len(sys.argv) == 1:
            sys.stderr.write("A file name must be given.\n")
            sys.exit()
        elif len(sys.argv) != 2:
            sys.stderr.write("Only a single argument is allowed.\n")
            sys.exit()

        # Test that the argument is a file.
        if not access(sys.argv[1], F_OK):
            sys.stderr.write(`sys.argv[1]` + " is not accessible as a file.\n")
            sys.exit()


    def trailing_whitespace(self):
        """Test for trailing whitespaces."""

        # Heading.
        sys.stdout.write(self.indent + "Trailing whitespace (low importance).\n")

        # Flags.
        ok = 1

        # Loop over the file.
        for i in range(len(self.lines)):
            # Find the whitespace.
            if search(" $", self.lines[i]):
                # Initial printout.
                if ok:
                    sys.stdout.write(self.indent + "    Line num: ")
                    sys.stdout.write(`i+1`)
                    ok = 0

                # Print the line number.
                else:
                    sys.stdout.write(", " + `i+1`)

        # Termination.
        self.terminate(ok)

if __name__ == "__main__":
    Validate()
