How can I create a continuous / infinite CLI with Click? - python-3.x

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()

Related

Python click option based logging in decorator

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)

Can't call a variable from another file

I have two files. The first file, we'll call it "a.py". The second, "b.py".
Here's an example of "a.py":
#!/usr/bin/env python
# coding=utf-8
CHOOSE = input ('''
\033[1;35m choose 1 or 2:\033[0m
1)tom
2)jack
''')
a = 666
b = "bbb"
def f():
print("this is a test")
return "function"
if __name__ == '__main__':
if CHOOSE == '1':
username = 'tom'
print(username)
elif CHOOSE == '2':
username = 'jack'
print(username)
else:
print('wrong choice,scipt is exit...')
Here's an example of "b.py":
#!/usr/bin/env python
# coding=utf-8
import a
from a import b,f,CHOOSE,username
a = a.a
f()
print(b,a,CHOOSE,username)
when i run python b.py,system feedback error:
wherem am i wrong?how to fix it?
Because this snippet:
if __name__ == '__main__':
if CHOOSE == '1':
username = 'tom'
print(username)
elif CHOOSE == '2':
username = 'jack'
print(username)
else:
print('wrong choice,scipt is exit...')
Will get executed only if the a.py run as the main python file not imported from other module, so username will not be defined so you can not import it. Here how to fix it:
a.py:
...
def foo(CHOOSE):
if CHOOSE == '1':
username = 'tom'
elif CHOOSE == '2':
username = 'jack'
else:
username = None
return username
b.py:
from a import foo
CHOOSE = input ('''
\033[1;35m choose 1 or 2:\033[0m
1)tom
2)jack
''')
username = foo(CHOOSE)
if username:
print(username)
else:
print('Wrong choice')
Explanation: First you need to extract the code that calculate the name into something reusable. Function are meant for code reusability so I defined one which take one parameter and it return the corresponding value. This function is then used (imported then invoked) in b.py.
Usually if __name__ == '__main__': is placed in the module that you consider your entry-point, so if you want to use it maybe b.py is a better place.
The block if __name__ == '__main__' only triggers if the script is run with a.py being the main module (e.g. you ran it using the command python a.py). If anything else was used as the main module, and a was imported, then this condition is not met and the code inside of it does not run.
The variable username is created and added to a's namespace in the if __name__ == '__main__' block. When you do python b.py, the code inside this block does not execute, and as a result username never gets added to the namespace. So when you try to import it, immediately after loading the file a, you get an error saying that the 'username' doesn't exist - it was never created.

How to use readline autocompletion without input()

I want to use readlines autocomplete functionality, but I don't want to use pythons input() function. However autocomplete doesn't work when I don't use input(). I want know how to make it work.
import readline
import sys
commands = ['start', 'pause', 'stop']
def completer(text, state):
options = [i for i in commands if i.startswith(text)]
if state < len(options):
return options[state]
else:
return None
readline.parse_and_bind("tab: complete")
readline.set_completer(completer)
while True:
prompt = 'cmd: '
# line = input(prompt)
sys.stdout.write(prompt)
sys.stdout.flush()
line = sys.stdin.readline()
print(line)

Defining a main_menu function to be called from other methods within a class in Python

I have a class with several methods to assign attributes from user input, and three methods that will add, delete, or update a nested dictionary with the input.
I would like to add a main_menu function so that the user can access the add, delete, and update methods and then choose to either continue adding/deleting/updating the dictionary, or go back to the main menu.
When I tried to make a main_menu function, I receive NameError: name 'command' is not defined. The program will loop through as expected if the main_menu is not a function, but once I tried to turn it into a function, I get the error. I've tried different levels of indentation, but I'm new to Python and don't know what else to try.
class MyClass:
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
def get_x(self):
#code to get x from user input
def get_y(self):
#code to get y from user
def get_z(self):
#code to get z from user
def add_info(self):
store_info = {}
id = 0
while command = '1':
new_info = {}
new_id = len(store_info) + 1
store_info[new_id] = new_info
x = h.get_x()
new_info['x'] = x
y = h.get_y()
new_info['y'] = y
z = h.get_z()
new_info['z'] = z
print('Your info has been updated.\n', store_info)
choice = input('To add more info, type 1. To return to the main menu, type 2')
if choice == '1':
continue
elif choice == '2':
main_menu()
else:
print('The End')
break
def delete_info(self):
#code to delete, with an option at the end to return to main_menu
def update_info(self):
#code to update, with option for main_menu
def main_menu():
main_menu_option = """Type 1 to add.
Type 2 to delete.
Type 3 to update.
Type any other key to quit.\n"""
h = MyClass()
command = input(main_menu_option)
if command == '1':
h.add_info()
elif command == '2':
h.delete_info()
elif command == '3':
h.update_info()
else:
print('Good bye.')
main_menu()
When I run the program, I get the main menu and type 1, but then receive the NameError for command.
Before I tried to make the main_menu a function, I could access the add method to add info to the nested dictionary.
In Python methods/functions only have access to variable in their scope or parent scopes. For example:
command = 1 # A
def foo():
print(command)
def bar():
command = 2 # B
print(command)
foo()
bar()
print(command)
prints out 1 and then 2 and then 1. This works because when we call foo, python looks at the local variables and realises there is no command variable there so it looks at the scope above and sees the command variable at label A. Then when we call bar() we add a variable called command (label B) to the local variables of bar and then print that, notice that python doesn't look at the global variables here so it prints 2 without changing the initial command variable (label A) as we can see when we finally print(command) at the end of the script.
It is because of this that your program is failing, your add_info method is trying to access a variable called command however none exists in its local variable or its global scope. The fix for this would be to add command to the add_info local variables by passing it as an argument to the method from main_menu.
# in main_menu
if command == '1':
h.add_info(command)
# in add_info
def add_info(self, command):
...

How emulate termial iteraction like putty using pySerial

I tried to use pySerial to build a simple terminal to interact COM1 in my PC,
I create 2 threads , one is for READ , the other is for Write
however,
def write_comport():
global ser,cmd, log_file
print("enter Q to quit")
while True:
cmd = raw_input(">>:")
# print(repr(var))
if cmd=='Q':
ser.close()
sys.exit()
log_file.close()
else:
ser.write(cmd + '\r\n')
write_to_file("[Input]"+cmd, log_file)
time.sleep(1)
pass
def read_comport():
global ser, cmd, log_file
while True:
element = ser.readline().strip('\n')
if "~ #" in str(element):
continue
if cmd == str(element).strip():
continue
if "img" in str(element):
print("got:"+element)
beep()
print element
write_to_file(cmd, log_file)
pass
def write_to_file(str,f):
f.write(str)
f.flush
def main():
try:
global read_thr,write_thr
beep()
port_num='COM4'
baudrate=115200
init_serial_port(port_num,baudrate)
read_thr =Thread(target=read_comport)
read_thr.start()
write_thr =Thread(target=write_comport)
write_thr.start()
while True:
pass
except Exception as e:
print(e)
exit_prog()
but the behavior of my code is not as smart as putty or anyother.
cause my function can Not detect whenever the reader is done.
is there any better way to achieve this goal?
By the way, I tried to save the log into txt file real-time . But when I open the file during the process is running, it seems nothing write to my text log file?

Resources