Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option to generate the boilerplate for a CLI #614

Merged
merged 10 commits into from
Nov 4, 2023
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ jobs:
- run: "poetry run make html"
dir: "docs"
name: "docs"
extra_options:
- label: "no extra options"
value: ""
- label: "with CLI"
value: "--data 'has_cli=yes' --data 'cli_name=mycli'"
runs-on: ubuntu-latest
name: "Generated: ${{ matrix.script.name }}"
name: "Generated: ${{ matrix.script.name }} ${{ matrix.extra_options.label }}"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
Expand All @@ -77,6 +82,7 @@ jobs:
--data 'project_name=My Amazing Project' \
--data 'project_short_description=Just a great project' \
--data 'open_source_license=MIT' \
${{ matrix.extra_options.value }} \
--defaults
shell: bash
- run: cat pyproject.toml
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ Project template for a Python Package using Copier.
## Features

- Project for Python 3.8+.
- Testing with Pytest using Github actions.
- Packaging powered by [poetry]
- Testing with Pytest using GitHub actions.
- Packaging powered by [poetry].
- Optionally generates a CLI entry point powered by [Typer] and [Rich].
- Follows the [black] style guide.
- Uses [Ruff] for linting.
- Comes with [pre-commit] hook config for [black] and [Ruff].
Expand Down Expand Up @@ -138,7 +139,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

[poetry]: https://python-poetry.org/
[poetry]: https://python-poetry.org
[Typer]: https://typer.tiangolo.com
[Rich]: https://rich.readthedocs.io
[black]: https://github.com/psf/black
[Ruff]: https://pypi.org/project/ruff/
[pre-commit]: https://pre-commit.com/
Expand Down
11 changes: 11 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ documentation:
help: "Generate documentation?"
default: yes

has_cli:
type: bool
help: "Does the project have a CLI?"
default: no

cli_name:
type: str
help: "The name of the CLI"
default: "{{ project_slug }}"
when: "{{ has_cli }}"

run_poetry_install:
type: bool
help: "Run poetry install after {{ package_name }} generation?"
Expand Down
9 changes: 9 additions & 0 deletions project/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,22 @@ Install this via pip (or your favourite package manager):
{%- if not documentation %}

## Usage
{%- if has_cli %}

Call the command line interface:

```bash
{{ cli_name }} --help
```
{%- else %}

Start by importing it:

```python
import {{ package_name }}
```
{%- endif %}
{%- endif %}

## Contributors ✨

Expand Down
13 changes: 12 additions & 1 deletion project/docs/usage.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@

# Usage

Assuming that you've followed the {ref}`installations steps <installation>`, you're now ready to use this package. Start by importing it:
Assuming that you've followed the {ref}`installations steps <installation>`, you're now ready to use this package.
{%- if has_cli %}

Call the command line interface:

```bash
{{ cli_name }} --help
```
{%- else %}

Start by importing it:

```python
import {{ package_name }}
```
{%- endif %}

TODO: Document usage
9 changes: 9 additions & 0 deletions project/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,18 @@ packages = [
[tool.poetry.urls]
"Bug Tracker" = "https://github.com/{{ github_username }}/{{ project_slug }}/issues"
"Changelog" = "https://github.com/{{ github_username }}/{{ project_slug }}/blob/main/CHANGELOG.md"
{%- if has_cli %}

[tool.poetry.scripts]
{{ cli_name }} = "{{ package_name }}.cli:app"
{%- endif %}

[tool.poetry.dependencies]
python = "^3.8"
{%- if has_cli %}
rich = ">=10"
typer = {extras = ["all"], version = "^0.9.0"}
{%- endif %}

[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Make the CLI runnable using python -m {{ package_name }}."""
from .cli import app

app(prog_name="{{ cli_name }}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import typer
from rich import print

from .main import add

app = typer.Typer()


@app.command()
def main(n1: int, n2: int) -> None:
"""Add the arguments and print the result."""
print(add(n1, n2))
1 change: 1 addition & 0 deletions project/tests/test_main.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ from {{ package_name }}.main import add


def test_add():
"""Adding two number works as expected."""
assert add(1, 1) == 2
12 changes: 12 additions & 0 deletions project/tests/{% if has_cli %}test_cli.py{% endif %}.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typer.testing import CliRunner

from {{ package_name }}.cli import app

runner = CliRunner()


def test_help():
"""The help message includes the CLI name."""
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Add the arguments and print the result" in result.stdout
13 changes: 13 additions & 0 deletions project/tests/{% if has_cli %}test_dunder_main.py{% endif %}.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import subprocess
import sys


def test_can_run_as_python_module():
"""Run the CLI as a Python module."""
result = subprocess.run(
[sys.executable, "-m", "{{ package_name }}", "--help"], # noqa S603,S607
check=True,
capture_output=True,
)
assert result.returncode == 0
assert b"{{ cli_name }} [OPTIONS]" in result.stdout
45 changes: 45 additions & 0 deletions tests/test_generate_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,48 @@ def test_documentation(
dst_path / "pyproject.toml",
unexpect_strs=["[tool.poetry.group.docs]"],
)


@pytest.mark.parametrize(
("has_cli", "cli_name"),
[
(True, "mycli"),
(False, ""),
],
)
def test_cli(
tmp_path: Path,
base_answers: dict[str, str | bool],
has_cli: bool,
cli_name: str,
):
dst_path = tmp_path / "snake-farm"
copier.run_copy(
src_path=str(PROJECT_ROOT),
dst_path=dst_path,
data={**base_answers, "has_cli": has_cli, "cli_name": cli_name},
defaults=True,
unsafe=True,
)

assert tmp_path.exists()
if has_cli:
_check_file_contents(
dst_path / "src" / "snake_farm" / "__main__.py",
expected_strs=['app(prog_name="mycli")'],
)
_check_file_contents(
dst_path / "src" / "snake_farm" / "cli.py",
expected_strs=["app = typer.Typer()"],
)
_check_file_contents(
dst_path / "pyproject.toml",
expected_strs=["[tool.poetry.scripts]", 'mycli = "snake_farm.cli:app"'],
)
else:
assert not (dst_path / "src" / "snake_farm" / "__main__.py").exists()
assert not (dst_path / "src" / "snake_farm" / "cli.py").exists()
_check_file_contents(
dst_path / "pyproject.toml",
unexpect_strs=["[tool.poetry.scripts]", 'mycli = "snake_farm.cli:app"'],
)