afterpython 0.1.0__py3-none-any.whl → 0.1.0.dev6__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.
- afterpython/{templates/pre-commit-config-template.yaml → .pre-commit-config.yaml} +1 -1
- afterpython/_io/toml.py +3 -5
- afterpython/_paths.py +1 -1
- afterpython/_typing.py +2 -1
- afterpython/builders/__init__.py +3 -12
- afterpython/builders/metadata.py +3 -7
- afterpython/builders/molab.py +99 -0
- afterpython/cli/__init__.py +1 -0
- afterpython/cli/commands/build.py +68 -97
- afterpython/cli/commands/bump.py +0 -30
- afterpython/cli/commands/check.py +1 -1
- afterpython/cli/commands/clean.py +3 -4
- afterpython/cli/commands/commit.py +3 -21
- afterpython/cli/commands/commitizen.py +1 -1
- afterpython/cli/commands/dev.py +9 -13
- afterpython/cli/commands/format.py +1 -1
- afterpython/cli/commands/init.py +39 -48
- afterpython/cli/commands/install.py +2 -8
- afterpython/cli/commands/pre_commit.py +1 -1
- afterpython/cli/commands/preview.py +1 -1
- afterpython/cli/commands/start.py +4 -7
- afterpython/cli/commands/sync.py +21 -53
- afterpython/cli/commands/update.py +8 -31
- afterpython/cli/main.py +14 -75
- afterpython/const.py +1 -0
- afterpython/cz.toml +27 -0
- afterpython/pcu.py +1 -3
- afterpython/templates/deploy-workflow-template.yml +5 -5
- afterpython/templates/release-workflow-template.yml +3 -3
- afterpython/templates/ruff-template.toml +1 -1
- afterpython/tools/_afterpython.py +10 -73
- afterpython/tools/_git.py +2 -28
- afterpython/tools/commitizen.py +45 -5
- afterpython/tools/github_actions.py +53 -23
- afterpython/tools/myst.py +5 -46
- afterpython/tools/pre_commit.py +31 -6
- afterpython/tools/pyproject.py +2 -40
- afterpython/utils.py +0 -82
- {afterpython-0.1.0.dist-info → afterpython-0.1.0.dev6.dist-info}/METADATA +10 -34
- afterpython-0.1.0.dev6.dist-info/RECORD +47 -0
- {afterpython-0.1.0.dist-info → afterpython-0.1.0.dev6.dist-info}/WHEEL +1 -1
- afterpython/builders/content_json.py +0 -131
- afterpython/builders/index_md.py +0 -94
- afterpython/builders/jupyter_notebook.py +0 -293
- afterpython/builders/markdown.py +0 -271
- afterpython/cli/commands/add.py +0 -67
- afterpython/cli/commands/init_branch_rules.py +0 -9
- afterpython/cli/commands/lock.py +0 -23
- afterpython/cli/commands/remove.py +0 -67
- afterpython/templates/ci-workflow-template.yml +0 -197
- afterpython/templates/cz-template.toml +0 -7
- afterpython/templates/dependabot-template.yml +0 -30
- afterpython/tools/branch_rules.py +0 -106
- afterpython-0.1.0.dist-info/RECORD +0 -57
- {afterpython-0.1.0.dist-info → afterpython-0.1.0.dev6.dist-info}/entry_points.txt +0 -0
|
@@ -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,71 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
2
|
from typing import TYPE_CHECKING
|
|
4
3
|
|
|
5
4
|
if TYPE_CHECKING:
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
5
|
from tomlkit.toml_document import TOMLDocument
|
|
9
6
|
|
|
10
|
-
from afterpython._typing import tContentType
|
|
11
|
-
|
|
12
7
|
import tomlkit
|
|
13
8
|
|
|
14
9
|
import afterpython as ap
|
|
15
|
-
from afterpython._io.toml import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_default_thumbnail(
|
|
19
|
-
content_type: tContentType, file_path: Path | None = None
|
|
20
|
-
) -> str:
|
|
21
|
-
"""Get the default thumbnail value for a content type.
|
|
22
|
-
|
|
23
|
-
Reads default thumbnail configuration from afterpython.toml:
|
|
24
|
-
- Content type specific thumbnail: [website.{content_type}.thumbnail]
|
|
25
|
-
- Website-wide default thumbnail: [website.thumbnail]
|
|
26
|
-
- Falls back to empty string if no default is configured
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
content_type: The content type (blog, tutorial, etc.)
|
|
30
|
-
file_path: Optional path to the file needing the thumbnail (for calculating relative path)
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
The thumbnail value to use (e.g., "./static/image.png", "../static/image.png", or "")
|
|
34
|
-
"""
|
|
35
|
-
from afterpython._io.toml import _from_tomlkit
|
|
36
|
-
from afterpython.utils import get_relative_static_prefix, normalize_static_path
|
|
37
|
-
|
|
38
|
-
afterpython = read_afterpython()
|
|
39
|
-
try:
|
|
40
|
-
# website_default_thumbnail = normalize_static_path(
|
|
41
|
-
# str(_from_tomlkit(afterpython.get("website", {})).get("thumbnail", ""))
|
|
42
|
-
# )
|
|
43
|
-
content_type_default_thumbnail = normalize_static_path(
|
|
44
|
-
str(
|
|
45
|
-
_from_tomlkit(afterpython.get("website", {}))
|
|
46
|
-
.get(content_type, {})
|
|
47
|
-
.get("thumbnail", "")
|
|
48
|
-
)
|
|
49
|
-
)
|
|
50
|
-
except ValueError as e:
|
|
51
|
-
print(f"Error in reading afterpython.toml for {content_type=}: {e}")
|
|
52
|
-
return "null"
|
|
53
|
-
|
|
54
|
-
# Determine the static prefix based on file location
|
|
55
|
-
if file_path is not None:
|
|
56
|
-
content_type_path = ap.paths.afterpython_path / content_type.lower()
|
|
57
|
-
static_prefix = get_relative_static_prefix(file_path, content_type_path)
|
|
58
|
-
else:
|
|
59
|
-
static_prefix = "./" # Default fallback
|
|
60
|
-
|
|
61
|
-
if content_type_default_thumbnail:
|
|
62
|
-
return f"{static_prefix}static{content_type_default_thumbnail}"
|
|
63
|
-
# use "thumbnail" set in afterpython.toml [website]
|
|
64
|
-
# elif website_default_thumbnail:
|
|
65
|
-
# Website-wide static is one level up from content_type folder
|
|
66
|
-
# return f"{static_prefix}../static{website_default_thumbnail}"
|
|
67
|
-
else:
|
|
68
|
-
return ""
|
|
10
|
+
from afterpython._io.toml import read_toml, write_toml, _to_tomlkit
|
|
69
11
|
|
|
70
12
|
|
|
71
13
|
def read_afterpython() -> TOMLDocument:
|
|
@@ -112,17 +54,12 @@ def init_afterpython():
|
|
|
112
54
|
return
|
|
113
55
|
afterpython_toml_path.touch()
|
|
114
56
|
print(f"Created {afterpython_toml_path}")
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"url": "",
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"logo_dark": "logo.svg",
|
|
125
|
-
"thumbnail": "thumbnail.png",
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
update_afterpython(default_data)
|
|
57
|
+
update_afterpython(
|
|
58
|
+
{
|
|
59
|
+
"company": {
|
|
60
|
+
"name": "",
|
|
61
|
+
"url": "",
|
|
62
|
+
},
|
|
63
|
+
"website": {"url": ""},
|
|
64
|
+
}
|
|
65
|
+
)
|
afterpython/tools/_git.py
CHANGED
|
@@ -58,7 +58,7 @@ def get_github_url() -> str | None:
|
|
|
58
58
|
return None
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def
|
|
61
|
+
def setup_github_auth():
|
|
62
62
|
"""Guide user through GitHub authentication."""
|
|
63
63
|
if not has_gh():
|
|
64
64
|
print("""
|
|
@@ -89,6 +89,7 @@ Please authenticate with GitHub:
|
|
|
89
89
|
""")
|
|
90
90
|
return False
|
|
91
91
|
|
|
92
|
+
print("✓ GitHub authentication verified")
|
|
92
93
|
return True
|
|
93
94
|
|
|
94
95
|
|
|
@@ -106,32 +107,5 @@ def get_github_username() -> str | None:
|
|
|
106
107
|
return result.stdout.strip()
|
|
107
108
|
|
|
108
109
|
|
|
109
|
-
def get_current_user_id() -> int | None:
|
|
110
|
-
"""
|
|
111
|
-
Get the GitHub user ID of the currently authenticated user.
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
User ID or None if error
|
|
115
|
-
"""
|
|
116
|
-
if not is_gh_authenticated():
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
|
-
result = subprocess.run(
|
|
120
|
-
["gh", "api", "user", "--jq", ".id"],
|
|
121
|
-
capture_output=True,
|
|
122
|
-
text=True,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
if result.returncode != 0:
|
|
126
|
-
print(f"Error fetching user ID: {result.stderr}")
|
|
127
|
-
return None
|
|
128
|
-
|
|
129
|
-
try:
|
|
130
|
-
return int(result.stdout.strip())
|
|
131
|
-
except ValueError:
|
|
132
|
-
print("Error: Failed to parse user ID")
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
|
-
|
|
136
110
|
# TODO: use gh's token for pygithub to get repo issues
|
|
137
111
|
# def get_repo_issues(owner: str, repo: str) -> list[dict]:
|
afterpython/tools/commitizen.py
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
|
-
import shutil
|
|
2
|
-
|
|
3
1
|
import afterpython as ap
|
|
4
2
|
|
|
5
3
|
|
|
4
|
+
commitizen_default = {
|
|
5
|
+
"tool": {
|
|
6
|
+
"commitizen": {
|
|
7
|
+
"name": "cz_conventional_commits",
|
|
8
|
+
"tag_format": "v$version",
|
|
9
|
+
"version_scheme": "pep440",
|
|
10
|
+
"version_provider": "uv",
|
|
11
|
+
"update_changelog_on_bump": False,
|
|
12
|
+
"major_version_zero": True,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
commitizen_pre_commit_hook = {
|
|
19
|
+
"repos": [
|
|
20
|
+
{
|
|
21
|
+
"repo": "https://github.com/commitizen-tools/commitizen",
|
|
22
|
+
"rev": "v4.10.0",
|
|
23
|
+
"hooks": [
|
|
24
|
+
{
|
|
25
|
+
"id": "commitizen",
|
|
26
|
+
"stages": ["commit-msg"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "commitizen-branch",
|
|
30
|
+
"stages": ["pre-push"],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
6
38
|
def init_commitizen():
|
|
7
|
-
|
|
39
|
+
from afterpython.tools.pre_commit import update_pre_commit
|
|
40
|
+
from afterpython._io.toml import write_toml
|
|
41
|
+
|
|
42
|
+
afterpython_path = ap.paths.afterpython_path
|
|
43
|
+
commitizen_toml_path = afterpython_path / "cz.toml"
|
|
8
44
|
if commitizen_toml_path.exists():
|
|
9
45
|
print(f"Commitizen configuration file {commitizen_toml_path} already exists")
|
|
10
46
|
return
|
|
11
|
-
|
|
12
|
-
|
|
47
|
+
|
|
48
|
+
# Create commitizen configuration with default values
|
|
49
|
+
write_toml(commitizen_toml_path, commitizen_default)
|
|
13
50
|
print(f"Created {commitizen_toml_path}")
|
|
51
|
+
|
|
52
|
+
# add commitizen hook to .pre-commit-config.yaml
|
|
53
|
+
update_pre_commit(commitizen_pre_commit_hook)
|
|
@@ -1,48 +1,78 @@
|
|
|
1
1
|
import shutil
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
import click
|
|
5
4
|
|
|
6
5
|
import afterpython as ap
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
def
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
def init_release_workflow():
|
|
9
|
+
"""Initialize GitHub Actions release workflow.
|
|
10
|
+
|
|
11
|
+
Creates .github/workflows/release.yml in the user's project root.
|
|
12
|
+
This workflow triggers on version tags and:
|
|
13
|
+
- Builds the package with uv
|
|
14
|
+
- Publishes to PyPI using trusted publishing
|
|
15
|
+
- Creates GitHub releases (with pre-release detection)
|
|
16
|
+
"""
|
|
17
|
+
user_path = ap.paths.user_path
|
|
18
|
+
workflow_dir = user_path / ".github" / "workflows"
|
|
19
|
+
workflow_path = workflow_dir / "release.yml"
|
|
20
|
+
|
|
21
|
+
if workflow_path.exists():
|
|
22
|
+
click.echo(f"GitHub Actions release workflow {workflow_path} already exists")
|
|
13
23
|
return
|
|
14
24
|
|
|
15
|
-
# Create
|
|
16
|
-
|
|
25
|
+
# Create .github/workflows directory if it doesn't exist
|
|
26
|
+
workflow_dir.mkdir(parents=True, exist_ok=True)
|
|
17
27
|
|
|
18
28
|
# Copy template from package
|
|
19
|
-
template_path = ap.paths.templates_path /
|
|
29
|
+
template_path = ap.paths.templates_path / "release-workflow-template.yml"
|
|
20
30
|
if not template_path.exists():
|
|
21
31
|
raise FileNotFoundError(
|
|
22
32
|
f"Template file not found: {template_path}\n"
|
|
23
33
|
"This might indicate a corrupted installation. Please reinstall afterpython."
|
|
24
34
|
)
|
|
25
35
|
|
|
26
|
-
shutil.copy(template_path,
|
|
27
|
-
|
|
36
|
+
shutil.copy(template_path, workflow_path)
|
|
37
|
+
click.echo(f"Created {workflow_path}")
|
|
38
|
+
click.echo(
|
|
39
|
+
"\n📋 Next steps to enable releases:\n"
|
|
40
|
+
" 1. Enable PyPI trusted publishing:\n"
|
|
41
|
+
" - Go to https://pypi.org/manage/account/publishing/\n"
|
|
42
|
+
" - Add your repository and workflow file: release.yml\n"
|
|
43
|
+
" 2. Push tags to trigger releases:\n"
|
|
44
|
+
" - Use 'ap bump --release' for stable releases (auto-pushes tag)\n"
|
|
45
|
+
" - Use 'ap bump --pre' then 'ap release' for pre-releases\n"
|
|
46
|
+
)
|
|
28
47
|
|
|
29
48
|
|
|
30
|
-
def
|
|
31
|
-
"""
|
|
32
|
-
if ".yml" in workflow_name:
|
|
33
|
-
workflow_name = workflow_name.replace(".yml", "")
|
|
49
|
+
def init_deploy_workflow():
|
|
50
|
+
"""Initialize GitHub Actions deployment workflow for GitHub Pages.
|
|
34
51
|
|
|
52
|
+
Creates .github/workflows/deploy.yml in the user's project root.
|
|
53
|
+
This workflow triggers on pushes to main branch and:
|
|
54
|
+
- Builds the project website with 'ap build'
|
|
55
|
+
- Deploys to GitHub Pages
|
|
56
|
+
- Allows manual deployment via workflow_dispatch
|
|
57
|
+
"""
|
|
35
58
|
user_path = ap.paths.user_path
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
workflow_dir = user_path / ".github" / "workflows"
|
|
60
|
+
workflow_path = workflow_dir / "deploy.yml"
|
|
38
61
|
|
|
39
|
-
|
|
62
|
+
if workflow_path.exists():
|
|
63
|
+
click.echo(f"GitHub Actions deploy workflow {workflow_path} already exists")
|
|
64
|
+
return
|
|
40
65
|
|
|
66
|
+
# Create .github/workflows directory if it doesn't exist
|
|
67
|
+
workflow_dir.mkdir(parents=True, exist_ok=True)
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
# Copy template from package
|
|
70
|
+
template_path = ap.paths.templates_path / "deploy-workflow-template.yml"
|
|
71
|
+
if not template_path.exists():
|
|
72
|
+
raise FileNotFoundError(
|
|
73
|
+
f"Template file not found: {template_path}\n"
|
|
74
|
+
"This might indicate a corrupted installation. Please reinstall afterpython."
|
|
75
|
+
)
|
|
47
76
|
|
|
48
|
-
|
|
77
|
+
shutil.copy(template_path, workflow_path)
|
|
78
|
+
click.echo(f"Created {workflow_path}")
|
afterpython/tools/myst.py
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
2
|
from typing import TYPE_CHECKING
|
|
4
3
|
|
|
5
4
|
if TYPE_CHECKING:
|
|
6
5
|
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from afterpython._typing import NodeEnv, tContentType
|
|
6
|
+
from afterpython._typing import tContentType, NodeEnv
|
|
9
7
|
|
|
10
8
|
import subprocess
|
|
11
9
|
|
|
12
10
|
import afterpython as ap
|
|
13
|
-
from afterpython._io.yaml import read_yaml, write_yaml
|
|
14
11
|
from afterpython.utils import deep_merge
|
|
12
|
+
from afterpython._io.yaml import write_yaml, read_yaml
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
def update_authors_yml(data_update: dict):
|
|
@@ -103,48 +101,6 @@ def update_myst_yml(data_update: dict, path: Path, add_comments: bool = False):
|
|
|
103
101
|
write_yaml(file_path, existing_data)
|
|
104
102
|
|
|
105
103
|
|
|
106
|
-
def _write_index_file(content_type: tContentType):
|
|
107
|
-
"""Create a placeholder index.md file for MyST to prevent first file from becoming index.
|
|
108
|
-
|
|
109
|
-
MyST treats the first file in TOC as index. This placeholder ensures all actual
|
|
110
|
-
content files get proper slugs. The generated index.html is deleted post-build
|
|
111
|
-
since SvelteKit owns the landing page route.
|
|
112
|
-
|
|
113
|
-
Note: This only creates the file. TOC modification is handled by the build process.
|
|
114
|
-
|
|
115
|
-
Raises:
|
|
116
|
-
FileExistsError: If index.md already exists (users shouldn't create this file).
|
|
117
|
-
"""
|
|
118
|
-
if content_type == "doc":
|
|
119
|
-
return # Doc doesn't need a placeholder index.md
|
|
120
|
-
|
|
121
|
-
content_path = ap.paths.afterpython_path / content_type
|
|
122
|
-
index_file = content_path / "index.md"
|
|
123
|
-
|
|
124
|
-
# Check if user created an index.md file
|
|
125
|
-
if index_file.exists():
|
|
126
|
-
raise FileExistsError(
|
|
127
|
-
f"\n"
|
|
128
|
-
f"Found existing 'index.md' in afterpython/{content_type}/\n"
|
|
129
|
-
f"\n"
|
|
130
|
-
f"The 'index.md' file is reserved for internal use by AfterPython.\n"
|
|
131
|
-
f"MyST treats the first file in TOC as index, which would conflict with\n"
|
|
132
|
-
f"SvelteKit (project-website-template)'s landing page route (/{content_type}).\n"
|
|
133
|
-
f"\n"
|
|
134
|
-
f"Please rename your file to something else (e.g., '{content_type}_intro.md')\n"
|
|
135
|
-
f"and update the reference in afterpython/{content_type}/myst.yml"
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
index_content = f"""---
|
|
139
|
-
title: ← {content_type.capitalize()}
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
This is a placeholder index page. The actual {content_type} landing page is rendered by SvelteKit.
|
|
143
|
-
"""
|
|
144
|
-
index_file.write_text(index_content)
|
|
145
|
-
return index_file
|
|
146
|
-
|
|
147
|
-
|
|
148
104
|
def _write_welcome_file(content_type: tContentType):
|
|
149
105
|
welcome_file = ap.paths.afterpython_path / content_type / "index.md"
|
|
150
106
|
if welcome_file.exists():
|
|
@@ -198,6 +154,9 @@ def init_myst():
|
|
|
198
154
|
},
|
|
199
155
|
"site": {
|
|
200
156
|
"options": {
|
|
157
|
+
"favicon": "../static/favicon.ico",
|
|
158
|
+
"logo": "../static/logo.svg",
|
|
159
|
+
"logo_dark": "../static/logo.svg",
|
|
201
160
|
"analytics_google": "{{ GOOGLE_ANALYTICS_ID }}",
|
|
202
161
|
# "twitter": "",
|
|
203
162
|
},
|
afterpython/tools/pre_commit.py
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import shutil
|
|
2
1
|
import subprocess
|
|
3
2
|
|
|
4
3
|
import afterpython as ap
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
from afterpython._io.yaml import write_yaml, read_yaml
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
pre_commit_default = {
|
|
9
|
+
# default is only ["pre-commit"], without changing the default,
|
|
10
|
+
# we need to explicitly pass in --hook-type whenever we run "pre-commit install --hook-type ..."
|
|
11
|
+
"default_install_hook_types": ["pre-commit", "commit-msg", "pre-push"],
|
|
12
|
+
"exclude": "^afterpython/_website/", # do not check the project website template
|
|
13
|
+
"repos": [
|
|
14
|
+
{
|
|
15
|
+
"repo": "https://github.com/pre-commit/pre-commit-hooks",
|
|
16
|
+
"rev": "v6.0.0",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{"id": "trailing-whitespace", "stages": ["pre-commit"]},
|
|
19
|
+
{"id": "end-of-file-fixer", "stages": ["pre-commit"]},
|
|
20
|
+
{"id": "check-yaml", "stages": ["pre-commit"]},
|
|
21
|
+
{"id": "check-toml", "stages": ["pre-commit"]},
|
|
22
|
+
{"id": "check-added-large-files", "stages": ["pre-commit"]},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
# {
|
|
26
|
+
# "repo": "https://github.com/astral-sh/uv-pre-commit",
|
|
27
|
+
# "rev": "0.9.8",
|
|
28
|
+
# "hooks": [
|
|
29
|
+
# {"id": "uv-lock"}, # Update the uv lockfile
|
|
30
|
+
# ]
|
|
31
|
+
# },
|
|
32
|
+
],
|
|
33
|
+
}
|
|
6
34
|
|
|
7
35
|
|
|
8
36
|
def install_pre_commit():
|
|
@@ -29,9 +57,6 @@ def init_pre_commit():
|
|
|
29
57
|
if pre_commit_path.exists():
|
|
30
58
|
print(f".pre-commit-config.yaml already exists at {pre_commit_path}")
|
|
31
59
|
return
|
|
32
|
-
|
|
33
|
-
ap.paths.templates_path / "pre-commit-config-template.yaml"
|
|
34
|
-
)
|
|
35
|
-
shutil.copy(pre_commit_template_path, pre_commit_path)
|
|
60
|
+
write_yaml(pre_commit_path, pre_commit_default)
|
|
36
61
|
print(f"Created {pre_commit_path}")
|
|
37
62
|
install_pre_commit()
|
afterpython/tools/pyproject.py
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
2
|
from typing import TYPE_CHECKING
|
|
4
3
|
|
|
5
4
|
if TYPE_CHECKING:
|
|
6
5
|
from tomlkit.toml_document import TOMLDocument
|
|
7
6
|
|
|
8
7
|
import asyncio
|
|
9
|
-
from pathlib import Path
|
|
10
8
|
|
|
11
9
|
from pyproject_metadata import StandardMetadata
|
|
12
10
|
|
|
13
11
|
import afterpython as ap
|
|
14
|
-
from afterpython._io.toml import
|
|
12
|
+
from afterpython._io.toml import read_toml, write_toml, _to_tomlkit
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
def read_pyproject() -> TOMLDocument:
|
|
@@ -28,50 +26,14 @@ def read_metadata() -> StandardMetadata:
|
|
|
28
26
|
return StandardMetadata.from_pyproject(read_pyproject())
|
|
29
27
|
|
|
30
28
|
|
|
31
|
-
def find_package_directory() -> Path:
|
|
32
|
-
"""Find the user's package directory.
|
|
33
|
-
|
|
34
|
-
Supports both common Python project layouts:
|
|
35
|
-
- src layout: user_project/src/package_name/__init__.py
|
|
36
|
-
- flat layout: user_project/package_name/__init__.py
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
Path to the package directory containing __init__.py
|
|
40
|
-
|
|
41
|
-
Raises:
|
|
42
|
-
FileNotFoundError: If package directory cannot be found in either layout
|
|
43
|
-
"""
|
|
44
|
-
metadata = read_metadata()
|
|
45
|
-
package_name = metadata.name
|
|
46
|
-
project_root = ap.paths.user_path
|
|
47
|
-
|
|
48
|
-
# Try src layout first (recommended layout)
|
|
49
|
-
src_layout_dir = project_root / "src" / package_name
|
|
50
|
-
if (src_layout_dir / "__init__.py").exists():
|
|
51
|
-
return src_layout_dir
|
|
52
|
-
|
|
53
|
-
# Try flat layout
|
|
54
|
-
flat_layout_dir = project_root / package_name
|
|
55
|
-
if (flat_layout_dir / "__init__.py").exists():
|
|
56
|
-
return flat_layout_dir
|
|
57
|
-
|
|
58
|
-
# Package not found in either location
|
|
59
|
-
raise FileNotFoundError(
|
|
60
|
-
f"Could not find package '{package_name}' in either src layout "
|
|
61
|
-
f"({src_layout_dir}) or flat layout ({flat_layout_dir}). "
|
|
62
|
-
f"Expected to find __init__.py in one of these locations."
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
29
|
def init_pyproject():
|
|
67
30
|
"""Initialize pyproject.toml with sensible defaults
|
|
68
31
|
- add [build-system] section with uv build backend (same as `uv init --package`)
|
|
69
32
|
- add [project.urls] section with homepage, repository, and documentation URLs
|
|
70
33
|
"""
|
|
71
34
|
import httpx
|
|
72
|
-
|
|
73
|
-
from afterpython.tools._git import get_git_user_config, get_github_url
|
|
74
35
|
from afterpython.utils import fetch_pypi_json
|
|
36
|
+
from afterpython.tools._git import get_git_user_config, get_github_url
|
|
75
37
|
|
|
76
38
|
build_backend = "uv_build"
|
|
77
39
|
|
afterpython/utils.py
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
2
|
from typing import TYPE_CHECKING
|
|
4
3
|
|
|
5
4
|
if TYPE_CHECKING:
|
|
6
5
|
from pathlib import Path
|
|
7
|
-
|
|
8
6
|
from httpx import AsyncClient
|
|
9
|
-
|
|
10
7
|
from afterpython._typing import NodeEnv
|
|
11
8
|
|
|
12
9
|
import os
|
|
@@ -23,7 +20,6 @@ def find_node_env() -> NodeEnv:
|
|
|
23
20
|
If no, install the Node.js version specified in NODEENV_VERSION
|
|
24
21
|
"""
|
|
25
22
|
from mystmd_py.nodeenv import find_any_node
|
|
26
|
-
|
|
27
23
|
from afterpython.const import NODEENV_VERSION
|
|
28
24
|
|
|
29
25
|
# from mystmd_py.main import ensure_valid_version
|
|
@@ -39,11 +35,6 @@ def has_uv() -> bool:
|
|
|
39
35
|
return shutil.which("uv") is not None
|
|
40
36
|
|
|
41
37
|
|
|
42
|
-
def has_pixi() -> bool:
|
|
43
|
-
"""Check if pixi is installed"""
|
|
44
|
-
return shutil.which("pixi") is not None
|
|
45
|
-
|
|
46
|
-
|
|
47
38
|
def has_gh() -> bool:
|
|
48
39
|
"""Check if gh is installed"""
|
|
49
40
|
return shutil.which("gh") is not None
|
|
@@ -224,76 +215,3 @@ def handle_passthrough_help(
|
|
|
224
215
|
subprocess.run([*underlying_command, "--help"])
|
|
225
216
|
|
|
226
217
|
ctx.exit(0)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def normalize_static_path(path: str) -> str:
|
|
230
|
-
"""
|
|
231
|
-
Normalize paths for static assets.
|
|
232
|
-
|
|
233
|
-
Only allows two formats:
|
|
234
|
-
1. Pure filename/path without ./ or ../ (e.g., "logo.svg", "somewhere/logo.svg")
|
|
235
|
-
2. Absolute path starting with / (e.g., "/somewhere/logo.svg")
|
|
236
|
-
|
|
237
|
-
Args:
|
|
238
|
-
path: User-provided path from afterpython.toml
|
|
239
|
-
|
|
240
|
-
Returns:
|
|
241
|
-
Normalized path
|
|
242
|
-
|
|
243
|
-
Raises:
|
|
244
|
-
ValueError: If path contains ./ or ../ or other invalid patterns
|
|
245
|
-
|
|
246
|
-
Examples:
|
|
247
|
-
>>> normalize_static_path("logo.svg")
|
|
248
|
-
'logo.svg'
|
|
249
|
-
>>> normalize_static_path("somewhere/logo.svg")
|
|
250
|
-
'/somewhere/logo.svg'
|
|
251
|
-
>>> normalize_static_path("/somewhere/logo.svg")
|
|
252
|
-
'/somewhere/logo.svg'
|
|
253
|
-
>>> normalize_static_path("./logo.svg") # Raises ValueError
|
|
254
|
-
>>> normalize_static_path("../logo.svg") # Raises ValueError
|
|
255
|
-
>>> normalize_static_path("static/logo.svg") # Raises ValueError
|
|
256
|
-
"""
|
|
257
|
-
if not path:
|
|
258
|
-
return ""
|
|
259
|
-
|
|
260
|
-
# Reject paths with ./ or ../
|
|
261
|
-
if "./" in path or "../" in path or path.startswith(".") or path.startswith(".."):
|
|
262
|
-
raise ValueError(
|
|
263
|
-
f"Invalid path '{path}': paths cannot contain './' or '../'. "
|
|
264
|
-
f"Use either a pure filename (e.g., 'logo.svg') or absolute path (e.g., '/somewhere/logo.svg')"
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
# Reject paths with 'static' in them
|
|
268
|
-
if "static" in path.lower():
|
|
269
|
-
raise ValueError(
|
|
270
|
-
f"Invalid path '{path}': do not include 'static' in the path. "
|
|
271
|
-
f"Files are assumed to be in the static/ directory."
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
# If it starts with /, it's absolute - keep as is
|
|
275
|
-
if not path.startswith("/"):
|
|
276
|
-
return "/" + path
|
|
277
|
-
else:
|
|
278
|
-
return path
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def get_relative_static_prefix(file_path: Path, content_type_path: Path) -> str:
|
|
282
|
-
"""Calculate the relative path prefix from a file to its content type's static/ folder.
|
|
283
|
-
|
|
284
|
-
Args:
|
|
285
|
-
file_path: Path to the content file (e.g., blog/layer/blog5.ipynb)
|
|
286
|
-
content_type_path: Path to the content type folder (e.g., blog/)
|
|
287
|
-
|
|
288
|
-
Returns:
|
|
289
|
-
Relative prefix to reach static/ (e.g., "./" or "../" or "../../")
|
|
290
|
-
"""
|
|
291
|
-
# Get the depth: how many directories deep the file is from content_type_path
|
|
292
|
-
relative = file_path.parent.relative_to(content_type_path)
|
|
293
|
-
depth = len(relative.parts)
|
|
294
|
-
|
|
295
|
-
# Build the correct prefix
|
|
296
|
-
if depth == 0:
|
|
297
|
-
return "./" # File is directly in content_type_path
|
|
298
|
-
else:
|
|
299
|
-
return "../" * depth
|