diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e88c94c4..a0980f81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,6 +43,7 @@ repos: args: ["--config=.flake8"] language: python types: [python] + exclude: ^examples/ require_serial: true additional_dependencies: - flake8 @@ -53,6 +54,7 @@ repos: entry: mypy language: python types: [python] + exclude: ^examples/ require_serial: true additional_dependencies: - mypy diff --git a/CHANGELOG.md b/CHANGELOG.md index 503218cb..637ea467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Version 1.2.2.dev0 +- Added initial support for [Typer](https://typer.tiangolo.com/) [[#26](https://github.com/ewels/rich-click/pull/26)] - Mark PEP 561 Compatibility [[#41](https://github.com/ewels/rich-click/pull/41)] - Distribution now available via MacPorts [[#42](https://github.com/ewels/rich-click/pull/42)] - Add typing information [[#39](https://github.com/ewels/rich-click/pull/39)] diff --git a/README.md b/README.md index a4ffd4b0..7e4ae907 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ click, formatted with rich, with minimal customisation required. ![rich-click](https://raw.githubusercontent.com/ewels/rich-click/main/docs/images/command_groups.png) -_Screenshot from [`examples/03_groups_sorting.py`](examples/03_groups_sorting.py)_ +_Screenshot from [`examples/click/03_groups_sorting.py`](examples/click/03_groups_sorting.py)_ ## Installation @@ -55,7 +55,7 @@ import rich_click as click That's it ✨ Then continue to use `click` as you would normally. -> See [`examples/01_simple.py`](examples/01_simple.py) for an example. +> See [`examples/click/01_simple.py`](examples/click/01_simple.py) for an example. The intention is to maintain most / all of the normal click functionality and arguments. If you spot something that breaks or is missing once you start using the plugin, please create an issue about it. @@ -65,7 +65,28 @@ If you spot something that breaks or is missing once you start using the plugin, If you prefer, you can `RichGroup` or `RichCommand` with the `cls` argument in your click usage instead. This means that you can continue to use the unmodified `click` package in parallel. -> See [`examples/02_declarative.py`](examples/02_declarative.py) for an example. +> See [`examples/click/02_declarative.py`](examples/click/02_declarative.py) for an example. + +## Typer support + +[`Typer`](https://github.com/tiangolo/typer) is also supported. +You need to use rich-click with the `typer` [extra](https://packaging.python.org/en/latest/tutorials/installing-packages/#installing-setuptools-extras) in your package requirements: `rich-click[typer]` + +For example, to install locally: + +```bash +python -m pip install rich-click[typer] +``` + +Then just replace your usual `typer` import by: + +```python +import rich_click.typer as typer +``` + +That's it ✨ All the usual `typer` API should be available. + +> See [`examples/typer/`](examples/typer/) for some example scripts. ### Command-line usage @@ -105,7 +126,7 @@ for example: `[dim]\[my-default: foo][\]` ![Rich markup example](https://raw.githubusercontent.com/ewels/rich-click/main/docs/images/rich_markup.png) -> See [`examples/04_rich_markup.py`](examples/04_rich_markup.py) fo +> See [`examples/click/04_rich_markup.py`](examples/click/04_rich_markup.py) fo ### Using Markdown @@ -118,7 +139,7 @@ click.rich_click.USE_MARKDOWN = True ![Markdown example](https://raw.githubusercontent.com/ewels/rich-click/main/docs/images/markdown.png) -> See [`examples/05_markdown.py`](examples/05_markdown.py) fo +> See [`examples/click/05_markdown.py`](examples/click/05_markdown.py) fo ### Positional arguments @@ -135,7 +156,7 @@ click.rich_click.GROUP_ARGUMENTS_OPTIONS = True ![Positional arguments example](https://raw.githubusercontent.com/ewels/rich-click/main/docs/images/arguments.png) -> See [`examples/06_arguments.py`](examples/06_arguments.py) for an example. +> See [`examples/click/06_arguments.py`](examples/click/06_arguments.py) for an example. ### Metavars and option choices @@ -158,7 +179,7 @@ click.rich_click.APPEND_METAVARS_HELP = True ![Appended metavar](https://raw.githubusercontent.com/ewels/rich-click/main/docs/images/metavars_appended.png) -> See [`examples/08_metavars.py`](examples/08_metavars.py) for an example. +> See [`examples/click/08_metavars.py`](examples/click/08_metavars.py) for an example. ### Error messages @@ -169,7 +190,7 @@ By default, rich-click gives some nice formatting to error messages: You can customise the _Try 'command --help' for help._ message with `ERRORS_SUGGESTION` using rich-click though, and add some text after the error with `ERRORS_EPILOGUE`. -For example, from [`examples/07_custom_errors.py`](examples/07_custom_errors.py): +For example, from [`examples/click/07_custom_errors.py`](examples/click/07_custom_errors.py): ```python click.rich_click.STYLE_ERRORS_SUGGESTION = "blue italic" @@ -212,7 +233,7 @@ It accepts a list of options / commands which means you can also choose a custom ![rich-click](https://raw.githubusercontent.com/ewels/rich-click/main/docs/images/command_groups.png) -See [`examples/03_groups_sorting.py`](examples/03_groups_sorting.py) for a full example. +See [`examples/click/03_groups_sorting.py`](examples/click/03_groups_sorting.py) for a full example. ### Options diff --git a/examples/01_simple.py b/examples/click/01_simple.py similarity index 98% rename from examples/01_simple.py rename to examples/click/01_simple.py index 59932b90..5149ecf9 100644 --- a/examples/01_simple.py +++ b/examples/click/01_simple.py @@ -61,7 +61,7 @@ def download(all): \f Also if you want to write function help text that won't be rendered to the terminal. - """ # noqa: D301, D400 + """ print("Downloading") diff --git a/examples/02_declarative.py b/examples/click/02_declarative.py similarity index 100% rename from examples/02_declarative.py rename to examples/click/02_declarative.py diff --git a/examples/03_groups_sorting.py b/examples/click/03_groups_sorting.py similarity index 100% rename from examples/03_groups_sorting.py rename to examples/click/03_groups_sorting.py diff --git a/examples/04_rich_markup.py b/examples/click/04_rich_markup.py similarity index 97% rename from examples/04_rich_markup.py rename to examples/click/04_rich_markup.py index 40e4d30c..47254f15 100644 --- a/examples/04_rich_markup.py +++ b/examples/click/04_rich_markup.py @@ -8,7 +8,7 @@ @click.option( "--input", type=click.Path(), - help="Input [magenta bold]file[/]. [dim]\[default: a custom default][/]", # noqa: W605 + help="Input [magenta bold]file[/]. [dim]\[default: a custom default][/]", ) @click.option( "--type", diff --git a/examples/05_markdown.py b/examples/click/05_markdown.py similarity index 100% rename from examples/05_markdown.py rename to examples/click/05_markdown.py diff --git a/examples/06_arguments.py b/examples/click/06_arguments.py similarity index 100% rename from examples/06_arguments.py rename to examples/click/06_arguments.py diff --git a/examples/07_custom_errors.py b/examples/click/07_custom_errors.py similarity index 100% rename from examples/07_custom_errors.py rename to examples/click/07_custom_errors.py diff --git a/examples/08_metavars.py b/examples/click/08_metavars.py similarity index 100% rename from examples/08_metavars.py rename to examples/click/08_metavars.py diff --git a/examples/typer/01_simple.py b/examples/typer/01_simple.py new file mode 100644 index 00000000..f4a48244 --- /dev/null +++ b/examples/typer/01_simple.py @@ -0,0 +1,10 @@ +import rich_click.typer as typer + + +def main(): + """Launch a CLI that says hello.""" + typer.echo("Hello World") + + +if __name__ == "__main__": + typer.run(main) diff --git a/examples/typer/02_subcommands.py b/examples/typer/02_subcommands.py new file mode 100644 index 00000000..1010ec26 --- /dev/null +++ b/examples/typer/02_subcommands.py @@ -0,0 +1,56 @@ +import rich_click.typer as typer + +app = typer.Typer() + + +@app.callback() +def cli(debug: bool = typer.Option(False, help="Enable debug mode.")): + """ + My amazing tool does all the things. + + This is a minimal example based on documentation + from the 'click' package. + + You can try using --help at the top level and also for + specific group subcommands. + """ + print(f"Debug mode is {'on' if debug else 'off'}") + + +@app.command() +def sync( + type: str = typer.Option("files", help="Type of file to sync"), + all: bool = typer.Option(False, help="Sync all the things?"), +): + """Synchronise all your files between two places.""" + print("Syncing") + + +@app.command() +def download(all: bool = typer.Option(False, help="Get everything")): + r""" + Pretend to download some files from + somewhere. Multi-line help strings are unwrapped + until you use a double newline. + + Only the first paragraph is used in group help texts. + Don't forget you can opt-in to rich and markdown formatting! + + \b + Click escape markers should still work. + * So you + * Can keep + * Your newlines + + And this is a paragraph + that will be rewrapped again. + + \f + Also if you want to write function help text that won't + be rendered to the terminal. + """ + print("Downloading") + + +if __name__ == "__main__": + app() diff --git a/setup.py b/setup.py index 1f2788cc..228b67ef 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,8 @@ "importlib-metadata; python_version < '3.8'", ], extras_require={ - "dev": ["pre-commit"], + "typer": "typer>=0.4", + "dev": "pre-commit", }, package_data={"rich-click": ["py.typed"]}, ) diff --git a/src/rich_click/typer.py b/src/rich_click/typer.py new file mode 100644 index 00000000..db2de1b1 --- /dev/null +++ b/src/rich_click/typer.py @@ -0,0 +1,35 @@ +from typing import Any, Callable + +from typer import * # noqa +from typer import Typer as BaseTyper +from typer.models import CommandFunctionType + +from rich_click import RichCommand, RichGroup + + +class Typer(BaseTyper): + """A custom subclassed version of typer.Typer to allow rich help.""" + + def __init__( + self, + *args, + cls=RichGroup, + **kwargs, + ) -> None: + """Initialise with a RichGroup class as the default.""" + super().__init__(*args, cls=cls, **kwargs) + + def command( + self, + *args, + cls=RichCommand, + **kwargs, + ) -> Callable[[CommandFunctionType], CommandFunctionType]: + return super().command(*args, cls=cls, **kwargs) + + +def run(function: Callable[..., Any]) -> Any: + """Redefine typer.run() to use our custom Typer class.""" # noqa D402 + app = Typer() + app.command()(function) + app()