Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Python Summit 2022: Never Write Scripts Again

Peter Bittner
September 22, 2022

Python Summit 2022: Never Write Scripts Again

As a DevOps engineer, when you write Python code, do you also write tests? If you're like the majority of the folks out there, chances are you don't. Why would you? It's usually just a script, even if it gets longer over time and starts feeling like Bash.

This talk explains why it makes sense to stop hacking glue code and start developing serious CLI applications, test-driven, with automated tests. Even if you have some experience with writing tests, it's not immediately obvious how to get started. You'll get to know the cli-test-helpers package and see a hands-on demonstration of developing a CLI application from scratch, TDD-style.

We'll scratch the surface of some popular CLI frameworks (argparse, click, docopt), and you'll take home working code samples that will help you refuse the temptation of writing code without tests, in future.

Original slides at: https://slides.com/bittner

Peter Bittner

September 22, 2022
Tweet

More Decks by Peter Bittner

Other Decks in Programming

Transcript

  1. from application import cli 1 from cli_test_helpers import shell 2

    3 def test_cli_entrypoint(): 4 result = shell("python-summit --help") 5 assert result.exit_code == 0 6 1. What's wrong with scripts? 2. Coding example #1 (refactoring) 3. Why CLI applications? 4. Challenges with writing tests 5. Coding example #2 (cli & tests)
  2. print("This is important code") 1 2 for index, arg in

    enumerate(sys.argv): 3 print(f"[{index}]: {arg}") 4 What's Wrong with Scripts? Easy to get started Limited possibilities for structure Hard to (unit) test No dependency management Deployment may require care Custom user experience
  3. $ cowsay -e ~~ WHAT IS WRONG WITH SCRIPTS? |

    lolcat _____________________________ < WHAT IS WRONG WITH SCRIPTS? > ----------------------------- \ ^__^ \ (~~)\_______ (__)\ )\/\ ||----w | || || Coding Example #1
  4. print("Usage: foo <bar> --baz") def main(): 1 2 3 if

    __name__ == "__main__": 4 main() 5 Why CLI Applications? Standardized user experience More possibilities for structure Possibilities for all kinds of testing Dependency management Packaging & distribution
  5. import argparse from . import __version__ def parse_arguments(): parser =

    argparse.ArgumentParser(description=__doc__) parser.add_argument('--version', action='version', version=__version__) parser.add_argument('filename') args = parser.parse_args() return args def main(): args = parse_arguments() ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Argparse
  6. import click @click.command() @click.version_option() @click.argument('filename') def main(filename): click.echo(filename) 1 2

    3 4 5 6 7 Click import click @click.command() @click.version_option() @click.argument('filename', type=click.Path(exists=True)) def main(filename): click.echo(filename) 1 2 3 4 5 6 7 import click @click.command() @click.version_option() @click.argument('infile', type=click.File()) def main(infile): click.echo(infile.read()) 1 2 3 4 5 6 7
  7. """Foobar Usage: foobar (-h | --help | --version) foobar [-s

    | --silent] <file> foobar [-v | --verbose] <file> Positional arguments: file target file path name Optional arguments: -h, --help show this help message and exit -s, --silent don't show progress output -v, --verbose explain progress verbosely --version show program's version number and exit """ from docopt import docopt from . import __version__ def parse_arguments(): args = docopt(__doc__, version=__version__) return dict( file=args['<file>'], silent=args['-s'] or args['--silent'], verbose=args['-v'] or args['--verbose'], ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Docopt
  8. with pytest.raises(SystemExit): foobar.cli.main() pytest.fail("CLI doesn't abort") def test_cli(): 1 2

    3 4 Challenges with Writing Tests How test our CLI configuration? Control taken away from us Package features require deployment
  9. result = shell('foobar') def a_functional_test(): 1 2 assert result.exit_code !=

    0, result.stdout 3 CLI Test Strategy 1. Start from the user interface with functional tests. 2. Work down towards unit tests. 1. Start from the user interface with functional tests. result = shell('foobar') with ArgvContext('foobar', 'myfile', '--verbose'): args = foobar.cli.parse_arguments() def a_functional_test(): 1 2 assert result.exit_code != 0, result.stdout 3 4 def a_unit_test(): 5 6 7 assert args['verbose'] == True 8
  10. result = shell('foobar --help') assert result.exit_code == 0 def test_entrypoint():

    1 """Is entrypoint script installed? (setup.py)""" 2 3 4 Functional Tests (User Interface)
  11. @patch('foobar.command.process') with ArgvContext('foobar', 'myfile', '-v'): foobar.cli.main() import foobar 1 from

    cli_test_helpers import ArgvContext 2 from unittest.mock import patch 3 4 5 def test_process_is_called(mock_command): 6 7 8 9 assert mock_command.called 10 assert mock_command.call_args.kwargs == dict( 11 file='myfile', silent=False, verbose=True) 12 Unit Tests
  12. [tox] [testenv] cli-test-helpers coverage[toml] pytest coverage run -m pytest {posargs}

    1 envlist = py{38,39,310} 2 3 4 description = Unit tests 5 deps = 6 7 8 9 commands = 10 11 coverage xml 12 coverage report 13 Tox
  13. $ cowsay MOO! I ♥ CLI-TEST-HELPERS | lolcat ___________________________ <

    MOO! I ♥ CLI-TEST-HELPERS > --------------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || Coding Example #2