Skip to content

Commit 6b711df

Browse files
committed
✨ feat: add support for a Typer CLI and strict typing
1 parent f554c2b commit 6b711df

9 files changed

Lines changed: 172 additions & 105 deletions

File tree

copier.yaml

Lines changed: 125 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -27,156 +27,176 @@ project_description:
2727
help: Your project description
2828

2929
project_keywords:
30-
default: "python,{{ project_name | lower }}"
31-
type: str
32-
help: "Provide a list of comma-separated keywords to be using in the Python package overview:"
30+
default: "python,{{ project_name | lower }}"
31+
type: str
32+
help: "Provide a list of comma-separated keywords to be using in the Python package overview:"
3333

3434
author_fullname:
35-
type: str
36-
help: Your full name
37-
default: "{{ 'Bassem Karoui' | git_user_name }}"
35+
type: str
36+
help: Your full name
37+
default: "{{ 'Bassem Karoui' | git_user_name }}"
3838

3939
author_email:
40-
type: str
41-
help: Your email
42-
default: "{{ 'bassem.karoui1@gmail.com' | git_user_email }}"
40+
type: str
41+
help: Your email
42+
default: "{{ 'bassem.karoui1@gmail.com' | git_user_email }}"
4343

4444
repository_provider:
45-
type: str
46-
help: Your repository provider
47-
default: github
48-
choices:
49-
- github
50-
- other
45+
type: str
46+
help: Your repository provider
47+
default: github
48+
choices:
49+
- github
50+
- other
5151

5252
author_github_handle:
53-
when: "{{ repository_provider == 'github' }}"
54-
type: str
55-
help: Your Github handle
56-
default: "{{ author_fullname | lower | replace(' ', '')}}"
53+
when: "{{ repository_provider == 'github' }}"
54+
type: str
55+
help: Your Github handle
56+
default: "{{ author_fullname | lower | replace(' ', '')}}"
5757

5858
homepage:
59-
when: "{{ repository_provider == 'github' }}"
60-
type: str
61-
help: "The project Homepage"
62-
default: "https://{{ author_github_handle }}.github.io/{{ project_name }}"
59+
when: "{{ repository_provider == 'github' }}"
60+
type: str
61+
help: "The project Homepage"
62+
default: "https://{{ author_github_handle }}.github.io/{{ project_name }}"
6363

6464
repository:
65-
when: "{{ repository_provider == 'github' }}"
66-
type: str
67-
help: "The project Repository"
68-
default: "https://github.com/{{ author_github_handle }}/{{ project_name }}"
65+
when: "{{ repository_provider == 'github' }}"
66+
type: str
67+
help: "The project Repository"
68+
default: "https://github.com/{{ author_github_handle }}/{{ project_name }}"
6969

7070
documentation:
71-
when: "{{ repository_provider == 'github' }}"
72-
type: str
73-
help: "The project Documentation"
74-
default: "https://{{ author_github_handle }}.github.io/{{ project_name }}"
71+
when: "{{ repository_provider == 'github' }}"
72+
type: str
73+
help: "The project Documentation"
74+
default: "https://{{ author_github_handle }}.github.io/{{ project_name }}"
7575

7676
copyright_license:
77-
type: str
78-
help: Choose the project's license (e.g. "MIT", "GPL-3.0").
79-
default: MIT License
80-
choices:
81-
- Apache Software License
82-
- BSD 3-clause License
83-
- Boost Software License 1.0 (BSL-1.0)
84-
- GNU General Public License v3 (GPLv3)
85-
- ISC License
86-
- MIT License
87-
- The Unlicense (Unlicense)
77+
type: str
78+
help: Choose the project's license (e.g. "MIT", "GPL-3.0").
79+
default: MIT License
80+
choices:
81+
- Apache Software License
82+
- BSD 3-clause License
83+
- Boost Software License 1.0 (BSL-1.0)
84+
- GNU General Public License v3 (GPLv3)
85+
- ISC License
86+
- MIT License
87+
- The Unlicense (Unlicense)
8888

8989
copyright_holder:
90-
type: str
91-
help: Name or organization holding the copyright
92-
default: "{{ author_fullname }}"
90+
type: str
91+
help: Name or organization holding the copyright
92+
default: "{{ author_fullname }}"
9393

9494
copyright_year:
95-
when: false
96-
type: str
97-
default: "{{ current_year }}"
95+
when: false
96+
type: str
97+
default: "{{ current_year }}"
9898

9999
python_version:
100-
type: str
101-
help: "Choose the default Python version for development, documentation generation, and package build:"
102-
default: "3.12"
103-
choices:
104-
- "3.10"
105-
- "3.11"
106-
- "3.12"
107-
- "3.13"
100+
type: str
101+
help: "Choose the default Python version for development, documentation generation, and package build:"
102+
default: "3.12"
103+
choices:
104+
- "3.10"
105+
- "3.11"
106+
- "3.12"
107+
- "3.13"
108108

109109
min_python_version:
110-
type: str
111-
help: "Choose the minimum Python version the project should support:"
112-
default: "3.10"
113-
choices:
114-
- "3.10"
115-
- "3.11"
116-
- "3.12"
117-
- "3.13"
110+
type: str
111+
help: "Choose the minimum Python version the project should support:"
112+
default: "3.10"
113+
choices:
114+
- "3.10"
115+
- "3.11"
116+
- "3.12"
117+
- "3.13"
118118

