Day 33 of 100 - Logging Day 3 of 3

Logging Day 3 of 3

Day 33 of 100

Yeah! 1/3 of the way through the 100 Days of Coding journey! This is my longest streak of coding. I have been pretty true to the commitment of 1 hour of coding per day, despite it being a rather busy time at my day job. I know I may miss a couple of days next weekend due to a family vacation but we need a break to relax and spend some time together as a family before the school year begins, with the challenges that will accompany it.

What I found to be the most challenging part of the this challenge was finding an application to integrate some logging. I don't have a lot of applications or finished projects to build on so I looked back at some of the applications that we worked with in this course. I settled on the number guessing game from the Pytest chapter.

I was not able to dig into this challenge as much as I would have liked but I was able to successfully use the example code from the first two days of the challenge to get some logging messages to print to a file for this application. I added some logging messages to the exceptions for the input validation section of the code. This is really just "proof of concept" code that shows that I could get log messages to print to a file. They essentially print the error messages to the log file so I could use conceivably use the log to see the most common errors for user input to improve the instructions. While the code for the game isn't mine, the logging additions are, but they are also heavily borrowed from the example code for this challenge...

import random
import logbook
import sys

MAX_GUESSES = 5
START, END = 1, 20

app_log = logbook.Logger('App')


def get_random_number():
    """Get a random number between START and END, returns int"""
    return random.randint(START, END)


class Game:
    """Number guess class, make it callable to initiate game"""

    def __init__(self):
        """Init _guesses, _answer, _win to set(), get_random_number(), False"""
        self._guesses = set()
        self._answer = get_random_number()
        self._win = False

    def guess(self):
        """Ask user for input, convert to int, raise ValueError outputting
           the following errors when applicable:
           'Please enter a number'
           'Should be a number'
           'Number not in range'
           'Already guessed'
           If all good, return the int"""
        guess = input(f'Guess a number between {START} and {END}: ')

        if not guess:
            msg = "Please enter a number"
            app_log.warn(msg)
            raise ValueError(msg)

        try:
            guess = int(guess)
        except ValueError:
            msg = "Should be a number"
            app_log.warn(msg)
            raise ValueError(msg)

        if guess not in range(START, END+1):
            msg = "Number not in range"
            app_log.warn(msg)
            raise ValueError(msg)

        if guess in self._guesses:
            msg = "Already guessed"
            app_log.warn(msg)
            raise ValueError(msg)

        self._guesses.add(guess)
        return guess

    def _validate_guess(self, guess):
        """Verify if guess is correct, print the following when applicable:
           {guess} is correct!
           {guess} is too high
           {guess} is too low
           Return a boolean"""
        if guess == self._answer:
            print(f'{guess} is correct!')
            return True
        else:
            high_or_low = 'low' if guess < self._answer else 'high'
            print(f'{guess} is too {high_or_low}')
            return False

    @property
    def num_guesses(self):
        return len(self._guesses)

    def __call__(self):
        """Entry point / game loop, use a loop break/continue,
           see the tests for the exact win/lose messaging"""
        while len(self._guesses) < MAX_GUESSES:
            try:
                guess = self.guess()
            except ValueError as ve:
                print(ve)
                continue

            win = self._validate_guess(guess)
            if win:
                guess_str = self.num_guesses == 1 and "guess" or "guesses"
                print(f'It took you {self.num_guesses} {guess_str}')
                self._win = True
                break
        else:
            # else on while/for = anti-pattern? do find it useful in this case!
            print(f'Guessed {MAX_GUESSES} times, answer was {self._answer}')

def init_logging(filename: str = None):
    level = logbook.TRACE

    if filename:
        logbook.TimedRotatingFileHandler(filename, level=level).push_application()
    else:
        logbook.StreamHandler(sys.stdout, level=level).push_application()

    msg = 'Logging initialized, level: {}, mode: {}'.format(
        level,
        "stdout mode" if not filename else 'file mode: ' + filename
    )
    logger = logbook.Logger('Startup')
    logger.notice(msg)


if __name__ == '__main__':
    init_logging('number_game.log')
    game = Game()
    game()