afterpython 0.3.18__tar.gz → 0.4.0__tar.gz

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 (78) hide show
  1. {afterpython-0.3.18 → afterpython-0.4.0}/PKG-INFO +21 -18
  2. {afterpython-0.3.18 → afterpython-0.4.0}/README.md +4 -4
  3. afterpython-0.4.0/pyproject.toml +73 -0
  4. afterpython-0.3.18/pyproject.toml → afterpython-0.4.0/pyproject.toml.orig +23 -28
  5. afterpython-0.4.0/src/afterpython/_typing.py +4 -0
  6. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/marimo_notebook.py +7 -0
  7. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/build.py +3 -3
  8. afterpython-0.4.0/src/afterpython/cli/commands/bump.py +333 -0
  9. afterpython-0.4.0/src/afterpython/cli/commands/changelog.py +45 -0
  10. afterpython-0.4.0/src/afterpython/cli/commands/check.py +43 -0
  11. afterpython-0.4.0/src/afterpython/cli/commands/commit_msg.py +115 -0
  12. afterpython-0.4.0/src/afterpython/cli/commands/init.py +344 -0
  13. afterpython-0.3.18/src/afterpython/cli/commands/commitizen.py → afterpython-0.4.0/src/afterpython/cli/commands/lint.py +7 -10
  14. afterpython-0.3.18/src/afterpython/cli/commands/pre_commit.py → afterpython-0.4.0/src/afterpython/cli/commands/prek.py +8 -8
  15. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/release.py +39 -10
  16. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/start.py +6 -4
  17. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/update.py +8 -7
  18. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/main.py +9 -9
  19. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/pcu.py +49 -43
  20. afterpython-0.4.0/src/afterpython/templates/afterpython-template.toml +33 -0
  21. afterpython-0.4.0/src/afterpython/templates/ci-workflow-template.yml +251 -0
  22. afterpython-0.4.0/src/afterpython/templates/cliff-template.toml +111 -0
  23. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/templates/deploy-workflow-template.yml +13 -17
  24. afterpython-0.4.0/src/afterpython/templates/pre-commit-config-template.yaml +68 -0
  25. afterpython-0.4.0/src/afterpython/templates/release-workflow-template.yml +107 -0
  26. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/templates/ruff-template.toml +8 -1
  27. afterpython-0.4.0/src/afterpython/templates/ty-template.toml +19 -0
  28. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/tools/_afterpython.py +31 -21
  29. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/tools/_git.py +56 -0
  30. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/tools/branch_rules.py +3 -1
  31. afterpython-0.4.0/src/afterpython/tools/prek.py +94 -0
  32. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/utils.py +1 -1
  33. afterpython-0.3.18/src/afterpython/_typing.py +0 -4
  34. afterpython-0.3.18/src/afterpython/cli/commands/bump.py +0 -118
  35. afterpython-0.3.18/src/afterpython/cli/commands/check.py +0 -36
  36. afterpython-0.3.18/src/afterpython/cli/commands/commit.py +0 -52
  37. afterpython-0.3.18/src/afterpython/cli/commands/init.py +0 -161
  38. afterpython-0.3.18/src/afterpython/templates/ci-workflow-template.yml +0 -220
  39. afterpython-0.3.18/src/afterpython/templates/cz-template.toml +0 -7
  40. afterpython-0.3.18/src/afterpython/templates/pre-commit-config-template.yaml +0 -49
  41. afterpython-0.3.18/src/afterpython/templates/release-workflow-template.yml +0 -56
  42. afterpython-0.3.18/src/afterpython/tools/commitizen.py +0 -13
  43. afterpython-0.3.18/src/afterpython/tools/pre_commit.py +0 -37
  44. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/__init__.py +0 -0
  45. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/_io/toml.py +0 -0
  46. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/_io/yaml.py +0 -0
  47. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/_paths.py +0 -0
  48. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/__init__.py +0 -0
  49. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/build_pdoc.py +0 -0
  50. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/content_json.py +0 -0
  51. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/faq_json.py +0 -0
  52. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/index_md.py +0 -0
  53. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/jupyter_notebook.py +0 -0
  54. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/llms_txt.py +0 -0
  55. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/markdown.py +0 -0
  56. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/metadata.py +0 -0
  57. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/builders/url_md.py +0 -0
  58. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/__init__.py +0 -0
  59. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/add.py +0 -0
  60. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/clean.py +0 -0
  61. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/dev.py +0 -0
  62. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/format.py +0 -0
  63. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/init_branch_rules.py +0 -0
  64. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/install.py +0 -0
  65. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/lock.py +0 -0
  66. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/preview.py +0 -0
  67. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/remove.py +0 -0
  68. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/serve.py +0 -0
  69. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/cli/commands/sync.py +0 -0
  70. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/const.py +0 -0
  71. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/main.py +0 -0
  72. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/py.typed +0 -0
  73. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/templates/dependabot-template.yml +0 -0
  74. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/templates/pdoc/frame.html.jinja2 +0 -0
  75. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/tools/github_actions.py +0 -0
  76. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/tools/myst.py +0 -0
  77. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/tools/pyproject.py +0 -0
  78. {afterpython-0.3.18 → afterpython-0.4.0}/src/afterpython/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: afterpython