119119
py_versions:
120-
default: ["3.10", "3.11", "3.12", "3.13"]
121-
when: false
120+
default: ["3.10", "3.11", "3.12", "3.13"]
121+
when: false
122+
123+
with_typer_cli:
124+
type: bool
125+
help: Does your project need a Typer CLI?
126+
default: false
127+
128+
cli_name:
129+
type: str
130+
help: Set the CLI's name
131+
default: "{{ project_name }}"
132+
validator: >-
133+
{% if not (project_name | regex_search('^[a-z][a-z0-9\-]+$')) %}
134+
project_name must start with a letter, followed by one or more letters,
135+
digits or dashes all lowercase.
136+
{% endif %}
137+
138+
with_strict_typing:
139+
type: bool
140+
help: Do you want to use strict typing?
141+
default: false
122142

123143
tox:
124-
type: bool
125-
default: true
126-
help: Do you want to use Tox?
144+
type: bool
145+
default: true
146+
help: Do you want to use Tox?
127147

128148
coverage_threshold:
129-
type: int
130-
default: 100
131-
help: "Set the threshold for test coverage, ranging from 0 to 100:"
132-
validator: "{% if not 0 <= coverage_threshold <= 100 %}Test Coverage threshold should be between 0 and 100{% endif %}"
149+
type: int
150+
default: 100
151+
help: "Set the threshold for test coverage, ranging from 0 to 100:"
152+
validator: "{% if not 0 <= coverage_threshold <= 100 %}Test Coverage threshold should be between 0 and 100{% endif %}"
133153

134154
with_conventional_commits:
135-
type: bool
136-
help: >-
137-
Do you want to follow the Conventional Commits standard (https://www.conventionalcommits.org)
138-
for your commit messages?
139-
default: true
155+
type: bool
156+
help: >-
157+
Do you want to follow the Conventional Commits standard (https://www.conventionalcommits.org)
158+
for your commit messages?
159+
default: true
140160

141161
cz_gitmoji:
142-
type: bool
143-
help: Do you want to use emojis for your commits?
144-
default: true
145-
when: "{{ with_conventional_commits }}"
162+
type: bool
163+
help: Do you want to use emojis for your commits?
164+
default: true
165+
when: "{{ with_conventional_commits }}"
146166

147167
gpg_sign_tags:
148-
type: bool
149-
default: "{{ false | are_tags_gpg_signed }}"
150-
when: false
168+
type: bool
169+
default: "{{ false | are_tags_gpg_signed }}"
170+
when: false
151171

152172
publish_to_pypi:
153-
type: bool
154-
default: false
155-
help: Do you want to publish a release to PyPI?
173+
type: bool
174+
default: false
175+
help: Do you want to publish a release to PyPI?
156176

157177
dockerfile:
158-
type: bool
159-
default: true
160-
help: Do you need a Dockerfile?
178+
type: bool
179+
default: true
180+
help: Do you need a Dockerfile?
161181

162182
privileged_container:
163-
type: bool
164-
when: "{{ dockerfile }}"
165-
default: false
166-
help: Do you want your container to have root access?
183+
type: bool
184+
when: "{{ dockerfile }}"
185+
default: false
186+
help: Do you want your container to have root access?
167187

168188
uid:
169-
type: int
170-
when: false
171-
default: "{{ uid }}"
189+
type: int
190+
when: false
191+
default: "{{ uid }}"
172192

173193
gid:
174-
type: int
175-
when: false
176-
default: "{{ gid }}"
194+
type: int
195+
when: false
196+
default: "{{ gid }}"
177197

178198
gpus:
179-
type: bool
180-
when: "{{ dockerfile }}"
181-
help: Do you need GPU capabilities for you containers?
182-
default: false
199+
type: bool
200+
when: "{{ dockerfile }}"
201+
help: Do you need GPU capabilities for you containers?
202+
default: false

template/pyproject.toml.jinja

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ Repository = "{{ repository }}"
3737
Documentation = "{{ documentation }}"
3838
{%- endif %}
3939
40+
41+
{% if with_typer_cli -%}
42+
[project.scripts]
43+
{{ cli_name }} = "{{ project_slug }}.cli.main:app"
44+
{%- endif %}
45+
4046
[dependency-groups]
4147
dev = [
4248
"pytest>=8.3.5",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from {{ project_slug }}.__version__ import __version__

template/src/{{project_slug}}/{% if with_strict_typing %}py.typed{% endif %}

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from {{ project_slug }}.cli.main import app
2+
3+
app(prog_name="{{ package_name }}")

template/src/{{project_slug}}/{% if with_typer_cli %}cli{% endif %}/__init__.py

Whitespace-only changes.

template/src/{{project_slug}}/{% if with_typer_cli %}cli{% endif %}/commands/__init__.py

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from rich import print as rprint
2+
from typer import Typer
3+
4+
from {{ project_slug }} import __version__
5+
6+
app = typer.Typer()
7+
8+
@app.command(rich_help_panel="Help and Others", help="Show the CLI's version.")
9+
def version() -> None:
10+
rprint(f"{{ cli_name }} Version: [green]{__version__}[/green]")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import Annotated
2+
3+
from rich import print as rprint
4+
from typer import Typer
5+
6+
from {{ project_slug }} import __version__
7+
from {{ project_slug }}.cli.commands import version
8+
9+
app = typer.Typer(
10+
help="{{ package_name }} CLI.",
11+
no_args_is_help=True,
12+
rich_markup_mode="rich",
13+
# epilog="Made with [red]:heart:[/red] by [bold]Bassem Karoui[/bold]",
14+
# pretty_exceptions_show_locals=False,
15+
)
16+
17+
18+
@app.callback(invoke_without_command=True)
19+
def callback(
20+
version: Annotated[bool, typer.Option("--version", help="Show the CLI's version.")] = False,
21+
) -> None:
22+
if version:
23+
rprint(f"{{ cli_name }} Version: [green]{__version__}[/green]")
24+
raise typer.Exit()
25+
26+
27+
app.add_typer(version.app, help="Show the CLI's version.")

0 commit comments

Comments
 (0)