afterpython 0.0.1.dev2__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. afterpython/__init__.py +13 -0
  2. afterpython/_io/toml.py +105 -0
  3. afterpython/_io/yaml.py +26 -0
  4. afterpython/_paths.py +39 -0
  5. afterpython/_typing.py +4 -0
  6. afterpython/builders/__init__.py +17 -0
  7. afterpython/builders/content_json.py +131 -0
  8. afterpython/builders/index_md.py +94 -0
  9. afterpython/builders/jupyter_notebook.py +293 -0
  10. afterpython/builders/markdown.py +271 -0
  11. afterpython/builders/metadata.py +50 -0
  12. afterpython/cli/__init__.py +3 -0
  13. afterpython/cli/commands/add.py +67 -0
  14. afterpython/cli/commands/build.py +260 -0
  15. afterpython/cli/commands/bump.py +118 -0
  16. afterpython/cli/commands/check.py +36 -0
  17. afterpython/cli/commands/clean.py +85 -0
  18. afterpython/cli/commands/commit.py +52 -0
  19. afterpython/cli/commands/commitizen.py +32 -0
  20. afterpython/cli/commands/dev.py +213 -0
  21. afterpython/cli/commands/format.py +28 -0
  22. afterpython/cli/commands/init.py +107 -0
  23. afterpython/cli/commands/init_branch_rules.py +9 -0
  24. afterpython/cli/commands/install.py +28 -0
  25. afterpython/cli/commands/lock.py +23 -0
  26. afterpython/cli/commands/pre_commit.py +66 -0
  27. afterpython/cli/commands/preview.py +22 -0
  28. afterpython/cli/commands/release.py +67 -0
  29. afterpython/cli/commands/remove.py +67 -0
  30. afterpython/cli/commands/start.py +144 -0
  31. afterpython/cli/commands/sync.py +152 -0
  32. afterpython/cli/commands/update.py +176 -0
  33. afterpython/cli/main.py +132 -0
  34. afterpython/const.py +4 -0
  35. afterpython/main.py +25 -0
  36. afterpython/pcu.py +166 -0
  37. afterpython/templates/ci-workflow-template.yml +197 -0
  38. afterpython/templates/cz-template.toml +7 -0
  39. afterpython/templates/dependabot-template.yml +30 -0
  40. afterpython/templates/deploy-workflow-template.yml +88 -0
  41. afterpython/templates/pre-commit-config-template.yaml +48 -0
  42. afterpython/templates/release-workflow-template.yml +56 -0
  43. afterpython/templates/ruff-template.toml +32 -0
  44. afterpython/tools/_afterpython.py +128 -0
  45. afterpython/tools/_git.py +137 -0
  46. afterpython/tools/branch_rules.py +106 -0
  47. afterpython/tools/commitizen.py +13 -0
  48. afterpython/tools/github_actions.py +48 -0
  49. afterpython/tools/myst.py +208 -0
  50. afterpython/tools/pre_commit.py +37 -0
  51. afterpython/tools/pyproject.py +110 -0
  52. afterpython/utils.py +299 -0
  53. afterpython/version.py +39 -0
  54. afterpython-0.1.0.dist-info/METADATA +131 -0
  55. afterpython-0.1.0.dist-info/RECORD +57 -0
  56. afterpython-0.1.0.dist-info/WHEEL +4 -0
  57. afterpython-0.1.0.dist-info/entry_points.txt +4 -0
  58. afterpython/README.py +0 -158
  59. afterpython/images/favicon.ico +0 -0
  60. afterpython/images/logo-trace.svg +0 -3
  61. afterpython/images/logo.png +0 -0
  62. afterpython/images/logo.svg +0 -2
  63. afterpython-0.0.1.dev2.dist-info/LICENSE +0 -201
  64. afterpython-0.0.1.dev2.dist-info/METADATA +0 -20
  65. afterpython-0.0.1.dev2.dist-info/RECORD +0 -9
  66. afterpython-0.0.1.dev2.dist-info/WHEEL +0 -4
