Python click option based logging in decorator - python-3.x

Fresh start. I have a CLI application that uses click for handling argument parsing. For the main "executable" script I have a defined verbosity flag (-v, -vv, -vvv, ...) that controls logging verbosity. I want to "trace" function calls for specific functions. Down below is a sample that hopefully will make it clear.
import click
import logging
import functools
class MyLogger(object):
def __init__(self):
self.__logger = None
def init_logger(self, name, verbosity):
logging_levels = {0: logging.CRITICAL,
1: logging.ERROR,
2: logging.INFO,
3: logging.DEBUG}
logging.basicConfig(level=logging_levels.get(verbosity, logging.WARNING))
self.__logger = logging.getLogger(name)
#property
def logger(self):
return self.__logger
myLogger = MyLogger()
class TraceFunction(object):
def __init__(self, logger):
self.logger = logger
def __call__(self, function):
name = function.__name__
#functools.wraps(function)
def wrapped(*args, **kwargs):
self.logger.debug(f'{name}({list(*args)}, {dict(**kwargs)})')
result = function(*args, **kwargs)
self.logger.debug(f'{result}')
return result
return wrapped
# (1) #TraceFunction(myLogger.logger)
def echo(message):
return message.upper()
#click.command('echo')
#click.option('-e', '--echo', 'message', required=True, type=str)
def echo_command(message):
myLogger.logger.info('echo_command')
return echo(message)
#click.group()
#click.option('-v', 'verbosity', count=True)
def main(verbosity: int):
myLogger.init_logger(__name__, verbosity)
# (2) TraceFunction(myLogger.logger)(echo)
myLogger.logger.info('main')
if __name__ == '__main__':
main.add_command(echo_command)
main()
The above if executed will correctly produce the following output:
script.py -vv echo -e "Hello World"
INFO: __main__:main
INFO: __main__:echo_command
I want to "trace" the function: echo. More precisely I want to log the actual function call with the actual arguments and the returned value. Okay, a bit more than that but I needed a minimal sample. For this purpose I tried two things, labeled with (1) and (2) placed in comments.
#TraceFunction(myLogger.logger)
def echo(message):
return message.upper()
It flat out doesn't work as with my original question python will execute TraceFunction.call(echo) before in "main" I call init_logger that essentially would configure the logger itself. As a result in TraceFunction.call the logger is None and I get:
AttributeError: 'NoneType' object has no attribute 'debug'
Fine, I can register it later on, at least I thought with (2). Well the exception surely went away, however "wrapped" defined in call is never invoked and well once again nothing gets logged other than the already shown
script.py -vvv echo -e "Hello World"
INFO: __main__:main
INFO: __main__:echo_command
#Update
Going by afterburner's answer things go a bit further but it doesn't do what it's supposed to:
script.py -vvv echo -e "Hello World"
DEBUG:__main__:echo(['F','o','o'],{})
DEBUG:__main__:FOO
INFO: __main__:main
INFO: __main__:echo_command
Which well is expected. The expected output on the other hand would be:
script.py -vvv echo -e "Hello World"
INFO: __main__:main
INFO: __main__:echo_command
DEBUG:__main__:echo(['Hello World'],{})
DEBUG:__main__:HELLO WORLD

So, the main issue I can see is that you're not calling the wrapped function.
TraceFunction(myLogger.logger)(echo)
# vs
TraceFunction(myLogger.logger)(echo)()
I have also made a couple of changes to your code, but the main issue was the fact the wrapped function was never getting invoked.
class MyLogger(object):
def __init__(self):
self.__logger = None
def init_logger(self, name, verbosity):
# extract log level based on verbosity flag
logging_levels = [logging.CRITICAL, logging.INFO, logging.DEBUG]
logging.basicConfig(level=logging_levels[verbosity])
self.__logger = logging.getLogger(name)
#property
def logger(self):
return self.__logger
myLogger = MyLogger()
class TraceFunction(object):
def __init__(self, logger):
self.logger = logger
def __call__(self, function):
name = function.__name__
#functools.wraps(function)
def wrapped(*args, **kwargs):
# improved the formatting of arguments
nicely_formatted_args = ', '.join(args)
nicely_formatted_kwargs = ', '.join(kwargs)
arguments = nicely_formatted_args
if nicely_formatted_kwargs != '':
arguments = f'{arguments}, {nicely_formatted_kwargs}'
self.logger.debug(f'{name}({arguments})')
result = function(*args, **kwargs)
self.logger.debug(f'{result}')
return result
return wrapped
# (1) #TraceFunction(myLogger.logger)
def echo(message):
return message.upper()
#click.command('echo')
#click.option('-e', '--echo', 'message', required=True, type=str)
def echo_command(message):
myLogger.logger.info('echo_command')
return echo(message)
#click.group()
#click.option('-v', 'verbosity', count=True) # <- made verbosity a count argument so you can extract all of your levels based on -v -vv -vvv etc.
def main(verbosity):
myLogger.init_logger(__name__, verbosity)
# (2)
# Invoking the function with argument 'Foo'
TraceFunction(myLogger.logger)(echo)("Foo")
def run_logging():
main.add_command(echo_command)
main()
if __name__ == '__main__':
run_logging()

