afterpython 0.3.18__py3-none-any.whl → 0.4.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 (36) hide show
  1. afterpython/_typing.py +2 -2
  2. afterpython/builders/marimo_notebook.py +7 -0
  3. afterpython/cli/commands/build.py +3 -3
  4. afterpython/cli/commands/bump.py +299 -84
  5. afterpython/cli/commands/changelog.py +45 -0
  6. afterpython/cli/commands/check.py +20 -13
  7. afterpython/cli/commands/commit_msg.py +115 -0
  8. afterpython/cli/commands/init.py +197 -14
  9. afterpython/cli/commands/{commitizen.py → lint.py} +7 -10
  10. afterpython/cli/commands/{pre_commit.py → prek.py} +8 -8
  11. afterpython/cli/commands/release.py +39 -10
  12. afterpython/cli/commands/start.py +6 -4
  13. afterpython/cli/commands/update.py +8 -7
  14. afterpython/cli/main.py +9 -9
  15. afterpython/pcu.py +49 -43
  16. afterpython/templates/afterpython-template.toml +33 -0
  17. afterpython/templates/ci-workflow-template.yml +108 -77
  18. afterpython/templates/cliff-template.toml +111 -0
  19. afterpython/templates/deploy-workflow-template.yml +13 -17
  20. afterpython/templates/pre-commit-config-template.yaml +36 -17
  21. afterpython/templates/release-workflow-template.yml +61 -10
  22. afterpython/templates/ruff-template.toml +8 -1
  23. afterpython/templates/ty-template.toml +19 -0
  24. afterpython/tools/_afterpython.py +31 -21
  25. afterpython/tools/_git.py +56 -0
  26. afterpython/tools/branch_rules.py +3 -1
  27. afterpython/tools/prek.py +94 -0
  28. afterpython/utils.py +1 -1
  29. {afterpython-0.3.18.dist-info → afterpython-0.4.0.dist-info}/METADATA +21 -18
  30. {afterpython-0.3.18.dist-info → afterpython-0.4.0.dist-info}/RECORD +32 -30
  31. {afterpython-0.3.18.dist-info → afterpython-0.4.0.dist-info}/WHEEL +1 -1
  32. afterpython/cli/commands/commit.py +0 -52
  33. afterpython/templates/cz-template.toml +0 -7
  34. afterpython/tools/commitizen.py +0 -13
  35. afterpython/tools/pre_commit.py +0 -37
  36. {afterpython-0.3.18.dist-info → afterpython-0.4.0.dist-info}/entry_points.txt +0 -0
afterpython/_typing.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Literal, TypeAlias
1
+ from typing import Literal
2
2
 
3
- NodeEnv: TypeAlias = dict[str, str]
3
+ type NodeEnv = dict[str, str]
4
4
  tContentType = Literal["doc", "blog", "tutorial", "example", "guide"]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import re
5
+ import shutil
5
6
  import subprocess
6
7
  from pathlib import Path
7
8
  from typing import Literal
@@ -48,6 +49,12 @@ def _export_marimo(
48
49
  execute: bool = False,
49
50
  ):
50
51
  """Run `marimo export` to produce HTML at `output_html`."""
52
+ # marimo is an optional dependency (the `marimo` extra)
53
+ if shutil.which("marimo") is None:
54
+ raise click.ClickException(
55
+ f"marimo is required to build {source.name} (a marimo notebook), "
56
+ 'install it with: uv add --dev "afterpython[marimo]"'
57
+ )
51
58
  output_html.parent.mkdir(parents=True, exist_ok=True)
52
59
  subcommand = "html-wasm" if mode == "wasm" else "html"
53
60
  cmd = [
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable
3
+ from collections.abc import Callable, Iterable
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
@@ -147,7 +147,7 @@ def postbuild(dev_build: bool = False):
147
147
  source: Path,
148
148
  destination: Path,
149
149
  is_copy: bool = True,
150
- ignore_copy: Callable[[str, list[str]], set[str]] | None = None,
150
+ ignore_copy: Callable[[str, list[str]], Iterable[str]] | None = None,
151
151
  ):
