Introduction While I was spending my weekend on one of my favorite pastimes, writing Python code, and found a way to generate a 3D QR code of my WIFI password. In the process, I had some interesting epiphanies, mainly that Command Line Interfaces (CLIs) and Web Apps share some striking commonalities: CLIs and web apps are nothing more than text end points to arbitrary code! To show this in detail, I’ll use the code base from my 3D model QR code project and create a command-line interface from the functions I used. Building a Command-Line Game If you’d like to learn how to build a simple command-line game, check out Kite’s Hangman tutorial. Building a Command-Line Interface (CLI) A CLI lets you access a program from the command line, say, the Linux/macOS bash shell or a Windows command prompt. A CLI lets you run scripts. For example a CLI would allow us to programmatically generate as many QR codes as we wish, with a single command. One disadvantage of a command-line interface to keep in mind is that it requires the end user to be familiar with supported text commands. This can feel somewhat like memorizing incantations to perform magic (well, this does seem to jive with Clarke’s law). A small price to pay for technological excellence! Overview: Preparation to build a CLI In the name of “good software engineering”, we are going to first organize our functions a bit, and prepare to build this into a Python package that can be easily distributed. The final directory structure we are targeting is as follows: ├── environment.yml ├── qrwifi │ ├── __init__.py │ ├── app.py │ ├── cli.py │ ├── functions.py ** │ └── templates │ ├── index.html.j2 │ ├── qr.html.j2 │ └── template.html.j2 └── setup.py (From this point on, I will highlight the file that we will be editing with a double asterisk (**)). Functions Library Let’s start by creating functions.py . It should house the functions that we can import and call on.

import numpy as np

import pyqrcode as pq





def wifi_qr(ssid: str, security: str, password: str):

"""

Creates the WiFi QR code object.

"""

qr = pq.create(f'WIFI:S:{ssid};T:{security};P:{password};;')

return qr





def qr2array(qr):

"""

Convert a QR code object into its array representation.

"""

arr = []

for line in qr.text().split('

'):

if line:

arr.append(list(map(int, line)))

return np.vstack(arr)





def png_b64(qr, scale: int = 10):

"""

Return the base64 encoded PNG of the QR code.

"""

return qr.png_data_uri(scale=scale)

CLI Module To build a command line interface, we are going to use a Python package called Click . Kite also hosts a mirror of the docs, which is served to your text editor when you use Kite.) You can install it using:

$ pip install click

What click provides is a clean and composable way of building command line interfaces to your Python code. $ tree . ├── environment.yml ├── qrwifi │ ├── __init__.py │ ├── cli.py ** │ └── functions.py └── setup.py Let’s now build cli.py . This will contain our package’s command line module. We’ll architect it so a user can use it as such:

$ qrwifi --ssid '<SSID_NAME>' \

--security '<SECURITY>' \

--password '<PASSWORD>' \

[terminal|png --filename '<FILEPATH>']

To clarify, we are replacing all of the <...> with appropriate strings, without the $ symbol, without the {} braces. I’ll build your intuition bit-by-bit, and then we can take a look at everything together at the end. You can always reference the full cli.py script at the end of this section.

import numpy as np



import pyqrcode as pq



import click



from .functions import wifi_qr, qr2array





@click.group()

@click.option('--ssid', help='WiFi network name.')

@click.option('--security', type=click.Choice(['WEP', 'WPA', '']))

@click.option('--password', help='WiFi password.')

@click.pass_context

def main(ctx, ssid: str, security: str = '', password: str = ''):

qr = wifi_qr(ssid=ssid, security=security, password=password)

ctx.obj['qr'] = qr

We start by importing the necessary packages, and begin with the main() function. According to its function signature, the main() function accepts a ctx object ( ctx is short for “context”, more on this later), as well as the keyword arguments that we need for putting together our WiFi QR code. In the body of main() , we call the wifi_qr() function defined in functions.py , and then assign the resulting qr object to ctx.obj (the context’s object dictionary). If you’re still wondering what this “context” object is all about, hang tight – I’m going to get there soon. Apart from the function definition, you’ll notice that we have decorated it with click functions. This is where click ‘s magic comes into play. By decorating main() with @click.group() , we can now expose main() at the command line and call it from there! To expose its options to the command line as well, we have to add one or more @click.option() decorators with the appropriate flags. You’ll also notice that there’s this decorator, @click.pass_context . This is perhaps a good time to introduce the “context” object. The simplest way to architect our CLI to output to the terminal or PNG file is to have a “child” function of main() , which knows about what has been set up in main() . In order to enable this, @click.pass_context decorates a function that accepts, as its first argument, a “context” object, whose child .obj attribute is a glorified dictionary. Using this programming pattern, “child” functions can act on the context object and do whatever it needs. It’s basically like passing state from the parent function to the child function. Let’s go on to build the “child” functions, which are named terminal() and png() .