I managed to get it working but it sure is ugly... at least in its current form. The only needed change was:
#click.group()
#click.option('-v', 'verbosity', count=True)
def main(verbosity: int = 0):
global echo
myLogger.init_logger(__name__, verbosity)
echo = TraceFunction(myLogger.logger)(echo) # <<< !!!
myLogger.logger.info('main')
In doing so the output becomes:
INFO:__main__:main
INFO:__main__:echo_command
TRACE:__main__:echo(['Hello World'], {})
TRACE:__main__:HELLO WORLD
So the answer was, that I completely missed:
TraceFunction(myLogger.logger)(echo)
is fine, but I needed to assign it to the original echo function:
echo = TraceFunction(myLogger.logger)(echo)

Related

Python unittest class issue

I'm trying to write a unit test class in python but feel like I'm missing something fundamental as it's not doing what I would expect. Here is my class:
from unittest import TestCase
class MyTestClass(TestCase):
def __init__(self):
self.file_name = None
def setUp(self):
self.file_name = 'give this file a name'
return self.file_name
def test_a_file_name(self):
assert self.file_name == 'give this file a name', 'fail'
tester = MyTestClass()
tester.setUp()
tester.test_a_file_name()
I would expect when running this that the test would pass but I'm getting a __init__() takes 1 positional argument but 2 were given error and I can't see why?
When running unittest.main your class that inherits from TestCase gets handed the test method to call. As such you need to allow your class to be handed that argument and pass it on to the parent class __init__.
from unittest import TestCase, main
class MyTestClass(TestCase):
# accept arbitrary positional and keyword arguments
def __init__(self, *args, **kwargs):
self.file_name = None
# pass them on to the parent
super().__init__(*args, **kwargs)
def setUp(self):
self.file_name = 'give this file a name'
return self.file_name
def test_a_file_name(self):
assert self.file_name == 'give this file a name', 'fail'
if __name__ == '__main__':
main()
As you noticed, you also don't need to handle instantiation and method calling. unittest.main() will do that for you.
In the future, if you ever get an error with arguments, a helpful debugging tip is throwing in an *args, **kwargs and printing them to see what is being handed that you're not handling.

__PRETTY_FUNCTION equivalent in Python

g++ has this nice variable PRETTY_FUNCTION that contains the name of the function called. For functions it produces the prototype (e.g., "void foo(long)"). For member functions in classes, the name includes the class name (e.g., "void Foo::foo(long)").
Is there an equivalent in Python. I have been playing around with "sys._getframe()" which gets close, but there doesn't seem to be a similarly simple mechanism that includes the class name when it is a member function.
I like to use this in error handlers.
Surely someone has a magical one-liner for this...
TIA,
-Joe
Example Python Code
This is about the closest I can get right now. Search for "<<< here".
Typing Foo.CLNAME is kind of silly. It would be nice if this could somehow be collapsed to an external function and stay outside of the class. There must be a way to do this in Python in a way similar to the way LINE and NAME work.
The implementations of those two are based on code I got from here:
How to determine file, function and line number?
Nice stuff!
-Joe
=====
#!/usr/bin/env python
# import system modules
#
import os
import sys
# define a hack to get the function name
#
class __MYNAME__(object):
def __repr__(self):
try:
raise Exception
except:
return str(sys.exc_info()[2].tb_frame.f_back.f_code.co_name)
def __init__(self):
pass
__NAME__ = __MYNAME__()
# define a hack to get the line number in a program
#
class __MYLINE__(object):
def __repr__(self):
try:
raise Exception
except:
return str(sys.exc_info()[2].tb_frame.f_back.f_lineno)
__LINE__ = __MYLINE__()
# use these in a function call
#
def joe():
print("[FUNCTION] name: %s, line: %s" %
(__NAME__, __LINE__)) # <<< here
# use these in a class member function
#
class Foo():
def __init__(self):
Foo.__CLNAME__ = self.__class__.__name__
def joe(self):
print("[CLASS] name: %s::%s, line: %s" %
(Foo.__CLNAME__, __NAME__, __LINE__)) # <<< here
#--------------------------------
# test all this in a main program
#--------------------------------
def main(argv):
# main program
#
print("[MAIN PROGRAM] name: %s, line: %s" %
(__NAME__, __LINE__)) # <<< here
# function call
#
joe()
# class member function
#
foo = Foo()
foo.joe()
# exit gracefully
#
sys.exit(os.EX_OK)
#
# end of main
# begin gracefully
#
if __name__ == "__main__":
main(sys.argv[0:])
#
# end of file