152
152
  """
153
153
  Move or copy files from source to destination.
@@ -156,7 +156,7 @@ def postbuild(dev_build: bool = False):
156
156
  source: Source path
157
157
  destination: Destination path
158
158
  is_copy: If True, copy files (merge with existing). If False, move files (replace destination)
159
- ignore_copy: Optional ignore function for copytree, takes (directory, contents) and returns set of names to ignore
159
+ ignore_copy: Optional ignore function for copytree, takes (directory, contents) and returns the names to ignore
160
160
  """
161
161
  if not source.exists():
162
162
  return
@@ -1,118 +1,333 @@
1
+ import os
1
2
  import subprocess
3
+ import sys
2
4
 
3
5
  import click
4
- from click.exceptions import Exit
6
+ from packaging.version import Version
5
7
 
6
- from afterpython.utils import has_pixi
8
+ # uv's --bump names for PEP 440 pre-release markers
9
+ PRE_KINDS = {"a": "alpha", "b": "beta", "rc": "rc"}
10
+ CHANGELOG = "CHANGELOG.md"
11
+ # git-cliff ignores pre-release and dev tags (e.g. v0.4.0rc1, v0.4.0.dev2),
12
+ # so a stable release's section has every commit since the previous stable release
13
+ PRE_RELEASE_TAGS = "(a|b|rc|dev)[0-9]+"
7
14
 
