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

Python Summit 2022: Never Write Scripts Again

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. View Slide

  2. View Slide

  3. View Slide

  4. Source: , 2015
    Technology and Friends, Episode 354

    View Slide

  5. 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)

    View Slide

  6. 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

    View Slide

  7. $ cowsay -e ~~ WHAT IS WRONG WITH SCRIPTS? | lolcat
    _____________________________
    < WHAT IS WRONG WITH SCRIPTS? >
    -----------------------------
    \ ^__^
    \ (~~)\_______
    (__)\ )\/\
    ||----w |
    || ||
    Coding Example #1

    View Slide

  8. print("Usage: foo --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

    View Slide

  9. 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

    View Slide

  10. 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

    View Slide

  11. """Foobar
    Usage:
    foobar (-h | --help | --version)
    foobar [-s | --silent]
    foobar [-v | --verbose]
    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[''],
    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

    View Slide

  12. 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

    View Slide

  13. 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

    View Slide

  14. 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)

    View Slide

  15. @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

    View Slide

  16. [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

    View Slide

  17. $ cowsay MOO! I ♥ CLI-TEST-HELPERS | lolcat
    ___________________________
    < MOO! I ♥ CLI-TEST-HELPERS >
    ---------------------------
    \ ^__^
    \ (oo)\_______
    (__)\ )\/\
    ||----w |
    || ||
    Coding Example #2

    View Slide

  18. Thank you!
    for your precious time
    Painless Software
    Less pain, more fun.

    View Slide