3
- Version: 0.3.18
3
+ Version: 0.4.0
4
4
  Summary: CLI tool to streamline Python package maintenance and generate a dedicated project website
5
5
  Keywords: package-maintenance,project-website,static-site-generator,developer-tools,documentation,cli-tool,ci/cd,mystmd,semantic-release,python-packaging
6
6
  Author: Stephen Yau
@@ -11,27 +11,30 @@ Classifier: Intended Audience :: Developers
11
11
  Classifier: Topic :: Software Development :: Build Tools
12
12
  Classifier: Topic :: Software Development :: Documentation
13
13
  Classifier: Typing :: Typed
14
- Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
17
- Requires-Dist: click>=8.1.8
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Dist: click>=8.5.0
18
18
  Requires-Dist: trogon>=0.6.0
19
- Requires-Dist: httpx2>=2.5.0
20
- Requires-Dist: tomlkit>=0.15.0
19
+ Requires-Dist: httpx2>=2.13.1
20
+ Requires-Dist: tomlkit>=0.15.1
21
21
  Requires-Dist: ruamel-yaml>=0.19.1
22
- Requires-Dist: gitpython>=3.1.50
23
- Requires-Dist: python-dotenv>=1.2.2
24
- Requires-Dist: pyproject-metadata>=0.11.0
25
- Requires-Dist: jupytext>=1.19.4
26
- Requires-Dist: mystmd>=1.10.1
22
+ Requires-Dist: gitpython>=3.2.0
23
+ Requires-Dist: python-dotenv>=1.2.3
24
+ Requires-Dist: pyproject-metadata>=0.12.1
25
+ Requires-Dist: jupytext>=1.19.5
26
+ Requires-Dist: mystmd>=1.11.0
27
27
  Requires-Dist: pdoc>=16.0.0
28
- Requires-Dist: ruff>=0.15.21
29
- Requires-Dist: pre-commit>=4.6.0
30
- Requires-Dist: commitizen>=4.16.4
31
- Requires-Python: >=3.11
28
+ Requires-Dist: ruff>=0.16.9
29
+ Requires-Dist: prek>=0.5.4
30
+ Requires-Dist: ty>=0.0.84
31
+ Requires-Dist: git-cliff>=2.14.2
32
+ Requires-Dist: marimo[recommended,lsp]>=0.25.0 ; extra == 'marimo'
33
+ Requires-Python: >=3.12
32
34
  Project-URL: homepage, https://afterpython.afterpython.org
33
35
  Project-URL: repository, https://github.com/AfterPythonOrg/afterpython
34
36
  Project-URL: documentation, https://afterpython.afterpython.org/doc
37
+ Provides-Extra: marimo
35
38
  Description-Content-Type: text/markdown
36
39
 
37
40
  # AfterPython: Python Package Maintenance Toolkit and Project Website Generator
@@ -59,10 +62,10 @@ Description-Content-Type: text/markdown
59
62
  [Scikit-Learn]: https://scikit-learn.org
60
63
  [WebLLM]: https://webllm.mlc.ai/