8
15
 
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
16
+ def _uv_version_args(
17
+ version: Version, release: bool, pre: bool, level: str
18
+ ) -> list[str]:
19
+ """Arguments for `uv version`, either `--bump ...` or an exact version"""
20
+ is_stable = not version.is_prerelease # dev versions count as pre-releases too
21
+ if not is_stable and level != "patch":
22
+ raise click.ClickException(
23
+ f"--{level} only applies to stable versions, current version is {version}"
24
+ )
25
+ if release:
26
+ # e.g. 0.1.0rc0 -> 0.1.0, 0.3.19 -> 0.3.20
27
+ return ["--bump", "stable" if not is_stable else level]
28
+ if is_stable:
29
+ # e.g. 0.3.19 -> 0.3.20 (or 0.3.20rc0 with --pre)
30
+ return ["--bump", level, *(["--bump", "rc=0"] if pre else [])]
31
+ if version.is_devrelease:
32
+ if not pre:
33
+ # e.g. 0.1.0.dev3 -> 0.1.0.dev4
34
+ return ["--bump", "dev"]
35
+ if version.pre is None:
36
+ # e.g. 0.1.0.dev3 -> 0.1.0rc0
37
+ return ["--bump", "rc=0"]
38
+ # e.g. 0.1.0rc0.dev2 -> 0.1.0rc0 (uv's --bump rc would skip to rc1)
39
+ kind, number = version.pre
40
+ return [str(Version(f"{version.base_version}{kind}{number}"))]
41
+ # e.g. 0.1.0a1 -> 0.1.0a2, 0.1.0rc0 -> 0.1.0rc1
42
+ assert version.pre is not None # not stable and not dev, so it's a pre-release
43
+ return ["--bump", PRE_KINDS[version.pre[0]]]
44
+
45
+
46
+ def _git(*args: str) -> subprocess.CompletedProcess:
47
+ import afterpython as ap
48
+
49
+ return subprocess.run(
50
+ ["git", *args],
51
+ cwd=ap.paths.user_path,
52
+ capture_output=True,
53
+ text=True,
54
+ check=False,
55
+ )
56
+
57
+
58
+ def _restore(files: list[str], new_files: list[str]):
59
+ """Undo the version bump in the working tree and index"""
60
+ import afterpython as ap
61
+
62
+ _git("restore", "--source=HEAD", "--staged", "--worktree", "--", *files)
63
+ for file in new_files:
64
+ # --force, a hook (e.g. end-of-file-fixer) may have changed the file after it was staged
65
+ _git("rm", "--cached", "--force", "--quiet", "--ignore-unmatch", "--", file)
66
+ (ap.paths.user_path / file).unlink(missing_ok=True)
67
+ click.echo(f"↩️ Restored {', '.join(files + new_files)}", err=True)
68
+
69
+
70
+ def _write_changelog(tag: str):
71
+ """Write the release section for tag to CHANGELOG.md with git-cliff"""
72
+ import afterpython as ap
73
+
74
+ cmd = [
75
+ "git-cliff",
76
+ "--config",
77
+ str(ap.paths.afterpython_path / "cliff.toml"),
78
+ "--tag",
79
+ tag,
80
+ "--ignore-tags",
81
+ PRE_RELEASE_TAGS,
82
+ ]
83
+ if (ap.paths.user_path / CHANGELOG).exists():
84
+ # only add the new section on top, keeping manual edits to older ones;
85
+ # a range from the previous stable tag, since --unreleased would drop
86
+ # the commits before an ignored pre-release tag (e.g. v0.4.1rc0)
87
+ previous = _git(
88
+ "describe",
89
+ "--tags",
90
+ "--abbrev=0",
91
+ "--match",
92
+ "v*",
93
+ *(f"--exclude=*{kind}[0-9]*" for kind in ("a", "b", "rc", "dev")),
94
+ ).stdout.strip()
95
+ cmd += [f"{previous}..HEAD" if previous else "--unreleased"]
96
+ cmd += ["--prepend", CHANGELOG]
97
+ else:
98
+ # git-cliff can't prepend to a missing file, so create it from the full history
99
+ cmd += ["--output", CHANGELOG]
100
+ result = subprocess.run(
101
+ cmd, cwd=ap.paths.user_path, capture_output=True, text=True, check=False
102
+ )
103
+ if result.returncode != 0:
104
+ raise click.ClickException(f"git-cliff failed:\n{result.stderr.strip()}")
105
+ # the blank line after each release also ends up at the end of the file,
106
+ # end it with a single newline as end-of-file-fixer wants
107
+ path = ap.paths.user_path / CHANGELOG
108
+ path.write_text(path.read_text().rstrip("\n") + "\n")
109
+
110
+
111
+ def _is_interactive() -> bool:
112
+ return sys.stdin.isatty() and sys.stdout.isatty()
113
+
114
+
115
+ def _edit_changelog():
116
+ """Open CHANGELOG.md in git's editor, the bump continues once it's closed"""
117
+ import afterpython as ap
118
+
119
+ result = _git("var", "GIT_EDITOR")
120
+ if result.returncode != 0:
121
+ raise click.ClickException(
122
+ f"Failed to find git's editor:\n{result.stderr.strip()}"
123
+ )
124
+ editor = result.stdout.strip()
125
+ click.echo(
126
+ f"✏️ Opening {CHANGELOG} in {editor}, save and close it to commit the bump "
127
+ "(to abort, press Ctrl+C or exit the editor with an error, e.g. :cq in vim)"
128
+ )
129
+ # raises ClickException if the editor exits with an error, which rolls back the bump
130
+ click.edit(filename=str(ap.paths.user_path / CHANGELOG), editor=editor)
131
+
132
+
133
+ @click.command()
17
134
  @click.option(
18
135
  "--pre",
19
136
  is_flag=True,
20
- help="bump to pre-release version (e.g., dev3 -> rc0, a1 -> a2)",
137
+ help="bump to pre-release version (e.g., 0.1.0.dev3 -> 0.1.0rc0, 0.1.0a1 -> 0.1.0a2)",
138
+ )
139
+ @click.option(
140
+ "--release",
141
+ is_flag=True,
142
+ help="bump to stable version (e.g., 0.1.0rc0 -> 0.1.0, 0.3.19 -> 0.3.20) and push it with its tag to trigger the release workflow (default branch only)",
21
143
  )
22
144
  @click.option(
23
- "--release", is_flag=True, help="bump to stable release (uses cz bump default)"
145
+ "--minor",
146
+ is_flag=True,
147
+ help="bump the minor version instead of patch (stable versions only, e.g., 0.3.19 -> 0.4.0)",
148
+ )
149
+ @click.option(
150
+ "--major",
151
+ is_flag=True,
152
+ help="bump the major version instead of patch (stable versions only, e.g., 0.3.19 -> 1.0.0)",
153
+ )
154
+ @click.option(
155
+ "--no-edit",
156
+ is_flag=True,
157
+ help="don't open CHANGELOG.md in git's editor before committing a stable bump",
24
158
  )
25
- def bump(ctx, release: bool, pre: bool):
26
- """Bump project version with granular control.
159
+ @click.option(
160
+ "--dry-run", is_flag=True, help="show the new version without changing anything"
161
+ )
162
+ def bump(
163
+ release: bool, pre: bool, minor: bool, major: bool, no_edit: bool, dry_run: bool
164
+ ):
165
+ """Bump project version, commit it and tag it.
27
166
 
