1
\$\begingroup\$

I want to document every exception handled in my code and some states of the code when it works properly. What I've done is two functions, one that creates an 'error report' (function: error) and one that creates a 'state report'(function: state), and they work by being called in functions where there's stuff I'd like to have documented, they have a code parameter which I use to select the message of the report (apart from the exception, in the case of it being an error report) and then they call another function which takes this information and loads it to a JSON file(function: writeReport). If I'm being clear so far, you can skip all the code examples, my question is at the end.

Here I show you the functions I mentioned before (messages are in spanish because its my main language):

# Exception mesagges (this is the one that records exceptions)
def error(code, exception, extra=None):
    if code == 0:
        stateReport.message = ('Error indefinido')
    elif code == 1:
        stateReport.message = ('Error en la conexion o creacion de la base de datos')
    elif code == 2:
        stateReport.message = ('No se pudieron crear las tablas o ya existen')
    elif code == 3:
        stateReport.message = (f'No se pudo recuperar la tabla {extra}')
    elif code == 4:
        stateReport.message = (f'Error indefinido que no afecta al funcionamiento')
    elif code == 5:
        stateReport.message = (f'Error en manipulacion de GUI que no afecta al funcionamiento')
    elif code == 100:
        stateReport.message = (f'Error desconocido al presionar una tecla')
    stateReport.function = (inspect.stack()[1].function)
    stateReport.date_time = date_time
    stateReport.error = str(exception)
    writeReport()
    return

# State messages (this I use when some special case goes right)
def state(code, extra=None):
    if code == 0:
        stateReport.message = (f'El programa funciono correctamente')
    elif code == 1:
        stateReport.message = (f'Se accedio a la base de datos')
    elif code == 2:
        stateReport.message = (f'Se creo exitosamente la nueva tabla')
    elif code == 3:
        stateReport.message = (f'Valores ingresados incorrectos. Valor incorrecto: {extra}')
    stateReport.function = (inspect.stack()[1].function)
    stateReport.date_time = date_time
    stateReport.error = '---------->Sin errores<----------' #This means "No errors"
    writeReport()


def writeReport():
    report = {'date': stateReport.date_time,
               'function': stateReport.function,
               'message': stateReport.message,
               'error': stateReport.error}
    if not os.path.exists(reportsFile):
        with open(reportsFile, 'w', newline='') as file:
            json.dump(report, file, indent=4)
    else:
        with open(reportsFile, 'a', newline='') as file:
            json.dump(report, file, indent=4)
    return

Now, heres a function where I use this feature on, just so you see how I actually implemented it:

table = 'clientes'
    ## This is the creation of a <new> table in an sqlite database
try:
    c.execute(f'''CREATE TABLE {table} (
    id integer,
    nombre text,
    apellido text,
    valoracion real)''')
    state(2) 
    ##If the table is successfully created a state report is saved with this info
except Exception as e:
    error(2, e, table) 
    ##On the contrary, if an exception occurs, en error report is recorded

This works great, but today I ran into the logging library (im kinda new in python) and it made me think. Is it better to use loggings in all of this cases? It would create a file with everything, it's allready a python library and I would avoid having three separate functions just for recording stuf, but I dont know if it's logging proper way of using it.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Why not doing both? \$\endgroup\$ – πάντα ῥεῖ Mar 5 '20 at 21:55
2
\$\begingroup\$

Your assumption is right: the logging library should cover your needs. I use it in every script.

Here is some semi-borrowed code that I use in some scripts:

# define logging options
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# create a file handler
handler = logging.FileHandler(current_dir + log_file)
handler.setLevel(logging.DEBUG)
# create a logging format
formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s', "%Y-%m-%d %H:%M:%S")
handler.setFormatter(formatter)
# add the handlers to the logger
logger.addHandler(handler)

Note that I am logging the line numbers as well so it's pretty easy to find the line that generated an exception and figure out the context.

The log looks like this:

2020-02-12 15:51:02 - script.py:507 - __main__ - <module> - INFO - Result: 1500, Message: User logged out

There is more code actually, because I also log text to the console but in a different format, and only the INFO or ERROR messages. The DEBUG level messages on the other hand are logged to the file, which contains much more information than the console.

Of course the exception handlers use the same logging routine. What I like is the flexibility and the ability to log to multiple destinations.

Obviously, in addition to this technique, you could also have multiple exception handlers rather than one single all-purpose except Exception block.

For example to handle key violations in SQLite you could have:

except sqlite3.IntegrityError:
    # do something

It depends on opportunity and the complexity of your application. For small scripts one single handler is usually sufficient.

If you really want to add your own special sauce or have special needs not immediately addressed you can still implement your own class by deriving from (or overriding) logging.Logger. Since logging is already widespread in Pythonland I would stick with it.

When an exception occurs you will usually log the Python stacktrace. But your exception handling is implemented in such a way that you are missing out on a lot of information that would really help for debugging purposes. The only information available is what you are effectively passing to your function:

except Exception as e:
    error(2, e, table) 

You are discarding all the exception information available from Python. The Python stacktrace is quite verbose and when you look at it you usually understand very quickly what's wrong, or at least where things went wrong. Here debugging becomes a guessing game because exception handling is being underutilized.

\$\endgroup\$
3
\$\begingroup\$

The Python community favors having just one way to do something. As the Zen of Python puts it:

There should be one-- and preferably only one --obvious way to do it.

Other people and packages will be using the logging module, in general. That makes it much easier for your code to interoperate with other packages, use consistent configuration, avoid non-obvious mistakes (like writing error messages to stdout), etc. As such, it's probably to your (and your code's) long-term advantage to use the logging module.

I'll say, one handy tip for logging in this particular case is the exc_info argument to .error, which makes it easy to log exceptions and stack traces while also printing your own messages.

@Peilonrayz responded first with a couple points that I planned to mention, so I'll just re-affirm those points:

  1. You should use exceptions with messages properly; don't just log something and move on as if nothing happened. Whoever called your code (I don't care if you're the only one calling your functions) deserves to know that what they asked for didn't happen, and you're begging for bugs if you don't throw an exception to tell them.

  2. Don't use a pile of if/else statements just to convert an integer into a string for an error message. Use named strings or a dictionary to store this data somewhere else in your code. (Once you do that, it'll make a whole lot more sense to just pass the error message to any handler function rather than have a custom function to look up error codes. This feels very much like an old habit from C/C++; welcome to Python, please don't use error codes that people need to look up, use one of the many excellent error types initialized with useful messages to communicate errors.)

\$\endgroup\$
0
2
\$\begingroup\$

Yes logging could replace writeReport, but it wouldn't write JSON. It would not be able to replace the other two functions.

But logging is a complex library, and can be rather daunting to get it to work correctly as a beginner. And overall the majority of your code would still be the same. Given that you're mutating globals than I think you may be overwhelmed when implementing it.


However I have never needed a function like error, as if I raise an error it's either mission critical - exiting the program. Or it's for control flow. And so I believe the rest of your system is misconfigured, as you shouldn't need error.

Additionally both error and state should be passed the message as an argument. If you need to use existing error messages then you can put them in a module:

states.py

CODE_0 = 'El programa funciono correctamente'
CODE_1  = 'Se accedio a la base de datos'
...
import states

state(states.CODE_0)

Or just a list:

STATES = [
    'El programa funciono correctamente'
]

state(STATES[0])
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.