@main.command()

@click.pass_context

def terminal(ctx):

"""Print QR code to the terminal."""

print(ctx.obj['qr'].terminal())





@main.command()

@click.option('--filename', help='full path to the png file')

@click.pass_context

def png(ctx, filename, scale: int = 10):

"""Create a PNG file of the QR code."""

ctx.obj['qr'].png(filename, scale)

Both of our functions are decorated with @main.command() , which indicates to click that this is a “child” command of the main() function. Decorating functions using @somecommand.command() allows us to nest commands into each other and separate logic, making our code clear. terminal() does not have any options, because we want it printed directly to the terminal. We want the png() command saved to disk at some pre-specified path. Thus, it has another @click.option() attached to it.

def start():

main(obj={})





if __name__ == '__main__':

start()

Finally, we have the start() function, which passes an empty dictionary to main() . The start() function has no arguments, so it is necessary to add it to setup.py as an entry point (coming later).

Want to Code Faster? Kite is a plugin for PyCharm, Atom, Vim, VSCode, Sublime Text, and IntelliJ that uses machine learning to provide you with code completions in real time sorted by relevance. Start coding faster today. Send Download Link Download Kite Free

cli.py in full As promised, here’s the full cli.py that you can copy/paste.

import numpy as np



import pyqrcode as pq



import click



from .functions import wifi_qr, qr2array





@click.group()

@click.option('--ssid', help='WiFi network name.')

@click.option('--security', type=click.Choice(['WEP', 'WPA', '']))

@click.option('--password', help='WiFi password.')

@click.pass_context

def main(ctx, ssid: str, security: str = '', password: str = ''):

qr = wifi_qr(ssid=ssid, security=security, password=password)

ctx.obj['qr'] = qr

ctx.obj['ssid'] = ssid

ctx.obj['security'] = security

ctx.obj['password'] = password





@main.command()

@click.pass_context

def terminal(ctx):

print(ctx.obj['qr'].terminal())





@main.command()

@click.option('--filename', help='full path to the png file')

@click.pass_context

def png(ctx, filename, scale: int = 10):

ctx.obj['qr'].png(filename, scale)





def start():

main(obj={})





if __name__ == '__main__':

start()

Tweaking the CLI UI qrwifi at the Command Line How does this look like at the command line? Let’s see: $ python cli.py --help Usage: python cli.py [OPTIONS] COMMAND [ARGS]... Options: --ssid TEXT WiFi network name. --security [WEP|WPA|] --password TEXT WiFi password. --help Show this message and exit. Commands: png terminal Look at that!! We didn’t have to do any argparse tricks to make this gorgeous output show up! We even got a “help menu” for free, complete with the “help” text that we specified at the command line. You’ll notice that there’s the Options section, with all of the options attached to the main() function, as well as a Commands section, with the child functions ( png() and terminal() ) available. The function name is exactly the command name at the CLI. We’re still not done though, because this cli.py is only accessible if we know where the file is. If we’re distributing this as a package, we’d ideally like to abstract away the location of cli.py , instead having our end user call on a memorable name, say, qrwifi . Create a setup.py To do this, we need another file, the setup.py file. $tree . ├── environment.yml ├── qrwifi │ ├── __init__.py │ ├── cli.py │ └── functions.py └── setup.py ** Let’s take a look at the structure of the setup.py file. (You can also copy/paste this in full.)

from setuptools import setup, find_packages



setup(

# mandatory

name='qrwifi',

# mandatory

version='0.1',

# mandatory

author_email='username@email.address',

packages=['qrwifi'],

package_data={},

install_requires=['pyqrcode', 'SolidPython', 'numpy', 'Flask', 'click'],

entry_points={

'console_scripts': ['qrwifi = qrwifi.cli:start']

}

)