├── lessons ├── __init__.py ├── part_01 │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── 01_hello.py │ │ ├── base.py │ │ ├── 02_args.py │ │ ├── 04_help.py │ │ ├── 03_opts.py │ │ └── 05_types.py │ ├── cli.py │ └── solutions │ │ ├── cli_01_hello.py │ │ ├── cli_02_args.py │ │ ├── cli_03_opts.py │ │ ├── cli_04_help.py │ │ └── cli_05_types.py ├── part_02 │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── 01_echo.py │ │ └── 02_file_io.py │ ├── infile.txt │ ├── cli.py │ └── solutions │ │ ├── cli_01_echo.py │ │ └── cli_02_file_io.py ├── part_03 │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── 01_groups.py │ │ └── 02_contexts.py │ ├── cli.py │ └── solutions │ │ ├── cli_01_groups.py │ │ └── cli_02_contexts.py ├── part_04 │ └── .gitkeep └── part_05 │ └── cli.py ├── click_tutorial ├── click_tutorial.py ├── __init__.py ├── checks.py ├── hello_example.py └── cli.py ├── presentation ├── PITCHME.yaml ├── PyCon-2019-Click-Tutorial-Dave-Forgac.pdf ├── README.md └── PITCHME.md ├── requirements_dev.txt ├── Pipfile ├── MANIFEST.in ├── .editorconfig ├── setup.cfg ├── tox.ini ├── .travis.yml ├── appveyor.yml ├── tests └── test_click_tutorial.py ├── .gitignore ├── setup.py ├── Makefile ├── README.md ├── tutorial.toml └── LICENSE /lessons/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_01/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_02/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_03/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_04/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_01/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_02/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_03/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/part_02/infile.txt: -------------------------------------------------------------------------------- 1 | Sample input data. 2 | -------------------------------------------------------------------------------- /click_tutorial/click_tutorial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Main module.""" 4 | -------------------------------------------------------------------------------- /presentation/PITCHME.yaml: -------------------------------------------------------------------------------- 1 | # See: https://gitpitch.com/docs/settings/pitchme 2 | theme: white 3 | layout: center 4 | highlight: googlecode -------------------------------------------------------------------------------- /presentation/PyCon-2019-Click-Tutorial-Dave-Forgac.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylerdave/PyCon2019-Click-Tutorial/HEAD/presentation/PyCon-2019-Click-Tutorial-Dave-Forgac.pdf -------------------------------------------------------------------------------- /lessons/part_01/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command() 7 | def cli(): 8 | print("Hello.") 9 | 10 | 11 | if __name__ == "__main__": 12 | cli() 13 | -------------------------------------------------------------------------------- /lessons/part_02/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command() 7 | def cli(): 8 | print("Hello.") 9 | 10 | 11 | if __name__ == "__main__": 12 | cli() 13 | -------------------------------------------------------------------------------- /lessons/part_03/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command() 7 | def cli(): 8 | print("Hello.") 9 | 10 | 11 | if __name__ == "__main__": 12 | cli() 13 | -------------------------------------------------------------------------------- /click_tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for Click Tutorial.""" 4 | 5 | __author__ = """Dave Forgac""" 6 | __email__ = "tylerdave@tylerdave.com" 7 | __version__ = "1.0.2" 8 | -------------------------------------------------------------------------------- /lessons/part_01/solutions/cli_01_hello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command() 7 | def cli(): 8 | print("Hello!") 9 | 10 | 11 | if __name__ == "__main__": 12 | cli() 13 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip>=18.1 2 | bumpversion>=0.5.3 3 | wheel>=0.32.1 4 | watchdog>=0.9.0 5 | flake8>=3.5.0 6 | tox>=3.5.2 7 | coverage>=4.5.1 8 | Sphinx>=1.8.1 9 | twine>=1.12.1 10 | 11 | pytest>=3.8.2 12 | pytest-runner>=4.2 13 | -------------------------------------------------------------------------------- /presentation/README.md: -------------------------------------------------------------------------------- 1 | Writing Command Line Applications that Click 2 | ============================================ 3 | 4 | Slideshow powered by GitPitch: [Launch Slideshow](https://gitpitch.com/tylerdave/PyCon2019-Click-Tutorial/master?p=presentation) 5 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | 9 | [packages] 10 | click-tutorial = {path = ".",editable = true} 11 | 12 | [pipenv] 13 | allow_prereleases = true 14 | -------------------------------------------------------------------------------- /lessons/part_01/tests/01_hello.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTutorialLesson 2 | 3 | 4 | class TestTutorialHelloWorld(BaseTutorialLesson): 5 | def test_cli_outputs_hello_message(self): 6 | result = self.run_command() 7 | assert result.output == "Hello!\n" 8 | -------------------------------------------------------------------------------- /lessons/part_03/solutions/cli_01_groups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.group() 7 | def cli(): 8 | """Displays greetings.""" 9 | 10 | 11 | @cli.command() 12 | def hello(): 13 | click.echo("Hello!") 14 | 15 | 16 | if __name__ == "__main__": 17 | cli() 18 | -------------------------------------------------------------------------------- /lessons/part_01/solutions/cli_02_args.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command() 7 | @click.argument("names", nargs=-1) 8 | def cli(names): 9 | for name in names: 10 | print("Hello, {}!".format(name)) 11 | 12 | 13 | if __name__ == "__main__": 14 | cli() 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /lessons/part_02/solutions/cli_01_echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.option("--red", is_flag=True) 7 | @click.command() 8 | def cli(red): 9 | click.echo("Printing...", err=True) 10 | if red: 11 | color = "red" 12 | else: 13 | color = None 14 | click.secho("Hello!", fg=color) 15 | 16 | 17 | if __name__ == "__main__": 18 | cli() 19 | -------------------------------------------------------------------------------- /lessons/part_01/tests/base.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from lessons.part_01.cli import cli 4 | 5 | 6 | class BaseTutorialLesson: 7 | def setup(self): 8 | self.runner = CliRunner() 9 | self.command = cli 10 | 11 | def run_command(self, arguments=None, **kwargs): 12 | result = self.runner.invoke(self.command, arguments, **kwargs) 13 | return result 14 | -------------------------------------------------------------------------------- /lessons/part_02/tests/base.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from lessons.part_02.cli import cli 4 | 5 | 6 | class BaseTutorialLesson: 7 | def setup(self): 8 | self.runner = CliRunner(mix_stderr=False) 9 | self.command = cli 10 | 11 | def run_command(self, arguments=None, **kwargs): 12 | result = self.runner.invoke(self.command, arguments, **kwargs) 13 | return result 14 | -------------------------------------------------------------------------------- /lessons/part_03/tests/base.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from lessons.part_03.cli import cli 4 | 5 | 6 | class BaseTutorialLesson: 7 | def setup(self): 8 | self.runner = CliRunner(mix_stderr=False) 9 | self.command = cli 10 | 11 | def run_command(self, arguments=None, **kwargs): 12 | result = self.runner.invoke(self.command, arguments, **kwargs) 13 | return result 14 | -------------------------------------------------------------------------------- /lessons/part_02/solutions/cli_02_file_io.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command() 7 | @click.argument("infile", type=click.File("r"), default="-") 8 | @click.argument("outfile", type=click.File("w"), default="-") 9 | def cli(infile, outfile): 10 | text = infile.read() 11 | click.echo("Input length: {}".format(len(text)), err=True) 12 | click.echo(text, file=outfile, nl=False) 13 | 14 | 15 | if __name__ == "__main__": 16 | cli() 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.0.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version="{current_version}" 8 | replace = version="{new_version}" 9 | 10 | [bumpversion:file:click_tutorial/__init__.py] 11 | search = __version__ = "{current_version}" 12 | replace = __version__ = "{new_version}" 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | test = pytest 22 | 23 | [tool:pytest] 24 | collect_ignore = ['setup.py'] 25 | 26 | -------------------------------------------------------------------------------- /lessons/part_01/solutions/cli_03_opts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command() 7 | @click.argument("names", nargs=-1) 8 | @click.option("--greeting", "-g", default="Hello") 9 | @click.option("--question/--no-question") 10 | def cli(names, greeting, question): 11 | if question: 12 | punctuation = "?" 13 | else: 14 | punctuation = "!" 15 | for name in names: 16 | print("{}, {}{}".format(greeting, name, punctuation)) 17 | 18 | 19 | if __name__ == "__main__": 20 | cli() 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35, py36, py37 3 | 4 | [travis] 5 | python = 6 | 3.7: py37 7 | 3.6: py36 8 | 3.5: py35 9 | 10 | [testenv] 11 | setenv = 12 | PYTHONPATH = {toxinidir} 13 | deps = 14 | -r{toxinidir}/requirements_dev.txt 15 | ; If you want to make tox run the tests with the same versions, create a 16 | ; requirements.txt with the pinned versions and uncomment the following line: 17 | ; -r{toxinidir}/requirements.txt 18 | commands = 19 | pip install -U pip 20 | py.test -v --basetemp={envtmpdir} 21 | 22 | 23 | -------------------------------------------------------------------------------- /lessons/part_02/tests/01_echo.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTutorialLesson 2 | 3 | 4 | class TestTutorialEchoAndStyle(BaseTutorialLesson): 5 | def test_00_cli_echo_to_stdout(self): 6 | result = self.run_command() 7 | assert "Hello!" in result.stdout 8 | 9 | def test_01_cli_echo_to_stderr(self): 10 | result = self.run_command() 11 | assert "Printing..." in result.stderr 12 | 13 | def test_02_cli_secho_with_red_option(self): 14 | result = self.run_command(["--red"], color=True) 15 | assert result.output == "\x1b[31mHello!\x1b[0m\n" 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | matrix: 6 | include: 7 | - python: 3.5 8 | - python: 3.6 9 | - python: 3.7 10 | dist: xenial 11 | sudo: true 12 | 13 | # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 14 | install: pip install -U tox-travis && pip install . 15 | 16 | # Command to run tests, e.g. python setup.py test 17 | script: >- 18 | tox && 19 | tutorial version && 20 | tutorial init && 21 | tutorial lesson && 22 | tutorial solve --yes && 23 | tutorial check 24 | 25 | -------------------------------------------------------------------------------- /lessons/part_01/solutions/cli_04_help.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command(name="greet") 7 | @click.argument("names", nargs=-1) 8 | @click.option("--greeting", "-g", default="Hello", help="The greeting to display.") 9 | @click.option("--question/--no-question", help="Make the greeting a question.") 10 | def cli(names, greeting, question): 11 | """Displays a greeting.""" 12 | if question: 13 | punctuation = "?" 14 | else: 15 | punctuation = "!" 16 | for name in names: 17 | print("{}, {}{}".format(greeting, name, punctuation)) 18 | 19 | 20 | if __name__ == "__main__": 21 | cli() 22 | -------------------------------------------------------------------------------- /click_tutorial/checks.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def get_python_version(): 5 | return sys.version.replace("\n", "") 6 | 7 | 8 | def check_python_35_plus(): 9 | if sys.version_info.major == 3 and sys.version_info.minor >= 5: 10 | return True 11 | else: 12 | return False 13 | 14 | 15 | def check_pytest(): 16 | import pytest 17 | 18 | return True 19 | 20 | 21 | def check_cookiecutter(): 22 | import cookiecutter 23 | 24 | return True 25 | 26 | 27 | ALL_CHECKS = { 28 | "Python Version": get_python_version, 29 | "Python 3.5+": check_python_35_plus, 30 | "Has PyTest": check_pytest, 31 | "Has Cookiecutter": check_cookiecutter, 32 | } 33 | -------------------------------------------------------------------------------- /lessons/part_01/tests/02_args.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTutorialLesson 2 | 3 | 4 | class TestTutorialBasicArguments(BaseTutorialLesson): 5 | def test_00_cli_with_single_argument(self): 6 | result = self.run_command(["Tutorial"]) 7 | assert result.output == "Hello, Tutorial!\n" 8 | 9 | def test_01_cli_with_multiple_arguments(self): 10 | result = self.run_command(["Tutorial", "Cleveland", "Everybody"]) 11 | assert ( 12 | result.output == "Hello, Tutorial!\nHello, Cleveland!\nHello, Everybody!\n" 13 | ) 14 | 15 | def test_02_cli_with_no_argument(self): 16 | result = self.run_command() 17 | assert result.output == "" 18 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - PYTHON: "C:\\Python37" 4 | - PYTHON: "C:\\Python37-x64" 5 | - PYTHON: "C:\\Python36" 6 | - PYTHON: "C:\\Python36-x64" 7 | - PYTHON: "C:\\Python35" 8 | - PYTHON: "C:\\Python35-x64" 9 | 10 | install: 11 | - "set PATH=%PYTHON%\\Scripts;%PATH%" 12 | - "%PYTHON%\\python.exe -m pip install -e ." 13 | 14 | build: off 15 | 16 | test_script: 17 | - "%PYTHON%\\python.exe -m pytest -v" 18 | - "pycon verify" 19 | - "tutorial version" 20 | - "tutorial init" 21 | - "tutorial lesson" 22 | - "tutorial solve --yes" 23 | - "tutorial check" 24 | 25 | #after_test: 26 | # None 27 | 28 | #artifacts: 29 | # None 30 | 31 | #on_success: 32 | # None 33 | -------------------------------------------------------------------------------- /lessons/part_01/tests/04_help.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .base import BaseTutorialLesson 4 | 5 | 6 | class TestTutorialBasicUsageDocumentation(BaseTutorialLesson): 7 | def test_00_cli_gets_help_output_from_docstring(self): 8 | result = self.run_command(["--help"]) 9 | assert result.output.startswith("Usage: ") 10 | assert "Displays a greeting." in result.output 11 | 12 | def test_01_cli_includes_help_text_from_question_option(self): 13 | result = self.run_command(["--help"]) 14 | assert "Make the greeting a question" in result.output 15 | 16 | def test_02_cli_includes_help_text_from_greeting_option(self): 17 | result = self.run_command(["--help"]) 18 | assert "The greeting to display" in result.output 19 | -------------------------------------------------------------------------------- /lessons/part_03/tests/01_groups.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTutorialLesson 2 | 3 | 4 | class TestTutorialBasicSubcommands(BaseTutorialLesson): 5 | def test_00_cli_without_command_prints_usage(self): 6 | result = self.run_command() 7 | assert result.output.startswith("Usage: cli [OPTIONS] COMMAND [ARGS]...") 8 | assert result.exit_code == 0 9 | 10 | def test_01_cli_with_hello_command(self): 11 | result = self.run_command(["hello"]) 12 | assert result.output == "Hello!\n" 13 | assert result.exit_code == 0 14 | 15 | def test_02_cli_wth_invalid_command(self): 16 | result = self.run_command(["invalid-subcommand"]) 17 | assert 'Error: No such command "invalid-subcommand".' in result.stderr 18 | assert result.exit_code == 2 19 | -------------------------------------------------------------------------------- /lessons/part_03/solutions/cli_02_contexts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.group() 7 | @click.option("--verbose", is_flag=True) 8 | @click.pass_context 9 | def cli(ctx, verbose): 10 | """Displays greetings.""" 11 | ctx.ensure_object(dict) 12 | ctx.obj["VERBOSE"] = verbose 13 | 14 | 15 | @cli.command() 16 | @click.pass_context 17 | def hello(ctx): 18 | verbose = ctx.obj.get("VERBOSE") 19 | if verbose: 20 | click.echo("VERBOSE is on", err=True) 21 | click.echo("Hello!") 22 | 23 | 24 | @cli.command() 25 | @click.pass_obj 26 | def goodbye(obj): 27 | verbose = obj.get("VERBOSE") 28 | if verbose: 29 | click.echo("VERBOSE is on", err=True) 30 | click.echo("Goodbye!") 31 | 32 | 33 | if __name__ == "__main__": 34 | cli() 35 | -------------------------------------------------------------------------------- /lessons/part_01/tests/03_opts.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTutorialLesson 2 | 3 | 4 | class TestTutorialBasicOptions(BaseTutorialLesson): 5 | def test_00_greeting_option(self): 6 | result = self.run_command(["--greeting", "Ahoy", "Tutorial"]) 7 | assert result.output == "Ahoy, Tutorial!\n" 8 | 9 | def test_01_greeting_short_option(self): 10 | result = self.run_command(["-g", "Ahoy", "Tutorial"]) 11 | assert result.output == "Ahoy, Tutorial!\n" 12 | 13 | def test_02_greeting_default(self): 14 | result = self.run_command(["Tutorial"]) 15 | assert result.output == "Hello, Tutorial!\n" 16 | 17 | def test_03_question_option(self): 18 | result = self.run_command(["--question", "Tutorial"]) 19 | assert result.output == "Hello, Tutorial?\n" 20 | -------------------------------------------------------------------------------- /click_tutorial/hello_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | import time 5 | 6 | 7 | @click.group() 8 | def cli(): 9 | """Command group.""" 10 | 11 | 12 | @cli.command() 13 | @click.argument("name") 14 | @click.option("--color", default="green", type=click.Choice(["red", "black", "green"])) 15 | @click.option( 16 | "--count", "-c", type=int, default=1, help="number of times to print message" 17 | ) 18 | def hello(name, color, count): 19 | """A command that says hello.""" 20 | click.echo("color: {}".format(color), err=True) 21 | for i in range(count): 22 | click.secho("Hello, {}!".format(name), fg=color) 23 | 24 | items = range(100) 25 | with click.progressbar(items) as bar: 26 | for item in bar: 27 | time.sleep(0.03) 28 | 29 | 30 | if __name__ == "__main__": 31 | cli() 32 | -------------------------------------------------------------------------------- /lessons/part_03/tests/02_contexts.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTutorialLesson 2 | 3 | 4 | class TestTutorialBasicSubcommands(BaseTutorialLesson): 5 | def test_00_cli_without_passed_option_doesnt_print_verbose(self): 6 | result = self.run_command(["hello"]) 7 | assert result.output == "Hello!\n" 8 | assert result.exit_code == 0 9 | 10 | def test_01_cli_with_verbose_passed_in_context(self): 11 | result = self.run_command(["--verbose", "hello"]) 12 | assert result.stderr == "VERBOSE is on\n" 13 | assert result.stdout == "Hello!\n" 14 | assert result.exit_code == 0 15 | 16 | def test_02_cli_with_verbose_passed_in_object(self): 17 | result = self.run_command(["--verbose", "goodbye"]) 18 | assert result.stderr == "VERBOSE is on\n" 19 | assert result.stdout == "Goodbye!\n" 20 | assert result.exit_code == 0 21 | -------------------------------------------------------------------------------- /lessons/part_01/solutions/cli_05_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | 5 | 6 | @click.command(name="greet") 7 | @click.argument("names", nargs=-1) 8 | @click.option("--int-option", type=click.INT) 9 | @click.option("--float-option", type=click.FLOAT) 10 | @click.option("--bool-option", type=click.BOOL) 11 | @click.option("--choice-option", type=click.Choice(["A", "B", "C"])) 12 | @click.option("--greeting", "-g", default="Hello", help="The greeting to display.") 13 | @click.option("--question/--no-question", help="Make the greeting a question.") 14 | def cli( 15 | names, int_option, float_option, bool_option, choice_option, greeting, question 16 | ): 17 | """Displays a greeting.""" 18 | if question: 19 | punctuation = "?" 20 | else: 21 | punctuation = "!" 22 | for name in names: 23 | print("{}, {}{}".format(greeting, name, punctuation)) 24 | if int_option: 25 | print("int: {}".format(int_option)) 26 | if float_option: 27 | print("float: {}".format(float_option)) 28 | if bool_option is not None: 29 | print("bool: {}".format(bool_option)) 30 | if choice_option: 31 | print("choice: {}".format(choice_option)) 32 | 33 | 34 | if __name__ == "__main__": 35 | cli() 36 | -------------------------------------------------------------------------------- /lessons/part_05/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import click 4 | import time 5 | 6 | 7 | @click.group() 8 | def cli(): 9 | """Examples.""" 10 | 11 | @cli.command() 12 | def editor(): 13 | """Launch an editor.""" 14 | message = click.edit("Prepopulated text") 15 | click.echo("Your message: {}".format(message)) 16 | 17 | @cli.command(name="find-app-dir") 18 | def find_app_dir(): 19 | """Find the appropriate application data folder.""" 20 | click.echo(click.get_app_dir('part05')) 21 | 22 | @cli.command() 23 | def launch(): 24 | """Launch applicaiton.""" 25 | click.launch("https://click.palletsprojects.com/") 26 | 27 | 28 | @cli.command() 29 | @click.option("--lines", default=100) 30 | def paging(lines): 31 | """Page through output.""" 32 | data = '\n'.join(['Line %d' % num for num in range(lines)]) 33 | click.echo_via_pager(data) 34 | 35 | 36 | @cli.command(name="progress-bar") 37 | @click.option('--delay', default=0.5) 38 | @click.option('--count', default=10) 39 | def progress_bar(count, delay): 40 | """Display a progress bar.""" 41 | data = range(count) 42 | with click.progressbar(data) as bar: 43 | for number in bar: 44 | time.sleep(delay) 45 | 46 | 47 | if __name__ == "__main__": 48 | cli() 49 | -------------------------------------------------------------------------------- /tests/test_click_tutorial.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for `click_tutorial` package.""" 5 | 6 | import pytest 7 | 8 | from click.testing import CliRunner 9 | 10 | from click_tutorial import click_tutorial 11 | from click_tutorial import cli 12 | 13 | 14 | @pytest.fixture 15 | def response(): 16 | """Sample pytest fixture. 17 | 18 | See more at: http://doc.pytest.org/en/latest/fixture.html 19 | """ 20 | # import requests 21 | # return requests.get('https://github.com/audreyr/cookiecutter-pypackage') 22 | 23 | 24 | def test_content(response): 25 | """Sample pytest test function with the pytest fixture as an argument.""" 26 | # from bs4 import BeautifulSoup 27 | # assert 'GitHub' in BeautifulSoup(response.content).title.string 28 | 29 | 30 | def test_command_line_interface(): 31 | """Test the CLI.""" 32 | runner = CliRunner() 33 | result = runner.invoke(cli.main) 34 | assert result.exit_code == 0 35 | assert "PyCon Tutorial." in result.output 36 | help_result = runner.invoke(cli.main, ["--help"]) 37 | assert help_result.exit_code == 0 38 | assert "--help Show this message and exit." in help_result.output 39 | 40 | 41 | def test_verify_subcommand(): 42 | runner = CliRunner() 43 | result = runner.invoke(cli.main, ["verify"]) 44 | assert "Verification successful!" in result.output 45 | assert result.exit_code == 0 46 | -------------------------------------------------------------------------------- /click_tutorial/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import click 6 | 7 | from click_tutorial.checks import ALL_CHECKS 8 | 9 | 10 | @click.group(name="pycon") 11 | def main(args=None): 12 | """PyCon Tutorial.""" 13 | 14 | 15 | @main.command() 16 | def hello(): 17 | """Say hello.""" 18 | click.echo("Hello!") 19 | 20 | 21 | @main.command() 22 | def verify(): 23 | """Verify that your environment is set up correctly.""" 24 | any_failures = False 25 | for check_name, check_func in ALL_CHECKS.items(): 26 | try: 27 | result = check_func() 28 | if result: 29 | click.secho("{:>20}: {}".format(check_name, result), fg="green") 30 | else: 31 | click.secho("{:>20}: {}".format(check_name, result), fg="red") 32 | any_failures = True 33 | except Exception as err: 34 | click.secho("{:>20}: ".format(check_name), fg="red", nl=False) 35 | click.secho(message=str(err), fg="red", bg="yellow") 36 | any_failures = True 37 | if any_failures: 38 | click.secho("\nVerification failed. Please see setup instructions.", fg="red") 39 | else: 40 | click.secho( 41 | "\nVerification successful! Your system will be albe to run the tutorial!", 42 | fg="blue", 43 | ) 44 | 45 | 46 | if __name__ == "__main__": 47 | sys.exit(main()) # pragma: no cover 48 | -------------------------------------------------------------------------------- /lessons/part_02/tests/02_file_io.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTutorialLesson 2 | 3 | 4 | class TestFileTypes(BaseTutorialLesson): 5 | def test_00_cli_reads_input_file(self): 6 | with self.runner.isolated_filesystem(): 7 | with open("infile.txt", "w") as f: 8 | f.write("Input data.") 9 | result = self.run_command(["infile.txt"]) 10 | assert "Input data." in result.output 11 | 12 | def test_01_cli_writes_output_file(self): 13 | with self.runner.isolated_filesystem(): 14 | with open("infile.txt", "w") as f: 15 | f.write("Input data.") 16 | result = self.run_command(["infile.txt", "outfile.txt"]) 17 | with open("outfile.txt", "r") as f: 18 | data = f.read() 19 | assert data == "Input data." 20 | assert "Input data." not in result.output 21 | 22 | def test_02_cli_reads_stdin_writes_output_file(self): 23 | with self.runner.isolated_filesystem(): 24 | result = self.run_command(["-", "outfile.txt"], input="Input data.") 25 | with open("outfile.txt", "r") as f: 26 | data = f.read() 27 | assert data == "Input data." 28 | assert "Input data." not in result.output 29 | 30 | def test_03_cli_writes_length_to_stderr(self): 31 | with self.runner.isolated_filesystem(): 32 | result = self.run_command(["-", "outfile.txt"], input="Input data.") 33 | assert "Input data." not in result.output 34 | assert "Input length: 11" in result.stderr 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | .vscode/ 105 | Pipfile.lock 106 | -------------------------------------------------------------------------------- /lessons/part_01/tests/05_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .base import BaseTutorialLesson 4 | 5 | 6 | class TestBasicTypes(BaseTutorialLesson): 7 | def test_00_cli_valid_int_option(self): 8 | result = self.run_command(["--int-option", "42"]) 9 | assert "int: 42\n" in result.output 10 | 11 | def test_01_cli_invalid_int_option(self): 12 | result = self.run_command(["--int-option", "3.14"]) 13 | assert "Invalid value" in result.output 14 | assert result.exit_code == 2 15 | 16 | def test_02_cli_valid_float_option(self): 17 | result = self.run_command(["--float-option", "3.14"]) 18 | assert "float: 3.14\n" in result.output 19 | 20 | def test_03_cli_invalid_float_option(self): 21 | result = self.run_command(["--float-option", "abcd"]) 22 | assert "Invalid value" in result.output 23 | assert result.exit_code == 2 24 | 25 | @pytest.mark.parametrize("test_input", ["True", "False"]) 26 | def test_04_cli_valid_bool_options(self, test_input): 27 | result = self.run_command(["--bool-option", test_input]) 28 | assert "bool: {}\n".format(test_input) in result.output 29 | 30 | def test_05_cli_invalid_bool_option(self): 31 | result = self.run_command(["--bool-option", "invalid"]) 32 | assert "Invalid value" in result.output 33 | assert result.exit_code == 2 34 | 35 | @pytest.mark.parametrize("test_input", ["A", "B", "C"]) 36 | def test_06_cli_valid_choice_option(self, test_input): 37 | result = self.run_command(["--choice-option", test_input]) 38 | assert "choice: {}\n".format(test_input) in result.output 39 | 40 | def test_07_cli_invalid_choice_option(self): 41 | result = self.run_command(["--choice-option", "1"]) 42 | assert "Invalid value" in result.output 43 | assert result.exit_code == 2 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from codecs import open 7 | from setuptools import setup, find_packages 8 | 9 | with open("README.md", encoding="utf-8") as readme_file: 10 | readme = readme_file.read() 11 | 12 | requirements = [ 13 | "Click>=6.0", 14 | "colorama", 15 | "cookiecutter", 16 | "coverage", 17 | "pytest-runner", 18 | "pytest>=3.5.0", 19 | "setuptools>=30.0.0" 20 | "tox", 21 | "tutorial-runner>=0.2.7", 22 | ] 23 | 24 | setup_requirements = ["pytest-runner"] 25 | 26 | test_requirements = ["pytest"] 27 | 28 | dev_requirements = ["bumpversion", "flake8", "pip", "twine", "watchdog", "wheel"] 29 | 30 | setup( 31 | author="Dave Forgac", 32 | author_email="tylerdave@tylerdave.com", 33 | classifiers=[ 34 | "Development Status :: 2 - Pre-Alpha", 35 | "Intended Audience :: Developers", 36 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 37 | "Natural Language :: English", 38 | "Programming Language :: Python :: 3", 39 | "Programming Language :: Python :: 3.5", 40 | "Programming Language :: Python :: 3.6", 41 | "Programming Language :: Python :: 3.7", 42 | ], 43 | description="A tutorial for writing command line applications using click.", 44 | entry_points={ 45 | "console_scripts": [ 46 | "pycon=click_tutorial.cli:main", 47 | "part01=lessons.part_01.cli:cli", 48 | "part02=lessons.part_02.cli:cli", 49 | "part03=lessons.part_03.cli:cli", 50 | "part05=lessons.part_05.cli:cli", 51 | ] 52 | }, 53 | install_requires=requirements, 54 | license="Mozilla Public License 2.0 (MPL 2.0)", 55 | long_description=readme, 56 | include_package_data=True, 57 | keywords="click_tutorial", 58 | name="click_tutorial", 59 | packages=find_packages(include=["click_tutorial", "lessons"]), 60 | setup_requires=setup_requirements, 61 | test_suite="tests", 62 | tests_require=test_requirements, 63 | extras_require={"dev": dev_requirements}, 64 | url="https://github.com/tylerdave/click_tutorial", 65 | version="1.0.2", 66 | zip_safe=False, 67 | ) 68 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | flake8 click_tutorial tests 55 | 56 | test: ## run tests quickly with the default Python 57 | py.test 58 | 59 | test-all: ## run tests on every Python version with tox 60 | tox 61 | 62 | coverage: ## check code coverage quickly with the default Python 63 | coverage run --source click_tutorial -m pytest 64 | coverage report -m 65 | coverage html 66 | $(BROWSER) htmlcov/index.html 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs 69 | rm -f docs/click_tutorial.rst 70 | rm -f docs/modules.rst 71 | sphinx-apidoc -o docs/ click_tutorial 72 | $(MAKE) -C docs clean 73 | $(MAKE) -C docs html 74 | $(BROWSER) docs/_build/html/index.html 75 | 76 | servedocs: docs ## compile the docs watching for changes 77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 78 | 79 | release: dist ## package and upload a release 80 | twine upload dist/* 81 | 82 | dist: clean ## builds source and wheel package 83 | python setup.py sdist 84 | python setup.py bdist_wheel 85 | ls -l dist 86 | 87 | install: clean ## install the package to the active Python's site-packages 88 | python setup.py install 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyCon2019 Click Tutorial 2 | ======================== 3 | 4 | [![TravisCI build status](https://travis-ci.org/tylerdave/PyCon2019-Click-Tutorial.svg?branch=master)](https://travis-ci.org/tylerdave/PyCon2019-Click-Tutorial) 5 | [![Appveyor build status](https://ci.appveyor.com/api/projects/status/3f5kpm416lb46bjo/branch/master?svg=true)](https://ci.appveyor.com/project/tylerdave/pycon2019-click-tutorial/branch/master) 6 | 7 | PyCon 2019 Tutorial: Writing Command Line Applications that Click 8 | 9 | ## Setup 10 | 11 | ### Prerequisites 12 | 13 | * Python 3.5+ installed. [Python Installation Guide](https://docs.python-guide.org/starting/installation/#python-3-installation-guides) 14 | * `virtualenv` & `pip` (Should be installed if you follow the guide above) 15 | * Git. [Git installation instructions](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 16 | * Optional: `pipenv` or `virtualenvwrapper` 17 | 18 | ### Installation 19 | 20 | This repo is a Python package. You will create a virtualenv and install the package which will install its dependencies and make new commands available. 21 | 22 | * Open a terminal / command prompt. 23 | * Recommended on Windows: [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell?view=powershell-6) or [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). 24 | * Clone this repo:
`git clone https://github.com/tylerdave/PyCon2019-Click-Tutorial.git` 25 | * If you'd like to save a remote copy of your changes, create a new empty repo at your source code hosting service of choice and add it as a git remote:
`git remote add personal $NEW_REPO_URL` 26 | * `cd` to the root of the cloned repo:
`cd PyCon2019-Click-Tutorial` 27 | * Create and activate a virtualenv using your favorite method and then install the package: 28 | * **Recommended:** using `pipenv` ([installation instructions](https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv)): 29 | * `pipenv --python python3` 30 | * `pipenv install` 31 | * On Mac/Linux: 32 | * `pipenv shell` 33 | * On Windows: 34 | * `pipenv shell` might give you a reduced feature shell. You should run commands with `pipenv run $COMMAND` instead like: `pipenv run pycon verify` instead of just `pycon verify` in the step below. 35 | * If using `virtualenvwrapper`: 36 | * `mkvirtualenv --python python3 click-tutorial` 37 | * `workon click-tutorial` 38 | * `pip install -e .` 39 | * Manually: 40 | * `python3 -m venv env` 41 | * On Mac/Linux: `source env/bin/activate` 42 | * On Windows: `.\env\Scripts\activate` 43 | * `pip install -e .` 44 | * Verify installation. One of the commands that's created by this package is `pycon`. You can use it to verify your setup was successful:
`pycon verify` 45 | * Initialize tutorial. In the root of the repo, run `tutorial init`. 46 | 47 | 48 | -------------------------------------------------------------------------------- /tutorial.toml: -------------------------------------------------------------------------------- 1 | name = "Click Tutorial" 2 | 3 | [[parts]] 4 | id = 1 5 | name = "Command Parsing" 6 | dir = "lessons/part_01" 7 | file = "cli.py" 8 | command = "part01" 9 | 10 | [[parts.lessons]] 11 | id = 1 12 | name = "Hello, PyCon!" 13 | test = "01_hello.py" 14 | solution = "cli_01_hello.py" 15 | objectives = """ 16 | * Learn how to use the `tutorial` command to run and check lessons. 17 | * Make the tests for the first lesson pass by editing the working file. 18 | """ 19 | doc-urls = "https://click.palletsprojects.com/en/7.x/quickstart/#basic-concepts-creating-a-command" 20 | 21 | [[parts.lessons]] 22 | id = 2 23 | name = "Arguments" 24 | test = "02_args.py" 25 | solution = "cli_02_args.py" 26 | objectives = """ 27 | * Make the command accept a positional NAME argument 28 | * accept any number of values 29 | * print "Hello, NAME!" on a new line for each name given 30 | * output nothing if no arguments are passed (noop) 31 | """ 32 | doc-urls = "https://click.palletsprojects.com/en/7.x/arguments/#variadic-arguments" 33 | 34 | [[parts.lessons]] 35 | id = 3 36 | name = "Options" 37 | test = "03_opts.py" 38 | solution = "cli_03_opts.py" 39 | objectives = """ 40 | * Add multiple options: 41 | * Add a `--greeting` option that accepts a custom greeting 42 | * Update to print "GREETING, NAME!" w/ the custom greeting 43 | * Add a short alias for the option: `-g` 44 | * Default to "Hello" if no greeting is passed 45 | * Add a `--question` option as a flag that doesn't take a value 46 | * If passed, end the greeting with "?" 47 | * If not passed, end the greeting with "!" 48 | """ 49 | doc-urls = """| 50 | https://click.palletsprojects.com/en/7.x/options/ 51 | https://click.palletsprojects.com/en/7.x/options/#boolean-flags 52 | """ 53 | 54 | [[parts.lessons]] 55 | id = 4 56 | name = "Help Documentation" 57 | test = "04_help.py" 58 | solution = "cli_04_help.py" 59 | objectives = """ 60 | * Document your script 61 | * Add general command usage help 62 | * Include "Displays a greeting." 63 | * Add help text to the `--greeting`: 64 | * "Make the greeting a question" 65 | * Add help text to and `--question`: 66 | * "The greeting to display" 67 | """ 68 | doc-urls = "https://click.palletsprojects.com/en/7.x/documentation/" 69 | 70 | [[parts.lessons]] 71 | id = 5 72 | name = "Input Validation" 73 | test = "05_types.py" 74 | solution = "cli_05_types.py" 75 | objectives = """ 76 | * Add new options to show how type validation works 77 | * Add `--int-option` 78 | * Validate as integer 79 | * Add output "int: {VALUE}" if value passed 80 | * Add `--float-option` 81 | * Validate as float 82 | * Add output "float: {VALUE}" if value passed 83 | * Add `--bool-option` 84 | * Validate as boolean 85 | * Add output "bool: {VALUE}" if value passed 86 | * Add `--choice-option` 87 | * Validate values are one of "A", "B", "C" 88 | * Add output "choice: {VALUE}" if value passed 89 | """ 90 | doc-urls = """| 91 | https://click.palletsprojects.com/en/7.x/parameters/#parameter-types 92 | https://click.palletsprojects.com/en/7.x/options/#basic-value-options""" 93 | 94 | [[parts]] 95 | id = 2 96 | name = "Input / Output" 97 | dir = "lessons/part_02" 98 | file = "cli.py" 99 | command = "part02" 100 | 101 | [[parts.lessons]] 102 | id = 1 103 | name = "Echo" 104 | test = "01_echo.py" 105 | solution = "cli_01_echo.py" 106 | objectives = """ 107 | * Customize output destination and formatting 108 | * Make "Hello!" print to stdout 109 | * Make "Printing..." print to stderr 110 | * Add a `--red` option that makes output text red when passed 111 | """ 112 | doc-urls = """| 113 | https://click.palletsprojects.com/en/7.x/api/#click.echo 114 | https://click.palletsprojects.com/en/7.x/api/#click.secho 115 | """ 116 | 117 | [[parts.lessons]] 118 | id = 2 119 | name = "File I/O" 120 | test = "02_file_io.py" 121 | solution = "cli_02_file_io.py" 122 | objectives = """ 123 | * Read from and write to files or stdin/stdout depending on arguments 124 | * Read the input source and write the contents to the output 125 | * Accept an input file argument, reading from stdin by default (using `-` arg) 126 | * Accept an output file argument, writing to stdout by default (using `-` arg) 127 | * Find the length of the input data and print a message to stderr 128 | """ 129 | doc-urls = """| 130 | https://click.palletsprojects.com/en/7.x/arguments/#file-arguments 131 | https://click.palletsprojects.com/en/7.x/utils/#intelligent-file-opening 132 | """ 133 | 134 | [[parts]] 135 | id = 3 136 | name = "Nested Commands" 137 | dir = "lessons/part_03" 138 | file = "cli.py" 139 | command = "part03" 140 | 141 | [[parts.lessons]] 142 | id = 1 143 | name = "Command Groups" 144 | test = "01_groups.py" 145 | solution = "cli_01_groups.py" 146 | objectives = """ 147 | * Make a command that has subcommands 148 | * Add `hello` subcommand that prints "Hello!" 149 | * See that trying to run nonexistent subcommands results in an error 150 | """ 151 | doc-urls = "https://click.palletsprojects.com/en/7.x/commands/" 152 | 153 | [[parts.lessons]] 154 | id = 2 155 | name = "Sharing Contexts" 156 | test = "02_contexts.py" 157 | solution = "cli_02_contexts.py" 158 | objectives = """ 159 | * Learn how parameters are handled by the group and by subcommands 160 | * Pass `--verbose` group option to `hello` subcommand via `pass_context` 161 | * Store the value in the context's `obj` 162 | * If verbose is True, print "VERBOSE is on" to `stderr` 163 | * Add a new `goodbye` subcommand 164 | * Pass `--verbose` group option to `goodbye` via `pass_obj` 165 | * If verbose is True, print "VERBOSE is on" to `stderr` 166 | 167 | """ 168 | doc-urls = "https://click.palletsprojects.com/en/7.x/commands/#passing-parameters" 169 | 170 | [[parts]] 171 | id = 4 172 | name = "Packaging" 173 | dir = "lessons/part_04" 174 | 175 | [[parts.lessons]] 176 | id = 1 177 | name = "Cookiecutter" 178 | objectives = """ 179 | * Use `cookiecutter` to make a new project directory 180 | * `cd` to `lessons/part_04` 181 | * `cookiecutter https://github.com/audreyr/cookiecutter-pypackage` 182 | * Fill in the prompts with your own values 183 | * Be sure to enable click CLI 184 | """ 185 | doc-urls = "https://cookiecutter.readthedocs.io/en/latest/" 186 | 187 | [[parts.lessons]] 188 | id = 2 189 | name = "Installing Editable" 190 | objectives = """ 191 | * Install your new package in editable mode 192 | * Optionally create a new virtualenv 193 | * Install via `pip install -e .` within the package dir. 194 | """ 195 | doc-urls = "https://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/pip.html#installing-from-a-vcs" 196 | 197 | [[parts.lessons]] 198 | id = 3 199 | name = "Distribution Packages" 200 | objectives = """ 201 | * Build distribution packages for your project 202 | * `python setup.py sdist bdist_wheel` 203 | * Via `Makefile`: `make build` 204 | """ 205 | doc-urls = "https://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/creation.html#creating-your-distribution-file" 206 | 207 | [[parts]] 208 | id = 5 209 | name = "Bonus" 210 | dir = "lessons/part_05" 211 | file = "cli.py" 212 | command = "part05" 213 | 214 | [[parts.lessons]] 215 | id = 1 216 | name = "Bonus Examples" 217 | objectives = """ 218 | * See the command for examples of click features 219 | * Paging 220 | * Launching Editor 221 | * Launching Applications 222 | * Finding Application Folder 223 | * Progress Bar 224 | """ 225 | doc-urls = "https://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/creation.html#creating-your-distribution-file" -------------------------------------------------------------------------------- /presentation/PITCHME.md: -------------------------------------------------------------------------------- 1 | Writing Command Line Applications that Click 2 | ============================================ 3 | 4 | Instructions: http://bit.ly/pycon-click 5 | 6 | dave@forgac.com - @tylerdave 7 | 8 | --- 9 | 10 | Welcome to Cleveland! 11 | ===================== 12 | 13 | # 🐍  🎸  🤘 14 | 15 | --- 16 | 17 | 18 | The Goal 19 | -------- 20 | 21 | Learn to write "well-behaved" command line applications in Python using `click`. 22 | 23 | +++ 24 | 25 | > Well behaved? 26 | 27 | Note: 28 | Let's talk about what makes a command line well behaved 29 | 30 | +++ 31 | 32 | The Unix Philosophy 33 | ------------------- 34 | 35 | - Write programs that do one thing and do it well. 36 | - Write programs that work together. 37 | - Write programs to handle text streams,
because that is a universal interface. 38 | 39 | Note: 40 | The Unix Philosophy is a set of guiding principals that influence how programs are written and are the key to these OS's power and flexibility. 41 | 42 | 43 | +++ 44 | 45 | Do one thing 46 | ------------ 47 | 48 | Note: 49 | A program should be self-contained and single-purposed. 50 | This allows it to be a well-tested understandable unit. 51 | 52 | +++ 53 | 54 | Work together 55 | ------------- 56 | 57 | Note: 58 | Programs should work together using standard interfaces. 59 | On POSIX systems this means stdin / stdout 60 | 61 | 62 | +++ 63 | 64 | Handle text streams 65 | ------------------- 66 | 67 | Note: 68 | Programs should be able to read and emit text streams in order to work together. 69 | This is what allows you to do things like run a program, using grep to filter the output, and then use less to page that output. 70 | 71 | --- 72 | 73 | > Why? 74 | 75 | Note: 76 | If you make your programs work in this way you can allow users of your programs to take advantage of the rich ecosystem in ways you might not even expect. 77 | 78 | +++ 79 | 80 | Why write CLIs? 81 | --------------- 82 | 83 | Note: 84 | Alternatives to CLIs are graphical programs and web interfaces. 85 | Command line interfaces are great for repetitive tasks especially where you may want to automate them. 86 | They're also useful as management interfaces for bigger applications. 87 | Django's manage.py is an example of this. 88 | 89 | +++ 90 | 91 | Why Python? 🐍 92 | ------------- 93 | 94 | Note: 95 | Python has a rich ecosystem of packages that do just about anything you'd want to do with a computer, from data science, to image manipulation. 96 | Many service providers also have Python SDKs so you can use Python CLIs to drive interactions with them. 97 | Python also makes sense when you're building a management interface for an existing Python application or service. 98 | There are times that Python may not be best: If you need to distribute a binary, or need something very fast another language might make more sense. 99 | If you are only calling other programs and don't have complex options then a shell script probably makes more sense 100 | 101 | +++ 102 | 103 | Why `click`? 104 | ------------ 105 | 106 | https://click.palletsprojects.com/why/ 107 | 108 | Note: 109 | Click is one of many CLI libraries for Python 110 | The reasons I use it are that it encourages POSIX conventions, supports file input and output, and supports composing nested subcommands. 111 | It also has some nice utility features like input validation, color support, confirmation prompts, and progress bars. 112 | 113 | --- 114 | 115 | Well Behaved CLIs 116 | ================= 117 | 118 | +++ 119 | 120 | Arguments & Options 121 | ------------------- 122 | 123 | ```text 124 | # Arguments: 125 | exmaple argumentA argumentB 126 | 127 | # Options: 128 | example --count 3 129 | 130 | # Option flags: 131 | exmaple --verbose 132 | 133 | # Arguments and Options: 134 | example --verbose --count3 argumentA argumentB 135 | ``` 136 | 137 | 138 | +++ 139 | 140 | `stdin`/`stdout`/`stderr` 141 | ------------------------- 142 | 143 | ```text 144 | # Read stdin: 145 | echo "input text" | example 146 | 147 | # Write stdout: 148 | example > outfile.txt 149 | 150 | # Write stdout w/ stderr: 151 | example > outfile.txt 152 | INFO: Generating output 153 | 154 | # Redirect both: 155 | example > outfile.txt 2> errfile.txt 156 | ``` 157 | 158 | +++ 159 | 160 | Exit code 161 | --------- 162 | 163 | ```text 164 | if ! [ -x "$(example argumentA)" ]; then 165 | echo 'Error: running example failed.' >&2 166 | exit 1 167 | fi 168 | ``` 169 | 170 | +++ 171 | 172 | `Ctrl-c` / Signals 173 | ---------------- 174 | 175 | ```text 176 | INFO: Running long process... 177 | [Ctrl-c] 178 | INFO: Shutting down gracefully... 179 | ``` 180 | 181 | +++ 182 | 183 | Configuration 184 | ------------- 185 | 186 | * Mac OS X:
187 | `~/Library/Application Support/Foo Bar` 188 | * Unix:
189 | `~/.config/foo-bar` 190 | * Windows (non-roaming):
191 | `C:\Users\\AppData\Local\Foo Bar` 192 | 193 | +++ 194 | 195 | Colors 196 | ------ 197 | 198 | ```text 199 | cat good_outfile.txt 200 | Output should look like this. 201 | 202 | cat bad_outfile.txt 203 | Output \u001b[31;1mshouln't\u001b[30;1m look like this. 204 | ``` 205 | 206 | --- 207 | 208 | Click 209 | ===== 210 | 211 | https://click.palletsprojects.com/ 212 | 213 | +++ 214 | 215 | ```python 216 | import click 217 | 218 | @click.command() 219 | @click.option('--count', default=1, 220 | help='Number of greetings.') 221 | @click.option('--name', prompt='Your name', 222 | help='The person to greet.') 223 | def hello(count, name): 224 | """Simple program that greets NAME for a total of 225 | COUNT times.""" 226 | for x in range(count): 227 | click.echo('Hello %s!' % name) 228 | 229 | if __name__ == '__main__': 230 | hello() 231 | ``` 232 | 233 | +++ 234 | 235 | ```text 236 | Usage: cl.py [OPTIONS] 237 | 238 | Simple program that greets NAME for a total of COUNT times. 239 | 240 | Options: 241 | --count INTEGER Number of greetings. 242 | --name TEXT The person to greet. 243 | --help Show this message and exit. 244 | ``` 245 | 246 | 247 | 248 | 249 | --- 250 | 251 | The Tutorial 252 | ============= 253 | 254 | +++ 255 | 256 | Installation 257 | ------------ 258 | 259 | - This repo is a Python package 260 | - Installs commands: 261 | - `pycon` and `tutorial` 262 | - Lessons: `part01`, `part02`, `part03` 263 | - `pytest` and `cookiecutter` 264 | - `pycon verify` will test your environment 265 | 266 | +++ 267 | 268 | 269 | Tutorial-Runner 270 | --------------- 271 | 272 | ```text 273 | Usage: tutorial [OPTIONS] COMMAND [ARGS]... 274 | 275 | Click tutorial runner. 276 | 277 | Options: 278 | --help Show this message and exit. 279 | Commands: 280 | check Check your work for the current lesson. 281 | init (Re-)Initialize the tutorial 282 | lesson Switch to a specific lesson 283 | peek Look at the solution file without overwriting 284 | solve Copy the solution file to the working file. 285 | status Show the status of your progress. 286 | version Display the version of this command. 287 | ``` 288 | 289 | +++ 290 | 291 | Initialize 292 | ---------- 293 | 294 | ```text 295 | tutorial init 296 | Tutorial initialized! Time to start your first lesson! 297 | ``` 298 | 299 | +++ 300 | 301 | Show Lesson 302 | ----------- 303 | 304 | ```text 305 | tutorial lesson 306 | 307 | Currently working on Part 01, Lesson 01 - Hello, PyCon! 308 | 309 | Working file: lessons/part_01/cli.py 310 | Tests: lessons/part_01/tests/01_hello.py 311 | Command: part01 312 | Related docs: https://click.palletsprojects.com/en/7.x/quickstart/#basic-concepts-creating-a-command 313 | 314 | Objectives: 315 | * Learn how to use the `tutorial` command to run and check lessons. 316 | * Make the tests for the first lesson pass by editing the working file. 317 | ``` 318 | 319 | +++ 320 | 321 | Try running the command 322 | 323 | ```text 324 | part01 325 | Hello. 326 | ``` 327 | 328 | ```text 329 | part01 --help 330 | Usage: part01 [OPTIONS] 331 | 332 | Options: 333 | --help Show this message and exit. 334 | ``` 335 | 336 | 337 | 338 | +++ 339 | 340 | Check Your Work 341 | --------------- 342 | 343 | * Runs tests 344 | * Outputs assertion results 345 | * Proceeds to next lesson upon success 346 | 347 | ```text 348 | tutorial check 349 | ``` 350 | 351 | +++ 352 | 353 | Need a hint? 354 | ------------ 355 | 356 | Display a solution file that makes test pass: 357 | 358 | ```text 359 | tutorial peek 360 | ``` 361 | 362 | +++ 363 | 364 | Solve 365 | ----- 366 | 367 | ```text 368 | tutorial solve 369 | This will make a backup of the working file and then copy the solution file into place. 370 | Working file: lessons/part_01/cli.py 371 | Backup file: lessons/part_01/cli.2019-04-29.12-54-03.py 372 | Solution file: lessons/part_01/solutions/cli_01_hello.py 373 | Do you wish to proceed? [y/N]: 374 | ``` 375 | 376 | Then run check to advance 377 | 378 | ```text 379 | tutorial check 380 | ``` 381 | 382 | +++ 383 | 384 | Other Commands 385 | -------------- 386 | 387 | Check overall status: 388 | 389 | ```text 390 | tutorial status 391 | ``` 392 | 393 | Skip to specific lesson: 394 | 395 | ```text 396 | tutorial lesson --part 1 --lesson 1 397 | ``` 398 | 399 | ```text 400 | tutorial lesson -p1 -l1 401 | ``` 402 | 403 | ---?color=#000000; 404 | 405 | Tutorial 406 | ======== 407 | 408 | --- 409 | 410 | # Part 01: 411 | # Command Parsing 412 | 413 | 414 | --- 415 | 416 | ## Hello, PyCon! 417 | 418 | ```python 419 | import click 420 | 421 | @click.command() 422 | def cli(): 423 | print("Hello.") 424 | ``` 425 | 426 | +++ 427 | 428 | ## 01-01: Hello, PyCon! 429 | 430 | * Learn how to use the `tutorial` command to run and check lessons. 431 | * Make the tests for the first lesson pass by editing the working file. 432 | 433 | --- 434 | ## Arguments 435 | 436 | ```python 437 | @click.command() 438 | @click.argument("names", nargs=1) 439 | def cli(name): 440 | print(f"Hello, {name}.") 441 | ``` 442 | 443 | +++ 444 | 445 | ## 01-02: Arguments 446 | 447 | * Make the command accept a positional NAME argument 448 | * accept any number of values 449 | * print "Hello, NAME!" on a new line for each name given 450 | * output nothing if no arguments are passed (noop) 451 | 452 | --- 453 | 454 | ## Options 455 | 456 | ```python 457 | @click.command() 458 | @click.argument("name") 459 | @click.option("--count", "-c", default=1) 460 | @click.option("--green", is_flag=True) 461 | @click.option("--debug/--no-debug") 462 | def cli(name): 463 | ... 464 | ``` 465 | 466 | +++ 467 | 468 | ## 01-03: Options 469 | 470 | * Add multiple options: 471 | * Add `--greeting` to specify greeting text 472 | * With a short alias: `-g` 473 | * Default to "Hello" if no greeting is passed 474 | * Add a `--question` option as a flag that doesn't take a value 475 | * If passed, end the greeting with "?" 476 | * If not passed, end the greeting with "!" 477 | " 478 | 479 | --- 480 | 481 | ## Help Documentation 482 | 483 | ```python 484 | @click.command() 485 | @click.option("--count", help="Number of times to print greeting.") 486 | def cli(count): 487 | """Print a greeting.""" 488 | ... 489 | ``` 490 | 491 | +++ 492 | 493 | ## 01-04: Help Documentation 494 | 495 | * Document your script 496 | * Add general command usage help 497 | * Add help text to the `--greeting` and `--question` options 498 | 499 | --- 500 | 501 | ## Input Validation 502 | 503 | ```python 504 | @click.command() 505 | @click.option("--example", default=1) 506 | @click.option("--another", type=int) 507 | @click.option("--color", type=click.Choice("red", "green", "blue")) 508 | def cli(example, another, color): 509 | ... 510 | ``` 511 | 512 | +++ 513 | 514 | ## 01-05: Input Validation 515 | 516 | * Add new options to learn how type validation works 517 | * Output "int: {VALUE}" if `--int-option` 518 | * Output "float: {VALUE}" if `--float-option` 519 | * Output "bool: {VALUE}" if `--bool-option` 520 | * Add `--choice-option` 521 | * Validate values are one of "A", "B", "C" 522 | * Output "choice: {VALUE}" if value passed 523 | 524 | --- 525 | 526 | # Part 02: 527 | # Input / Output 528 | 529 | --- 530 | 531 | ## Echo 532 | 533 | ```python 534 | @click.command() 535 | def cli(): 536 | click.echo("I'm about to print...", err=True) 537 | 538 | click.echo("Hello!") 539 | 540 | click.echo(click.style("Green text!", fg="green")) 541 | # equivalent: 542 | click.secho("Green text!", fg="green") 543 | ``` 544 | 545 | +++ 546 | 547 | ## 02-01: Echo 548 | 549 | * Customize output destination and formatting 550 | * Make "Hello!" print to stdout 551 | * Make "Printing..." print to stderr 552 | * Add a `--red` option that makes output text red when passed 553 | 554 | --- 555 | 556 | ## File I/O 557 | 558 | ```python 559 | @click.command() 560 | @click.argument("infile", type=click.File("r"), default="-") 561 | def cli(infile): 562 | text = infile.read() 563 | ``` 564 | 565 | +++ 566 | 567 | ## 02-02: File I/O 568 | 569 | * Read from and write to files or stdin/stdout depending on arguments 570 | * Read the input source and write the contents to the output 571 | * Accept an input file argument, reading from stdin by default (using `-` arg) 572 | * Accept an output file argument, writing to stdout by default (using `-` arg) 573 | * Find the length of the input data and print a message to stderr 574 | 575 | --- 576 | # Part 03: 577 | # Nested Commands 578 | 579 | --- 580 | 581 | ## Command Groups 582 | 583 | ```python 584 | @click.group() 585 | def example_command(): 586 | """I'm an example command.""" 587 | 588 | @example_command.command() 589 | def example_subcommand(): 590 | """Says hi.""" 591 | click.echo("Hello, world!") 592 | ``` 593 | 594 | +++ 595 | 596 | ## 03-01: Command Groups 597 | 598 | * Make a command that has subcommands 599 | * Add `hello` subcommand that prints "Hello!" 600 | * See that trying to run nonexistent subcommands results in an error 601 | 602 | --- 603 | 604 | ## Sharing Contexts 605 | 606 | ```python 607 | @click.group() 608 | @click.pass_context 609 | def example_command(ctx): 610 | ctx.obj = {"setting": "value"} 611 | 612 | @example_command.command() 613 | @click.pass_context 614 | def example_subcommand(ctx): 615 | sttings = ctx.obj 616 | click.echo(settings.get("setting")) 617 | 618 | @example_command.command() 619 | @click.pass_obj 620 | def another_subcommand(obj): 621 | click.echo(obj.get("setting")) 622 | ``` 623 | 624 | +++ 625 | 626 | ## 03-02: Sharing Contexts 627 | 628 | * Learn how parameters are handled by the group and by subcommands 629 | * Pass `--verbose` group option to `hello` subcommand via `pass_context` 630 | * Add a new `goodbye` subcommand 631 | * Pass `--verbose` group option to `goodbye` via `pass_obj` 632 | 633 | --- 634 | 635 | 636 | # Part 04: 637 | # Projects & Packaging 638 | 639 | --- 640 | 641 | ## 04-01: Create a Project 642 | 643 | * Use `cookiecutter` to create a new project 644 | * Follow the prompts to enable the CLI 645 | 646 | +++ 647 | 648 | ## 04-01: Cookiecutter Example 649 | 650 | ```text 651 | full_name [Audrey Roy Greenfeld]: Dave Forgac 652 | email [audreyr@example.com]: tylerdave@tylerdave.com 653 | github_username [audreyr]: tylerdave 654 | project_name [Python Boilerplate]: Example CLI 655 | project_slug [example_cli]: 656 | project_short_description [Python Boilerplate.]: An example CLI project 657 | pypi_username [tylerdave]: tylerdave 658 | version [0.1.0]: 0.0.1 659 | use_pytest [n]: y 660 | use_pypi_deployment_with_travis [y]: n 661 | Select command_line_interface: 662 | 1 - Click 663 | 2 - No command-line interface 664 | Choose from 1, 2 (1, 2) [1]: 1 665 | ``` 666 | 667 | --- 668 | 669 | ## 04-02: Package Layout 670 | 671 | * Explore package layout 672 | * `setup.py` 673 | * `setup.cfg` 674 | * $PACKAGE_NAME/`cli.py` 675 | 676 | --- 677 | 678 | ## 04-03: Development 679 | 680 | * Create a virtualenv 681 | * Install the package in editable mode 682 | * See changes reflected 683 | 684 | --- 685 | 686 | ## 04-04: Testing 687 | 688 | * Update tests to match CLI output 689 | 690 | --- 691 | 692 | ## 04-05: Build & Publish 693 | 694 | * Build distribution files 695 | * See how to publish on PyPI 696 | 697 | --- 698 | 699 | # Part 05: 700 | # Extras 701 | 702 | --- 703 | 704 | ## Examples 705 | 706 | * Pagination 707 | * Progress Bars 708 | * Pagination 709 | * Launch Editor 710 | * Handle `ctrl-c` 711 | 712 | --- 713 | 714 | Thank You! 715 | ========== 716 | 717 | Feedback / Questions? 718 | --------------------- 719 | 720 | dave@forgac.com 721 | 722 | @tylerdave 723 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------