afterpython 0.1.0.dev8__tar.gz → 0.1.0.dev9__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 (51) hide show
  1. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9}/PKG-INFO +9 -7
  2. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9}/README.md +3 -2
  3. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9}/pyproject.toml +8 -42
  4. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/_io/toml.py +5 -3
  5. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/_typing.py +1 -2
  6. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/builders/__init__.py +0 -1
  7. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/builders/molab.py +2 -1
  8. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/__init__.py +0 -1
  9. afterpython-0.1.0.dev9/src/afterpython/cli/commands/add.py +67 -0
  10. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/build.py +9 -8
  11. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/clean.py +2 -1
  12. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/commit.py +16 -1
  13. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/dev.py +3 -2
  14. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/init.py +5 -5
  15. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/install.py +8 -2
  16. afterpython-0.1.0.dev9/src/afterpython/cli/commands/lock.py +23 -0
  17. afterpython-0.1.0.dev9/src/afterpython/cli/commands/remove.py +67 -0
  18. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/start.py +6 -3
  19. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/update.py +26 -3
  20. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/main.py +73 -15
  21. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/const.py +0 -1
  22. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/pcu.py +3 -1
  23. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/templates/ruff-template.toml +1 -1
  24. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/_afterpython.py +2 -1
  25. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/branch_rules.py +0 -1
  26. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/commitizen.py +1 -2
  27. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/myst.py +4 -2
  28. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/pre_commit.py +1 -3
  29. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/pyproject.py +4 -2
  30. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/utils.py +4 -0
  31. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/__init__.py +0 -0
  32. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/_io/yaml.py +0 -0
  33. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/_paths.py +1 -1
  34. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/builders/metadata.py +1 -1
  35. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/bump.py +0 -0
  36. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/check.py +0 -0
  37. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/commitizen.py +0 -0
  38. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/format.py +0 -0
  39. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/init_branch_rules.py +0 -0
  40. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/pre_commit.py +0 -0
  41. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/preview.py +0 -0
  42. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/release.py +0 -0
  43. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/cli/commands/sync.py +3 -3
  44. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/main.py +0 -0
  45. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/templates/ci-workflow-template.yml +0 -0
  46. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/templates/dependabot-template.yml +0 -0
  47. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/templates/deploy-workflow-template.yml +0 -0
  48. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/templates/release-workflow-template.yml +0 -0
  49. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/_git.py +0 -0
  50. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/tools/github_actions.py +0 -0
  51. {afterpython-0.1.0.dev8 → afterpython-0.1.0.dev9/src}/afterpython/version.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: afterpython
3
- Version: 0.1.0.dev8
4
- Summary: CLI tool for AfterPython
3
+ Version: 0.1.0.dev9
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
7
7
  Author-email: Stephen Yau <softwareentrepreneer+afterpython@gmail.com>
@@ -10,6 +10,7 @@ Classifier: Operating System :: OS Independent
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Topic :: Software Development :: Build Tools
12
12
  Classifier: Topic :: Software Development :: Documentation
13
+ Classifier: Typing :: Typed
13
14
  Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Classifier: Programming Language :: Python :: 3.13
@@ -21,11 +22,11 @@ Requires-Dist: ruamel-yaml>=0.18.16
21
22
  Requires-Dist: gitpython>=3.1.45
22
23
  Requires-Dist: pygithub>=2.8.1
23
24
  Requires-Dist: python-dotenv>=1.2.1
24
- Requires-Dist: pyproject-metadata>=0.9.1
25
+ Requires-Dist: pyproject-metadata>=0.10.0
25
26
  Requires-Dist: mystmd>=1.6.6
26
27
  Requires-Dist: pdoc>=16.0.0
27
- Requires-Dist: ruff>=0.14.5
28
- Requires-Dist: pre-commit>=4.4.0
28
+ Requires-Dist: ruff>=0.14.6
29
+ Requires-Dist: pre-commit>=4.5.0
29
30
  Requires-Dist: commitizen>=4.10.0
30
31
  Requires-Python: >=3.11
31
32
  Project-URL: documentation, https://afterpython.afterpython.org/doc
