├── 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 | [](https://travis-ci.org/tylerdave/PyCon2019-Click-Tutorial)
5 | [](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 |
--------------------------------------------------------------------------------