Skip to content

Commit 80b0bef

Browse files
committed
feat(mise): detect lazy loader from project dependencies
Select the generated __init__.py template from pyproject dependencies so init generation works with both lazy-loader variants. Resolve the project root from pyproject.toml as well, which makes generation work reliably from nested paths while cleaning up local editor artifacts.
1 parent 803b6ae commit 80b0bef

9 files changed

Lines changed: 108 additions & 53 deletions

File tree

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# This file is @generated by <https://github.com/liblaf/copier-release>.
22
# DO NOT EDIT!
3-
# prettier-ignore
4-
_commit: v0.2.4
3+
_commit: v0.2.5
54
_src_path: gh:liblaf/copier-release

.config/copier/.copier-answers.shared.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is @generated by <https://github.com/liblaf/copier-shared>.
22
# DO NOT EDIT!
3-
_commit: v0.2.6
3+
_commit: v0.2.7
44
_src_path: gh:liblaf/copier-share
55
author_email: 30631553+liblaf@users.noreply.github.com
66
author_name: liblaf

.github/workflows/release-draft.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
steps:
2424
- id: auth
2525
name: Auth
26-
uses: liblaf/actions/auth@cadf1f6ac2bed9a0ebc033e74a2c0994813e7f04 # v1
26+
uses: liblaf/actions/auth@1f83732587ff97f1babbe9218fdc5436dda1a16f # v1
2727
with:
2828
app-id: ${{ vars.APP_ID }}
2929
private-key: ${{ secrets.PRIVATE_KEY }}

.github/workflows/release-pr.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
tool: git-cliff
3838
- id: version
3939
name: Next version
40-
uses: liblaf/actions/next-version@cadf1f6ac2bed9a0ebc033e74a2c0994813e7f04 # v1
40+
uses: liblaf/actions/next-version@1f83732587ff97f1babbe9218fdc5436dda1a16f # v1
4141
env:
4242
GIT_CLIFF_CONFIG_URL: ${{ env.GIT_CLIFF_CONFIG_URL }}
4343
GITHUB_TOKEN: ${{ github.token }}
@@ -58,7 +58,7 @@ jobs:
5858
steps:
5959
- id: auth
6060
name: Auth
61-
uses: liblaf/actions/auth@cadf1f6ac2bed9a0ebc033e74a2c0994813e7f04 # v1
61+
uses: liblaf/actions/auth@1f83732587ff97f1babbe9218fdc5436dda1a16f # v1
6262
with:
6363
app-id: ${{ vars.APP_ID }}
6464
private-key: ${{ secrets.PRIVATE_KEY }}

.github/workflows/release-publish.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ jobs:
2020
steps:
2121
- id: auth
2222
name: Auth
23-
uses: liblaf/actions/auth@cadf1f6ac2bed9a0ebc033e74a2c0994813e7f04 # v1
23+
uses: liblaf/actions/auth@1f83732587ff97f1babbe9218fdc5436dda1a16f # v1
2424
with:
2525
app-id: ${{ vars.APP_ID }}
2626
private-key: ${{ secrets.PRIVATE_KEY }}
2727
- name: Publish drafts
28-
uses: liblaf/actions/publish-drafts@cadf1f6ac2bed9a0ebc033e74a2c0994813e7f04 # v1
28+
uses: liblaf/actions/publish-drafts@1f83732587ff97f1babbe9218fdc5436dda1a16f # v1
2929
with:
3030
older-than: 6h
3131
token: ${{ steps.auth.outputs.token }}

.github/workflows/shared-mega-linter.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
name: MegaLinter
4545
uses: liblaf/megalinter-custom-flavor-all@4c245a0bdb312d24c66108433d4d701fb0875746 # main
4646
env:
47-
GITHUB_TOKEN: ${{ github.token }}
47+
GITHUB_TOKEN: ${{ steps.auth.outputs.token }}
4848
MEGALINTER_CONFIG: https://raw.githubusercontent.com/liblaf/megalinter-custom-flavor-all/refs/heads/main/.mega-linter.yml
4949
- if: success() || failure()
5050
name: Upload reports

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/

.vscode/settings.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

mise-tasks/gen/init.py

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,145 @@
11
#!/usr/bin/env python
22
import argparse
33
import ast
4+
import enum
45
import os
56
import subprocess
67
from pathlib import Path
8+
from typing import Any, Never
79

8-
TEMPLATE: str = """\
10+
import tomllib
11+
from packaging.requirements import Requirement
12+
13+
14+
class LazyLoader(enum.StrEnum):
15+
LAZY_LOADER = enum.auto()
16+
LIBLAF_LAZY_LOADER = enum.auto()
17+
18+
19+
TEMPLATES: dict[LazyLoader, str] = {
20+
LazyLoader.LAZY_LOADER: """\
21+
from lazy_loader import attach_stub
22+
23+
__getattr__, __dir__, __all__ = attach_stub(__name__, __file__)
24+
25+
del attach_stub
26+
""",
27+
LazyLoader.LIBLAF_LAZY_LOADER: """\
928
from liblaf.lazy_loader import attach_stub
1029
11-
__getattr__, __dir__, __all__ = attach_stub(__name__, __package__, __file__)
30+
__getattr__, __dir__, __all__ = attach_stub(__name__, __file__, __package__)
1231
1332
del attach_stub
14-
"""
33+
""",
34+
}
1535

1636

