diff --git a/docs/tutorial/arguments/optional.md b/docs/tutorial/arguments/optional.md index ec13be1edb..da80e1f5be 100644 --- a/docs/tutorial/arguments/optional.md +++ b/docs/tutorial/arguments/optional.md @@ -39,7 +39,7 @@ In the [First Steps](../first-steps.md#add-a-cli-argument){.internal-link target Now let's see an alternative way to create the same *CLI argument*: -{* docs_src/arguments/optional/tutorial000_py39.py hl[4] *} +{* docs_src/arguments/optional/tutorial000_an_py39.py hl[6] *} Or, using an explicit `Typer()` instance creation: diff --git a/docs/tutorial/options/version.md b/docs/tutorial/options/version.md index b4bb36d1ce..a903d91069 100644 --- a/docs/tutorial/options/version.md +++ b/docs/tutorial/options/version.md @@ -57,7 +57,7 @@ Awesome CLI Version: 0.1.0 But now let's say that the `--name` *CLI option* that we declared before `--version` is required, and it has a callback that could exit the program: -{* docs_src/options/version/tutorial002_an_py310.py hl[16:18,24:26] *} +{* docs_src/options/version/tutorial002_an_py310.py hl[16:19,25:27] *} Then our CLI program could not work as expected in some cases as it is *right now*, because if we use `--version` after `--name` then the callback for `--name` will be processed before and we can get its error: diff --git a/docs_src/arguments/optional/tutorial000_an_py39.py b/docs_src/arguments/optional/tutorial000_an_py39.py new file mode 100644 index 0000000000..65904062cd --- /dev/null +++ b/docs_src/arguments/optional/tutorial000_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +import typer + + +def main(name: Annotated[str, typer.Argument()]): + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/options/version/tutorial002_an_py310.py b/docs_src/options/version/tutorial002_an_py310.py index a867fe199b..9104195dff 100644 --- a/docs_src/options/version/tutorial002_an_py310.py +++ b/docs_src/options/version/tutorial002_an_py310.py @@ -16,6 +16,7 @@ def version_callback(value: bool): def name_callback(name: str): if name != "Camila": raise typer.BadParameter("Only Camila is allowed") + return name @app.command() diff --git a/docs_src/options/version/tutorial002_an_py39.py b/docs_src/options/version/tutorial002_an_py39.py index 32f5315133..060fa312a9 100644 --- a/docs_src/options/version/tutorial002_an_py39.py +++ b/docs_src/options/version/tutorial002_an_py39.py @@ -16,6 +16,7 @@ def version_callback(value: bool): def name_callback(name: str): if name != "Camila": raise typer.BadParameter("Only Camila is allowed") + return name @app.command() diff --git a/docs_src/options/version/tutorial002_py310.py b/docs_src/options/version/tutorial002_py310.py index 471ae2e303..9e881619c0 100644 --- a/docs_src/options/version/tutorial002_py310.py +++ b/docs_src/options/version/tutorial002_py310.py @@ -14,6 +14,7 @@ def version_callback(value: bool): def name_callback(name: str): if name != "Camila": raise typer.BadParameter("Only Camila is allowed") + return name @app.command() diff --git a/docs_src/options/version/tutorial002_py39.py b/docs_src/options/version/tutorial002_py39.py index a880c15c0a..75581cc293 100644 --- a/docs_src/options/version/tutorial002_py39.py +++ b/docs_src/options/version/tutorial002_py39.py @@ -16,6 +16,7 @@ def version_callback(value: bool): def name_callback(name: str): if name != "Camila": raise typer.BadParameter("Only Camila is allowed") + return name @app.command() diff --git a/pyproject.toml b/pyproject.toml index eb13579db6..5fb7841547 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,34 +130,6 @@ source = [ ] omit = [ "typer/_typing.py", - "docs_src/app_dir/tutorial001_py39.py", # ToDo: add test - "docs_src/arguments/optional/tutorial000_py39.py", # ToDo: add test - "docs_src/launch/tutorial001_py39.py", # ToDo: add test - "docs_src/launch/tutorial002_py39.py", # ToDo: add test - "docs_src/options/callback/tutorial002_*.py", # ToDo: add test - "docs_src/options/help/tutorial004_*.py", # ToDo: add test - "docs_src/options/password/tutorial001_*.py", # ToDo: add test - "docs_src/options/password/tutorial002_*.py", # ToDo: add test - "docs_src/options/version/tutorial001_*.py", # ToDo: add test - "docs_src/options/version/tutorial002_*.py", # ToDo: add test - "docs_src/options_autocompletion/tutorial001_*.py", # ToDo: add test - "docs_src/options_autocompletion/tutorial005_*.py", # ToDo: add test - "docs_src/options_autocompletion/tutorial006_*.py", # ToDo: add test - "docs_src/printing/tutorial001_py39.py", # ToDo: add test - "docs_src/printing/tutorial002_py39.py", # ToDo: add test - "docs_src/printing/tutorial003_py39.py", # ToDo: add test - "docs_src/printing/tutorial004_py39.py", # ToDo: add test - "docs_src/printing/tutorial005_py39.py", # ToDo: add test - "docs_src/printing/tutorial006_py39.py", # ToDo: add test - "docs_src/progressbar/tutorial001_py39.py", # ToDo: add test - "docs_src/progressbar/tutorial002_py39.py", # ToDo: add test - "docs_src/progressbar/tutorial003_py39.py", # ToDo: add test - "docs_src/progressbar/tutorial004_py39.py", # ToDo: add test - "docs_src/progressbar/tutorial005_py39.py", # ToDo: add test - "docs_src/progressbar/tutorial006_py39.py", # ToDo: add test - "docs_src/prompt/tutorial004_py39.py", # ToDo: add test - "docs_src/using_click/tutorial001_py39.py", # ToDo: add test - "docs_src/using_click/tutorial002_py39.py", # ToDo: add test ] context = '${CONTEXT}' relative_files = true diff --git a/tests/test_tutorial/test_app_dir/__init__.py b/tests/test_tutorial/test_app_dir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_app_dir/test_tutorial001.py b/tests/test_tutorial/test_app_dir/test_tutorial001.py new file mode 100644 index 0000000000..25a40e5fdb --- /dev/null +++ b/tests/test_tutorial/test_app_dir/test_tutorial001.py @@ -0,0 +1,45 @@ +import subprocess +import sys +from pathlib import Path + +import pytest +import typer +from typer.testing import CliRunner + +from docs_src.app_dir import tutorial001_py39 as mod + +runner = CliRunner() + + +@pytest.fixture(name="config_file") +def create_config_file(): + app_dir = Path(typer.get_app_dir("my-super-cli-app")) + app_dir.mkdir(parents=True, exist_ok=True) + config_path = app_dir / "config.json" + config_path.touch(exist_ok=True) + + yield config_path + + config_path.unlink() + app_dir.rmdir() + + +def test_cli_config_doesnt_exist(): + result = runner.invoke(mod.app) + assert result.exit_code == 0 + assert "Config file doesn't exist yet" in result.output + + +def test_cli_config_exists(config_file: Path): + result = runner.invoke(mod.app) + assert result.exit_code == 0 + assert "Config file doesn't exist yet" not in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_arguments/test_optional/test_tutorial000.py b/tests/test_tutorial/test_arguments/test_optional/test_tutorial000.py new file mode 100644 index 0000000000..bbe1ddc646 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_optional/test_tutorial000.py @@ -0,0 +1,51 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +import pytest +import typer +from typer.testing import CliRunner + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial000_py39"), + pytest.param("tutorial000_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.arguments.optional.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +@pytest.fixture(name="app") +def get_app(mod: ModuleType) -> typer.Typer: + app = typer.Typer() + app.command()(mod.main) + return app + + +def test_cli(app: typer.Typer): + result = runner.invoke(app, ["World"]) + assert result.exit_code == 0 + assert "Hello World" in result.output + + +def test_cli_missing_argument(app: typer.Typer): + result = runner.invoke(app) + assert result.exit_code == 2 + assert "Missing argument 'NAME'" in result.output + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_launch/test_tutorial001.py b/tests/test_tutorial/test_launch/test_tutorial001.py new file mode 100644 index 0000000000..570627387a --- /dev/null +++ b/tests/test_tutorial/test_launch/test_tutorial001.py @@ -0,0 +1,27 @@ +import subprocess +import sys +from unittest.mock import patch + +from typer.testing import CliRunner + +from docs_src.launch import tutorial001_py39 as mod + +runner = CliRunner() + + +def test_cli(): + with patch("typer.launch") as launch_mock: + result = runner.invoke(mod.app) + + assert result.exit_code == 0 + assert result.output.strip() == "Opening Typer's docs" + launch_mock.assert_called_once_with("https://typer.tiangolo.com") + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_launch/test_tutorial002.py b/tests/test_tutorial/test_launch/test_tutorial002.py new file mode 100644 index 0000000000..42a2dae6c7 --- /dev/null +++ b/tests/test_tutorial/test_launch/test_tutorial002.py @@ -0,0 +1,47 @@ +import subprocess +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest +import typer +from typer.testing import CliRunner + +from docs_src.launch import tutorial002_py39 as mod + +runner = CliRunner() + + +@pytest.fixture(name="app_dir") +def app_dir(): + app_dir = Path(typer.get_app_dir("my-super-cli-app")) + if app_dir.exists(): # pragma: no cover + for item in app_dir.iterdir(): + if item.is_file(): + item.unlink() + + yield app_dir + + if app_dir.exists(): + for item in app_dir.iterdir(): + if item.is_file(): + item.unlink() + app_dir.rmdir() + + +def test_cli(app_dir: Path): + with patch("typer.launch") as launch_mock: + result = runner.invoke(mod.app) + + assert result.exit_code == 0 + assert "Opening config directory" in result.output + launch_mock.assert_called_with(str(app_dir / "config.json"), locate=True) + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_callback/test_tutorial002.py b/tests/test_tutorial/test_options/test_callback/test_tutorial002.py new file mode 100644 index 0000000000..f06d7b668c --- /dev/null +++ b/tests/test_tutorial/test_options/test_callback/test_tutorial002.py @@ -0,0 +1,50 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +from ....utils import needs_py310 + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options.callback.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_1(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Validating name" in result.output + assert "Hello Camila" in result.output + + +def test_2(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "rick"]) + assert result.exit_code != 0 + assert "Validating name" in result.output + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_help/test_tutorial004.py b/tests/test_tutorial/test_options/test_help/test_tutorial004.py new file mode 100644 index 0000000000..74d7f3bee3 --- /dev/null +++ b/tests/test_tutorial/test_options/test_help/test_tutorial004.py @@ -0,0 +1,45 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial004_py39"), + pytest.param("tutorial004_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options.help.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_call(mod: ModuleType): + result = runner.invoke(mod.app) + assert result.exit_code == 0 + assert "Hello Wade Wilson" in result.output + + +def test_help(mod: ModuleType): + result = runner.invoke(mod.app, ["--help"]) + assert result.exit_code == 0 + assert "--fullname" in result.output + assert "TEXT" in result.output + assert "[default: (Deadpoolio the amazing's name)]" in result.output + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_password/__init__.py b/tests/test_tutorial/test_options/test_password/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_options/test_password/test_tutorial001.py b/tests/test_tutorial/test_options/test_password/test_tutorial001.py new file mode 100644 index 0000000000..4f0a79b8b4 --- /dev/null +++ b/tests/test_tutorial/test_options/test_password/test_tutorial001.py @@ -0,0 +1,56 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +from tests.utils import strip_double_spaces + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options.password.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_option_email(mod: ModuleType): + result = runner.invoke(mod.app, ["Camila", "--email", "camila@example.com"]) + assert result.exit_code == 0 + assert "Hello Camila, your email is camila@example.com" in result.output + + +def test_option_email_prompt(mod: ModuleType): + result = runner.invoke( + mod.app, ["Camila"], input="camila@example.com\ncamila@example.com\n" + ) + assert result.exit_code == 0 + assert "Email: camila@example.com" in result.output + assert "Repeat for confirmation: camila@example.com" in result.output + assert "Hello Camila, your email is camila@example.com" in result.output + + +def test_help(mod: ModuleType): + result = runner.invoke(mod.app, ["--help"]) + assert result.exit_code == 0 + output_without_double_spaces = strip_double_spaces(result.output) + assert "--email TEXT [required]" in output_without_double_spaces + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_password/test_tutorial002.py b/tests/test_tutorial/test_options/test_password/test_tutorial002.py new file mode 100644 index 0000000000..ce67001a67 --- /dev/null +++ b/tests/test_tutorial/test_options/test_password/test_tutorial002.py @@ -0,0 +1,60 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +from tests.utils import strip_double_spaces + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options.password.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_option_password(mod: ModuleType): + result = runner.invoke(mod.app, ["Camila", "--password", "secretpassword"]) + assert result.exit_code == 0 + assert "Hello Camila. Doing something very secure with password." in result.output + assert "...just kidding, here it is, very insecure: secretpassword" in result.output + + +def test_option_password_prompt(mod: ModuleType): + result = runner.invoke( + mod.app, ["Camila"], input="secretpassword\nsecretpassword\n" + ) + assert result.exit_code == 0 + assert "Password: " in result.output + assert "Password: secretpassword" not in result.output + assert "Repeat for confirmation: " in result.output + assert "Repeat for confirmation: secretpassword" not in result.output + assert "Hello Camila. Doing something very secure with password." in result.output + assert "...just kidding, here it is, very insecure: secretpassword" in result.output + + +def test_help(mod: ModuleType): + result = runner.invoke(mod.app, ["--help"]) + assert result.exit_code == 0 + output_without_double_spaces = strip_double_spaces(result.output) + assert "--password TEXT [required]" in output_without_double_spaces + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options/test_version/test_tutorial001.py b/tests/test_tutorial/test_options/test_version/test_tutorial001.py new file mode 100644 index 0000000000..88ab0c7e0f --- /dev/null +++ b/tests/test_tutorial/test_options/test_version/test_tutorial001.py @@ -0,0 +1,72 @@ +import importlib +import os +import subprocess +import sys +from pathlib import Path +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +from ....utils import needs_py310 + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_py310", marks=needs_py310), + pytest.param("tutorial001_an_py39"), + pytest.param("tutorial001_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options.version.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_version(mod: ModuleType): + result = runner.invoke(mod.app, ["--version"]) + assert result.exit_code == 0 + assert "Awesome CLI Version: 0.1.0" in result.output + + +def test_version_with_name(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "rick", "--version"]) + assert result.exit_code == 0 + assert "Awesome CLI Version: 0.1.0" in result.output + assert "Hello" not in result.output + + +def test_no_version(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout + + +def test_completion(mod: ModuleType): + file_name = Path(mod.__file__).name + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + capture_output=True, + encoding="utf-8", + env={ + **os.environ, + f"_{file_name.upper()}_COMPLETE": "complete_bash", + "COMP_WORDS": f"{file_name} --name Rick --v", + "COMP_CWORD": "3", + }, + ) + assert "--version" in result.stdout diff --git a/tests/test_tutorial/test_options/test_version/test_tutorial002.py b/tests/test_tutorial/test_options/test_version/test_tutorial002.py new file mode 100644 index 0000000000..17aed359ca --- /dev/null +++ b/tests/test_tutorial/test_options/test_version/test_tutorial002.py @@ -0,0 +1,72 @@ +import importlib +import os +import subprocess +import sys +from pathlib import Path +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +from ....utils import needs_py310 + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial002_py39"), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an_py39"), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options.version.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_version_camila(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "Camila", "--version"]) + assert result.exit_code == 0 + assert "Awesome CLI Version: 0.1.0" in result.output + + +def test_version_not_camila(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "rick", "--version"]) + assert result.exit_code != 0 + assert "Invalid value for '--name'" in result.output + assert "Only Camila is allowed" in result.output + + +def test_version_camila_no_version(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout + + +def test_completion(mod: ModuleType): + file_name = Path(mod.__file__).name + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, " "], + capture_output=True, + encoding="utf-8", + env={ + **os.environ, + f"_{file_name.upper()}_COMPLETE": "complete_bash", + "COMP_WORDS": f"{file_name} --name Rick --v", + "COMP_CWORD": "3", + }, + ) + assert "--version" in result.stdout diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial001.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial001.py new file mode 100644 index 0000000000..69b141521b --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial001.py @@ -0,0 +1,37 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial001_py39"), + pytest.param("tutorial001_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options_autocompletion.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_1(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial004.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial004_tutorial005.py similarity index 94% rename from tests/test_tutorial/test_options_autocompletion/test_tutorial004.py rename to tests/test_tutorial/test_options_autocompletion/test_tutorial004_tutorial005.py index 25bfa108b3..919b836bbf 100644 --- a/tests/test_tutorial/test_options_autocompletion/test_tutorial004.py +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial004_tutorial005.py @@ -16,6 +16,8 @@ params=[ pytest.param("tutorial004_py39"), pytest.param("tutorial004_an_py39"), + pytest.param("tutorial005_py39"), + pytest.param("tutorial005_an_py39"), ], ) def get_mod(request: pytest.FixtureRequest) -> ModuleType: diff --git a/tests/test_tutorial/test_options_autocompletion/test_tutorial006.py b/tests/test_tutorial/test_options_autocompletion/test_tutorial006.py new file mode 100644 index 0000000000..4aa21d7f53 --- /dev/null +++ b/tests/test_tutorial/test_options_autocompletion/test_tutorial006.py @@ -0,0 +1,44 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +import pytest +from typer.testing import CliRunner + +runner = CliRunner() + + +@pytest.fixture( + name="mod", + params=[ + pytest.param("tutorial006_py39"), + pytest.param("tutorial006_an_py39"), + ], +) +def get_mod(request: pytest.FixtureRequest) -> ModuleType: + module_name = f"docs_src.options_autocompletion.{request.param}" + mod = importlib.import_module(module_name) + return mod + + +def test_1(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_2(mod: ModuleType): + result = runner.invoke(mod.app, ["--name", "Camila", "--name", "Sebastian"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + assert "Hello Sebastian" in result.output + + +def test_script(mod: ModuleType): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_printing/__init__.py b/tests/test_tutorial/test_printing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_printing/test_tutorial001.py b/tests/test_tutorial/test_printing/test_tutorial001.py new file mode 100644 index 0000000000..27ed3b0e79 --- /dev/null +++ b/tests/test_tutorial/test_printing/test_tutorial001.py @@ -0,0 +1,44 @@ +import subprocess +import sys +from unittest.mock import patch + +from rich.console import Console # noqa: TID251 +from typer.testing import CliRunner + +import docs_src.printing.tutorial001_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): + console = Console(force_terminal=True, width=100) + with patch("rich.get_console", return_value=console): + result = runner.invoke(app) + + assert result.exit_code == 0 + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output, squash_whitespaces=False) + + assert ( + "Here's the data\n" + "*{*\n" + " *'name'*: *'Rick'*,\n" + " *'age'*: *42*,\n" + " *'items'*: *[*{*'name'*: *'Portal Gun'*}*, *{*'name'*: *'Plumbus'*}*]*,\n" + " *'active'*: *True*,\n" + " *'affiliation'*: *None*\n" + "*}*\n" + ) in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_printing/test_tutorial002.py b/tests/test_tutorial/test_printing/test_tutorial002.py new file mode 100644 index 0000000000..158dea4dc4 --- /dev/null +++ b/tests/test_tutorial/test_printing/test_tutorial002.py @@ -0,0 +1,41 @@ +import subprocess +import sys +from unittest.mock import patch + +from rich.console import Console # noqa: TID251 +from typer.testing import CliRunner + +import docs_src.printing.tutorial002_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): + console = Console(force_terminal=True, width=100) + with patch("rich.get_console", return_value=console): + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert "*Alert!* *Portal gun* shooting! *" in normalized_output + + +def test_cli_without_formatting(): + result = runner.invoke(app) + + assert result.exit_code == 0 + assert "Alert! Portal gun shooting! 💥" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_printing/test_tutorial003.py b/tests/test_tutorial/test_printing/test_tutorial003.py new file mode 100644 index 0000000000..ac4bf0deb6 --- /dev/null +++ b/tests/test_tutorial/test_printing/test_tutorial003.py @@ -0,0 +1,32 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +import docs_src.printing.tutorial003_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert ( + "*\n* Name * Item *\n*\n* Rick * Portal Gun *\n* Morty * Plumbus *\n*\n" + ) in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_printing/test_tutorial004.py b/tests/test_tutorial/test_printing/test_tutorial004.py new file mode 100644 index 0000000000..f8a6cc8d69 --- /dev/null +++ b/tests/test_tutorial/test_printing/test_tutorial004.py @@ -0,0 +1,32 @@ +import platform +import subprocess +import sys + +import pytest +from typer.testing import CliRunner + +import docs_src.printing.tutorial004_py39 as mod + +app = mod.app + +runner = CliRunner() + + +@pytest.mark.xfail( + condition=((platform.system() == "Windows") and (sys.version_info < (3, 10))), + reason="On Windows with Python 3.9, output is in stdout instead of stderr", +) +def test_cli(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert result.stdout == "" + assert "Here is something written to standard error" in result.stderr + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_printing/test_tutorial005.py b/tests/test_tutorial/test_printing/test_tutorial005.py new file mode 100644 index 0000000000..9335245551 --- /dev/null +++ b/tests/test_tutorial/test_printing/test_tutorial005.py @@ -0,0 +1,42 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +import docs_src.printing.tutorial005_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_good_true(): + result = runner.invoke(app, color=True) + assert result.exit_code == 0 + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert "everything is *good*" in normalized_output + # We don't check exact colors here, just that text has formatting + + +def test_good_false(): + result = runner.invoke(app, ["--no-good"], color=True) + assert result.exit_code == 0 + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert "everything is *bad*" in normalized_output + # We don't check exact colors here, just that text has formatting + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_printing/test_tutorial006.py b/tests/test_tutorial/test_printing/test_tutorial006.py new file mode 100644 index 0000000000..1e7803d2ef --- /dev/null +++ b/tests/test_tutorial/test_printing/test_tutorial006.py @@ -0,0 +1,31 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +import docs_src.printing.tutorial006_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): + result = runner.invoke(app, ["everyone"], color=True) + assert result.exit_code == 0 + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert "*Welcome here everyone*" in normalized_output + # We don't check exact colors here, just that text has formatting + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_progressbar/__init__.py b/tests/test_tutorial/test_progressbar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_progressbar/test_tutorial001.py b/tests/test_tutorial/test_progressbar/test_tutorial001.py new file mode 100644 index 0000000000..c5a3c845e2 --- /dev/null +++ b/tests/test_tutorial/test_progressbar/test_tutorial001.py @@ -0,0 +1,48 @@ +import subprocess +import sys +from unittest.mock import patch + +import typer +from typer.testing import CliRunner + +import docs_src.progressbar.tutorial001_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli_one_step(): + with patch("time.sleep") as sleep_mock: + sleep_mock.side_effect = typer.Exit() # Exit on first `time.sleep()` call + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert "Processing... 0%" in normalized_output + + +def test_cli(): + with patch("time.sleep") as mock_sleep: + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert mock_sleep.call_count == 100 + assert "Processing..." in normalized_output + assert "100%" in normalized_output + assert "Processed 100 things." in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_progressbar/test_tutorial002.py b/tests/test_tutorial/test_progressbar/test_tutorial002.py new file mode 100644 index 0000000000..8d744806ac --- /dev/null +++ b/tests/test_tutorial/test_progressbar/test_tutorial002.py @@ -0,0 +1,33 @@ +import subprocess +import sys +from unittest.mock import patch + +from typer.testing import CliRunner + +import docs_src.progressbar.tutorial002_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): # Checking only final state of spinner progress bar + with patch("time.sleep") as mock_sleep: + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + mock_sleep.assert_called_once_with(5) + assert "Done!" in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_progressbar/test_tutorial003.py b/tests/test_tutorial/test_progressbar/test_tutorial003.py new file mode 100644 index 0000000000..545fb9bfc3 --- /dev/null +++ b/tests/test_tutorial/test_progressbar/test_tutorial003.py @@ -0,0 +1,33 @@ +import subprocess +import sys +from unittest.mock import patch + +from typer.testing import CliRunner + +import docs_src.progressbar.tutorial003_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): # Checking only final state of progress bar + with patch("time.sleep") as mock_sleep: + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert mock_sleep.call_count == 100 + assert "Processed 100 things." in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_progressbar/test_tutorial004.py b/tests/test_tutorial/test_progressbar/test_tutorial004.py new file mode 100644 index 0000000000..43ecc451d6 --- /dev/null +++ b/tests/test_tutorial/test_progressbar/test_tutorial004.py @@ -0,0 +1,61 @@ +import subprocess +import sys +from unittest.mock import patch + +from typer.testing import CliRunner + +import docs_src.progressbar.tutorial004_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): # Checking only final state of progress bar + consumed = [] + + def fake_iterate_user_ids(): + for i in range(100): + consumed.append(i) + yield i + + with ( + patch("time.sleep") as mock_sleep, + patch( + "docs_src.progressbar.tutorial004_py39.iterate_user_ids", + side_effect=fake_iterate_user_ids, + ), + ): + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert len(consumed) == 100 + assert mock_sleep.call_count == 100 + assert "Processed 100 user IDs." in normalized_output + + +def test_cli_no_mock_generator(): + with ( + patch("time.sleep") as mock_sleep, + ): + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert mock_sleep.call_count == 100 + assert "Processed 100 user IDs." in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_progressbar/test_tutorial005.py b/tests/test_tutorial/test_progressbar/test_tutorial005.py new file mode 100644 index 0000000000..011de5db3c --- /dev/null +++ b/tests/test_tutorial/test_progressbar/test_tutorial005.py @@ -0,0 +1,33 @@ +import subprocess +import sys +from unittest.mock import patch + +from typer.testing import CliRunner + +import docs_src.progressbar.tutorial005_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): # Checking only final state of progress bar + with patch("time.sleep") as mock_sleep: + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert mock_sleep.call_count == 100 + assert "Processed 100 things." in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_progressbar/test_tutorial006.py b/tests/test_tutorial/test_progressbar/test_tutorial006.py new file mode 100644 index 0000000000..3fbe78d4e1 --- /dev/null +++ b/tests/test_tutorial/test_progressbar/test_tutorial006.py @@ -0,0 +1,33 @@ +import subprocess +import sys +from unittest.mock import patch + +from typer.testing import CliRunner + +import docs_src.progressbar.tutorial006_py39 as mod +from tests.utils import normalize_rich_output + +app = mod.app + +runner = CliRunner() + + +def test_cli(): # Checking only final state of progress bar + with patch("time.sleep") as mock_sleep: + result = runner.invoke(app) + + # Replace all Rich formatting with `*` characters + normalized_output = normalize_rich_output(result.output) + + assert result.exit_code == 0 + assert mock_sleep.call_count == 4 + assert "Processed 1000 things in batches." in normalized_output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_prompt/test_tutorial004.py b/tests/test_tutorial/test_prompt/test_tutorial004.py new file mode 100644 index 0000000000..97e9475b97 --- /dev/null +++ b/tests/test_tutorial/test_prompt/test_tutorial004.py @@ -0,0 +1,25 @@ +import subprocess +import sys + +from typer.testing import CliRunner + +from docs_src.prompt import tutorial004_py39 as mod + +runner = CliRunner() +app = mod.app + + +def test_cli(): + result = runner.invoke(app, input="World\n") + assert result.exit_code == 0 + assert "Enter your name" in result.output + assert "Hey there World!" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_using_click/test_tutorial001.py b/tests/test_tutorial/test_using_click/test_tutorial001.py new file mode 100644 index 0000000000..92d0f8e254 --- /dev/null +++ b/tests/test_tutorial/test_using_click/test_tutorial001.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +from click.testing import CliRunner + +from docs_src.using_click import tutorial001_py39 as mod + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(mod.hello, ["--help"]) + assert result.exit_code == 0 + assert ( + "Simple program that greets NAME for a total of COUNT times." in result.output + ) + assert "--name" in result.output + assert "--count" in result.output + + +def test_cli_prompt(): + result = runner.invoke(mod.hello, input="Camila\n") + assert result.exit_code == 0 + assert "Your name: Camila" in result.stdout + assert "Hello Camila!" in result.stdout + + +def test_cli_with_name(): + result = runner.invoke(mod.hello, ["--name", "Camila"]) + assert result.exit_code == 0 + assert "Hello Camila!" in result.stdout + + +def test_cli_with_name_and_count(): + result = runner.invoke(mod.hello, ["--name", "Camila", "--count", "3"]) + assert result.exit_code == 0 + assert "Hello Camila!" in result.stdout + assert result.stdout.count("Hello Camila!") == 3 + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_using_click/test_tutorial002.py b/tests/test_tutorial/test_using_click/test_tutorial002.py new file mode 100644 index 0000000000..bcc71ac37c --- /dev/null +++ b/tests/test_tutorial/test_using_click/test_tutorial002.py @@ -0,0 +1,35 @@ +import subprocess +import sys + +from click.testing import CliRunner + +from docs_src.using_click import tutorial002_py39 as mod + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(mod.cli, ["--help"]) + assert result.exit_code == 0 + assert "Commands" in result.output + assert "initdb" in result.output + assert "dropdb" in result.output + + +def test_initdb(): + result = runner.invoke(mod.cli, ["initdb"]) + assert "Initialized the database" in result.stdout + + +def test_dropdb(): + result = runner.invoke(mod.cli, ["dropdb"]) + assert "Dropped the database" in result.stdout + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/utils.py b/tests/utils.py index ee190d8438..786e57014f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,4 @@ +import re import sys from os import getenv @@ -21,3 +22,22 @@ not getenv("_TYPER_RUN_INSTALL_COMPLETION_TESTS", False), reason="Test requires permission to run completion installation tests", ) + + +def strip_double_spaces(text: str) -> str: + return re.sub(r" {2,}", " ", text) + + +def normalize_rich_output( + text: str, replace_with: str = "*", squash_whitespaces: bool = True +) -> str: + """ + Replace all rich formatting characters with a simple placeholder. + """ + text = re.sub(r"\x1b\[[0-9;]*m", replace_with, text) + text = re.sub(r"[\u2500-\u257F]", replace_with, text) + text = re.sub(r"[\U0001F300-\U0001F6FF]", replace_with, text) + text = re.sub(f"{re.escape(replace_with)}+", replace_with, text) + if squash_whitespaces: + text = strip_double_spaces(text) + return text