Is there a way in python to run a different function if a class is called from command line rather than invoked in code?

Lets say I have a class as follows:
class MyClass:
def __init__(self):
pass
def my_func_1(self):
print("This class has been invoked from another code")
def my_func_2(self):
print("This class has been called from the command prompt")
if __name__ == "__main__":
MyClass()
Is there a way to have the class run my_func_1 if it is invoked from a code and my_func_2 if from command line? Also, by from command line I mean by the if __name__ == "__main__": part.
The context in which I wanted to use this was to have the main class' init read the access level of the command line or ask user to login to an admin user. I ended up passing a value which can be true only if the code is ran from command line.
class MyClass:
def __init__(self, from_command_line: bool = False):
if from_command_line:
my_func_2()
else:
my_func_1()
def my_func_1(self):
print("This class has been invoked from another code")
def my_func_2(self):
print("This class has been called from the command line")
if __name__ == "__main__":
MyClass(from_command_line = True)
Like this, if the execution comes from command line, the variable from_command_line will be true and in any other case it will be false. Unless someone makes a mistake. Which an exception handling can fix. If you have a better way to do this I would very much like to learn about it.

How can I redirect hardcoded calls to open to custom files?

I've written some python code that needs to read a config file at /etc/myapp/config.conf . I want to write a unit test for what happens if that file isn't there, or contains bad values, the usual stuff. Lets say it looks like this...
""" myapp.py
"""
def readconf()
""" Returns string of values read from file
"""
s = ''
with open('/etc/myapp/config.conf', 'r') as f:
s = f.read()
return s
And then I have other code that parses s for its values.
Can I, through some magic Python functionality, make any calls that readconf makes to open redirect to custom locations that I set as part of my test environment?
Example would be:
main.py
def _open_file(path):
with open(path, 'r') as f:
return f.read()
def foo():
return _open_file("/sys/conf")
test.py
from unittest.mock import patch
from main import foo
def test_when_file_not_found():
with patch('main._open_file') as mopen_file:
# Setup mock to raise the error u want
mopen_file.side_effect = FileNotFoundError()
# Run actual function
result = foo()
# Assert if result is expected
assert result == "Sorry, missing file"
Instead of hard-coding the config file, you can externalize it or parameterize it. There are 2 ways to do it:
Environment variables: Use a $CONFIG environment variable that contains the location of the config file. You can run the test with an environment variable that can be set using os.environ['CONFIG'].
CLI params: Initialize the module with commandline params. For tests, you can set sys.argv and let the config property be set by that.
In order to mock just calls to open in your function, while not replacing the call with a helper function, as in Nf4r's answer, you can use a custom patch context manager:
from contextlib import contextmanager
from types import CodeType
#contextmanager
def patch_call(func, call, replacement):
fn_code = func.__code__
try:
func.__code__ = CodeType(
fn_code.co_argcount,
fn_code.co_kwonlyargcount,
fn_code.co_nlocals,
fn_code.co_stacksize,
fn_code.co_flags,
fn_code.co_code,
fn_code.co_consts,
tuple(
replacement if call == name else name
for name in fn_code.co_names
),
fn_code.co_varnames,
fn_code.co_filename,
fn_code.co_name,
fn_code.co_firstlineno,
fn_code.co_lnotab,
fn_code.co_freevars,
fn_code.co_cellvars,
)
yield
finally:
func.__code__ = fn_code
Now you can patch your function:
def patched_open(*args):
raise FileNotFoundError
with patch_call(readconf, "open", "patched_open"):
...
You can use mock to patch a module's instance of the 'open' built-in to redirect to a custom function.
""" myapp.py
"""
def readconf():
s = ''
with open('./config.conf', 'r') as f:
s = f.read()
return s
""" test_myapp.py
"""
import unittest
from unittest import mock
import myapp
def my_open(path, mode):
return open('asdf', mode)
class TestSystem(unittest.TestCase):
#mock.patch('myapp.open', my_open)
def test_config_not_found(self):
try:
result = myapp.readconf()
assert(False)
except FileNotFoundError as e:
assert(True)
if __name__ == '__main__':
unittest.main()
You could also do it with a lambda like this, if you wanted to avoid declaring another function.
#mock.patch('myapp.open', lambda path, mode: open('asdf', mode))
def test_config_not_found(self):
...

How can I create a continuous / infinite CLI with Click?