1737
class Args(argparse.Namespace):
1838
path: Path
1939

2040

21-
def parse_args() -> Args:
22-
parser: argparse.ArgumentParser = argparse.ArgumentParser()
23-
parser.add_argument("path", nargs="?", type=Path)
24-
args: argparse.Namespace = parser.parse_args(namespace=Args())
25-
if args.path is None:
26-
if (path := os.getenv("MISE_PROJECT_ROOT")) is not None:
27-
args.path = Path(path)
28-
else:
29-
args.path = Path.cwd()
30-
return args
41+
def detect_lazy_loader(project_root: Path) -> LazyLoader:
42+
pyproject: Path = project_root / "pyproject.toml"
43+
with pyproject.open("rb") as fp:
44+
data: dict[str, Any] = tomllib.load(fp)
45+
project: Any = data.get("project")
46+
if not isinstance(project, dict):
47+
error(f"`project` table not found in: {pyproject}")
48+
dependencies: Any = project.get("dependencies")
49+
if not isinstance(dependencies, list):
50+
error(f"`project.dependencies` not found in: {pyproject}")
51+
dependency_names: set[str] = set()
52+
for dep in dependencies:
53+
req: Requirement = Requirement(dep)
54+
dependency_names.add(req.name.lower())
55+
if "liblaf-lazy-loader" in dependency_names:
56+
return LazyLoader.LIBLAF_LAZY_LOADER
57+
if "lazy-loader" in dependency_names:
58+
return LazyLoader.LAZY_LOADER
59+
error(
60+
"no supported lazy loader found in project dependencies: "
61+
"expected `liblaf-lazy-loader` or `lazy-loader`"
62+
)
63+
64+
65+
def error(message: str) -> Never:
66+
raise SystemExit(message)
67+
68+
69+
def find_project_root(path: Path) -> Path:
70+
for candidate in [path, *path.parents]:
71+
if (candidate / "pyproject.toml").exists():
72+
return candidate
73+
error(f"pyproject.toml not found for: {path}")
3174

3275

33-
def git_ls_files(path: Path) -> list[Path]:
76+
def get_docstring(file: Path) -> str | None:
77+
if not file.exists():
78+
return None
79+
source: str = file.read_text()
80+
module: ast.Module = ast.parse(source)
81+
if not (module.body and isinstance(module.body[0], ast.Expr)):
82+
return None
83+
node: ast.expr = module.body[0].value
84+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
85+
return ast.get_source_segment(source, node)
86+
return None
87+
88+
89+
def git_ls_files(*, project_root: Path, path: Path) -> list[Path]:
90+
try:
91+
relative_path: Path = path.relative_to(project_root)
92+
except ValueError:
93+
error(f"path is not under project root: {path}")
94+
3495
result: subprocess.CompletedProcess[str] = subprocess.run(
3596
[
3697
"git",
98+
"-C",
99+
project_root,
37100
"ls-files",
38101
"--cached",
39102
"--others",
40103
"--exclude-standard",
41104
"--",
42-
f"{path}/**/__init__.pyi",
105+
relative_path / "__init__.pyi",
106+
relative_path / "**/__init__.pyi",
43107
],
44108
stdout=subprocess.PIPE,
45109
check=True,
46110
text=True,
47111
)
48-
return [Path(line) for line in result.stdout.splitlines() if line]
112+
return [project_root / line for line in result.stdout.splitlines() if line]
49113

50114

51-
def get_docstring(file: Path) -> str | None:
52-
if not file.exists():
53-
return None
54-
source: str = file.read_text()
55-
module: ast.Module = ast.parse(source)
56-
if not (module.body and isinstance(module.body[0], ast.Expr)):
57-
return None
58-
node: ast.expr = module.body[0].value
59-
if isinstance(node, ast.Constant) and isinstance(node.value, str):
60-
return ast.get_source_segment(source, node)
61-
return None
115+
def parse_args() -> Args:
116+
parser: argparse.ArgumentParser = argparse.ArgumentParser()
117+
parser.add_argument("path", nargs="?", type=Path)
118+
args: argparse.Namespace = parser.parse_args(namespace=Args())
119+
if args.path is None:
120+
if (path := os.getenv("MISE_PROJECT_ROOT")) is not None:
121+
args.path = Path(path)
122+
else:
123+
args.path = Path.cwd()
124+
args.path = args.path.resolve()
125+
return args
62126

63127

64-
def render_init(file: Path) -> str:
128+
def render_init(file: Path, *, loader: LazyLoader) -> str:
65129
docstring: str | None = get_docstring(file)
130+
template: str = TEMPLATES[loader]
66131
if docstring is None:
67-
return TEMPLATE
68-
return f"{docstring}\n\n{TEMPLATE}"
132+
return template
133+
return f"{docstring}\n\n{template}"
69134

70135

71136
def main() -> None:
72137
args: Args = parse_args()
73-
for pyi in git_ls_files(args.path):
138+
project_root: Path = find_project_root(args.path)
139+
loader: LazyLoader = detect_lazy_loader(project_root)
140+
for pyi in git_ls_files(project_root=project_root, path=args.path):
74141
py: Path = pyi.with_suffix(".py")
75-
content: str = render_init(py)
142+
content: str = render_init(py, loader=loader)
76143
if py.exists():
77144
if py.read_text() == content:
78145
print("skipped:", py)

0 commit comments

Comments
 (0)