@@ -0,0 +1,260 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from pathlib import Path
8
+
9
+ from afterpython._typing import NodeEnv
10
+
11
+ import shutil
12
+ import subprocess
13
+
14
+ import click
15
+ from click.exceptions import Exit
16
+
17
+ import afterpython as ap
18
+ from afterpython.builders import (
19
+ build_content_json,
20
+ build_jupyter_notebooks,
21
+ build_markdown,
22
+ build_metadata,
23
+ create_placeholder_index_md_files,
24
+ delete_placeholder_index_md_files,
25
+ )
26
+ from afterpython.const import CONTENT_TYPES
27
+
28
+
29
+ def determine_base_path() -> str:
30
+ """Determine BASE_PATH based on website URL configuration.
31
+
32
+ Returns:
33
+ Empty string if custom domain is configured (no github.io in URL)
34
+ /repo-name if using default GitHub Pages URL (contains github.io)
35
+ """
36
+ from afterpython._io.toml import _from_tomlkit
37
+ from afterpython.tools._afterpython import read_afterpython
38
+ from afterpython.tools.pyproject import read_metadata
39
+
40
+ # Read website URL from afterpython.toml
41
+ afterpython = read_afterpython()
42
+ website_url = str(_from_tomlkit(afterpython.get("website", {})).get("url", ""))
43
+
44
+ # If custom domain (no github.io), no BASE_PATH needed
45
+ if "github.io" not in website_url:
46
+ return ""
47
+
48
+ # For GitHub Pages default URL, extract repo name from repository URL
49
+ pyproject = read_metadata()
50
+ github_url = str(pyproject.urls.get("repository", ""))
51
+
52
+ if not github_url:
53
+ click.echo(
54
+ "Warning: No repository URL found in pyproject.toml, using empty BASE_PATH"
55
+ )
56
+ return ""
57
+
58
+ # Extract repo name from URL like "https://github.com/AfterPythonOrg/afterpython"
59
+ # -> "afterpython"
60
+ repo_name = github_url.rstrip("/").split("/")[-1]
61
+ return f"/{repo_name}"
62
+
63
+
64
+ def prebuild():
65
+ def _check_initialized():
66
+ # Check if 'ap init' has been run
67
+ afterpython_toml = ap.paths.afterpython_path / "afterpython.toml"
68
+ if not afterpython_toml.exists():
69
+ raise click.ClickException(
70
+ "AfterPython is not initialized!\n"
71
+ "Run 'ap init' first to set up your project."
72
+ )
73
+
74
+ def _clean_up_builds():
75
+ click.echo("Cleaning up builds...")
76
+ build_path = ap.paths.build_path
77
+ website_build_path = ap.paths.website_path / "build"
78
+ # clean up content builds in _website/static/{content_type}
79
+ website_static_path = ap.paths.website_path / "static"
80
+ content_build_paths = [
81
+ website_static_path / content_type for content_type in CONTENT_TYPES
82
+ ]
83
+ for path in [build_path, website_build_path, *content_build_paths]:
84
+ if path.exists():
85
+ shutil.rmtree(path)
86
+ build_path.mkdir(parents=True, exist_ok=True)
87
+
88
+ _check_initialized()
89
+ _clean_up_builds()
90
+
91
+ delete_placeholder_index_md_files()
92
+ create_placeholder_index_md_files()
93
+ build_metadata()
94
+ build_markdown()
95
+ build_jupyter_notebooks()
96
+
97
+
98
+ def postbuild(dev_build: bool = False):
99
+ def _move_files(
100
+ source: Path,
101
+ destination: Path,
102
+ is_copy: bool = True,
103
+ ignore_copy: Callable[[str, list[str]], set[str]] | None = None,
104
+ ):
105
+ """
106
+ Move or copy files from source to destination.
107
+
108
+ Args:
109
+ source: Source path
110
+ destination: Destination path
111
+ is_copy: If True, copy files (merge with existing). If False, move files (replace destination)
112
+ ignore_copy: Optional ignore function for copytree, takes (directory, contents) and returns set of names to ignore
113
+ """
114
+ if not source.exists():
115
+ return
116
+ if is_copy:
117
+ shutil.copytree(source, destination, dirs_exist_ok=True, ignore=ignore_copy)
118
+ print(f"Copied: {source} to {destination}")
119
+ else:
120
+ # Remove destination if it exists (move will fail otherwise)
121
+ if destination.exists():
122
+ shutil.rmtree(destination)
123
+ shutil.move(str(source), str(destination))
124
+ print(f"Moved: {source} to {destination}")
125
+
126
+ build_content_json()
127
+ delete_placeholder_index_md_files()
128
+
129
+ website_static = ap.paths.website_path / "static"
130
+ website_static.mkdir(parents=True, exist_ok=True)
131
+
132
+ for content_type in CONTENT_TYPES:
133
+ myst_build = ap.paths.afterpython_path / content_type / "_build"
134
+ afterpython_content_build = ap.paths.build_path / content_type
135
+ # Move myst builds from afterpython/{content_type}/_build to afterpython/_build/{content_type}
136
+ _move_files(myst_build, afterpython_content_build, is_copy=True)
137
+ # for production build, copy afterpython/_build/{content_type}/html to afterpython/_website/static/{content_type}
138
+ if not dev_build:
139
+ _move_files(
140
+ afterpython_content_build / "html",
141
+ website_static / content_type,
142
+ is_copy=True,
143
+ )
144
+ # for dev build, copy the files (mainly images, e.g. thumbnails) in site/public/ to afterpython/_website/static/{content_type}/build
145
+ else:
146
+ _move_files(
147
+ afterpython_content_build / "site" / "public",
148
+ # add build/ to mimic the structure of the production build
149
+ website_static / content_type / "build",
150
+ is_copy=True,
151
+ )
152
+
153
+ # Copy all files (e.g. metadata.json, blog.json etc.) from afterpython/_build to afterpython/_website/static/
154
+ _move_files(
155
+ ap.paths.build_path,
156
+ website_static,
157
+ is_copy=True,
158
+ # ignore all content builds e.g. doc/, blog/, only their html/ files will be copied
159
+ ignore_copy=lambda dir, contents: [
160
+ name for name in contents if name in CONTENT_TYPES
161
+ ],
162
+ )
163
+
164
+ # Copy all static files from afterpython/static/ to afterpython/_website/static/
165
+ _move_files(ap.paths.static_path, website_static, is_copy=True)
166
+
167
+
168
+ @click.command(
169
+ add_help_option=False, # disable click's --help option so that ap build --help can work
170
+ context_settings=dict(
171
+ ignore_unknown_options=True,
172
+ allow_extra_args=True,
173
+ ),
174
+ )
175
+ @click.pass_context
176
+ @click.option(
177
+ "--execute", is_flag=True, help="Execute Jupyter notebooks for all content types"
178
+ )
179
+ def build(ctx: click.Context, execute: bool):
180
+ """Build the project website and all contents for production.
181
+
182
+ This command builds MyST content (doc/blog/tutorial/example/guide) and the SvelteKit website.
183
+
184
+ Any extra arguments are passed to the 'myst build --html' command for each content type.
185
+ See "myst build --help" for more details.
186
+
187
+ Use --execute to execute Jupyter notebooks for all content types.
188
+ """
189
+ from afterpython.utils import (
190
+ find_node_env,
191
+ handle_passthrough_help,
192
+ has_content_for_myst,
193
+ )
194
+
195
+ # Show both our options and myst's help and exit
196
+ handle_passthrough_help(
197
+ ctx,
198
+ ["myst", "build"],
199
+ show_underlying=True,
200
+ )
201
+
202
+ paths = ctx.obj["paths"]
203
+ prebuild()
204
+
205
+ # Determine BASE_PATH based on website URL configuration
206
+ base_path = determine_base_path()
207
+ if base_path:
208
+ click.echo(f"Using BASE_PATH: {base_path}")
209
+ else:
210
+ click.echo("Using BASE_PATH: (empty - custom domain)")
211
+ node_env: NodeEnv = find_node_env()
212
+
213
+ # myst's production build
214
+ for content_type in CONTENT_TYPES:
215
+ content_path = ap.paths.afterpython_path / content_type
216
+ myst_yml_path = content_path / "myst.yml"
217
+
218
+ if not content_path.exists():
219
+ click.echo(f"{content_path} does not exist, skip building {content_type}/")
220
+ continue
221
+
222
+ if not myst_yml_path.exists():
223
+ click.echo(
224
+ f"No myst.yml found in {content_path}, skip building {content_type}/"
225
+ )
226
+ continue
227
+
228
+ if not has_content_for_myst(content_path):
229
+ click.echo(f"Skipping {content_type}/ (no content files found)")
230
+ continue
231
+
232
+ click.echo(f"Building {content_type}/...")
233
+ # NOTE: needs to set BASE_URL (used by MyST) so that the project website can link to the content pages correctly at e.g. localhost:5173/doc
234
+ base_url = f"{base_path}/{content_type}"
235
+ build_env = {**node_env, "BASE_URL": base_url, "BASE_PATH": base_path}
236
+ result = subprocess.run(
237
+ [
238
+ "myst",
239
+ "build",
240
+ "--html",
241
+ *(["--execute"] if execute else []),
242
+ *ctx.args,
243
+ ],
244
+ cwd=content_path,
245
+ env=build_env,
246
+ check=False,
247
+ )
248
+
249
+ if result.returncode != 0:
250
+ raise Exit(result.returncode)
251
+
252
+ postbuild(dev_build=False)
253
+
254
+ click.echo("Building project website...")
255
+ website_env = {**node_env, "BASE_PATH": base_path}
256
+ result = subprocess.run(
257
+ ["pnpm", "build"], cwd=paths.website_path, env=website_env, check=False
258
+ )
259
+ if result.returncode != 0:
260
+ raise Exit(result.returncode)
@@ -0,0 +1,118 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+ from afterpython.utils import has_pixi
7
+
8
+
9
+ @click.command(
10
+ add_help_option=False, # disable click's --help option so that cz --help can work
11
+ context_settings=dict(
12
+ ignore_unknown_options=True,
13
+ allow_extra_args=True,
14
+ ),
15
+ )
16
+ @click.pass_context
17
+ @click.option(
18
+ "--pre",
19
+ is_flag=True,
20
+ help="bump to pre-release version (e.g., dev3 -> rc0, a1 -> a2)",
21
+ )
22
+ @click.option(
23
+ "--release", is_flag=True, help="bump to stable release (uses cz bump default)"
24
+ )
25
+ def bump(ctx, release: bool, pre: bool):
26
+ """Bump project version with granular control.
27
+
28
+ By default, stays within current release phase:
29
+ - Dev releases (e.g., 0.1.0.dev3) -> increment dev number (0.1.0.dev4)
30
+ - Pre-releases (e.g., 0.1.0a1) -> increment pre-release (0.1.0a2)
31
+
32
+ Use --pre to transition from dev to pre-release, or pre-release to next pre-release.
33
+ Use --release to bump to next stable version (cz bump default behavior).
34
+ """
35
+ from afterpython.tools.pyproject import read_metadata
36
+ from afterpython.utils import handle_passthrough_help
37
+
38
+ # Show both our options and commitizen's help and exit
39
+ handle_passthrough_help(
40
+ ctx,
41
+ ["cz", "bump"],
42
+ show_underlying=True,
43
+ )
44
+
45
+ if release and pre:
46
+ raise click.ClickException("Only one of --release or --pre can be specified")
47
+
48
+ # bump using cz bump's default behavior
49
+ if release:
50
+ subprocess.run(["ap", "cz", "bump", *ctx.args])
51
+
52
+ # Skip tag push if this is a dry run
53
+ if "--dry-run" in ctx.args:
54
+ return
55
+
56
+ # Get the new version after bump
57
+ metadata = read_metadata()
58
+ new_version = metadata.version
59
+ if new_version is None:
60
+ raise click.ClickException("Unable to read version after bump")
61
+
62
+ tag = f"v{new_version}"
63
+
64
+ # Auto-push the specific tag to trigger release workflow
65
+ click.echo(f"\n🚀 Pushing tag {tag} to trigger release workflow...")
66
+ subprocess.run(["git", "push", "origin", tag])
67
+ else:
68
+ metadata = read_metadata()
69
+ version = metadata.version
70
+
71
+ if version is None:
72
+ raise click.ClickException("Unable to read version from pyproject.toml")
73
+
74
+ args = ctx.args # default: pass through all extra args
75
+
76
+ # by default, bump dev-release version
77
+ if version.is_devrelease and not pre:
78
+ devrelease_number = int(version.dev)
79
+ if "--devrelease" not in ctx.args:
80
+ args = ["--devrelease", str(devrelease_number + 1), *ctx.args]
81
+ # exclude e.g. 0.1.0.dev1 which is both dev-release and pre-release (with version.pre = None)
82
+ elif version.is_prerelease:
83
+ prerelease_type = version.pre[0] if version.pre is not None else "rc"
84
+ if prerelease_type == "a":
85
+ prerelease_type = "alpha"
86
+ elif prerelease_type == "b":
87
+ prerelease_type = "beta"
88
+ if "--prerelease" not in ctx.args:
89
+ args = ["--prerelease", prerelease_type, *ctx.args]
90
+
91
+ subprocess.run(["ap", "cz", "bump", *args])
92
+
93
+ # Update pixi.lock after version bump (editable install requires lock refresh)
94
+ if has_pixi():
95
+ result = subprocess.run(["pixi", "install"], check=False)
96
+ if result.returncode != 0:
97
+ raise Exit(result.returncode)
98
+
99
+ # Check if pixi.lock was modified
100
+ status = subprocess.run(
101
+ ["git", "diff", "--name-only", "pixi.lock"],
102
+ capture_output=True,
103
+ text=True,
104
+ check=False,
105
+ )
106
+ if status.stdout.strip():
107
+ # Stage and commit pixi.lock
108
+ subprocess.run(["git", "add", "pixi.lock"], check=True)
109
+ subprocess.run(
110
+ [
111
+ "git",
112
+ "commit",
113
+ "-m",
114
+ "build: update pixi.lock after version bump",
115
+ ],
116
+ check=True,
117
+ )
118
+ click.echo("✓ Committed pixi.lock update")
@@ -0,0 +1,36 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+
7
+ @click.command(
8
+ add_help_option=False, # disable click's --help option so that ruff check --help can work
9
+ context_settings=dict(
10
+ ignore_unknown_options=True,
11
+ allow_extra_args=True,
12
+ ),
13
+ )
14
+ @click.pass_context
15
+ def check(ctx):
16
+ """Run ruff linter (uses afterpython/ruff.toml if available)"""
17
+ from afterpython.utils import handle_passthrough_help
18
+
19
+ # Show both our options and ruff's help and exit
20
+ handle_passthrough_help(
21
+ ctx,
22
+ ["ruff", "check"],
23
+ show_underlying=True,
24
+ )
25
+
26
+ paths = ctx.obj["paths"]
27
+ ruff_toml = paths.afterpython_path / "ruff.toml"
28
+ if ruff_toml.exists():
29
+ click.echo(f"Using ruff configuration from {ruff_toml}")
30
+ result = subprocess.run(
31
+ ["ruff", "check", "--config", str(ruff_toml), *ctx.args], check=False
32
+ )
33
+ else:
34
+ result = subprocess.run(["ruff", "check", *ctx.args], check=False)
35
+ if result.returncode != 0:
36
+ raise Exit(result.returncode)
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from afterpython._typing import NodeEnv
7
+
8
+ import shutil
9
+ import subprocess
10
+
11
+ import click
12
+ from click.exceptions import Exit
13
+
14
+ from afterpython.const import CONTENT_TYPES
15
+ from afterpython.utils import find_node_env, has_content_for_myst
16
+
17
+
18
+ @click.command(
19
+ add_help_option=False, # disable click's --help option so that ap clean --help can work
20
+ context_settings=dict(
21
+ ignore_unknown_options=True,
22
+ allow_extra_args=True,
23
+ ),
24
+ )
25
+ @click.pass_context
26
+ @click.option(
27
+ "--all",
28
+ is_flag=True,
29
+ help="Also clean AfterPython build directories (_build/ and _website/build/)",
30
+ )
31
+ def clean(ctx, all: bool):
32
+ """Clean build artifacts from content types and website"""
33
+ from afterpython.utils import handle_passthrough_help
34
+
35
+ # Show both our options and myst's help and exit
36
+ handle_passthrough_help(
37
+ ctx,
38
+ ["myst", "clean"],
39
+ show_underlying=True,
40
+ )
41
+
42
+ paths = ctx.obj["paths"]
43
+ node_env: NodeEnv = find_node_env()
44
+
45
+ # Clean MyST builds for each content type
46
+ for content_type in CONTENT_TYPES:
47
+ content_path = paths.afterpython_path / content_type
48
+
49
+ # Skip if no content exists
50
+ if not has_content_for_myst(content_path):
51
+ click.echo(f"Skipping {content_type}/ (no content found)")
52
+ continue
53
+
54
+ click.echo(f"Cleaning {content_type}/...")
55
+ # Pass through any extra args to myst clean (e.g., --cache, --templates)
56
+ result = subprocess.run(
57
+ ["myst", "clean", *ctx.args, *(["--all"] if all else [])],
58
+ cwd=content_path,
59
+ env=node_env,
60
+ check=False,
61
+ )
62
+ if result.returncode != 0:
63
+ raise Exit(result.returncode)
64
+
65
+ # Clean afterpython's build directories if --all flag is used
66
+ if all:
67
+ click.echo("Cleaning afterpython build directories...")
68
+
69
+ # Clean _build/
70
+ build_path = paths.build_path
71
+ if build_path.exists():
72
+ shutil.rmtree(build_path)
73
+ click.echo(f" Removed: {build_path}")
74
+
75
+ # Clean _website/build/
76
+ website_build = paths.website_path / "build"
77
+ if website_build.exists():
78
+ shutil.rmtree(website_build)
79
+ click.echo(f" Removed: {website_build}")
80
+
81
+ click.echo("Done cleaning all build directories!")
82
+ else:
83
+ click.echo(
84
+ "Done! Use 'ap clean --all' to also remove afterpython build directories."
85
+ )
@@ -0,0 +1,52 @@
1
+ import os
2
+ import subprocess
3
+
4
+ import click
5
+
6
+
7
+ @click.command(
8
+ add_help_option=False, # disable click's --help option so that cz --help can work
9
+ context_settings=dict(
10
+ ignore_unknown_options=True,
11
+ allow_extra_args=True,
12
+ ),
13
+ )
14
+ @click.pass_context
15
+ @click.option(
16
+ "--no-cz",
17
+ "--no-commitizen",
18
+ is_flag=True,
19
+ help="Skip 'commitizen' and 'commitizen-branch' pre-commit hooks",
20
+ )
21
+ @click.option(
22
+ "--no-pc",
23
+ "--no-pre-commit",
24
+ is_flag=True,
25
+ help="Skip running pre-commit checks before commit",
26
+ )
27
+ def commit(ctx, no_cz: bool, no_pc: bool):
28
+ """Create a conventional commit with interactive prompts"""
29
+ from afterpython.utils import handle_passthrough_help
30
+
31
+ # Show both our options and commitizen's help and exit
32
+ handle_passthrough_help(
33
+ ctx,
34
+ ["cz", "commit"],
35
+ show_underlying=True,
36
+ )
37
+
38
+ # Run pre-commit checks first (unless --no-pc is set)
39
+ if not no_pc:
40
+ result = subprocess.run(["ap", "pc", "run"])
41
+ if result.returncode != 0:
42
+ click.echo(
43
+ "❌ Pre-commit checks failed. Please fix the issues and try again."
44
+ )
45
+ ctx.exit(1)
46
+
47
+ if not no_cz:
48
+ subprocess.run(["ap", "cz", "commit", *ctx.args])
49
+ else:
50
+ env = os.environ.copy()
51
+ env["SKIP"] = "commitizen,commitizen-branch"
52
+ subprocess.run(["git", "commit", *ctx.args], env=env)
@@ -0,0 +1,32 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+
7
+ @click.command(
8
+ add_help_option=False, # disable click's --help option so that cz --help can work
9
+ context_settings=dict(
10
+ ignore_unknown_options=True,
11
+ allow_extra_args=True,
12
+ ),
13
+ )
14
+ @click.pass_context
15
+ def commitizen(ctx):
16
+ """Run commitizen CLI (uses afterpython/cz.toml)"""
17
+ from afterpython.utils import handle_passthrough_help
18
+
19
+ # Show both our options and commitizen's help and exit
20
+ handle_passthrough_help(
21
+ ctx,
22
+ ["cz"],
23
+ show_underlying=True,
24
+ )
25
+
26
+ paths = ctx.obj["paths"]
27
+ cz_toml_path = paths.afterpython_path / "cz.toml"
28
+ result = subprocess.run(
29
+ ["cz", "--config", str(cz_toml_path), *ctx.args], check=False
30
+ )
31
+ if result.returncode != 0:
32
+ raise Exit(result.returncode)