afterpython 0.3.10__tar.gz → 0.3.12__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 (64) hide show
  1. {afterpython-0.3.10 → afterpython-0.3.12}/PKG-INFO +2 -2
  2. {afterpython-0.3.10 → afterpython-0.3.12}/pyproject.toml +2 -2
  3. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/marimo_notebook.py +28 -4
  4. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/init.py +69 -39
  5. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/_afterpython.py +1 -0
  6. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/myst.py +59 -4
  7. {afterpython-0.3.10 → afterpython-0.3.12}/README.md +0 -0
  8. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/__init__.py +0 -0
  9. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/_io/toml.py +0 -0
  10. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/_io/yaml.py +0 -0
  11. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/_paths.py +0 -0
  12. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/_typing.py +0 -0
  13. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/__init__.py +0 -0
  14. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/build_pdoc.py +0 -0
  15. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/content_json.py +0 -0
  16. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/faq_json.py +0 -0
  17. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/index_md.py +0 -0
  18. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/jupyter_notebook.py +0 -0
  19. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/llms_txt.py +0 -0
  20. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/markdown.py +0 -0
  21. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/metadata.py +0 -0
  22. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/builders/url_md.py +0 -0
  23. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/__init__.py +0 -0
  24. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/add.py +0 -0
  25. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/build.py +0 -0
  26. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/bump.py +0 -0
  27. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/check.py +0 -0
  28. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/clean.py +0 -0
  29. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/commit.py +0 -0
  30. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/commitizen.py +0 -0
  31. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/dev.py +0 -0
  32. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/format.py +0 -0
  33. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/init_branch_rules.py +0 -0
  34. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/install.py +0 -0
  35. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/lock.py +0 -0
  36. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/pre_commit.py +0 -0
  37. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/preview.py +0 -0
  38. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/release.py +0 -0
  39. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/remove.py +0 -0
  40. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/serve.py +0 -0
  41. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/start.py +0 -0
  42. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/sync.py +0 -0
  43. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/commands/update.py +0 -0
  44. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/cli/main.py +0 -0
  45. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/const.py +0 -0
  46. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/main.py +0 -0
  47. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/pcu.py +0 -0
  48. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/py.typed +0 -0
  49. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/ci-workflow-template.yml +0 -0
  50. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/cz-template.toml +0 -0
  51. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/dependabot-template.yml +0 -0
  52. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/deploy-workflow-template.yml +0 -0
  53. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/pdoc/frame.html.jinja2 +0 -0
  54. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/pre-commit-config-template.yaml +0 -0
  55. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/release-workflow-template.yml +0 -0
  56. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/templates/ruff-template.toml +0 -0
  57. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/_git.py +0 -0
  58. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/branch_rules.py +0 -0
  59. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/commitizen.py +0 -0
  60. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/github_actions.py +0 -0
  61. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/pre_commit.py +0 -0
  62. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/tools/pyproject.py +0 -0
  63. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/utils.py +0 -0
  64. {afterpython-0.3.10 → afterpython-0.3.12}/src/afterpython/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: afterpython
3
- Version: 0.3.10
3
+ Version: 0.3.12
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
@@ -23,7 +23,7 @@ Requires-Dist: gitpython>=3.1.50
23
23
  Requires-Dist: python-dotenv>=1.2.2
24
24
  Requires-Dist: pyproject-metadata>=0.11.0
25
25
  Requires-Dist: jupytext>=1.19.2
26
- Requires-Dist: mystmd>=1.9.0
26
+ Requires-Dist: mystmd>=1.9.1
27
27
  Requires-Dist: pdoc>=16.0.0
28
28
  Requires-Dist: ruff>=0.15.12
29
29
  Requires-Dist: pre-commit>=4.6.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "afterpython"
