Skip to content

Commit 76a4bcd

Browse files
authored
Merge pull request #42 from mpkocher/improve-docs
Improve docs for testing and improve mypy typing support
2 parents ff8755a + 5ced745 commit 76a4bcd

File tree

5 files changed

+120
-5
lines changed

5 files changed

+120
-5
lines changed

README.md

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,9 +590,121 @@ See [shtab](https://github.com/iterative/shtab) for more details.
590590

591591
Note, that due to the (typically) global zsh completions directory, this can create some friction points with different virtual (or conda) ENVS with the same executable name.
592592

593+
# General Suggested Testing Model
594+
595+
At a high level, `pydantic_cli` is (hopefully) a thin bridge between your `Options` defined as a Pydantic model and your
596+
main `runner(opts: Options)` func that has hooks into the startup, shutdown and error handling of the command line tool.
597+
It also supports loading config files defined as JSON. By design, `pydantic_cli` explicitly doesn't expose, or leak the argparse instance
598+
because it would add too much surface area and it would enable users' to start mucking with the argparse instance in all kinds of unexpected ways.
599+
The use of `argparse` internally is an hidden implementation detail.
600+
601+
Testing can be done by leveraging the `to_runner` interface.
602+
603+
604+
605+
1. It's recommend trying to do the majority of testing via unit tests (independent of `pydantic_cli`) with your main function and different instances of your pydantic data model.
606+
2. Once this test coverage is reasonable, it can be useful to add a few smoke tests at the integration level leveraging `to_runner` to make sure the tool is functional. Any bugs at this level are probably at the `pydantic_cli` level, not your library code.
607+
608+
Note, that `to_runner(Opts, my_main)` returns a `Callable[[List[str]], int]` that can be used with `argv` to return an integer exit code of your program. The `to_runner` layer will also catch any exceptions.
609+
610+
```python
611+
import unittest
612+
613+
from pydantic import BaseModel
614+
from pydantic_cli import to_runner
615+
616+
617+
class Options(BaseModel):
618+
alpha: int
619+
620+
621+
def main(opts: Options) -> int:
622+
if opts.alpha < 0:
623+
raise Exception(f"Got options {opts}. Forced raise for testing.")
624+
return 0
625+
626+
627+
class TestExample(unittest.TestCase):
628+
629+
def test_core(self):
630+
# Note, this has nothing to do with pydantic_cli
631+
# If possible, this is where the bulk of the testing should be
632+
self.assertEqual(0, main(Options(alpha=1)))
633+
634+
def test_example(self):
635+
f = to_runner(Options, main)
636+
self.assertEqual(0, f(["--alpha", "100"]))
637+
638+
def test_expected_error(self):
639+
f = to_runner(Options, main)
640+
self.assertEqual(1, f(["--alpha", "-10"]))
641+
```
642+
643+
644+
645+
For more scrappy, interactive local development, it can be useful to add `ipdb` or `pdb` and create a custom `exception_handler`.
646+
647+
```python
648+
import sys
649+
from pydantic import BaseModel
650+
from pydantic_cli import default_exception_handler, run_and_exit
651+
652+
653+
class Options(BaseModel):
654+
alpha: int
655+
656+
657+
def exception_handler(ex: BaseException) -> int:
658+
exit_code = default_exception_handler(ex)
659+
import ipdb; ipdb.set_trace()
660+
return exit_code
661+
662+
663+
def main(opts: Options) -> int:
664+
if opts.alpha < 0:
665+
raise Exception(f"Got options {opts}. Forced raise for testing.")
666+
return 0
667+
668+
669+
if __name__ == "__main__":
670+
run_and_exit(Options, main, exception_handler=exception_handler)(sys.argv[1:])
671+
```
672+
673+
Alternatively, wrap your main function to call `ipdb`.
674+
675+
```python
676+
import sys
677+
678+
from pydantic import BaseModel
679+
from pydantic_cli import run_and_exit
680+
681+
682+
class Options(BaseModel):
683+
alpha: int
684+
685+
686+
def main(opts: Options) -> int:
687+
if opts.alpha < 0:
688+
raise Exception(f"Got options {opts}. Forced raise for testing.")
689+
return 0
690+
691+
692+
def main_with_ipd(opts: Options) -> int:
693+
import ipdb; ipdb.set_trace()
694+
return main(opts)
695+
696+
697+
if __name__ == "__main__":
698+
run_and_exit(Options, main_with_ipd)([sys.argv[1:]])
699+
```
700+
701+
The core design choice in `pydantic_cli` is leveraging composable functions `f(g(x))` style providing a straight-forward mechanism to plug into.
702+
593703
# More Examples
594704

595-
[More examples are provided here](https://github.com/mpkocher/pydantic-cli/tree/master/pydantic_cli/examples)
705+
[More examples are provided here](https://github.com/mpkocher/pydantic-cli/tree/master/pydantic_cli/examples) and [Testing Examples can be seen here](https://github.com/mpkocher/pydantic-cli/tree/master/pydantic_cli/tests).
706+
707+
The [TestHarness](https://github.com/mpkocher/pydantic-cli/blob/master/pydantic_cli/tests/__init__.py) might provide examples of how to test your CLI tool(s)
596708

597709
# Limitations
598710

pydantic_cli/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.1.0"
1+
__version__ = "4.2.0"

pydantic_cli/py.typed

Whitespace-only changes.

pydantic_cli/tests/test_examples_simple_with_shell_autocomplete_support.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import unittest
2+
13
from pydantic_cli.examples.simple_with_shell_autocomplete_support import (
24
Options,
35
example_runner,
@@ -19,12 +21,12 @@ def test_simple_02(self):
1921
def _test_auto_complete_shell(self, shell_id):
2022
if HAS_AUTOCOMPLETE_SUPPORT:
2123
args = ["--emit-completion", shell_id]
22-
else:
23-
args = ["-i", "/path/to/file.txt", "-f", "1.0", "2"]
24-
self.run_config(args)
24+
self.run_config(args)
2525

26+
@unittest.skipIf(not HAS_AUTOCOMPLETE_SUPPORT, "shtab not installed")
2627
def test_auto_complete_zsh(self):
2728
self._test_auto_complete_shell("zsh")
2829

30+
@unittest.skipIf(not HAS_AUTOCOMPLETE_SUPPORT, "shtab not installed")
2931
def test_auto_complete_bash(self):
3032
self._test_auto_complete_shell("bash")

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def get_version():
4242
python_requires=">=3.7",
4343
install_requires=_get_requirements("REQUIREMENTS.txt"),
4444
packages=['pydantic_cli', 'pydantic_cli.examples'],
45+
package_data={"pydantic_cli": ["py.typed"]},
4546
tests_require=_get_requirements("REQUIREMENTS-TEST.txt"),
4647
extras_require={"shtab": "shtab>=1.3.1"},
4748
zip_safe=False,

0 commit comments

Comments
 (0)