@@ -75,8 +76,9 @@ First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [Sem
75
76
 
76
77
  ## Core Features
77
78
  - [x] Write content directly in [MyST Markdown] or [Jupyter Notebook]
78
- - [x] Go from writing to website deployment in minutes — no need to learn any of the underlying tools
79
- - [x] Centralize all your content in a modern, unified project website — from documentation to blog posts
79
+ - [x] Go from writing to **website deployment in minutes** — no need to learn any of the underlying tools
80
+ - [x] Centralize all your content in a modern, **unified project website** — from documentation to blog posts
81
+ - [x] Zero-config orchestration — Pre-configured modern tooling with sane defaults (see [Tech Stack](#tech-stack)), so you can start maintaining packages immediately **without learning each tool**
80
82
  - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
81
83
  - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
82
84
  - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
@@ -40,8 +40,9 @@ First-time maintainers must learn multiple tools and concepts, e.g. [MyST], [Sem
40
40
 
41
41
  ## Core Features
42
42
  - [x] Write content directly in [MyST Markdown] or [Jupyter Notebook]
43
- - [x] Go from writing to website deployment in minutes — no need to learn any of the underlying tools
44
- - [x] Centralize all your content in a modern, unified project website — from documentation to blog posts
43
+ - [x] Go from writing to **website deployment in minutes** — no need to learn any of the underlying tools
44
+ - [x] Centralize all your content in a modern, **unified project website** — from documentation to blog posts
45
+ - [x] Zero-config orchestration — Pre-configured modern tooling with sane defaults (see [Tech Stack](#tech-stack)), so you can start maintaining packages immediately **without learning each tool**
45
46
  - [ ] Export content as PDF — for example, combine all blog posts into a single PDF file
46
47
  - [ ] **⚡ Full-text search** across **ALL** your content in your website — docs, blogs, tutorials, everything
47
48
  - [ ] **🤖 Embedded AI Chatbot** that answers questions directly using an in-browser LLM — at no cost
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "afterpython"
3
- version = "0.1.0.dev8"
4
- description = "CLI tool for AfterPython"
3
+ version = "0.1.0.dev9"
4
+ description = "CLI tool to streamline Python package maintenance and generate a dedicated project website"
5
5
  license = "Apache-2.0"
6
6
  authors = [
7
7
  { name = "Stephen Yau", email = "softwareentrepreneer+afterpython@gmail.com" },
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Intended Audience :: Developers",
25
25
  "Topic :: Software Development :: Build Tools",
26
26
  "Topic :: Software Development :: Documentation",
27
+ "Typing :: Typed",
27
28
  "Programming Language :: Python :: 3.11",
28
29
  "Programming Language :: Python :: 3.12",
29
30
  "Programming Language :: Python :: 3.13",
@@ -39,13 +40,13 @@ dependencies = [
39
40
  "gitpython>=3.1.45",
40
41
  "pygithub>=2.8.1",
41
42
  "python-dotenv>=1.2.1",
42
- "pyproject-metadata>=0.9.1",
43
+ "pyproject-metadata>=0.10.0",
43
44
  # "jupytext>=1.18.1",
44
45
  # "jinja2>=3.1.6",
45
46
  "mystmd>=1.6.6",
46
47
  "pdoc>=16.0.0",
47
- "ruff>=0.14.5",
48
- "pre-commit>=4.4.0",
48
+ "ruff>=0.14.6",
49
+ "pre-commit>=4.5.0",
49
50
  "commitizen>=4.10.0",
50
51
  # "python-semantic-release>=10.5.2",
51
52
  ]
@@ -64,44 +65,9 @@ pcu = "afterpython.main:run_pcu"
64
65
  override-dependencies = ["platformdirs>=4.5.0", "nodeenv>=1.9.1"]
65
66
 
66
67
  [build-system]
67
- requires = ["uv_build>=0.9.10,<0.10.0"]
68
+ requires = ["uv_build>=0.9.13,<0.10.0"]
68
69
  build-backend = "uv_build"
69
70
 
70
- [dependency-groups]
71
- dev = ["notebook>=7.4.7"]
72
-
73
71
  [tool.uv.build-backend]
74
72
  module-name = "afterpython"
75
- module-root = ""
76
- source-exclude = [
77
- "afterpython/doc",
78
- "afterpython/blog",
79
- "afterpython/tutorial",
80
- "afterpython/example",
81
- "afterpython/guide",
82
- "afterpython/static",
83
- "afterpython/_website",
84
- "afterpython/_build",
85
- "afterpython/cz.toml",
86
- "afterpython/.pre-commit-config.yaml",
87
- "afterpython/ruff.toml",
88
- "afterpython/afterpython.toml",
89
- "afterpython/authors.yml",
90
- "afterpython/faq.yml",
91
- ]
92
- wheel-exclude = [
93
- "afterpython/doc",
94
- "afterpython/blog",
95
- "afterpython/tutorial",
96
- "afterpython/example",
97
- "afterpython/guide",
98
- "afterpython/static",
99
- "afterpython/_website",
100
- "afterpython/_build",
101
- "afterpython/cz.toml",
102
- "afterpython/.pre-commit-config.yaml",
103
- "afterpython/ruff.toml",
104
- "afterpython/afterpython.toml",
105
- "afterpython/authors.yml",
106
- "afterpython/faq.yml",
107
- ]
73
+ module-root = "src"
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
5
- from tomlkit.toml_document import TOMLDocument
6
6
  from pathlib import Path
7
7
 
8
+ from tomlkit.toml_document import TOMLDocument
9
+
8
10
  import tomlkit
9
11
 
10
12
 
@@ -16,7 +18,7 @@ def _from_tomlkit(value):
16
18
  - tomlkit containers → plain Python containers
17
19
  - tomlkit primitives → plain Python primitives
18
20
  """
19
- from tomlkit.items import Array, InlineTable, Table, AoT
21
+ from tomlkit.items import AoT, Array, InlineTable, Table
20
22
 
21
23
  if value is None:
22
24
  return None
@@ -58,7 +60,7 @@ def _to_tomlkit(value):
58
60
  - List of dicts → array of inline tables: [{key = val}]
59
61
  - Nested dicts → nested tables with proper structure
60
62
  """
61
- from tomlkit import inline_table, array
63
+ from tomlkit import array, inline_table
62
64
 
63
65
  if value is None:
64
66
  return None
@@ -1,5 +1,4 @@
1
- from typing import TypeAlias, Literal
2
-
1
+ from typing import Literal, TypeAlias
3
2
 
4
3
  NodeEnv: TypeAlias = dict[str, str]
5
4
  tContentType = Literal["doc", "blog", "tutorial", "example", "guide"]
@@ -1,7 +1,6 @@
1
1
  from afterpython.builders.metadata import build_metadata
2
2
  from afterpython.builders.molab import add_molab_badge_to_jupyter_notebooks
3
3
 
4
-
5
4
  __all__ = (
6
5
  "add_molab_badge_to_jupyter_notebooks",
7
6
  "build_metadata",
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -10,8 +11,8 @@ from pathlib import Path
10
11
  from pyproject_metadata import StandardMetadata
11
12
 
12
13
  import afterpython as ap
13
- from afterpython.tools.pyproject import read_metadata
14
14
  from afterpython.const import CONTENT_TYPES
15
+ from afterpython.tools.pyproject import read_metadata
15
16
 
16
17
 
17
18
  def _get_molab_badge() -> str:
@@ -1,4 +1,3 @@
1
1
  from afterpython.cli.main import afterpython_group
2
2
 
3
-
4
3
  __all__ = ["afterpython_group"]
@@ -0,0 +1,67 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+ from afterpython.utils import has_pixi, has_uv
7
+
8
+
9
+ @click.command()
10
+ @click.option(
11
+ "--optional",
12
+ type=str,
13
+ default=None,
14
+ required=False,
15
+ help="Add to an optional dependency group (pixi: mapped to 'optional' feature)",
16
+ )
17
+ @click.option(
18
+ "--group",
19
+ type=str,
20
+ default=None,
21
+ required=False,
22
+ help="Add to a dependency group (pixi: mapped to same-named feature)",
23
+ )
24
+ @click.argument("lib", type=str, required=True)
25
+ def add(optional: str | None, group: str | None, lib: str):
26
+ """Add a new dependency to the project (manages both uv and pixi if present)"""
27
+ if not has_uv():
28
+ click.echo("uv not found. Please install uv first.")
29
+ return
30
+
31
+ if optional and group:
32
+ click.echo("Error: Cannot specify both --optional and --group")
33
+ raise Exit(1)
34
+
35
+ result = subprocess.run(
36
+ [
37
+ "uv",
38
+ "add",
39
+ *(["--optional", optional] if optional else []),
40
+ *(["--group", group] if group else []),
41
+ lib,
42
+ ],
43
+ check=False,
44
+ )
45
+ if result.returncode != 0:
46
+ raise Exit(result.returncode)
47
+
48
+ if has_pixi():
49
+ # NOTE: pixi doesn't support --optional, so we need to use --feature instead
50
+ if optional:
51
+ feature_name = "optional"
52
+ elif group:
53
+ feature_name = group
54
+ else:
55
+ feature_name = None
56
+ result = subprocess.run(
57
+ [
58
+ "pixi",
59
+ "add",
60
+ "--pypi",
61
+ *(["--feature", feature_name] if feature_name else []),
62
+ lib,
63
+ ],
64
+ check=False,
65
+ )
66
+ if result.returncode != 0:
67
+ raise Exit(result.returncode)
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
2
+
3
3
  from collections.abc import Callable
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  if TYPE_CHECKING:
6
- from afterpython._typing import NodeEnv
7
7
  from pathlib import Path
8
8
 
9
+ from afterpython._typing import NodeEnv
10
+
9
11
  import os
10
12
  import shutil
11
13
  import subprocess
@@ -14,12 +16,12 @@ import click
14
16
  from click.exceptions import Exit
15
17
 
16
18
  import afterpython as ap
17
- from afterpython.utils import find_node_env
18
- from afterpython.const import CONTENT_TYPES
19
19
  from afterpython.builders import (
20
- build_metadata,
21
20
  add_molab_badge_to_jupyter_notebooks,
21
+ build_metadata,
22
22
  )
23
+ from afterpython.const import CONTENT_TYPES
24
+ from afterpython.utils import find_node_env
23
25
 
24
26
 
25
27
  def determine_base_path() -> str:
@@ -29,9 +31,9 @@ def determine_base_path() -> str:
29
31
  Empty string if custom domain is configured (no github.io in URL)
30
32
  /repo-name if using default GitHub Pages URL (contains github.io)
31
33
  """
34
+ from afterpython._io.toml import _from_tomlkit
32
35
  from afterpython.tools._afterpython import read_afterpython
33
36
  from afterpython.tools.pyproject import read_metadata
34
- from afterpython._io.toml import _from_tomlkit
35
37
 
36
38
  # Read website URL from afterpython.toml
37
39
  afterpython = read_afterpython()
@@ -167,8 +169,7 @@ def build(ctx, dev: bool, execute: bool):
167
169
 
168
170
  Use --execute to execute Jupyter notebooks for all content types.
169
171
  """
170
- from afterpython.utils import has_content_for_myst
171
- from afterpython.utils import handle_passthrough_help
172
+ from afterpython.utils import handle_passthrough_help, has_content_for_myst
172
173
 
173
174
  # Show both our options and myst's help and exit
174
175
  handle_passthrough_help(
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -10,8 +11,8 @@ import subprocess
10
11
  import click
11
12
  from click.exceptions import Exit
12
13
 
13
- from afterpython.utils import find_node_env, has_content_for_myst
14
14
  from afterpython.const import CONTENT_TYPES
15
+ from afterpython.utils import find_node_env, has_content_for_myst
15
16
 
16
17
 
17
18
  @click.command(
@@ -15,7 +15,13 @@ import click
15
15
  @click.option(
16
16
  "--no-cz", "--no-commitizen", is_flag=True, help="Skip running 'cz commit'"
17
17
  )
18
- def commit(ctx, no_cz: bool):
18
+ @click.option(
19
+ "--no-pc",
20
+ "--no-pre-commit",
21
+ is_flag=True,
22
+ help="Skip running pre-commit checks before commit",
23
+ )
24
+ def commit(ctx, no_cz: bool, no_pc: bool):
19
25
  """Run 'cz commit'"""
20
26
  from afterpython.utils import handle_passthrough_help
21
27
 
@@ -26,6 +32,15 @@ def commit(ctx, no_cz: bool):
26
32
  show_underlying=True,
27
33
  )
28
34
 
35
+ # Run pre-commit checks first (unless --no-pc is set)
36
+ if not no_pc:
37
+ result = subprocess.run(["ap", "pc", "run"])
38
+ if result.returncode != 0:
39
+ click.echo(
40
+ "❌ Pre-commit checks failed. Please fix the issues and try again."
41
+ )
42
+ ctx.exit(1)
43
+
29
44
  if not no_cz:
30
45
  subprocess.run(["ap", "cz", "commit", *ctx.args])
31
46
  else:
@@ -1,17 +1,18 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
5
6
  from afterpython._typing import NodeEnv
6
7
 
7
- import time
8
8
  import subprocess
9
+ import time
9
10
 
10
11
  import click
11
12
  from click.exceptions import Exit
12
13
 
13
- from afterpython.utils import find_node_env, find_available_port
14
14
  from afterpython.const import CONTENT_TYPES
15
+ from afterpython.utils import find_available_port, find_node_env
15
16
 
16
17
 
17
18
  @click.command(
@@ -22,7 +22,7 @@ def init_ruff_toml():
22
22
  "repos": [
23
23
  {
24
24
  "repo": "https://github.com/astral-sh/ruff-pre-commit",
25
- "rev": "v0.14.5",
25
+ "rev": "v0.14.6",
26
26
  "hooks": [
27
27
  {
28
28
  "id": "ruff-check",
@@ -56,15 +56,15 @@ def init_website():
56
56
  @click.pass_context
57
57
  def init(ctx, yes):
58
58
  """Initialize afterpython with MyST Markdown (by default) and project website template"""
59
- from afterpython.tools.pyproject import init_pyproject
60
59
  from afterpython.tools._afterpython import init_afterpython
61
- from afterpython.tools.myst import init_myst
62
- from afterpython.tools.pre_commit import init_pre_commit
63
60
  from afterpython.tools.commitizen import init_commitizen
64
61
  from afterpython.tools.github_actions import (
65
- create_workflow,
66
62
  create_dependabot,
63
+ create_workflow,
67
64
  )
65
+ from afterpython.tools.myst import init_myst
66
+ from afterpython.tools.pre_commit import init_pre_commit
67
+ from afterpython.tools.pyproject import init_pyproject
68
68
 
69
69
  paths = ctx.obj["paths"]
70
70
  click.echo("Initializing afterpython...")
@@ -3,7 +3,7 @@ import subprocess
3
3
  import click
4
4
  from click.exceptions import Exit
5
5
 
6
- from afterpython.utils import has_uv
6
+ from afterpython.utils import has_pixi, has_uv
7
7
 
8
8
 
9
9
  @click.command(
@@ -13,10 +13,16 @@ from afterpython.utils import has_uv
13
13
  )
14
14
  )
15
15
  def install():
16
- """Run 'uv sync --all-extras --all-groups' to install all dependencies"""
16
+ """Install all dependencies (runs 'uv sync --all-extras --all-groups' and 'pixi install' if present)"""
17
17
  if not has_uv():
18
18
  click.echo("uv not found. Please install uv first.")
19
19
  return
20
+
20
21
  result = subprocess.run(["uv", "sync", "--all-extras", "--all-groups"], check=False)
21
22
  if result.returncode != 0:
22
23
  raise Exit(result.returncode)
24
+
25
+ if has_pixi():
26
+ result = subprocess.run(["pixi", "install"], check=False)
27
+ if result.returncode != 0:
28
+ raise Exit(result.returncode)
@@ -0,0 +1,23 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+ from afterpython.utils import has_pixi, has_uv
7
+
8
+
9
+ @click.command()
10
+ def lock():
11
+ """Lock the dependencies (runs 'uv lock' and 'pixi lock' if present)"""
12
+ if not has_uv():
13
+ click.echo("uv not found. Please install uv first.")
14
+ return
15
+
16
+ result = subprocess.run(["uv", "lock"], check=False)
17
+ if result.returncode != 0:
18
+ raise Exit(result.returncode)
19
+
20
+ if has_pixi():
21
+ result = subprocess.run(["pixi", "lock"], check=False)
22
+ if result.returncode != 0:
23
+ raise Exit(result.returncode)
@@ -0,0 +1,67 @@
1
+ import subprocess
2
+
3
+ import click
4
+ from click.exceptions import Exit
5
+
6
+ from afterpython.utils import has_pixi, has_uv
7
+
8
+
9
+ @click.command()
10
+ @click.option(
11
+ "--optional",
12
+ type=str,
13
+ default=None,
14
+ required=False,
15
+ help="Remove from an optional dependency group (pixi: mapped to 'optional' feature)",
16
+ )
17
+ @click.option(
18
+ "--group",
19
+ type=str,
20
+ default=None,
21
+ required=False,
22
+ help="Remove from a dependency group (pixi: mapped to same-named feature)",
23
+ )
24
+ @click.argument("lib", type=str, required=True)
25
+ def remove(optional: str | None, group: str | None, lib: str):
26
+ """Remove a dependency from the project (manages both uv and pixi if present)"""
27
+ if not has_uv():
28
+ click.echo("uv not found. Please install uv first.")
29
+ return
30
+
31
+ if optional and group:
32
+ click.echo("Error: Cannot specify both --optional and --group")
33
+ raise Exit(1)
34
+
35
+ result = subprocess.run(
36
+ [
37
+ "uv",
38
+ "remove",
39
+ *(["--optional", optional] if optional else []),
40
+ *(["--group", group] if group else []),
41
+ lib,
42
+ ],
43
+ check=False,
44
+ )
45
+ if result.returncode != 0:
46
+ raise Exit(result.returncode)
47
+
48
+ if has_pixi():
49
+ # NOTE: pixi doesn't support --optional, so we need to use --feature instead
50
+ if optional:
51
+ feature_name = "optional"
52
+ elif group:
53
+ feature_name = group
54
+ else:
55
+ feature_name = None
56
+ result = subprocess.run(
57
+ [
58
+ "pixi",
59
+ "remove",
60
+ "--pypi",
61
+ *(["--feature", feature_name] if feature_name else []),
62
+ lib,
63
+ ],
64
+ check=False,
65
+ )
66
+ if result.returncode != 0:
67
+ raise Exit(result.returncode)
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -9,7 +10,6 @@ import subprocess
9
10
  import click
10
11
  from click.exceptions import Exit
11
12
 
12
-
13
13
  command_kwargs = {
14
14
  "add_help_option": False, # disable click's --help option so that myst start --help can work
15
15
  "context_settings": dict(
@@ -43,9 +43,12 @@ command_kwargs = {
43
43
  )
44
44
  def start(ctx, doc: bool, blog: bool, tutorial: bool, example: bool, guide: bool):
45
45
  """Simple wrapper for `myst start` for convenience"""
46
- from afterpython.utils import find_node_env, has_content_for_myst
47
46
  from afterpython.const import CONTENT_TYPES
48
- from afterpython.utils import handle_passthrough_help
47
+ from afterpython.utils import (
48
+ find_node_env,
49
+ handle_passthrough_help,
50
+ has_content_for_myst,
51
+ )
49
52
 
50
53
  # Show both our options and myst's help and exit
51
54
  handle_passthrough_help(
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
5
6
  from afterpython._typing import NodeEnv
6
7
  from afterpython.pcu import Dependencies
7
8
 
8
- import subprocess
9
9
  import shutil
10
+ import subprocess
10
11
 
11
12
  import click
12
13
  from click.exceptions import Exit
@@ -33,7 +34,7 @@ def update():
33
34
  def dependencies(upgrade: bool, all: bool):
34
35
  """Update pyproject.toml dependencies to the latest version"""
35
36
  from afterpython.pcu import get_dependencies, update_dependencies
36
- from afterpython.utils import has_uv
37
+ from afterpython.utils import has_pixi, has_uv
37
38
 
38
39
  dependencies: Dependencies = get_dependencies()
39
40
  has_at_least_one_update = False
@@ -79,7 +80,9 @@ def dependencies(upgrade: bool, all: bool):
79
80
  raise Exit(result.returncode)
80
81
  click.echo(
81
82
  click.style(
82
- "✓ All dependencies upgraded successfully 🎉", fg="green", bold=True
83
+ "✓ All dependencies in pyproject.toml upgraded successfully 🎉",
84
+ fg="green",
85
+ bold=True,
83
86
  )
84
87
  )
85
88
  else:
@@ -89,6 +92,26 @@ def dependencies(upgrade: bool, all: bool):
89
92
  if all:
90
93
  subprocess.run(["ap", "pre-commit", "autoupdate"])
91
94
  click.echo("All pre-commit hooks updated successfully.")
95
+ if has_pixi():
96
+ click.echo("Upgrading dependencies with pixi...")
97
+ result = subprocess.run(
98
+ ["pixi", "upgrade", "--exclude", "python"], check=False
99
+ )
100
+ if result.returncode != 0:
101
+ raise Exit(result.returncode)
102
+ result = subprocess.run(["pixi", "lock"], check=False)
103
+ if result.returncode != 0:
104
+ raise Exit(result.returncode)
105
+ result = subprocess.run(["pixi", "install"], check=False)
106
+ if result.returncode != 0:
107
+ raise Exit(result.returncode)
108
+ click.echo(
109
+ click.style(
110
+ "✓ All dependencies in pixi.toml upgraded successfully 🎉",
111
+ fg="green",
112
+ bold=True,
113
+ )
114
+ )
92
115
 
93
116
 
94
117
  update.add_command(dependencies, name="deps") # alias for "dependencies"
@@ -1,33 +1,89 @@
1
1
  import os
2
2
  import subprocess
3
+ from collections import defaultdict
3
4
 
4
5
  import click
6
+ from dotenv import find_dotenv, load_dotenv
5
7
  from trogon import tui
6
- from dotenv import load_dotenv, find_dotenv
7
8
 
8
9
  import afterpython as ap
9
10
  from afterpython import __version__
10
- from afterpython.cli.commands.init import init
11
+ from afterpython.cli.commands.add import add
11
12
  from afterpython.cli.commands.build import build
12
- from afterpython.cli.commands.dev import dev
13
- from afterpython.cli.commands.update import update
13
+ from afterpython.cli.commands.bump import bump
14
14
  from afterpython.cli.commands.check import check
15
- from afterpython.cli.commands.install import install
16
- from afterpython.cli.commands.format import format
17
- from afterpython.cli.commands.sync import sync
18
- from afterpython.cli.commands.start import start, doc, blog, tutorial, example, guide
19
- from afterpython.cli.commands.preview import preview
20
15
  from afterpython.cli.commands.clean import clean
21
- from afterpython.cli.commands.pre_commit import pre_commit
22
- from afterpython.cli.commands.commitizen import commitizen
23
16
  from afterpython.cli.commands.commit import commit
24
- from afterpython.cli.commands.bump import bump
25
- from afterpython.cli.commands.release import release
17
+ from afterpython.cli.commands.commitizen import commitizen
18
+ from afterpython.cli.commands.dev import dev
19
+ from afterpython.cli.commands.format import format
20
+ from afterpython.cli.commands.init import init
26
21
  from afterpython.cli.commands.init_branch_rules import init_branch_rules
22
+ from afterpython.cli.commands.install import install
23
+ from afterpython.cli.commands.lock import lock
24
+ from afterpython.cli.commands.pre_commit import pre_commit
25
+ from afterpython.cli.commands.preview import preview
26
+ from afterpython.cli.commands.release import release
27
+ from afterpython.cli.commands.remove import remove
28
+ from afterpython.cli.commands.start import blog, doc, example, guide, start, tutorial
29
+ from afterpython.cli.commands.sync import sync
30
+ from afterpython.cli.commands.update import update
31
+
32
+
33
+ class AliasGroup(click.Group):
34
+ """Custom group that displays command aliases together."""
35
+
36
+ def __init__(self, *args, **kwargs):
37
+ super().__init__(*args, **kwargs)
38
+ self._aliases = defaultdict(list) # Maps primary command -> list of aliases
39
+ self._alias_to_primary = {} # Maps alias -> primary command name
40
+
41
+ def add_command(self, cmd, name=None):
42
+ """Track aliases when the same command is added with different names."""
43
+ name = name or cmd.name
44
+
45
+ # Check if this command object was already added under a different name
46
+ for existing_name, existing_cmd in self.commands.items():
47
+ if existing_cmd is cmd and existing_name != name:
48
+ # This is an alias
49
+ self._aliases[existing_name].append(name)
50
+ self._alias_to_primary[name] = existing_name
51
+ break
52
+
53
+ super().add_command(cmd, name)
54
+
55
+ def format_commands(self, ctx, formatter):
56
+ """Format commands with aliases shown together."""
57
+ commands = []
58
+ for subcommand in self.list_commands(ctx):
59
+ # Skip aliases - they'll be shown with their primary command
60
+ if subcommand in self._alias_to_primary:
61
+ continue
62
+
63
+ cmd = self.get_command(ctx, subcommand)
64
+ if cmd is None:
65
+ continue
66
+
67
+ # Skip hidden commands
68
+ if cmd.hidden:
69
+ continue
70
+
71
+ help_text = cmd.get_short_help_str(limit=formatter.width)
72
+
73
+ # Build command name with aliases
74
+ aliases = self._aliases.get(subcommand, [])
75
+ if aliases:
76
+ subcommand = f"{subcommand}, {', '.join(aliases)}"
77
+
78
+ commands.append((subcommand, help_text))
79
+
80
+ if commands:
81
+ with formatter.section("Commands"):
82
+ formatter.write_dl(commands)
27
83
 
28
84
 
29
85
  @tui(command="tui", help="Open terminal UI")
30
- @click.group(context_settings={"help_option_names": ["-h", "--help"]})
86
+ @click.group(cls=AliasGroup, context_settings={"help_option_names": ["-h", "--help"]})
31
87
  @click.pass_context
32
88
  @click.version_option(version=__version__)
33
89
  def afterpython_group(ctx):
@@ -65,10 +121,12 @@ afterpython_group.add_command(preview)
65
121
  afterpython_group.add_command(clean)
66
122
  afterpython_group.add_command(pre_commit)
67
123
  afterpython_group.add_command(pre_commit, name="pc")
68
- afterpython_group.add_command(pre_commit, name="precommit")
69
124
  afterpython_group.add_command(commitizen)
70
125
  afterpython_group.add_command(commitizen, name="cz")
71
126
  afterpython_group.add_command(commit)
72
127
  afterpython_group.add_command(bump)
73
128
  afterpython_group.add_command(release)
74
129
  afterpython_group.add_command(init_branch_rules)
130
+ afterpython_group.add_command(add)
131
+ afterpython_group.add_command(remove)
132
+ afterpython_group.add_command(lock)
@@ -1,5 +1,4 @@
1
1
  from afterpython._typing import tContentType
2
2
 
3
-
4
3
  NODEENV_VERSION = "24.11.0"
5
4
  CONTENT_TYPES: set[tContentType] = {"doc", "blog", "tutorial", "example", "guide"}
@@ -3,7 +3,8 @@ pcu = "pip check updates", similar to ncu (npm check updates in Node.js)
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
- from typing import TYPE_CHECKING, TypedDict, NamedTuple, TypeAlias
6
+
7
+ from typing import TYPE_CHECKING, NamedTuple, TypeAlias, TypedDict
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from tomlkit.toml_document import TOMLDocument
@@ -60,6 +61,7 @@ async def get_latest_versions(
60
61
  ) -> dict[str, Version | None]:
61
62
  """Get latest versions for a list of dependencies from PyPI."""
62
63
  import httpx
64
+
63
65
  from afterpython.utils import fetch_pypi_json
64
66
 
65
67
  async def fetch_version(
@@ -18,7 +18,7 @@ ignore = [
18
18
  ]
19
19
  # extended rules:
20
20
  extend-select = [
21
- # "I", # isort - import sorting
21
+ "I", # isort - import sorting
22
22
  "UP", # pyupgrade - upgrade syntax for newer Python
23
23
  "B", # flake8-bugbear - find likely bugs
24
24
  "SIM", # flake8-simplify - simplify code
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -7,7 +8,7 @@ if TYPE_CHECKING:
7
8
  import tomlkit
8
9
 
9
10
  import afterpython as ap
10
- from afterpython._io.toml import read_toml, write_toml, _to_tomlkit
11
+ from afterpython._io.toml import _to_tomlkit, read_toml, write_toml
11
12
 
12
13
 
13
14
  def read_afterpython() -> TOMLDocument:
@@ -9,7 +9,6 @@ from copy import deepcopy
9
9
 
10
10
  from afterpython.tools._git import is_gh_authenticated
11
11
 
12
-
13
12
  DEFAULT_RULESET = {
14
13
  "name": "afterpython-default", # The ruleset identifier in GitHub
15
14
  "target": "branch", # This ruleset applies to branches (not tags)
@@ -1,6 +1,5 @@
1
1
  import afterpython as ap
2
2
 
3
-
4
3
  commitizen_default = {
5
4
  "tool": {
6
5
  "commitizen": {
@@ -36,8 +35,8 @@ commitizen_pre_commit_hook = {
36
35
 
37
36
 
38
37
  def init_commitizen():
39
- from afterpython.tools.pre_commit import update_pre_commit
40
38
  from afterpython._io.toml import write_toml
39
+ from afterpython.tools.pre_commit import update_pre_commit
41
40
 
42
41
  afterpython_path = ap.paths.afterpython_path
43
42
  commitizen_toml_path = afterpython_path / "cz.toml"
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
5
6
  from pathlib import Path
6
- from afterpython._typing import tContentType, NodeEnv
7
+
8
+ from afterpython._typing import NodeEnv, tContentType
7
9
 
8
10
  import subprocess
9
11
 
10
12
  import afterpython as ap
13
+ from afterpython._io.yaml import read_yaml, write_yaml
11
14
  from afterpython.utils import deep_merge
12
- from afterpython._io.yaml import write_yaml, read_yaml
13
15
 
14
16
 
15
17
  def update_authors_yml(data_update: dict):
@@ -1,9 +1,7 @@
1
1
  import subprocess
2
2
 
3
3
  import afterpython as ap
4
-
5
- from afterpython._io.yaml import write_yaml, read_yaml
6
-
4
+ from afterpython._io.yaml import read_yaml, write_yaml
7
5
 
8
6
  pre_commit_default = {
9
7
  # default is only ["pre-commit"], without changing the default,
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -9,7 +10,7 @@ import asyncio
9
10
  from pyproject_metadata import StandardMetadata
10
11
 
11
12
  import afterpython as ap
12
- from afterpython._io.toml import read_toml, write_toml, _to_tomlkit
13
+ from afterpython._io.toml import _to_tomlkit, read_toml, write_toml
13
14
 
14
15
 
15
16
  def read_pyproject() -> TOMLDocument:
@@ -32,8 +33,9 @@ def init_pyproject():
32
33
  - add [project.urls] section with homepage, repository, and documentation URLs
33
34
  """
34
35
  import httpx
35
- from afterpython.utils import fetch_pypi_json
36
+
36
37
  from afterpython.tools._git import get_git_user_config, get_github_url
38
+ from afterpython.utils import fetch_pypi_json
37
39
 
38
40
  build_backend = "uv_build"
39
41
 
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
5
6
  from pathlib import Path
7
+
6
8
  from httpx import AsyncClient
9
+
7
10
  from afterpython._typing import NodeEnv
8
11
 
9
12
  import os
@@ -20,6 +23,7 @@ def find_node_env() -> NodeEnv:
20
23
  If no, install the Node.js version specified in NODEENV_VERSION
21
24
  """
22
25
  from mystmd_py.nodeenv import find_any_node
26
+
23
27
  from afterpython.const import NODEENV_VERSION
24
28
 
25
29
  # from mystmd_py.main import ensure_valid_version
@@ -1,5 +1,5 @@
1
- from pathlib import Path
2
1
  from dataclasses import dataclass, field
2
+ from pathlib import Path
3
3
 
4
4
 
5
5
  @dataclass
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -8,7 +9,6 @@ import json
8
9
 
9
10
  import afterpython as ap
10
11
 
11
-
12
12
  build_path = ap.paths.build_path
13
13
 
14
14
 
@@ -10,8 +10,8 @@ from afterpython.utils import convert_author_name_to_id
10
10
 
11
11
  def _sync_authors_yml(authors: list[tuple[str, str | None]]):
12
12
  """Sync authors.yml with authors in pyproject.toml"""
13
- from afterpython.tools.myst import update_authors_yml
14
13
  from afterpython._io.yaml import read_yaml
14
+ from afterpython.tools.myst import update_authors_yml
15
15
 
16
16
  # read myst.yml from docs path to get "version"
17
17
  doc_myst_yml = read_yaml(ap.paths.afterpython_path / "doc" / "myst.yml")
@@ -37,15 +37,15 @@ def _sync_authors_yml(authors: list[tuple[str, str | None]]):
37
37
  @click.command()
38
38
  def sync():
39
39
  """Sync between pyproject.toml+afterpython.toml and authors.yml+myst.yml files"""
40
+ from afterpython._io.toml import _from_tomlkit
40
41
  from afterpython.const import CONTENT_TYPES
41
42
  from afterpython.tools._afterpython import read_afterpython
43
+ from afterpython.tools.myst import update_myst_yml
42
44
  from afterpython.tools.pyproject import (
43
45
  read_metadata,
44
46
  read_pyproject,
45
47
  write_pyproject,
46
48
  )
47
- from afterpython._io.toml import _from_tomlkit
48
- from afterpython.tools.myst import update_myst_yml
49
49
 
50
50
  pyproject: StandardMetadata = read_metadata()
51
51
  project_name = str(pyproject.name)