3
- version = "0.3.10"
3
+ version = "0.3.12"
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 = [
@@ -43,7 +43,7 @@ dependencies = [
43
43
  "pyproject-metadata>=0.11.0",
44
44
  "jupytext>=1.19.2",
45
45
  # "jinja2>=3.1.6",
46
- "mystmd>=1.9.0",
46
+ "mystmd>=1.9.1",
47
47
  "pdoc>=16.0.0",
48
48
  "ruff>=0.15.12",
49
49
  "pre-commit>=4.6.0",
@@ -41,15 +41,33 @@ def is_marimo_notebook(path: Path) -> bool:
41
41
  return "__generated_with" in head and "marimo.App(" in head
42
42
 
43
43
 
44
- def _export_marimo(source: Path, output_html: Path, mode: MarimoExportMode):
44
+ def _export_marimo(
45
+ source: Path,
46
+ output_html: Path,
47
+ mode: MarimoExportMode,
48
+ execute: bool = False,
49
+ ):
45
50
  """Run `marimo export` to produce HTML at `output_html`."""
46
51
  output_html.parent.mkdir(parents=True, exist_ok=True)
47
52
  subcommand = "html-wasm" if mode == "wasm" else "html"
48
- cmd = ["marimo", "export", subcommand, str(source), "-o", str(output_html)]
53
+ cmd = [
54
+ "marimo",
55
+ "export",
56
+ subcommand,
57
+ str(source),
58
+ "-o",
59
+ str(output_html),
60
+ "--sandbox",
61
+ "--force",
62
+ ]
49
63
  # WASM-only flag: --mode edit shows code cells (interactive). Without it,
50
64
  # marimo's html-wasm subcommand defaults to "run" which hides them.
65
+ # --sandbox auto-answers marimo's interactive "Run in a sandboxed venv?"
66
+ # prompt; non-interactive builds (CI, `ap build`) would otherwise hang.
51
67
  if mode == "wasm":
52
68
  cmd += ["--mode", "edit"]
69
+ if execute:
70
+ cmd += ["--execute"]
53
71
  result = subprocess.run(cmd, check=False)
54
72
  if result.returncode != 0:
55
73
  raise click.ClickException(f"marimo export failed for {source}")
@@ -85,6 +103,7 @@ def build_marimo_notebook(
85
103
  output_html: Path,
86
104
  content_path: Path,
87
105
  mode: MarimoExportMode = "wasm",
106
+ execute: bool = False,
88
107
  ):
89
108
  """Build a single marimo notebook to HTML and inject the molab badge.
90
109
 
@@ -104,7 +123,7 @@ def build_marimo_notebook(
104
123
  return
105
124
 
106
125
  click.echo(f"Building marimo notebook {source.name} (mode={mode})...")
107
- _export_marimo(source, output_html, mode)
126
+ _export_marimo(source, output_html, mode, execute=execute)
108
127
 
109
128
  # WASM mode runs the notebook in-browser via Pyodide, so molab (a remote
110
129
  # runtime) is redundant. Skip the badge.
@@ -136,9 +155,14 @@ def build_marimo_readme(mode: MarimoExportMode = "wasm"):
136
155
  Skips silently if README.py is absent or isn't a marimo notebook so users
137
156
  who keep only README.md aren't penalized.
138
157
  """
158
+ from afterpython._io.toml import _from_tomlkit
159
+ from afterpython.tools._afterpython import read_afterpython
160
+
139
161
  readme_path = ap.paths.afterpython_path / "README.py"
140
162
  if not readme_path.exists():
141
163
  return
142
164
  output_html = ap.paths.build_path / "readme_py" / "readme_py.html"
143
165
  content_path = Path("README.py")
144
- build_marimo_notebook(readme_path, output_html, content_path, mode)
166
+ website = _from_tomlkit(read_afterpython().get("website", {}))
167
+ execute = bool(website.get("execute_readme_py", False))
168
+ build_marimo_notebook(readme_path, output_html, content_path, mode, execute=execute)
@@ -43,6 +43,16 @@ def init_py_typed():
43
43
  click.echo(f"Created {py_typed_path}")
44
44
 
45
45
 
46
+ def _preflight_init(skip_website: bool) -> None:
47
+ """Validate external prerequisites before any filesystem writes, so a
48
+ failure aborts cleanly instead of leaving a half-initialized project."""
49
+ if not skip_website:
50
+ from afterpython.tools.myst import ensure_pnpm_11
51
+ from afterpython.utils import find_node_env
52
+
53
+ ensure_pnpm_11(find_node_env())
54
+
55
+
46
56
  @click.group(invoke_without_command=True)
47
57
  @click.option(
48
58
  "--yes",
@@ -70,48 +80,60 @@ def init(ctx, yes, skip_website: bool):
70
80
  from afterpython.tools.pre_commit import init_pre_commit
71
81
  from afterpython.tools.pyproject import init_pyproject
72
82
 
83
+ _preflight_init(skip_website)
84
+
73
85
  paths = ctx.obj["paths"]
74
86
  click.echo("Initializing afterpython...")
75
87
  afterpython_path = paths.afterpython_path
76
88
  static_path = paths.static_path
77
89
 
78
- afterpython_path.mkdir(parents=True, exist_ok=True)
79
- static_path.mkdir(parents=True, exist_ok=True)
80
-
81
- init_pyproject()
82
-
83
- init_afterpython()
84
-
85
- if not skip_website:
86
- subprocess.run(["ap", "init", "website"])
87
-
88
- # TODO: add type checking related stuff here
89
- init_py_typed()
90
-
91
- create_workflow("ci")
92
-
93
- if yes or click.confirm(
94
- f"\nCreate .pre-commit-config.yaml in {afterpython_path}?", default=True
95
- ):
96
- init_pre_commit()
97
-
98
- if yes or click.confirm(f"\nCreate ruff.toml in {afterpython_path}?", default=True):
99
- init_ruff_toml()
100
-
101
- if yes or click.confirm(
102
- f"\nCreate commitizen configuration (cz.toml) in {afterpython_path} "
103
- f"and release workflow in .github/workflows/release.yml?",
104
- default=True,
105
- ):
106
- init_commitizen()
107
- create_workflow("release")
108
-
109
- if yes or click.confirm(
110
- "\nCreate Dependabot configuration (.github/dependabot.yml) "
111
- "to auto-update GitHub Actions versions?",
112
- default=True,
113
- ):
114
- create_dependabot()
90
+ try:
91
+ afterpython_path.mkdir(parents=True, exist_ok=True)
92
+ static_path.mkdir(parents=True, exist_ok=True)
93
+
94
+ init_pyproject()
95
+
96
+ init_afterpython()
97
+
98
+ if not skip_website:
99
+ # check=True so a failure inside `ap init website` aborts the parent
100
+ # run instead of silently continuing to write more files.
101
+ subprocess.run(["ap", "init", "website"], check=True)
102
+
103
+ # TODO: add type checking related stuff here
104
+ init_py_typed()
105
+
106
+ create_workflow("ci")
107
+
108
+ if yes or click.confirm(
109
+ f"\nCreate .pre-commit-config.yaml in {afterpython_path}?", default=True
110
+ ):
111
+ init_pre_commit()
112
+
113
+ if yes or click.confirm(
114
+ f"\nCreate ruff.toml in {afterpython_path}?", default=True
115
+ ):
116
+ init_ruff_toml()
117
+
118
+ if yes or click.confirm(
119
+ f"\nCreate commitizen configuration (cz.toml) in {afterpython_path} "
120
+ f"and release workflow in .github/workflows/release.yml?",
121
+ default=True,
122
+ ):
123
+ init_commitizen()
124
+ create_workflow("release")
125
+
126
+ if yes or click.confirm(
127
+ "\nCreate Dependabot configuration (.github/dependabot.yml) "
128
+ "to auto-update GitHub Actions versions?",
129
+ default=True,
130
+ ):
131
+ create_dependabot()
132
+ except BaseException:
133
+ if afterpython_path.exists():
134
+ click.echo(f"ap init failed — removing {afterpython_path}", err=True)
135
+ shutil.rmtree(afterpython_path, ignore_errors=True)
136
+ raise
115
137
 
116
138
 
117
139
  @init.command("website")
@@ -122,10 +144,18 @@ def init_website_subcommand():
122
144
  the website to an existing AfterPython project.
123
145
  """
124
146
  from afterpython.tools.github_actions import create_workflow
125
- from afterpython.tools.myst import init_myst
147
+ from afterpython.tools.myst import ensure_pnpm_11, init_myst
148
+ from afterpython.utils import find_node_env
149
+
150
+ # Pre-flight: same rationale as `ap init` — guard the standalone entry too,
151
+ # since this subcommand can be invoked directly via `ap init website`.
152
+ ensure_pnpm_11(find_node_env())
126
153
 
127
154
  init_faq()
128
155
  init_myst()
129
156
  click.echo(f"Initializing project website template in {ap.paths.website_path}...")
130
- subprocess.run(["ap", "update", "website"])
157
+ # check=True so a network/pnpm failure inside `ap update website` aborts
158
+ # the subcommand instead of silently dropping the deploy workflow on top
159
+ # of a broken website install.
160
+ subprocess.run(["ap", "update", "website"], check=True)
131
161
  create_workflow("deploy")
@@ -125,6 +125,7 @@ def init_afterpython():
125
125
  "thumbnail": "thumbnail.png",
126
126
  "announcement": "",
127
127
  "readme_py": "wasm",
128
+ "execute_readme_py": False,
128
129
  "api_reference": False,
129
130
  },
130
131
  }
@@ -179,6 +179,56 @@ Start building your amazing project! 🚀
179
179
  return welcome_file
180
180
 
181
181
 
182
+ def ensure_pnpm_11(node_env: NodeEnv) -> None:
183
+ """Ensure pnpm 11.x is available on `node_env`'s PATH. No-op if already satisfied.
184
+
185
+ Pinned to major 11 because an unpinned `npm install -g pnpm` previously did
186
+ a silent 9→10 jump that broke `ap init` via ERR_PNPM_IGNORED_BUILDS. We
187
+ pre-check the installed version so we don't redundantly invoke `npm install
188
+ -g`, which fails with EEXIST when pnpm was installed by a different package
189
+ manager (e.g. Homebrew) that owns the shim filenames npm wants to write.
190
+ """
191
+ current: str | None = None
192
+ try:
193
+ result = subprocess.run(
194
+ ["pnpm", "--version"],
195
+ env=node_env,
196
+ capture_output=True,
197
+ text=True,
198
+ check=False,
199
+ )
200
+ if result.returncode == 0:
201
+ current = result.stdout.strip()
202
+ if current.startswith("11."):
203
+ return
204
+ except FileNotFoundError:
205
+ pass
206
+
207
+ install = subprocess.run(
208
+ ["npm", "install", "-g", "pnpm@11"],
209
+ env=node_env,
210
+ capture_output=True,
211
+ text=True,
212
+ check=False,
213
+ )
214
+ if install.returncode == 0:
215
+ return
216
+
217
+ msg = ["Failed to install pnpm@11 via npm."]
218
+ if current:
219
+ msg.append(f"Detected existing pnpm version: {current}")
220
+ msg.append(
221
+ "If pnpm is already installed via another package manager "
222
+ "(e.g. Homebrew: `brew install pnpm`), npm refuses to overwrite "
223
+ "its shims. Please either:\n"
224
+ " - upgrade your existing pnpm to major version 11, or\n"
225
+ " - uninstall it and let afterpython manage pnpm via npm."
226
+ )
227
+ if install.stderr:
228
+ msg.append(f"\nnpm stderr:\n{install.stderr}")
229
+ raise RuntimeError("\n".join(msg))
230
+
231
+
182
232
  def init_myst():
183
233
  """
184
234
  Initialize MyST Markdown (mystmd) and myst.yml files in
@@ -189,14 +239,19 @@ def init_myst():
189
239
 
190
240
  # find any existing node.js version and use it, if no, install the Node.js version specified in NODEENV_VERSION
191
241
  node_env: NodeEnv = find_node_env()
192
- # Pin to pnpm major version — `npm install -g pnpm` (unpinned) caused a
193
- # silent 9→10 jump that broke `ap init` via ERR_PNPM_IGNORED_BUILDS.
194
- subprocess.run(["npm", "install", "-g", "pnpm@11"], env=node_env, check=True)
242
+ ensure_pnpm_11(node_env)
195
243
  for content_type in CONTENT_TYPES:
196
244
  path = ap.paths.afterpython_path / content_type
197
245
  print(f"Initializing MyST Markdown (mystmd) in {path.name}/ directory ...")
198
246
  path.mkdir(parents=True, exist_ok=True)
199
- subprocess.run(["myst", "init"], cwd=path, input="n\n", text=True, env=node_env)
247
+ subprocess.run(
248
+ ["myst", "init"],
249
+ cwd=path,
250
+ input="n\n",
251
+ text=True,
252
+ env=node_env,
253
+ check=True,
254
+ )
200
255
  myst_yml_defaults = {
201
256
  "extends": "../authors.yml",
202
257
  "project": {
File without changes