Name | clirunner JSON |
Version |
0.0.4
JSON |
| download |
home_page | None |
Summary | CliRunner test runner for command line applications. |
upload_time | 2023-12-02 21:48:13 |
maintainer | None |
docs_url | None |
author | None |
requires_python | None |
license | None |
keywords |
|
VCS |
 |
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# CliRunner
A test helper for invoking and testing command line interfaces (CLIs). This is adapted from the [Click](https://click.palletsprojects.com/) [CliRunner](https://click.palletsprojects.com/en/8.1.x/testing/) but modified to work with non-Click scripts, such as those using [argparse](https://docs.python.org/3/library/argparse.html) for parsing command line arguments.
## Installation
`python3 -m pip install clirunner`
## Source Code
The source code is available on [GitHub](https://github.com/RhetTbull/clirunner).
## Motivation
I write a lot of Python command line tools. I usually reach for Click to build the CLI but sometimes will use argparse or even just manual `sys.argv` parsing for simple scripts or where I do not want to introduce a dependency on Click. Click provides a very useful [CliRunner](https://click.palletsprojects.com/en/8.1.x/testing/) for testing CLIs, but it only works with Click applications. This project is a derivative of Click's CliRunner that works with non-Click scripts. The API is the same as Click's CliRunner, so it should be easy to switch between the two if you later refactor to use Click.
## Supported Platforms
Tested on macOS, Ubuntu Linux, and Windows using "*-latest" GitHub Workflow runners with Python 3.9 - 3.12.
## Documentation
Full documentation is available [here](https://rhettbull.github.io/clirunner/).
## Basic Testing
CliRunner can invoke your CLI's main function as a command line script. The CliRunner.invoke() method runs the command line script in isolation and captures the output as both bytes and binary data.
The return value is a Result object, which has the captured output data, exit code, and optional exception attached:
### hello.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/hello.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Simple CLI """
import argparse
def hello():
"""Print Hello World"""
argp = argparse.ArgumentParser(description="Print Hello World")
argp.add_argument("-n", "--name", help="Name to greet")
args = argp.parse_args()
print(f"Hello {args.name or 'World'}!")
if __name__ == "__main__":
hello()
```
<!--[[[end]]]-->
### test_hello.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/test_hello.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Test hello.py"""
from hello import hello
from clirunner import CliRunner
def test_hello_world():
runner = CliRunner()
result = runner.invoke(hello, ["--name", "Peter"])
assert result.exit_code == 0
assert result.output == "Hello Peter!\n"
```
<!--[[[end]]]-->
Note that `result.output` will contain the combined output of `stdout` and `stderr`. If you want to capture `stdout` and `stderr` separately, use `result.stdout` and `result.stderr`.
## File System Isolation
For basic command line tools with file system operations, the `CliRunner.isolated_filesystem()` method is useful for setting the current working directory to a new, empty folder.
### cat.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/cat.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Simple cat program for testing isolated file system"""
import argparse
def cat():
argp = argparse.ArgumentParser()
argp.add_argument("file", type=argparse.FileType("r"))
args = argp.parse_args()
print(args.file.read(), end="")
if __name__ == "__main__":
cat()
```
<!--[[[end]]]-->
### test_cat.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/test_cat.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Test cat.py example."""
from cat import cat
from clirunner import CliRunner
def test_cat():
runner = CliRunner()
with runner.isolated_filesystem():
with open("hello.txt", "w") as f:
f.write("Hello World!\n")
result = runner.invoke(cat, ["hello.txt"])
assert result.exit_code == 0
assert result.output == "Hello World!\n"
```
<!--[[[end]]]-->
Pass `temp_dir` to control where the temporary directory is created. The directory will not be removed by `CliRunner` in this case. This is useful to integrate with a framework like Pytest that manages temporary files.
```python
def test_keep_dir(tmp_path):
runner = CliRunner()
with runner.isolated_filesystem(temp_dir=tmp_path) as td:
...
```
## Input Streams
The test wrapper can also be used to provide input data for the input stream (stdin). This is very useful for testing prompts, for instance:
### prompt.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/prompt.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Simple example for testing input streams"""
def prompt():
foo = input("Foo: ")
print(f"foo = {foo}")
if __name__ == "__main__":
prompt()
```
<!--[[[end]]]-->
### test_prompt.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/test_prompt.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Test prompt.py example"""
from prompt import prompt
from clirunner import CliRunner
def test_prompts():
runner = CliRunner()
result = runner.invoke(prompt, input="wau wau\n")
assert not result.exception
# note: unlike click.CliRunner, clirunner.CliRunner does not echo the input
assert "foo = wau wau\n" in result.output
```
<!--[[[end]]]-->
Note that the input will not be echoed to the output stream. This is different from the behavior of the `input()` function, which does echo the input and from click's `prompt()` function, which also echo's the input when under test.
## Environment Variable Isolation
The `CliRunner.invoke()` method can also be used to set environment variables for the command line script. This is useful for testing command line tools that use environment variables for configuration.
### hello_env.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/hello_env.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Say hello to the world, shouting if desired."""
import os
def hello():
"""Say hello to the world, shouting if desired."""
if os.getenv("SHOUT") == "1":
print("HELLO WORLD!")
else:
print("Hello World!")
if __name__ == "__main__":
hello()
```
<!--[[[end]]]-->
### test_hello_env.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/test_hello_env.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Test hello2.py showing how to set environment variables for testing."""
from hello_env import hello
from clirunner import CliRunner
def test_hello():
"""Test hello2.py"""
runner = CliRunner()
result = runner.invoke(hello)
assert result.exit_code == 0
assert result.output == "Hello World!\n"
def test_hello_shouting():
"""Test hello2.py"""
runner = CliRunner()
result = runner.invoke(hello, env={"SHOUT": "1"})
assert result.exit_code == 0
assert result.output == "HELLO WORLD!\n"
```
<!--[[[end]]]-->
## Handling Exceptions
Normally the `CliRunner.invoke()` method will catch exceptions in the CLI under test. If an exception is raised, it will be available via the `Result.exception` property. This can be disabled by passing `catch_exceptions=False` to the `CliRunner.invoke()` method.
### raise_exception.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/raise_exception.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Simple script that raises an exception"""
def raise_exception():
"""Raises a ValueError exception"""
raise ValueError("Exception raised")
if __name__ == "__main__":
raise_exception()
```
<!--[[[end]]]-->
### test_raise_exception.py
<!--[[[cog
cog.out("\n```python\n")
with open("tests/test_raise_exception.py", "r") as f:
cog.out(f.read())
cog.out("```\n")
]]]-->
```python
"""Test raise_exception.py"""
import pytest
from raise_exception import raise_exception
from clirunner import CliRunner
def test_exception_caught():
"""CliRunner normally catches exceptions"""
runner = CliRunner()
result = runner.invoke(raise_exception)
# exit code will not be 0 if exception is raised
assert result.exit_code != 0
assert isinstance(result.exception, ValueError)
def test_exception_not_caught():
"""CliRunner can be configured to not catch exceptions"""
runner = CliRunner()
with pytest.raises(ValueError):
runner.invoke(raise_exception, catch_exceptions=False)
```
<!--[[[end]]]-->
## Testing Click Applications
Do not use `clirunner.CliRunner` to test applications built with [Click](https://pypi.org/project/click/), [Typer](https://pypi.org/project/typer/), or another Click derivative. Instead, use Click's built-in [CliRunner](https://click.palletsprojects.com/en/8.1.x/testing). `clirunner.CliRunner` is only for testing non-Click scripts such as those using [argparse](https://docs.python.org/3/library/argparse.html) or manual [sys.argv](https://docs.python.org/3/library/sys.html#sys.argv) argument parsing.
## License
CliRunner is a derivative work of Click's CliRunner, and so it is licensed under the same BSD 3-clause license as Click. See the [LICENSE](https://github.com/RhetTbull/clirunner/blob/main/LICENSE) and [LICENSE_CLICK](https://github.com/RhetTbull/clirunner/blob/main/LICENSE_CLICK) files for details.
Raw data
{
"_id": null,
"home_page": null,
"name": "clirunner",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": null,
"author": null,
"author_email": "Rhet Turnbull <rturnbull+git@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/d6/94/ae9eec413bf0095b448c53a80f99b6533b937aa85a029aebed05f6135f24/clirunner-0.0.4.tar.gz",
"platform": null,
"description": "# CliRunner\n\nA test helper for invoking and testing command line interfaces (CLIs). This is adapted from the [Click](https://click.palletsprojects.com/) [CliRunner](https://click.palletsprojects.com/en/8.1.x/testing/) but modified to work with non-Click scripts, such as those using [argparse](https://docs.python.org/3/library/argparse.html) for parsing command line arguments.\n\n## Installation\n\n`python3 -m pip install clirunner`\n\n## Source Code\n\nThe source code is available on [GitHub](https://github.com/RhetTbull/clirunner).\n\n## Motivation\n\nI write a lot of Python command line tools. I usually reach for Click to build the CLI but sometimes will use argparse or even just manual `sys.argv` parsing for simple scripts or where I do not want to introduce a dependency on Click. Click provides a very useful [CliRunner](https://click.palletsprojects.com/en/8.1.x/testing/) for testing CLIs, but it only works with Click applications. This project is a derivative of Click's CliRunner that works with non-Click scripts. The API is the same as Click's CliRunner, so it should be easy to switch between the two if you later refactor to use Click.\n\n## Supported Platforms\n\nTested on macOS, Ubuntu Linux, and Windows using \"*-latest\" GitHub Workflow runners with Python 3.9 - 3.12.\n\n## Documentation\n\nFull documentation is available [here](https://rhettbull.github.io/clirunner/).\n\n## Basic Testing\n\nCliRunner can invoke your CLI's main function as a command line script. The CliRunner.invoke() method runs the command line script in isolation and captures the output as both bytes and binary data.\n\nThe return value is a Result object, which has the captured output data, exit code, and optional exception attached:\n\n### hello.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/hello.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Simple CLI \"\"\"\n\nimport argparse\n\n\ndef hello():\n \"\"\"Print Hello World\"\"\"\n argp = argparse.ArgumentParser(description=\"Print Hello World\")\n argp.add_argument(\"-n\", \"--name\", help=\"Name to greet\")\n args = argp.parse_args()\n print(f\"Hello {args.name or 'World'}!\")\n\n\nif __name__ == \"__main__\":\n hello()\n```\n<!--[[[end]]]-->\n\n### test_hello.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/test_hello.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Test hello.py\"\"\"\n\nfrom hello import hello\n\nfrom clirunner import CliRunner\n\n\ndef test_hello_world():\n runner = CliRunner()\n result = runner.invoke(hello, [\"--name\", \"Peter\"])\n assert result.exit_code == 0\n assert result.output == \"Hello Peter!\\n\"\n```\n<!--[[[end]]]-->\n\nNote that `result.output` will contain the combined output of `stdout` and `stderr`. If you want to capture `stdout` and `stderr` separately, use `result.stdout` and `result.stderr`.\n\n## File System Isolation\n\nFor basic command line tools with file system operations, the `CliRunner.isolated_filesystem()` method is useful for setting the current working directory to a new, empty folder.\n\n### cat.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/cat.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Simple cat program for testing isolated file system\"\"\"\n\nimport argparse\n\n\ndef cat():\n argp = argparse.ArgumentParser()\n argp.add_argument(\"file\", type=argparse.FileType(\"r\"))\n args = argp.parse_args()\n print(args.file.read(), end=\"\")\n\n\nif __name__ == \"__main__\":\n cat()\n```\n<!--[[[end]]]-->\n\n### test_cat.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/test_cat.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Test cat.py example.\"\"\"\n\nfrom cat import cat\n\nfrom clirunner import CliRunner\n\n\ndef test_cat():\n runner = CliRunner()\n with runner.isolated_filesystem():\n with open(\"hello.txt\", \"w\") as f:\n f.write(\"Hello World!\\n\")\n\n result = runner.invoke(cat, [\"hello.txt\"])\n assert result.exit_code == 0\n assert result.output == \"Hello World!\\n\"\n```\n<!--[[[end]]]-->\n\nPass `temp_dir` to control where the temporary directory is created. The directory will not be removed by `CliRunner` in this case. This is useful to integrate with a framework like Pytest that manages temporary files.\n\n```python\ndef test_keep_dir(tmp_path):\n runner = CliRunner()\n\n with runner.isolated_filesystem(temp_dir=tmp_path) as td:\n ...\n```\n\n## Input Streams\n\nThe test wrapper can also be used to provide input data for the input stream (stdin). This is very useful for testing prompts, for instance:\n\n### prompt.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/prompt.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Simple example for testing input streams\"\"\"\n\n\ndef prompt():\n foo = input(\"Foo: \")\n print(f\"foo = {foo}\")\n\n\nif __name__ == \"__main__\":\n prompt()\n```\n<!--[[[end]]]-->\n\n### test_prompt.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/test_prompt.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Test prompt.py example\"\"\"\n\nfrom prompt import prompt\n\nfrom clirunner import CliRunner\n\n\ndef test_prompts():\n runner = CliRunner()\n result = runner.invoke(prompt, input=\"wau wau\\n\")\n assert not result.exception\n # note: unlike click.CliRunner, clirunner.CliRunner does not echo the input\n assert \"foo = wau wau\\n\" in result.output\n```\n<!--[[[end]]]-->\n\nNote that the input will not be echoed to the output stream. This is different from the behavior of the `input()` function, which does echo the input and from click's `prompt()` function, which also echo's the input when under test.\n\n## Environment Variable Isolation\n\nThe `CliRunner.invoke()` method can also be used to set environment variables for the command line script. This is useful for testing command line tools that use environment variables for configuration.\n\n### hello_env.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/hello_env.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Say hello to the world, shouting if desired.\"\"\"\n\nimport os\n\n\ndef hello():\n \"\"\"Say hello to the world, shouting if desired.\"\"\"\n if os.getenv(\"SHOUT\") == \"1\":\n print(\"HELLO WORLD!\")\n else:\n print(\"Hello World!\")\n\n\nif __name__ == \"__main__\":\n hello()\n```\n<!--[[[end]]]-->\n\n### test_hello_env.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/test_hello_env.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Test hello2.py showing how to set environment variables for testing.\"\"\"\n\nfrom hello_env import hello\n\nfrom clirunner import CliRunner\n\n\ndef test_hello():\n \"\"\"Test hello2.py\"\"\"\n runner = CliRunner()\n result = runner.invoke(hello)\n assert result.exit_code == 0\n assert result.output == \"Hello World!\\n\"\n\n\ndef test_hello_shouting():\n \"\"\"Test hello2.py\"\"\"\n runner = CliRunner()\n result = runner.invoke(hello, env={\"SHOUT\": \"1\"})\n assert result.exit_code == 0\n assert result.output == \"HELLO WORLD!\\n\"\n```\n<!--[[[end]]]-->\n\n## Handling Exceptions\n\nNormally the `CliRunner.invoke()` method will catch exceptions in the CLI under test. If an exception is raised, it will be available via the `Result.exception` property. This can be disabled by passing `catch_exceptions=False` to the `CliRunner.invoke()` method.\n\n### raise_exception.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/raise_exception.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Simple script that raises an exception\"\"\"\n\n\ndef raise_exception():\n \"\"\"Raises a ValueError exception\"\"\"\n raise ValueError(\"Exception raised\")\n\n\nif __name__ == \"__main__\":\n raise_exception()\n```\n<!--[[[end]]]-->\n\n### test_raise_exception.py\n\n<!--[[[cog\ncog.out(\"\\n```python\\n\")\nwith open(\"tests/test_raise_exception.py\", \"r\") as f:\n cog.out(f.read())\ncog.out(\"```\\n\")\n]]]-->\n\n```python\n\"\"\"Test raise_exception.py\"\"\"\n\nimport pytest\nfrom raise_exception import raise_exception\n\nfrom clirunner import CliRunner\n\n\ndef test_exception_caught():\n \"\"\"CliRunner normally catches exceptions\"\"\"\n runner = CliRunner()\n result = runner.invoke(raise_exception)\n # exit code will not be 0 if exception is raised\n assert result.exit_code != 0\n assert isinstance(result.exception, ValueError)\n\n\ndef test_exception_not_caught():\n \"\"\"CliRunner can be configured to not catch exceptions\"\"\"\n runner = CliRunner()\n with pytest.raises(ValueError):\n runner.invoke(raise_exception, catch_exceptions=False)\n```\n<!--[[[end]]]-->\n\n## Testing Click Applications\n\nDo not use `clirunner.CliRunner` to test applications built with [Click](https://pypi.org/project/click/), [Typer](https://pypi.org/project/typer/), or another Click derivative. Instead, use Click's built-in [CliRunner](https://click.palletsprojects.com/en/8.1.x/testing). `clirunner.CliRunner` is only for testing non-Click scripts such as those using [argparse](https://docs.python.org/3/library/argparse.html) or manual [sys.argv](https://docs.python.org/3/library/sys.html#sys.argv) argument parsing.\n\n## License\n\nCliRunner is a derivative work of Click's CliRunner, and so it is licensed under the same BSD 3-clause license as Click. See the [LICENSE](https://github.com/RhetTbull/clirunner/blob/main/LICENSE) and [LICENSE_CLICK](https://github.com/RhetTbull/clirunner/blob/main/LICENSE_CLICK) files for details.\n",
"bugtrack_url": null,
"license": null,
"summary": "CliRunner test runner for command line applications.",
"version": "0.0.4",
"project_urls": {
"Home": "https://github.com/RhetTbull/clirunner",
"Issues": "https://github.com/RhetTbull/clirunner/issues",
"Source": "https://github.com/RhetTbull/clirunner"
},
"split_keywords": [],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "fa7cb6ae54411a1663375ec032b813680b66ebc17b041e04a845c62e18ddbb3d",
"md5": "8c5664058b9571e548243b511d29b5ed",
"sha256": "0c7397870ad83570c89db584d9a37899aca3516c802dee81f4ae227977cdbb92"
},
"downloads": -1,
"filename": "clirunner-0.0.4-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "8c5664058b9571e548243b511d29b5ed",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": null,
"size": 24457,
"upload_time": "2023-12-02T21:48:12",
"upload_time_iso_8601": "2023-12-02T21:48:12.045910Z",
"url": "https://files.pythonhosted.org/packages/fa/7c/b6ae54411a1663375ec032b813680b66ebc17b041e04a845c62e18ddbb3d/clirunner-0.0.4-py2.py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "d694ae9eec413bf0095b448c53a80f99b6533b937aa85a029aebed05f6135f24",
"md5": "f5db2f84f2ce9e7cf73645e0194b2682",
"sha256": "00a4cd10c71d7ffa3441b092e0962ff426b0f0b26e0c21ea15d4479572ededc0"
},
"downloads": -1,
"filename": "clirunner-0.0.4.tar.gz",
"has_sig": false,
"md5_digest": "f5db2f84f2ce9e7cf73645e0194b2682",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 29682,
"upload_time": "2023-12-02T21:48:13",
"upload_time_iso_8601": "2023-12-02T21:48:13.696440Z",
"url": "https://files.pythonhosted.org/packages/d6/94/ae9eec413bf0095b448c53a80f99b6533b937aa85a029aebed05f6135f24/clirunner-0.0.4.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-12-02 21:48:13",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "RhetTbull",
"github_project": "clirunner",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "clirunner"
}