Skip to content

Commit 630d174

Browse files
authored
Add lessons (#2)
* add initial tests * add lesson 02 * rename tests so they don't get collected * add lesson 03 * add help lesson * add types lesson * blacken * blacken all * add part 02 * remove shebangs from tests * move i/o to part 03 * add lesson 02 * add part 02 skeleton * add stderr test * add solution 01 * reorder decorators * add test 01 * add 02 * formatting * update setup instructions * update tox tests
1 parent 5cc5264 commit 630d174

39 files changed

+520
-79
lines changed

README.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ PyCon 2019 Tutorial: Writing Command Line Applications that Click
1717

1818
### Installation
1919

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+
2022
* Open a terminal / command prompt.
2123
* 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).
2224
* Clone this repo:<br> `git clone https://github.com/tylerdave/PyCon2019-Click-Tutorial.git`
@@ -36,10 +38,7 @@ PyCon 2019 Tutorial: Writing Command Line Applications that Click
3638
* On Mac/Linux: `source env/bin/activate`
3739
* On Windows: `.\env\Scripts\activate`
3840
* `pip install -e .`
39-
* Verify installation:<br>`tutorial verify`
40-
41-
## Running the Tutorial
41+
* Verify installation:<br>`pycon verify`
4242

43-
Instructions coming soon 😁
4443

4544

click_tutorial/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"""Top-level package for Click Tutorial."""
44

55
__author__ = """Dave Forgac"""
6-
__email__ = '[email protected]'
7-
__version__ = '0.0.1'
6+
__email__ = "[email protected]"
7+
__version__ = "0.0.1"

click_tutorial/checks.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
11
import sys
22

3+
34
def get_python_version():
4-
return sys.version.replace('\n', '')
5+
return sys.version.replace("\n", "")
6+
57

68
def check_python_35_plus():
7-
if sys.version_info.major == 3 and sys.version_info.minor >=5:
9+
if sys.version_info.major == 3 and sys.version_info.minor >= 5:
810
return True
911
else:
1012
return False
1113

14+
1215
def check_pytest():
1316
import pytest
17+
1418
return True
1519

20+
21+
def check_cookiecutter():
22+
import cookiecutter
23+
24+
return True
25+
26+
1627
ALL_CHECKS = {
17-
'Python Version': get_python_version,
18-
'Python 3.5+': check_python_35_plus,
19-
'PyTest Installed': check_pytest,
20-
}
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+
}

click_tutorial/cli.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@
55

66
from click_tutorial.checks import ALL_CHECKS
77

8-
@click.group()
8+
9+
@click.group(name="pycon")
910
def main(args=None):
10-
"""Click tutorial runner."""
11+
"""PyCon Tutorial."""
12+
13+
14+
@main.command()
15+
def hello():
16+
"""Say hello."""
17+
click.echo("Hello!")
18+
1119

1220
@main.command()
1321
def verify():
@@ -26,9 +34,12 @@ def verify():
2634
click.secho(message=str(err), fg="red", bg="yellow")
2735
any_failures = True
2836
if any_failures:
29-
click.secho("\nVerification failed. Please see setup instructions.", fg="red")
37+
click.secho("\nVerification failed. 😢 Please see setup instructions.", fg="red")
3038
else:
31-
click.secho("\nVerification successful! You're ready to run the tutorial!", fg="blue")
39+
click.secho(
40+
"\nVerification successful! Your system will be albe to run the tutorial! ✨",
41+
fg="blue",
42+
)
3243

3344

3445
if __name__ == "__main__":

click_tutorial/hello_example.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,29 @@
33
import click
44
import time
55

6+
67
@click.group()
78
def cli():
89
"""Command group."""
910

11+
1012
@cli.command()
1113
@click.argument("name")
12-
@click.option("--color", default='green', type=click.Choice(['red', 'black', 'green']))
13-
@click.option("--count", "-c", type=int, default=1, help="number of times to print message")
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+
)
1418
def hello(name, color, count):
1519
"""A command that says hello."""
1620
click.echo("color: {}".format(color), err=True)
1721
for i in range(count):
1822
click.secho("Hello, {}!".format(name), fg=color)
19-
23+
2024
items = range(100)
2125
with click.progressbar(items) as bar:
2226
for item in bar:
23-
time.sleep(0.03)
27+
time.sleep(0.03)
28+
2429

2530
if __name__ == "__main__":
26-
cli()
31+
cli()

lessons/part_01/__init__.py

Whitespace-only changes.

click_tutorial/hello.py renamed to lessons/part_01/cli.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import click
44

5+
56
@click.command()
67
def cli():
7-
print("Hello")
8+
print("Hello.")
9+
810

911
if __name__ == "__main__":
10-
cli()
12+
cli()
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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()
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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()
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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()
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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:
29+
print("bool: {}".format(bool_option))
30+
if choice_option:
31+
print("choice: {}".format(choice_option))
32+
33+
34+
if __name__ == "__main__":
35+
cli()

lessons/part_01/tests/01_hello.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
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"

lessons/part_01/tests/02_args.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 == ""

lessons/part_01/tests/03_opts.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
21+
def test_04_no_question_option(self):
22+
result = self.run_command(["--no-question", "Tutorial"])
23+
assert result.output == "Hello, Tutorial!\n"

lessons/part_01/tests/04_help.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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: greet")
10+
assert "Displays a greeting." in result.output
11+
12+
def test_01_cli_includes_help_text_from_option(self):
13+
result = self.run_command(["--help"])
14+
assert re.search(
15+
r"--question / --no-question\W+Make the greeting a question\.",
16+
result.output,
17+
)
18+
19+
def test_03_cli_with_subcommand_shows_short_help(self):
20+
result = self.run_command(["--help"])
21+
assert re.search(
22+
r"-g, --greeting TEXT\W+The greeting to display.", result.output
23+
)

lessons/part_01/tests/05_types.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
def test_04_cli_valid_bool_option(self):
26+
result = self.run_command(["--bool-option", "True"])
27+
assert "bool: True\n" in result.output
28+
29+
def test_05_cli_invalid_bool_option(self):
30+
result = self.run_command(["--bool-option", "3.14"])
31+
assert "Invalid value" in result.output
32+
assert result.exit_code == 2
33+
34+
@pytest.mark.parametrize("test_input", ["A", "B", "C"])
35+
def test_06_cli_valid_choice_option(self, test_input):
36+
result = self.run_command(["--choice-option", test_input])
37+
assert "choice: {}\n".format(test_input) in result.output
38+
39+
def test_07_cli_invalid_choice_option(self):
40+
result = self.run_command(["--choice-option", "1"])
41+
assert "Invalid value" in result.output
42+
assert result.exit_code == 2

lessons/part_01/tests/__init__.py

Whitespace-only changes.

lessons/part_01/tests/base.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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

lessons/part_02/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)