61
64
  [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
62
- [commitizen]: https://github.com/commitizen-tools/commitizen
63
65
  [uv]: https://docs.astral.sh/uv/
64
66
  [ruff]: https://docs.astral.sh/ruff/
65
-
67
+ [ty]: https://docs.astral.sh/ty/
68
+ [git-cliff]: https://git-cliff.org/
66
69
 
67
70
  ## Problem
68
71
  Going from **writing Python code to publishing and maintaining a package** is **time-consuming**.
@@ -125,10 +128,10 @@ ap tui
125
128
  - [pre-commit]
126
129
  - [GitHub Actions]
127
130
  - [pdoc]
128
- - [commitizen]
129
131
  - [uv]
130
132
  - [ruff]
133
+ - [ty]
131
134
  - [pagefind]
132
135
  - [WebLLM]
133
- <!-- - ty -->
136
+ - [git-cliff]
134
137
  <!-- - [pixi] -->
@@ -23,10 +23,10 @@
23
23
  [Scikit-Learn]: https://scikit-learn.org
24
24
  [WebLLM]: https://webllm.mlc.ai/
25
25
  [project-website-template]: https://github.com/AfterPythonOrg/project-website-template
26
- [commitizen]: https://github.com/commitizen-tools/commitizen
27
26
  [uv]: https://docs.astral.sh/uv/
28
27
  [ruff]: https://docs.astral.sh/ruff/
29
-
28
+ [ty]: https://docs.astral.sh/ty/
29
+ [git-cliff]: https://git-cliff.org/
30
30
 
31
31
  ## Problem
32
32
  Going from **writing Python code to publishing and maintaining a package** is **time-consuming**.
@@ -89,10 +89,10 @@ ap tui
89
89
  - [pre-commit]
90
90
  - [GitHub Actions]
91
91
  - [pdoc]
92
- - [commitizen]
93
92
  - [uv]
94
93
  - [ruff]
94
+ - [ty]
95
95
  - [pagefind]
96
96
  - [WebLLM]
97
- <!-- - ty -->
97
+ - [git-cliff]
98
98
  <!-- - [pixi] -->
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "afterpython"
3
+ version = "0.4.0"
4
+ description = "CLI tool to streamline Python package maintenance and generate a dedicated project website"
5
+ license = "Apache-2.0"
6
+ readme = "README.md"
7
+ keywords = [
8
+ "package-maintenance",
9
+ "project-website",
10
+ "static-site-generator",
11
+ "developer-tools",
12
+ "documentation",
13
+ "cli-tool",
14
+ "ci/cd",
15
+ "mystmd",
16
+ "semantic-release",
17
+ "python-packaging",
18
+ ]
19
+ classifiers = [
20
+ "Operating System :: OS Independent",
21
+ "Intended Audience :: Developers",
22
+ "Topic :: Software Development :: Build Tools",
23
+ "Topic :: Software Development :: Documentation",
24
+ "Typing :: Typed",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ ]
29
+ requires-python = ">=3.12"
30
+ dependencies = [
31
+ "click>=8.5.0",
32
+ "trogon>=0.6.0",
33
+ "httpx2>=2.13.1",
34
+ "tomlkit>=0.15.1",
35
+ "ruamel-yaml>=0.19.1",
36
+ "gitpython>=3.2.0",
37
+ "python-dotenv>=1.2.3",
38
+ "pyproject-metadata>=0.12.1",
39
+ "jupytext>=1.19.5",
40
+ "mystmd>=1.11.0",
41
+ "pdoc>=16.0.0",
42
+ "ruff>=0.16.9",
43
+ "prek>=0.5.4",
44
+ "ty>=0.0.84",
45
+ "git-cliff>=2.14.2",
46
+ ]
47
+
48
+ [[project.authors]]
49
+ name = "Stephen Yau"
50
+ email = "softwareentrepreneer+afterpython@gmail.com"
51
+
52
+ [project.optional-dependencies]
53
+ marimo = ["marimo[recommended,lsp]>=0.25.0"]
54
+
55
+ [project.urls]
56
+ homepage = "https://afterpython.afterpython.org"
57
+ repository = "https://github.com/AfterPythonOrg/afterpython"
58
+ documentation = "https://afterpython.afterpython.org/doc"
59
+
60
+ [project.scripts]
61
+ ap = "afterpython.main:run_cli"
62
+ pcu = "afterpython.main:run_pcu"
63
+
64
+ [build-system]
65
+ requires = ["uv_build>=0.12.21,<0.13.0"]
66
+ build-backend = "uv_build"
67
+
68
+ [tool.uv.build-backend]
69
+ module-name = "afterpython"
70
+ module-root = "src"
71
+
72
+ [tool.ruff]
73
+ extend = "afterpython/ruff.toml"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "afterpython"
3
- version = "0.3.18"
3
+ version = "0.4.0"
4
4
  description = "CLI tool to streamline Python package maintenance and generate a dedicated project website"
5
5
  license = "Apache-2.0"
6
6
  authors = [
@@ -25,32 +25,33 @@ classifiers = [
25
25
  "Topic :: Software Development :: Build Tools",
26
26
  "Topic :: Software Development :: Documentation",
27
27
  "Typing :: Typed",
28
- "Programming Language :: Python :: 3.11",
29
28
  "Programming Language :: Python :: 3.12",
30
29
  "Programming Language :: Python :: 3.13",
31
- # "Programming Language :: Python :: 3.14",
30
+ "Programming Language :: Python :: 3.14",
31
+ # "Programming Language :: Python :: 3.15",
32
32
  ]
33
- requires-python = ">=3.11"
33
+ requires-python = ">=3.12"
34
34
  dependencies = [
35
- "click>=8.1.8",
35
+ "click>=8.5.0",
36
36
  "trogon>=0.6.0",
37
- "httpx2>=2.5.0",
38
- "tomlkit>=0.15.0",
37
+ "httpx2>=2.13.1",
38
+ "tomlkit>=0.15.1",
39
39
  "ruamel-yaml>=0.19.1",
40
- "gitpython>=3.1.50",
41
- # "pygithub>=2.8.1",
42
- "python-dotenv>=1.2.2",
43
- "pyproject-metadata>=0.11.0",
44
- "jupytext>=1.19.4",
45
- # "jinja2>=3.1.6",
46
- "mystmd>=1.10.1",
40
+ "gitpython>=3.2.0",
41
+ "python-dotenv>=1.2.3",
42
+ "pyproject-metadata>=0.12.1",
43
+ "jupytext>=1.19.5",
44
+ "mystmd>=1.11.0",
47
45
  "pdoc>=16.0.0",
48
- "ruff>=0.15.21",
49
- "pre-commit>=4.6.0",
50
- "commitizen>=4.16.4",
51
- # "python-semantic-release>=10.5.2",
46
+ "ruff>=0.16.9",
47
+ "prek>=0.5.4",
48
+ "ty>=0.0.84",
49
+ "git-cliff>=2.14.2",
52
50
  ]
53
51
 
52
+ [project.optional-dependencies]
53
+ marimo = ["marimo[recommended,lsp]>=0.25.0"]
54
+
54
55
  [project.urls]
55
56
  homepage = "https://afterpython.afterpython.org"
56
57
  repository = "https://github.com/AfterPythonOrg/afterpython"
@@ -60,22 +61,16 @@ documentation = "https://afterpython.afterpython.org/doc"
60
61
  ap = "afterpython.main:run_cli"
61
62
  pcu = "afterpython.main:run_pcu"
62
63
 
63
- # [tool.uv]
64
- # override-dependencies = ["platformdirs>=4.5.0", "nodeenv>=1.9.1"]
65
-
66
64
  [build-system]
67
- requires = ["uv_build>=0.11.28,<0.12.0"]
65
+ requires = ["uv_build>=0.12.21,<0.13.0"]
68
66
  build-backend = "uv_build"
69
67
 
70
68
  [tool.uv]
71
- exclude-newer = "14 days"
69
+ # exclude-newer = "14 days"
72
70
 
73
71
  [tool.uv.build-backend]
74
72
  module-name = "afterpython"
75
73
  module-root = "src"
76
74
 
77
- # [tool.uv.sources]
78
- # afterpython = { path = ".", editable = true }
79
-
80
- [tool.pyright]
81
- typeCheckingMode = "basic"
75
+ [tool.ruff]
76
+ extend = "afterpython/ruff.toml"
@@ -0,0 +1,4 @@
1
+ from typing import Literal
2
+
3
+ type NodeEnv = dict[str, str]
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
@@ -0,0 +1,333 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+
5
+ import click
6
+ from packaging.version import Version
7
+
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]+"
14
+
15
+
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()
134
+ @click.option(
135
+ "--pre",
136
+ is_flag=True,
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)",
143
+ )
144
+ @click.option(
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",
158
+ )
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.
166
+
167
+ \b
168
+ By default, stays within current release phase:
169
+ - Dev releases (e.g., 0.1.0.dev3) -> increment dev number (0.1.0.dev4)
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
172
+
173
+ Use --pre to transition from dev to pre-release, or pre-release to next pre-release.
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).
179
+ """
180
+ import afterpython as ap
181
+ from afterpython.tools._git import check_release_branch
182
+ from afterpython.tools.pyproject import read_metadata
183
+ from afterpython.utils import has_pixi
184
+
185
+ if release and pre:
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"
190
+
191
+ # before changing anything, so a wrong branch doesn't leave a bump behind
192
+ if release:
193
+ check_release_branch()
194
+
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)
199
+
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}"
215
+
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)
245
+
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
+ )
280
+ if result.returncode != 0:
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()}")
301
+
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,
311
+ check=False,
312
+ )
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"
333
+ )