#1
  1. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Jan 2013
    Posts
    78
    Rep Power
    2

    My quest for the perfect example program


    For some time now I have been looking for a perfect example program, a program that does everything right and is above all very readable, uses unit testing, uses logging, and is divided/designed in a good way between classes/modules.

    I have written a small program for making a set of dices, throwing them, and displaying them for use in any program(originally a yatzy game). And was hoping you guys could be me some feedback on how i could improve readability, effiency, compliance with standards and such on it. I will post it here, and edit it as I get comments/critic/feedback on it, to try to make it better.

    you might want to copy and paste into your own editor, as I have ignored the 80 character limit to make my code more readable.(yes, I know I'm violating PEP)
  2. #2
  3. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Jan 2013
    Posts
    78
    Rep Power
    2
    Code:
    #!/usr/bin/python
    '''
    Look into the function UnitTests for information about unit tests.
    '''
    
    #IMPORTS AND SETS UP MODULES
    from __future__ import print_function
    import sys
    import random
    
    #TURNS ON LOGGING IF THE PARAMETER -l HAS BEEN PASSED
    if "-l" in sys.argv:
        from logging import debug as l
        import logging as log
        log.basicConfig(level=log.DEBUG, format='%(levelname)s : %(message)s') #COMMENT OUT THIS LINE TO TURN OF DEBUG MESSAGES
    else:
        def l(throwaway):pass
        
    #DEFINES FUNCTIONS
    def UnitTests():
        '''
        Unit tests can be embedded in the doclines for your functions, methods, classes and modules
        just paste the next line, and erase/change any parameters you want into the unit test
        &&& sCommandToTest='1+1', lCommandInput=[], ExpectedOutput=2, lExpressionsToTest=['True'] &&&
        
        #UNIT TESTS
        &&& sCommandToTest='oDices = cDices(3, sDefaultState="ROLL")', lExpressionsToTest=['oDices.miValue(1) == 0', 'oDices.msState(1) == "ROLL"'] &&&
        &&& sCommandToTest='oDices.mThrow()', lExpressionsToTest=['0 < oDices.miValue(1) < 7', '0 < oDices.miValue(2) < 7', '0 < oDices.miValue(3) < 7'] &&&
        &&& sCommandToTest='oDices.mFlipState(1,)', lExpressionsToTest=['oDices.msState(1) == "SAVE"'] &&&
        &&& sCommandToTest='oDices.mFlipState(1)', lExpressionsToTest=['oDices.msState(1) == "ROLL"'] &&&
        &&& sCommandToTest='oDices.mSetStateToSave(1)', lExpressionsToTest=['oDices.msState(1) == "SAVE"'] &&&
        &&& sCommandToTest='oDices.mSetStateToRoll(1)', lExpressionsToTest=['oDices.msState(1) == "ROLL"'] &&&
        &&& sCommandToTest='oDices.mDrawHorizontally()' &&&
        &&& sCommandToTest='oDices.mDrawVertically' &&&
        &&& sCommandToTest='oDices.mReset()', lExpressionsToTest=['oDices.msState(1) == "ROLL"', 'oDices.miValue(1) == 0'] &&&
        '''
    def fsInsertStringOnto(sOriginalString, sReplaceMentString, iInsertIndex):
        '''Insert a string into another, but does not extend the original string'''
        sOutput = sOriginalString[:iInsertIndex] + sReplaceMentString[:len(sOriginalString)-iInsertIndex] + sOriginalString[len(sReplaceMentString)+iInsertIndex:]
        return sOutput
    #DEFINES CLASSES
    class cDices():
        '''Provides a way to make a set of dices for which you can roll, and display easily on screen.'''
        def __init__(self, iNumberOfDices, sDefaultState = "    "):
            #CREATES THE LIST OF THE DICES
            self.llDices = []
            for dDice in range(iNumberOfDices):
                self.llDices.append({"Look":[], "Value":0, "State": sDefaultState})
        def mThrow(self):
            '''Throws all unsaved dices'''
            #LOOPS THROUGH ALL DICES
            for Dice in self.llDices:
                #SET THE DICE TO A RANDOM VALUE BETWEEN 1 AND 6 IF IT IS NOT SUPPOSED TO BE SAVED
                if Dice["State"] != "SAVE":
                    Dice["Value"] = random.randrange(1,7)
            
            #UPDATE THE LOOKS OF ALL DICES
            self.mUpdateLook()
        def mUpdateLook(self):
            '''Loops through all dices and updates the look'''
            #LOOPS THROUGH ALL DICES, UPDATING THE LOOK
            for Dice in self.llDices: 
                #SET THE LOOK TO A BLANK TEMPLATE
                Dice["Look"]=[    
                "     _______ ",
                "    |       |", 
                "    |       |",
                "    |       |",
                "    |       |",
                "    |_______|"]
                
                #INSERTS EITHER 'ROLL', 'SAVE', OR '   ' INTO "Look"
                Dice["Look"][0] = fsInsertStringOnto(Dice["Look"][0], Dice["State"], 0)
                
                #TAKES CARE OF THE EYES ON THE DICE
                if Dice["Value"] in [3,4,5,6]:
                    Dice["Look"][2] = Dice["Look"][2][:7] + "." + Dice["Look"][2][8:]
                if Dice["Value"] in [2,4,5,6]:
                    Dice["Look"][2] = Dice["Look"][2][:9] + "." + Dice["Look"][2][10:]
                if Dice["Value"] in [6]:
                    Dice["Look"][3] = Dice["Look"][3][:7] + "." + Dice["Look"][3][8:]
                if Dice["Value"] in [1,3,5]:
                    Dice["Look"][3] = Dice["Look"][3][:8] + "." + Dice["Look"][3][9:]
                if Dice["Value"] in [6]:
                    Dice["Look"][3] = Dice["Look"][3][:9] + "." + Dice["Look"][3][10:]
                if Dice["Value"] in [2,4,5,6]:
                    Dice["Look"][4] = Dice["Look"][4][:7] + "." + Dice["Look"][4][8:]
                if Dice["Value"] in [3,4,5,6]:
                    Dice["Look"][4] = Dice["Look"][4][:9] + "." + Dice["Look"][4][10:]
        def mReset(self):
            '''Resets all dices to blank''' 
            for Dice in range(len(self.llDices)):
                self.llDices[Dice] = {"Look":[], "Value":0, "State":"ROLL"}
        def miValue(self, iDiceNumber):
            '''Returns the value of a specific dice.
            (the first dice is dice number 1)'''
            return(self.llDices[iDiceNumber-1]["Value"])
        def msState(self, iDiceNumber):
            '''Returns the state of the specified dice.'''
            return(self.llDices[iDiceNumber]["State"])
        def mDrawHorizontally(self):
            '''Draws all the dices to the screen horizontally'''
            for i in range(6):
                for Dice in self.llDices:            
                    print(Dice["Look"][i],end="")
                print()
        def mDrawVertically(self):
            '''Draws all dices to the screen vertically'''
            for i in range(1,len(self.llDices)+1):
                print(i)
                self.mDrawDice(i)
        def mDrawDice(self, iDiceNumber):
            '''Draws specified dice to the screen'''
            for s in self.llDices[iDiceNumber-1]["Look"]:
                print(s)
        def mSetStateToSave(self, iDiceNumber):
            '''Changes the state of the specified dice to "SAVE"'''
            self.llDices[iDiceNumber]["State"] = "SAVE"
        def mSetStateToRoll(self, iDiceNumber):
            '''Changes the state of the specified dice to "ROLL"'''
            self.llDices[iDiceNumber]["State"] = "ROLL"
        def mFlipState(self, iDiceNumber):
            '''Flip the state of the specified dice("ROLL" turns to "SAVE" and "SAVE" turns to "ROLL")'''
            if self.llDices[iDiceNumber]["State"] == "SAVE":
                self.llDices[iDiceNumber]["State"] = "ROLL"
            elif self.llDices[iDiceNumber]["State"] == "ROLL":
                self.llDices[iDiceNumber]["State"] = "SAVE"
            
    #RUNS THE MAIN PROGRAM
    if __name__ == "__main__":
        print("This program is not designed to be runned as main, but used as a library")
  4. #3
  5. Contributing User
    Devshed Demi-God (4500 - 4999 posts)

    Join Date
    Aug 2011
    Posts
    4,904
    Rep Power
    481
    How do I run the unit tests?

    Maybe the unit tests should run when you load the program as '__main__'.

    Shouldn't have to change the source to turn off debug messages.
    #COMMENT OUT THIS LINE TO TURN OF DEBUG MESSAGES
    Set the log level on the command line.
    [code]Code tags[/code] are essential for python code and Makefiles!
  6. #4
  7. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Jan 2013
    Posts
    78
    Rep Power
    2
    I've made a program that runs these unit tests, they all pass.

    the logging is only turned on if you put -l behind the program name when you run it, it basically prints out lines to the console. i forgot to remove that comment you are talking about from an earlier version. thanks.


    how about the "design" of it, any way to improve it?

    thought abit about how programs could use this module, is it better to have methods that return important variable such as value of dices then to access these values directly? if I have methods for that, i can prob document it better and it might be less subject to change between versions.(if I refactor later)
  8. #5
  9. Contributing User
    Devshed Demi-God (4500 - 4999 posts)

    Join Date
    Aug 2011
    Posts
    4,904
    Rep Power
    481
    I'm not going to try to refactor or read closely without ability to run and test.
    [code]Code tags[/code] are essential for python code and Makefiles!
  10. #6
  11. No Profile Picture
    Contributing User
    Devshed Newbie (0 - 499 posts)

    Join Date
    Jan 2013
    Posts
    78
    Rep Power
    2
    well, here's my unit testing program, just run that from commandline and put the path to the dice file as a parameter. (my unit testing program has a few kinks in it still, its a work in progress.)

    Code:
    #!/usr/bin/python3
    '''Unit tests files using their doclines as input for the tests.
    
    Runnable both in python 2 and 3.
    Any unit test inside a docline should look like this:
    &&&(sCommandToTest='1+1', lCommandInput=[], ExpectedOutput=100, lExpressionsToTest=['True'])&&&
    '''
    #IMPORTS MODULES
    from __future__ import print_function
    import sys
    
    #TURNS ON LOGGING IF THE PARAMETER -l HAS BEEN PASSED
    if "-l" in sys.argv:
        from logging import debug as l
        import logging as log
        log.basicConfig(level=log.DEBUG, format='%(levelname)s : %(message)s') #COMMENT OUT THIS LINE TO TURN OF DEBUG MESSAGES
    else:
        def l(throwaway):pass
    
    #DEFINES CLASSES AND FUNCTIONS
    def fsDocStrings(oModule, sModuleName):#collect docstrings
        '''Recursively goes through the module and return a concetated string of all docstrings'''
        sOutput = ""
        
        #RECURSIVELY LOOPS THROUGH EVERYTHING INSIDE THE MODULE, ADDING DOC LINES TO THE END OF sOutput
        for sObject in dir(oModule):
            #SET oObject
            exec('oObject = ' + sModuleName + '.' + sObject, globals())        
            
            #IF THE CURRENT OBJECT IS A DOCLINE, ADD THE DOCLINE TO A LIST
            if "__doc__" in sObject and oObject != None :
                l("Collecting Doc from " + sModuleName)
                sOutput += oObject
            
            #SKIP ALL BUILTINS AND RECURSIVE FUNCTIONS/METHODS/CLASSES
            if sObject.endswith("__"):
                continue
            if sObject in ["denominator","imag","numerator","real", "im_class", "im_func", "compiler_flag"]:
                continue
            
            #IF THE CURRENT OBJECT IS A FUNCTION/METHOD, RECURSIVELY RUN IT
            if (("function" in str(type(oObject))
                or "method" in str(type(oObject))
                or "class" in str(type(oObject)))
                and "module" not in str(type(oObject))
                and "builtin" not in str(type(oObject))):
                l("going into the function/method/class " + str(sObject) + str(type(oObject)))
                sOutput += (fsDocStrings(oObject, sModuleName + '.' + sObject))
                
        return(sOutput)
    def flsDocStrings(sString):#converts docstrings to unittests
        'Returns a list of Docstrings extracted from a string '
        
        #DECLARES VARIABLES
        lsOutput = []
        iStart = 0
        iStop = -3
        
        #LOOPS THROUGH THE WHOLE STRING, SEARCHING FOR &&&, RETURNING A LIST WITH ANYTHING INBETWEEN
        while "&&&" in sString[iStart:]:
            iStart = 3+sString.find("&&&", iStop+3)
            iStop = sString.find("&&&", iStart)
            lsOutput.append(sString[iStart:iStop])
            iStart = iStop+3
        return(lsOutput)
    def input(Text = ""):#replaces inputfunction
        'This function replaces the built in input function, it will return the variables from the WantedInput list one by one'
        
        input.iInputExecuteCount += 1
        if len(input.lWantedInput) < input.iInputExecuteCount or input.lWantedInput[input.iInputExecuteCount-1] == "Blank":
            raise ValueError("The program demands atleast " + str(len(input.lWantedInput)+1) + " inputs")
        return input.lWantedInput[input.iInputExecuteCount-1]
    def fbUnitTest(oModule='', sCommandToTest="'Blank'", lCommandInput=["Blank"], ExpectedOutput="Blank", lExpressionsToTest=["True"]):#runs unittests
        'Runs any command passed to it, pass it the necessary input, checks its output and passed expressions.'
        
        #TAKES CARE OF THE INPUT FUNCTION IF THE COMMAND NEEDS AN INPUT
        exec("from unittesting import input", oModule.__dict__)
        exec("input.lWantedInput = "+str(lCommandInput), oModule.__dict__)   #Stores the list of inputs in a global list
        exec("input.iInputExecuteCount = 0", oModule.__dict__)   #Set the list index the input should be taken from next time input is run
        
        #PRINTS OUT THE INITIAL TEXT FOR THE TEST
        if sCommandToTest == "'Blank'":
            print("Executing test without any commands".ljust(115),end="")    
        elif lCommandInput != [] and lCommandInput != ["Blank"]:
            print(str("Executing " + str(sCommandToTest) + ", The input is set to:" + str(lCommandInput)).ljust(115),end="")
        else:
            print(str("Executing " + str(sCommandToTest)).ljust(115),end="") 
        
        #TRIES TO EVALUATE THE COMMAND PASSED TO IT
        try:
            if sCommandToTest != 'Blank':
                CommandOutput = eval(sCommandToTest, oModule.__dict__)
        
        #HANDLES ANY NAME ERRORS FROM THE EVALUATION
        except NameError as ErrorMessage:
            print("\n\tTEST FAILED, NameError:",ErrorMessage)
            return(0)
        
        #HANDLES ANY ATTRIBUTESERROR FROM THE EVALUATION
        except AttributeError as ErrorMessage:
            print("\n\tTEST FAILED, AttributeError:",ErrorMessage)
            return(0)
        
        #HANDLES THE "NOTENOUGHINPUTS" ERROR FROM THE EVALUATION
        except ValueError as ErrorMessage:
            print("\n\tTEST INVALID, " + str(ErrorMessage))
            return(0)    
        
        #TRIES TO EXECUTE, IF THE EVALUATION PRODUCED A SYNTAX ERROR
        except SyntaxError:#if the command can't be evaluated, it will try to execute the command instead.
            try:
                exec(sCommandToTest, oModule.__dict__)
                CommandOutput = "Blank"
            
            #HANDLES ANY NAME ERRORS FROM THE EXECUTING COMMAND
            except NameError as ErrorMessage:
                print("\n\tTEST FAILED, NameError:",ErrorMessage)
                return(0)
            
             #HANDLES ANY ATTRIBUTESERROR FROM THE EVALUATION
            except AttributeError as ErrorMessage:
                print("\n\tTEST FAILED, AttributeError:",ErrorMessage)
                return(0)
        
        #COMPARES THE OUTPUT FROM THE EVALUATION AGAINST THE EXPECTED OUTPUT FROM IT
        if CommandOutput != ExpectedOutput and ExpectedOutput != "Blank":
            print("\n\tTEST FAILED, expected",ExpectedOutput,"as the output, received",CommandOutput)
            return(0)
        
        #CHECKS THAT ALL THE EXPRESSIONS PASSED TO THIS METHOD ARE TRUE, AND HANDLES ANY EXCEPTIONS
        ExpressionFails = []
        try:
            #LOOPS THROUGH EVERY EXPRESSION IN THE LIST, EVALUATES EACH ONE, AND PUT FALSE ONES ON A LIST
            for i in range(len(lExpressionsToTest)):
                if eval(lExpressionsToTest[i], oModule.__dict__) != True:
                    ExpressionFails.append([sCommandToTest,lExpressionsToTest[i]])
            
            #IF THE LIST OF FALSE EXPRESSIONS IS NOT EMPTY       
            if len(ExpressionFails) != 0:
                #DECLARES THE TEST A FAILURE
                print("\n\tTEST FAILED!!!, the expressions ",end="")
                
                #PRINTS OUT THE LIST OF EXPRESSIONS THAT WERE FALSE
                for i in ExpressionFails:
                    print("[",i[1],"] ",end="",sep="")
                print("did not evaulate as True")
                return(0)
        
        #HANDLES ANY NAMEERRORS FROM THE EXPRESSIONS TESTED
        except Exception as ErrorMessage:
            print("\n\tTEST INVALID, You tried to test the expression '",lExpressionsToTest[i],"' and got the errormessage '",ErrorMessage,"'",sep="")
            return(0)
        
        #DECLARES THE TEST A SUCCESS, BECAUSE NO TEST HAS ENDED THIS METHOD EARLY
        print("\t Test Passed")
        return(1)
        
    #DEF VARIABLES, LISTS AND OBJECTS
    if sys.argv[1][-3:] == ".py":
        sModuleName = sys.argv[1][:-3] #makes the first variable sent the module to test
    else:
        print("Not enough arguments are sent to this program")
        exit()
    input.lWantedInput = []    #Makes an empty list which will later be used as the "input" list
    input.iInputExecuteCount = 0   #The number of times input has been run since the last time SetInput has been run
    iSuccessfulTests = 0
    
    #RUNS THE MAIN PROGRAM
    if __name__ == "__main__":
        #IMPORTS THE MODULE TO BE TESTED
        exec("import " + sModuleName)
        exec("oModule = " + sModuleName)     
        
        #DISABLES THE PRINT FUNCTION IN THE MODULE
        exec("def print(sep='',end='',*Throwaway):pass", oModule.__dict__)
        
        #COLLECT ALL DOCSTRINGS IN THE MODULE RECURSIVELY
        l("Collecting Doc Strings")
        sDocStrings = fsDocStrings(oModule, sModuleName)    
        
        #CONVERT ALL DOCSTRINGS TO UNIT TESTS
        l("Converting Doc Strings")
        lsUnitTests = flsDocStrings(sDocStrings)
        
        
        #PRINT AND RUN ALL UNIT TESTS    
        l("Running unit tests")
        print("\t\tUNIT TESTING:", sModuleName,":")
        for lTest in lsUnitTests:
            try:
                l("testing " + "fbUnitTest(oModule, " + str(lTest) + ")")
                iSuccessfulTests += eval("fbUnitTest(oModule, " + str(lTest) + ")" )
            
            #HANDLES ANY NAME ERRORS 
            except NameError as ErrorMessage:
                print("\n\tTEST INVALID, NameError:",ErrorMessage)
            
            #HANDLES ANY UNHANDLED ERRORS
            except SyntaxError as ErrorMessage:
                print("\n\tTEST INVALID, You tried to test the expression '",lExpressionsToTest[i],"' and got the errormessage '",ErrorMessage,"'",sep="")
        #PRINT RESULTS
        print("\n\t\t", len(lsUnitTests), "TESTS RUNNED, AND",len(lsUnitTests)-iSuccessfulTests, "OF THOSE HAVE FAILED\n")

IMN logo majestic logo threadwatch logo seochat tools logo