I'm trying to use Click to create a CLI for my Python 3 app. Basically I need the app to run continuously, waiting for user commands and executing them, and quitting if a specific command (say, "q") is entered. Couldn't find an example in Click docs or elsewhere.
An example of interactive shell would be like this:
myapp.py
> PLEASE ENTER LOGIN:
mylogin
> PLEASE ENTER PASSWORD:
mypwd
> ENTER COMMAND:
a
> Wrong command!
> USAGE: COMMAND [q|s|t|w|f] OPTIONS ARGUMENTS
> ENTER COMMAND:
f
> (output of "f" command...)
> ENTER COMMAND:
q
> QUITTING APP...
I've tried like so:
import click
quitapp = False # global flag
#click.group()
def cli():
pass
#cli.command(name='c')
#click.argument('username')
def command1(uname):
pass # do smth
# other commands...
#cli.command(name='q')
def quitapp():
global quitapp
quitapp = True
def main():
while not quitapp:
cli()
if __name__ == '__main__':
main()
But the console just runs the app once all the same.
I've actually switched to fire and managed to make a shell-like continuous function like so:
COMMAND_PROMPT = '\nCOMMAND? [w to quit] >'
CAPTCHA_PROMPT = '\tEnter captcha text (see your browser) >'
BYE_MSG = 'QUITTING APP...'
WRONG_CMD_MSG = 'Wrong command! Type "h" for help.'
EMPTY_CMD_MSG = 'Empty command!'
class MyClass:
def __init__(self):
# dict associating one-letter commands to methods of this class
self.commands = {'r': self.reset, 'q': self.query, 'l': self.limits_next, 'L': self.limits_all,
'y': self.yandex_logo, 'v': self.view_params, 'h': self.showhelp, 'c': self.sample_captcha, 'w': None}
# help (usage) strings
self.usage = '\nUSAGE:\t[{}] [value1] [value2] [--param3=value3] [--param4=value4]'.format('|'.join(sorted(self.commands.keys())))
self.usage2 = '\t' + '\n\t'.join(['{}:{}'.format(fn, self.commands[fn].__doc__) for fn in self.commands if fn != 'w'])
def run(self):
"""
Provides a continuously running commandline shell.
The one-letter commands used are listed in the commands dict.
"""
entered = ''
while True:
try:
print(COMMAND_PROMPT, end='\t')
entered = str(input())
if not entered:
print(EMPTY_CMD_MSG)
continue
e = entered[0]
if e in self.commands:
if self.commands[e] is None:
print(BYE_MSG)
break
cmds = entered.split(' ')
# invoke Fire to process command & args
fire.Fire(self.commands[e], ' '.join(cmds[1:]) if len(cmds) > 1 else '-')
else:
print(WRONG_CMD_MSG)
self.showhelp()
continue
except KeyboardInterrupt:
print(BYE_MSG)
break
except Exception:
continue
# OTHER METHODS...
if __name__ == '__main__':
fire.Fire(MyClass)
Still, I'd appreciate if someone showed how to do that with click (which appears to me to be more feature-rich than fire).
I've finally found out other libraries for interactive shells in Python: cmd2 and prompt, which are way more advanced for REPL-like shells out of the box...
There's a quick example of how to do a continuous CLI application with Click here: python click module input for each function
It only has a way of running click commands on a loop, but you can put in any custom logic you want, either in commands or the main body of the loop. Hope it helps!
Here I found click in the loop but it is error prone when we try to use different commands with different options
!Caution: This is not a perfect solution
import click
import cmd
import sys
from click import BaseCommand, UsageError
class REPL(cmd.Cmd):
def __init__(self, ctx):
cmd.Cmd.__init__(self)
self.ctx = ctx
def default(self, line):
subcommand = line.split()[0]
args = line.split()[1:]
subcommand = cli.commands.get(subcommand)
if subcommand:
try:
subcommand.parse_args(self.ctx, args)
self.ctx.forward(subcommand)
except UsageError as e:
print(e.format_message())
else:
return cmd.Cmd.default(self, line)
#click.group(invoke_without_command=True)
#click.pass_context
def cli(ctx):
if ctx.invoked_subcommand is None:
repl = REPL(ctx)
repl.cmdloop()
# Both commands has --foo but if it had different options,
# it throws error after using other command
#cli.command()
#click.option('--foo', required=True)
def a(foo):
print("a")
print(foo)
return 'banana'
#cli.command()
#click.option('--foo', required=True)
def b(foo):
print("b")
print(foo)
# Throws c() got an unexpected keyword argument 'foo' after executing above commands
#cli.command()
#click.option('--bar', required=True)
def c(bar):
print("b")
print(bar)
if __name__ == "__main__":
cli()

Resources