167
+ \b
28
168
  By default, stays within current release phase:
29
169
  - Dev releases (e.g., 0.1.0.dev3) -> increment dev number (0.1.0.dev4)
30
170
  - Pre-releases (e.g., 0.1.0a1) -> increment pre-release (0.1.0a2)
171
+ - Stable releases (e.g., 0.3.19) -> increment patch (0.3.20), or --minor/--major
31
172
 
32
173
  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).
174
+ Stable bumps also add the new release section to CHANGELOG.md with git-cliff
175
+ (if afterpython/cliff.toml exists, see `ap init cliff`) and open it in git's editor,
176
+ the bump is committed once you close it (use --no-edit to skip).
177
+ Use --release to bump to a stable version and push it with its tag (triggers the release workflow),
178
+ only allowed on the default branch (e.g. main).
34
179
  """
180
+ import afterpython as ap
181
+ from afterpython.tools._git import check_release_branch
35
182
  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
- )
183
+ from afterpython.utils import has_pixi
44
184
 
45
185
  if release and pre:
46
186
  raise click.ClickException("Only one of --release or --pre can be specified")
187
+ if minor and major:
188
+ raise click.ClickException("Only one of --minor or --major can be specified")
189
+ level = "minor" if minor else "major" if major else "patch"
47
190
 
48
- # bump using cz bump's default behavior
191
+ # before changing anything, so a wrong branch doesn't leave a bump behind
49
192
  if release:
50
- subprocess.run(["ap", "cz", "bump", *ctx.args])
193
+ check_release_branch()
51
194
 
52
- # Skip tag push if this is a dry run
53
- if "--dry-run" in ctx.args:
54
- return
195
+ version = read_metadata().version
196
+ if version is None:
197
+ raise click.ClickException("Unable to read version from pyproject.toml")
198
+ uv_args = _uv_version_args(version, release, pre, level)
55
199
 
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")
200
+ user_path = ap.paths.user_path
201
+ # without uv.lock, --frozen stops uv from creating one; with it, uv updates it
202
+ has_uv_lock = (user_path / "uv.lock").exists()
203
+ uv_lock_arg = "--no-sync" if has_uv_lock else "--frozen"
204
+ result = subprocess.run(
205
+ ["uv", "version", *uv_args, "--dry-run", "--short", "--frozen"],
206
+ cwd=user_path,
207
+ capture_output=True,
208
+ text=True,
209
+ check=False,
210
+ )
211
+ if result.returncode != 0:
212
+ raise click.ClickException(result.stderr.strip())
213
+ new_version = result.stdout.strip()
214
+ tag = f"v{new_version}"
61
215
 
62
- tag = f"v{new_version}"
216
+ uses_pixi = (user_path / "pixi.lock").exists()
217
+ if uses_pixi and not has_pixi():
218
+ click.echo("⚠️ pixi.lock found but pixi is not installed, skip updating it")
219
+ uses_pixi = False
220
+ candidates = [
221
+ "pyproject.toml",
222
+ *(["uv.lock"] if has_uv_lock else []),
223
+ *(["pixi.lock"] if uses_pixi else []),
224
+ ]
225
+ # only stable bumps write the changelog, and only when afterpython/cliff.toml was set up
226
+ writes_changelog = (
227
+ not Version(new_version).is_prerelease
228
+ and (ap.paths.afterpython_path / "cliff.toml").exists()
229
+ )
230
+ if writes_changelog:
231
+ candidates.append(CHANGELOG)
232
+ # no editor in CI or tests
233
+ edits_changelog = writes_changelog and not no_edit and _is_interactive()
234
+ files = _git("ls-files", "--", *candidates).stdout.split()
235
+ if "pyproject.toml" not in files:
236
+ raise click.ClickException("pyproject.toml is not tracked by git")
237
+ # CHANGELOG.md created by this bump, removed again if the bump fails
238
+ new_files: list[str] = []
239
+ if writes_changelog and CHANGELOG not in files:
240
+ if (user_path / CHANGELOG).exists():
241
+ raise click.ClickException(
242
+ f"{CHANGELOG} exists but is not tracked by git, commit or remove it first"
243
+ )
244
+ new_files.append(CHANGELOG)
63
245
 
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) if version.dev is not None else 0
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)
246
+ # the bump commit only contains the version change
247
+ dirty = _git("status", "--porcelain", "--", *files).stdout.strip()
248
+ if dirty:
249
+ raise click.ClickException(
250
+ f"Commit or stash your changes to these files first:\n{dirty}"
251
+ )
252
+ if _git("rev-parse", "--quiet", "--verify", f"refs/tags/{tag}").returncode == 0:
253
+ raise click.ClickException(f"Tag {tag} already exists")
254
+ # otherwise uv would re-lock other packages into the bump commit
255
+ if has_uv_lock and (
256
+ subprocess.run(
257
+ ["uv", "lock", "--check"], cwd=user_path, capture_output=True, check=False
258
+ ).returncode
259
+ != 0
260
+ ):
261
+ raise click.ClickException(
262
+ "uv.lock is out of date, run `uv lock` and commit it first"
263
+ )
264
+
265
+ if dry_run:
266
+ click.echo(f"{version} → {new_version}")
267
+ if writes_changelog:
268
+ click.echo(f"Would write the {new_version} section to {CHANGELOG}")
269
+ if edits_changelog:
270
+ click.echo(f"Would open {CHANGELOG} in git's editor before committing")
271
+ click.echo(f"Would commit {', '.join(files + new_files)} and create tag {tag}")
272
+ if release:
273
+ click.echo(f"Would push the current branch and tag {tag} to origin")
274
+ return
275
+
276
+ try:
277
+ result = subprocess.run(
278
+ ["uv", "version", *uv_args, uv_lock_arg], cwd=user_path, check=False
279
+ )
96
280
  if result.returncode != 0:
97
- raise Exit(result.returncode)
281
+ raise click.ClickException("uv version failed")
282
+ if read_metadata().version != Version(new_version):
283
+ raise click.ClickException(
284
+ f"Expected version {new_version} in pyproject.toml after the bump"
285
+ )
286
+ # refresh the pixi env; older pixi versions also record the editable version in pixi.lock
287
+ if uses_pixi:
288
+ result = subprocess.run(["pixi", "install"], cwd=user_path, check=False)
289
+ if result.returncode != 0:
290
+ raise click.ClickException("pixi install failed")
291
+ if writes_changelog:
292
+ _write_changelog(tag)
293
+ click.echo(f"📝 Wrote the {new_version} section to {CHANGELOG}")
294
+ if edits_changelog:
295
+ _edit_changelog()
296
+ if new_files:
297
+ # `git commit -- <files>` only takes files git already knows about
298
+ result = _git("add", "--", *new_files)
299
+ if result.returncode != 0:
300
+ raise click.ClickException(f"git add failed:\n{result.stderr.strip()}")
98
301
 
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,
302
+ # skip the commit message check, "bump" is not one of the commit types
303
+ env = os.environ.copy()
304
+ env["SKIP"] = ",".join(filter(None, [env.get("SKIP"), "ap-commit"]))
305
+ message = f"bump: version {version} → {new_version}"
306
+ # `git commit -- <files>` commits only these files, leaving anything else staged as is
307
+ result = subprocess.run(
308
+ ["git", "commit", "-m", message, "--", *files, *new_files],
309
+ cwd=user_path,
310
+ env=env,
104
311
  check=False,
105
312
  )
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,
313
+ if result.returncode != 0:
314
+ raise click.ClickException("git commit failed")
315
+ except (click.ClickException, KeyboardInterrupt):
316
+ _restore(files, new_files)
317
+ raise
318
+
319
+ result = _git("tag", tag)
320
+ if result.returncode != 0:
321
+ raise click.ClickException(
322
+ f"Committed the bump but failed to create tag {tag}:\n{result.stderr.strip()}"
323
+ )
324
+ click.echo(f"✅ Bumped {version} → {new_version} and tagged {tag}")
325
+
326
+ if release:
327
+ # ap release pushes the branch and the tag, which triggers the release workflow
328
+ click.echo()
329
+ result = subprocess.run(["ap", "release"], cwd=user_path, check=False)
330
+ if result.returncode != 0:
331
+ raise click.ClickException(
332
+ f"Bumped and tagged {tag} but didn't push it, retry with: ap release"
117
333
  )
118
- click.echo("✓ Committed pixi.lock update")
@@ -0,0 +1,45 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+ import afterpython as ap
7
+
8
+
9
+ @click.command(
10
+ add_help_option=False, # disable click's --help option so that git-cliff --help can work
11
+ context_settings=dict(
12
+ ignore_unknown_options=True,
13
+ allow_extra_args=True,
14
+ ),
15
+ )
16
+ @click.pass_context
17
+ def changelog(ctx):
18
+ """Preview the changelog with git-cliff (using afterpython/cliff.toml)
19
+
20
+ \b
21
+ Without arguments, prints the unreleased changes, i.e. the section
22
+ the next stable `ap bump` will add to CHANGELOG.md.
23
+ Any arguments are passed to git-cliff instead, e.g.
24
+ `ap changelog --latest` for the latest release.
25
+ """
26
+ from afterpython.cli.commands.bump import PRE_RELEASE_TAGS
27
+ from afterpython.utils import handle_passthrough_help
28
+
29
+ # Show both our options and git-cliff's help and exit
30
+ handle_passthrough_help(ctx, ["git-cliff"], show_underlying=True)
31
+
32
+ cliff_toml_path = ap.paths.afterpython_path / "cliff.toml"
33
+ if not cliff_toml_path.exists():
34
+ raise click.ClickException(
35
+ f"{cliff_toml_path} not found, run `ap init cliff` first"
36
+ )
37
+ cmd = ["git-cliff", "--config", str(cliff_toml_path)]
38
+ # like `ap bump`: no sections for pre-release and dev tags,
39
+ # their commits belong to the next stable release
40
+ if "--ignore-tags" not in ctx.args:
41
+ cmd += ["--ignore-tags", PRE_RELEASE_TAGS]
42
+ args = ctx.args or ["--unreleased"]
43
+ result = subprocess.run([*cmd, *args], cwd=ap.paths.user_path, check=False)
44
+ if result.returncode != 0:
45
+ raise Exit(result.returncode)
@@ -1,11 +1,19 @@
1
+ # NOTE: ty has no `extend` option yet (unlike ruff), so we must pass --config-file ourselves,
2
+ # and plain `ty` / editors don't find afterpython/ty.toml without extra setup.
3
+ # TODO: once ty supports `extend`, migrate to pyproject.toml's
4
+ # [tool.ty] extend = "afterpython/ty.toml" (like [tool.ruff]) and drop --config-file here.
5
+ # NOTE: the ty hook runs this command (see pre-commit-config-template.yaml for why it's not
6
+ # the official astral-sh/ty-pre-commit hook), so it uses the same config and environment.
1
7
  import subprocess
2
8
 
3
9
  import click
4
10
  from click.exceptions import Exit
5
11
 
12
+ import afterpython as ap
13
+
6
14
 
7
15
  @click.command(
8
- add_help_option=False, # disable click's --help option so that ruff check --help can work
16
+ add_help_option=False, # disable click's --help option so that ty check --help can work
9
17
  context_settings=dict(
10
18
  ignore_unknown_options=True,
11
19
  allow_extra_args=True,
@@ -13,24 +21,23 @@ from click.exceptions import Exit
13
21
  )
14
22
  @click.pass_context
15
23
  def check(ctx):
16
- """Run ruff linter (uses afterpython/ruff.toml if available)"""
24
+ """Run ty type checker"""
17
25
  from afterpython.utils import handle_passthrough_help
18
26
 
19
- # Show both our options and ruff's help and exit
27
+ # Show both our options and ty's help and exit
20
28
  handle_passthrough_help(
21
29
  ctx,
22
- ["ruff", "check"],
30
+ ["ty", "check"],
23
31
  show_underlying=True,
24
32
  )
25
33
 
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)
34
+ # absolute paths so that it works from any folder, e.g. inside afterpython/
35
+ # (ty would otherwise treat the folder with the nearest ty.toml as the project root)
36
+ cmd = ["ty", "check", "--project", str(ap.paths.user_path)]
37
+ ty_toml_path = ap.paths.afterpython_path / "ty.toml"
38
+ # without afterpython/ty.toml, let ty find its config itself (e.g. the user's own [tool.ty])
39
+ if ty_toml_path.exists():
40
+ cmd += ["--config-file", str(ty_toml_path)]
41
+ result = subprocess.run([*cmd, *ctx.args], check=False)
35
42
  if result.returncode != 0:
36
43
  raise Exit(result.returncode)