cutf 0.0.8__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 (44) hide show
  1. cutf-0.0.8/.github/workflows/ci.yml +36 -0
  2. cutf-0.0.8/.github/workflows/publish-chocolatey.yml +63 -0
  3. cutf-0.0.8/.github/workflows/publish-dockerhub.yml +62 -0
  4. cutf-0.0.8/.github/workflows/publish-pypi.yml +58 -0
  5. cutf-0.0.8/.github/workflows/release.yml +169 -0
  6. cutf-0.0.8/.gitignore +5 -0
  7. cutf-0.0.8/.mk-scripts/convert_logo.py +187 -0
  8. cutf-0.0.8/.mk-scripts/gen-project-py +70 -0
  9. cutf-0.0.8/.run/Makefile Targets.run.xml +350 -0
  10. cutf-0.0.8/LICENSE +21 -0
  11. cutf-0.0.8/Makefile +829 -0
  12. cutf-0.0.8/PKG-INFO +190 -0
  13. cutf-0.0.8/README.md +153 -0
  14. cutf-0.0.8/changelog.md +1 -0
  15. cutf-0.0.8/cutf/__init__.py +0 -0
  16. cutf-0.0.8/cutf/app.py +201 -0
  17. cutf-0.0.8/cutf/controller/__init__.py +0 -0
  18. cutf-0.0.8/cutf/controller/fileChecker.py +86 -0
  19. cutf-0.0.8/cutf/controller/fileController.py +119 -0
  20. cutf-0.0.8/cutf/controller/resultHandler.py +174 -0
  21. cutf-0.0.8/cutf/model/AppSetting.py +29 -0
  22. cutf-0.0.8/cutf/model/FileScanResult.py +31 -0
  23. cutf-0.0.8/cutf/model/MissingCharResult.py +23 -0
  24. cutf-0.0.8/cutf/model/__init__.py +8 -0
  25. cutf-0.0.8/cutf/util/__init__.py +0 -0
  26. cutf-0.0.8/cutf/util/code.py +43 -0
  27. cutf-0.0.8/cutf/util/iconv.py +51 -0
  28. cutf-0.0.8/cutf/util/log.py +35 -0
  29. cutf-0.0.8/cutf/util/path.py +38 -0
  30. cutf-0.0.8/genexe.txt +1 -0
  31. cutf-0.0.8/project.mk +262 -0
  32. cutf-0.0.8/pyproject.toml +48 -0
  33. cutf-0.0.8/ruff.toml +6 -0
  34. cutf-0.0.8/tests/test_app.py +56 -0
  35. cutf-0.0.8/tests/test_code_utils.py +20 -0
  36. cutf-0.0.8/tests/test_file_checker.py +17 -0
  37. cutf-0.0.8/tests/test_file_controller.py +104 -0
  38. cutf-0.0.8/tests/test_iconv_utils.py +21 -0
  39. cutf-0.0.8/tests/test_log_utils.py +14 -0
  40. cutf-0.0.8/tests/test_models.py +35 -0
  41. cutf-0.0.8/tests/test_path_utils.py +24 -0
  42. cutf-0.0.8/tests/test_result_handler.py +40 -0
  43. cutf-0.0.8/ty.toml +3 -0
  44. cutf-0.0.8/uv.lock +435 -0
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ quality-and-build:
15
+ name: Quality And Build
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v6
24
+ with:
25
+ version: "latest"
26
+ enable-cache: true
27
+
28
+ - name: Setup CI Environment
29
+ run: make ci-setup
30
+
31
+ - name: Run Quality Gates
32
+ run: make ci-quality
33
+
34
+ - name: Build Package
35
+ run: make build
36
+
@@ -0,0 +1,63 @@
1
+ name: Publish Chocolatey
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ ci-config:
14
+ name: Read CI Config
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ enable_chocolatey_publish: ${{ steps.config.outputs.enable_chocolatey_publish }}
18
+ chocolatey_environment: ${{ steps.config.outputs.chocolatey_environment }}
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Export Config To GitHub Output
25
+ id: config
26
+ run: make ci-export-config
27
+
28
+ publish:
29
+ name: Publish To Chocolatey
30
+ needs: ci-config
31
+ if: needs.ci-config.outputs.enable_chocolatey_publish == '1'
32
+ runs-on: windows-latest
33
+ environment: ${{ needs.ci-config.outputs.chocolatey_environment }}
34
+
35
+ steps:
36
+ - name: Checkout
37
+ uses: actions/checkout@v4
38
+ with:
39
+ fetch-depth: 0
40
+
41
+ - name: Install Make
42
+ run: choco install make
43
+
44
+ - name: Install uv
45
+ uses: astral-sh/setup-uv@v6
46
+ with:
47
+ version: "latest"
48
+ enable-cache: true
49
+
50
+ - name: Setup CI Environment
51
+ run: make ci-setup
52
+
53
+ - name: Build Windows Assets
54
+ run: make ci-build-chocolatey-assets
55
+
56
+ - name: Pack Chocolatey Package
57
+ run: make ci-pack-chocolatey
58
+
59
+ - name: Publish Chocolatey Package
60
+ run: make ci-publish-chocolatey
61
+ env:
62
+ CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
63
+
@@ -0,0 +1,62 @@
1
+ name: Publish Docker Hub
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ ci-config:
14
+ name: Read CI Config
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ enable_dockerhub_publish: ${{ steps.config.outputs.enable_dockerhub_publish }}
18
+ dockerhub_environment: ${{ steps.config.outputs.dockerhub_environment }}
19
+ dockerhub_image: ${{ steps.config.outputs.dockerhub_image }}
20
+ dockerfile: ${{ steps.config.outputs.dockerfile }}
21
+ docker_build_context: ${{ steps.config.outputs.docker_build_context }}
22
+ docker_push_latest: ${{ steps.config.outputs.docker_push_latest }}
23
+
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Export Config To GitHub Output
29
+ id: config
30
+ run: make ci-export-config
31
+
32
+ publish:
33
+ name: Publish Docker Image
34
+ needs: ci-config
35
+ if: needs.ci-config.outputs.enable_dockerhub_publish == '1'
36
+ runs-on: ubuntu-latest
37
+ environment: ${{ needs.ci-config.outputs.dockerhub_environment }}
38
+
39
+ steps:
40
+ - name: Checkout
41
+ uses: actions/checkout@v4
42
+ with:
43
+ fetch-depth: 0
44
+
45
+ - name: Setup Docker Buildx
46
+ uses: docker/setup-buildx-action@v3
47
+
48
+ - name: Docker Hub Login
49
+ run: make ci-docker-login
50
+ env:
51
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
52
+ DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
53
+
54
+ - name: Resolve Docker Image Tag
55
+ id: docker_tag
56
+ run: make ci-export-docker-tag
57
+
58
+ - name: Build And Push Docker Image
59
+ run: make ci-publish-dockerhub
60
+ env:
61
+ DOCKER_IMAGE_TAG: ${{ steps.docker_tag.outputs.docker_image_tag }}
62
+
@@ -0,0 +1,58 @@
1
+ name: Publish PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ id-token: write
12
+
13
+ jobs:
14
+ ci-config:
15
+ name: Read CI Config
16
+ runs-on: ubuntu-latest
17
+ outputs:
18
+ enable_pypi_publish: ${{ steps.config.outputs.enable_pypi_publish }}
19
+ pypi_environment: ${{ steps.config.outputs.pypi_environment }}
20
+
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Export Config To GitHub Output
26
+ id: config
27
+ run: make ci-export-config
28
+
29
+ publish:
30
+ name: Publish To PyPI
31
+ needs: ci-config
32
+ if: needs.ci-config.outputs.enable_pypi_publish == '1'
33
+ runs-on: ubuntu-latest
34
+ environment: ${{ needs.ci-config.outputs.pypi_environment }}
35
+
36
+ steps:
37
+ - name: Checkout
38
+ uses: actions/checkout@v4
39
+ with:
40
+ fetch-depth: 0
41
+
42
+ - name: Install uv
43
+ uses: astral-sh/setup-uv@v6
44
+ with:
45
+ version: "latest"
46
+ enable-cache: true
47
+
48
+ - name: Setup CI Environment
49
+ run: make ci-setup
50
+
51
+ - name: Build Package
52
+ run: make build-uv
53
+
54
+ - name: Publish Package
55
+ run: make ci-publish-pypi
56
+ env:
57
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
58
+
@@ -0,0 +1,169 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: write
11
+
12
+ jobs:
13
+ ci-config:
14
+ name: Read CI Config
15
+ runs-on: ubuntu-latest
16
+ outputs:
17
+ release_branch: ${{ steps.config.outputs.release_branch }}
18
+ windows_installer_enabled: ${{ steps.config.outputs.windows_installer_enabled }}
19
+ build_linux: ${{ steps.config.outputs.build_linux }}
20
+ build_macos: ${{ steps.config.outputs.build_macos }}
21
+ build_windows: ${{ steps.config.outputs.build_windows }}
22
+ enable_release_docs: ${{ steps.config.outputs.enable_release_docs }}
23
+ release_artifacts: ${{ steps.config.outputs.release_artifacts }}
24
+ changelog_file: ${{ steps.config.outputs.changelog_file }}
25
+ release_notes_file: ${{ steps.config.outputs.release_notes_file }}
26
+
27
+ steps:
28
+ - name: Checkout
29
+ uses: actions/checkout@v4
30
+
31
+ - name: Export Config To GitHub Output
32
+ id: config
33
+ run: make ci-export-config
34
+
35
+ changelog-and-release:
36
+ name: Generate Changelog And Create Release
37
+ needs: ci-config
38
+ runs-on: ubuntu-latest
39
+
40
+ steps:
41
+ - name: Checkout
42
+ uses: actions/checkout@v4
43
+ with:
44
+ fetch-depth: 0
45
+
46
+ - name: Install uv
47
+ uses: astral-sh/setup-uv@v6
48
+ with:
49
+ version: "latest"
50
+ enable-cache: true
51
+
52
+ - name: Setup CI Environment
53
+ run: make ci-setup
54
+
55
+ - name: Install git-cliff
56
+ run: make ci-install-git-cliff
57
+
58
+ - name: Generate Changelog
59
+ run: make ci-generate-changelog
60
+
61
+ - name: Commit Changelog
62
+ run: make ci-commit-changelog
63
+
64
+ - name: Generate Docs
65
+ if: needs.ci-config.outputs.enable_release_docs == '1'
66
+ run: make ci-generate-docs
67
+
68
+ - name: Commit Docs
69
+ if: needs.ci-config.outputs.enable_release_docs == '1'
70
+ run: make ci-commit-docs
71
+
72
+ - name: Generate Release Notes
73
+ run: make ci-generate-release-notes
74
+
75
+ - name: Create Release
76
+ uses: softprops/action-gh-release@v2
77
+ with:
78
+ body_path: ${{ needs.ci-config.outputs.release_notes_file }}
79
+
80
+ build-linux:
81
+ name: Build Assets (Linux)
82
+ needs:
83
+ - ci-config
84
+ - changelog-and-release
85
+ if: needs.ci-config.outputs.build_linux == '1'
86
+ runs-on: ubuntu-latest
87
+
88
+ steps:
89
+ - name: Checkout
90
+ uses: actions/checkout@v4
91
+
92
+ - name: Install uv
93
+ uses: astral-sh/setup-uv@v6
94
+ with:
95
+ version: "latest"
96
+ enable-cache: true
97
+
98
+ - name: Setup CI Environment
99
+ run: make ci-setup
100
+
101
+ - name: Build Release Assets
102
+ run: make ci-build-release-assets
103
+
104
+ - name: Upload Artifacts
105
+ uses: softprops/action-gh-release@v2
106
+ with:
107
+ files: ${{ needs.ci-config.outputs.release_artifacts }}
108
+
109
+ build-macos:
110
+ name: Build Assets (macOS)
111
+ needs:
112
+ - ci-config
113
+ - changelog-and-release
114
+ if: needs.ci-config.outputs.build_macos == '1'
115
+ runs-on: macos-latest
116
+
117
+ steps:
118
+ - name: Checkout
119
+ uses: actions/checkout@v4
120
+
121
+ - name: Install uv
122
+ uses: astral-sh/setup-uv@v6
123
+ with:
124
+ version: "latest"
125
+ enable-cache: true
126
+
127
+ - name: Setup CI Environment
128
+ run: make ci-setup
129
+
130
+ - name: Build Release Assets
131
+ run: make ci-build-release-assets
132
+
133
+ - name: Upload Artifacts
134
+ uses: softprops/action-gh-release@v2
135
+ with:
136
+ files: ${{ needs.ci-config.outputs.release_artifacts }}
137
+
138
+ build-windows:
139
+ name: Build Assets (Windows)
140
+ needs:
141
+ - ci-config
142
+ - changelog-and-release
143
+ if: needs.ci-config.outputs.build_windows == '1'
144
+ runs-on: windows-latest
145
+
146
+ steps:
147
+ - name: Checkout
148
+ uses: actions/checkout@v4
149
+
150
+ - name: Install Make
151
+ run: choco install make
152
+
153
+ - name: Install uv
154
+ uses: astral-sh/setup-uv@v6
155
+ with:
156
+ version: "latest"
157
+ enable-cache: true
158
+
159
+ - name: Setup CI Environment
160
+ run: make ci-setup
161
+
162
+ - name: Build Release Assets
163
+ run: make ci-build-release-assets
164
+
165
+ - name: Upload Artifacts
166
+ uses: softprops/action-gh-release@v2
167
+ with:
168
+ files: ${{ needs.ci-config.outputs.release_artifacts }}
169
+
cutf-0.0.8/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .idea
2
+ build
3
+ dist
4
+ cutf.spec
5
+ temp
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ convert_logo.py – Convert a .svg logo to all standard icon/image formats.
4
+
5
+ Usage:
6
+ uv run python .mk-scripts/convert_logo.py <path/to/logo.svg>
7
+
8
+ Generates in the SAME directory as the source SVG:
9
+ • PNG – 16, 32, 48, 64, 128, 256, 512, 1024 px
10
+ • JPG – 64, 128, 256, 512, 1024 px (white background, 95 % quality)
11
+ • ICO – multi-size (16, 24, 32, 48, 64, 128, 256 px)
12
+ • ICNS – macOS icon (16, 32, 64, 128, 256, 512, 1024 px)
13
+
14
+ Renderer: skia-python – ships pre-compiled Skia wheels on PyPI,
15
+ zero system-library dependencies (no cairo, no ImageMagick).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import io
21
+ import struct
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ # ── Sizes ─────────────────────────────────────────────────────────────────────
26
+
27
+ PNG_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024]
28
+ JPG_SIZES = [64, 128, 256, 512, 1024]
29
+ ICO_SIZES = [16, 24, 32, 48, 64, 128, 256]
30
+ ICNS_SIZES = [16, 32, 64, 128, 256, 512, 1024]
31
+
32
+ # Apple ICNS OSType codes per pixel size (1x variants – PNG-compressed entries)
33
+ ICNS_TYPES: dict[int, bytes] = {
34
+ 16: b"icp4",
35
+ 32: b"icp5",
36
+ 64: b"icp6",
37
+ 128: b"ic07",
38
+ 256: b"ic08",
39
+ 512: b"ic09",
40
+ 1024: b"ic10",
41
+ }
42
+
43
+ # ── SVG renderer (skia-python) ────────────────────────────────────────────────
44
+
45
+ def _render_svg(svg_path: Path, size: int) -> bytes:
46
+ """
47
+ Render *svg_path* at *size* × *size* pixels using Skia.
48
+ Returns raw PNG bytes (RGBA).
49
+ """
50
+ import skia # type: ignore[import-untyped]
51
+
52
+ svg_bytes = svg_path.read_bytes()
53
+ stream = skia.MemoryStream(svg_bytes)
54
+ svg_dom = skia.SVGDOM.MakeFromStream(stream)
55
+ if svg_dom is None:
56
+ raise RuntimeError(f"Skia could not parse SVG: {svg_path}")
57
+
58
+ surface = skia.Surface(size, size)
59
+ with surface as canvas:
60
+ canvas.clear(skia.ColorTRANSPARENT)
61
+ svg_dom.setContainerSize(skia.Size.Make(size, size))
62
+ svg_dom.render(canvas)
63
+
64
+ image = surface.makeImageSnapshot()
65
+ png_data = image.encodeToData()
66
+ return bytes(png_data)
67
+
68
+
69
+ # ── Image helpers ─────────────────────────────────────────────────────────────
70
+
71
+ def _open_rgba(png_bytes: bytes):
72
+ """Open raw PNG bytes as a PIL RGBA Image."""
73
+ from PIL import Image # type: ignore[import-untyped]
74
+ return Image.open(io.BytesIO(png_bytes)).convert("RGBA")
75
+
76
+
77
+ def _resize(base_img, size: int):
78
+ from PIL import Image # type: ignore[import-untyped]
79
+ return base_img.resize((size, size), Image.LANCZOS)
80
+
81
+
82
+ def _to_png_bytes(img) -> bytes:
83
+ buf = io.BytesIO()
84
+ img.save(buf, format="PNG", optimize=True)
85
+ return buf.getvalue()
86
+
87
+
88
+ # ── ICNS builder ──────────────────────────────────────────────────────────────
89
+
90
+ def _build_icns(png_map: dict[int, bytes]) -> bytes:
91
+ """
92
+ Hand-craft an ICNS binary from {size: png_bytes}.
93
+
94
+ ICNS layout:
95
+ magic 4 bytes b"icns"
96
+ length 4 bytes total file length (big-endian uint32)
97
+ entries …
98
+ type 4 bytes
99
+ length 4 bytes (type + length + data)
100
+ data N bytes PNG bytes (ic07 … ic10 / icp4 … icp6)
101
+ """
102
+ body = b""
103
+ for size in sorted(png_map):
104
+ ostype = ICNS_TYPES.get(size)
105
+ if ostype is None:
106
+ continue
107
+ data = png_map[size]
108
+ body += ostype + struct.pack(">I", 8 + len(data)) + data
109
+ return b"icns" + struct.pack(">I", 8 + len(body)) + body
110
+
111
+
112
+ # ── Main conversion ───────────────────────────────────────────────────────────
113
+
114
+ def convert(svg_path: Path) -> None:
115
+ if not svg_path.exists():
116
+ print(f"ERROR: file not found: {svg_path}", file=sys.stderr)
117
+ sys.exit(1)
118
+ if svg_path.suffix.lower() != ".svg":
119
+ print(f"ERROR: expected a .svg file, got: {svg_path}", file=sys.stderr)
120
+ sys.exit(1)
121
+
122
+ out_dir = svg_path.parent
123
+ stem = svg_path.stem
124
+
125
+ print(f"Source : {svg_path}")
126
+ print(f"Output : {out_dir}/")
127
+ print()
128
+
129
+ # Render at the largest size once; downscale from that master image.
130
+ max_size = max(PNG_SIZES + ICNS_SIZES)
131
+ print(f"Rendering SVG at {max_size}×{max_size} px (skia-python) …")
132
+ base_img = _open_rgba(_render_svg(svg_path, max_size))
133
+ print()
134
+
135
+ from PIL import Image as _Image # type: ignore[import-untyped]
136
+
137
+ # ── PNG ──────────────────────────────────────────────────────────────────
138
+ print("PNG files:")
139
+ for size in PNG_SIZES:
140
+ img = _resize(base_img, size)
141
+ out_path = out_dir / f"{stem}-{size}x{size}.png"
142
+ img.save(out_path, format="PNG", optimize=True)
143
+ print(f" {out_path}")
144
+ print()
145
+
146
+ # ── JPG ──────────────────────────────────────────────────────────────────
147
+ print("JPG files:")
148
+ for size in JPG_SIZES:
149
+ rgba = _resize(base_img, size)
150
+ bg = _Image.new("RGB", (size, size), (255, 255, 255))
151
+ bg.paste(rgba, mask=rgba.split()[3]) # composite over white bg
152
+ out_path = out_dir / f"{stem}-{size}x{size}.jpg"
153
+ bg.save(out_path, format="JPEG", quality=95, optimize=True)
154
+ print(f" {out_path}")
155
+ print()
156
+
157
+ # ── ICO ───────────────────────────────────────────────────────────────────
158
+ print("ICO file:")
159
+ ico_images = [_resize(base_img, s) for s in ICO_SIZES]
160
+ ico_path = out_dir / f"{stem}.ico"
161
+ ico_images[0].save(
162
+ ico_path,
163
+ format="ICO",
164
+ sizes=[(s, s) for s in ICO_SIZES],
165
+ append_images=ico_images[1:],
166
+ )
167
+ print(f" {ico_path}")
168
+ print()
169
+
170
+ # ── ICNS ─────────────────────────────────────────────────────────────────
171
+ print("ICNS file:")
172
+ icns_png_map = {s: _to_png_bytes(_resize(base_img, s)) for s in ICNS_SIZES}
173
+ icns_path = out_dir / f"{stem}.icns"
174
+ icns_path.write_bytes(_build_icns(icns_png_map))
175
+ print(f" {icns_path}")
176
+ print()
177
+
178
+ print("Done.")
179
+
180
+
181
+ if __name__ == "__main__":
182
+ if len(sys.argv) != 2:
183
+ print("Usage: python convert_logo.py <path/to/logo.svg>", file=sys.stderr)
184
+ sys.exit(1)
185
+ convert(Path(sys.argv[1]))
186
+
187
+
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env python
2
+ from pathlib import Path
3
+ import sys
4
+ import tomllib
5
+
6
+
7
+ def main() -> int:
8
+ if len(sys.argv) != 3:
9
+ print(
10
+ "Usage: python .mk-scripts/gen-project-py <pyproject.toml> <output.py>",
11
+ file=sys.stderr,
12
+ )
13
+ return 1
14
+
15
+ pyproject_path = Path(sys.argv[1])
16
+ py_file = Path(sys.argv[2])
17
+
18
+ try:
19
+ if not pyproject_path.exists():
20
+ raise FileNotFoundError(f"File {pyproject_path} does not exist.")
21
+
22
+ with pyproject_path.open("rb") as f:
23
+ data = tomllib.load(f)
24
+
25
+ project = data.get("project", {})
26
+ raw_authors = project.get("authors", [])
27
+ authors: list[tuple[str | None, str | None]] = []
28
+ for entry in raw_authors:
29
+ name = entry.get("name")
30
+ email = entry.get("email")
31
+ if name or email:
32
+ authors.append((name, email))
33
+
34
+ info = {
35
+ "name": project.get("name"),
36
+ "version": project.get("version"),
37
+ "description": project.get("description"),
38
+ "requires_python": project.get("requires-python"),
39
+ "authors": authors,
40
+ }
41
+
42
+ authors_repr = "[" + ", ".join(
43
+ f"({repr(name)}, {repr(email)})" for name, email in info["authors"]
44
+ ) + "]"
45
+
46
+ lines = [
47
+ "# fmt: off",
48
+ "name = " + repr(info["name"]),
49
+ "version = " + repr(info["version"]),
50
+ "description = " + repr(info["description"]),
51
+ "requires_python = " + repr(info["requires_python"]),
52
+ "authors = " + authors_repr,
53
+ "# fmt: on",
54
+ ]
55
+
56
+ py_file.parent.mkdir(parents=True, exist_ok=True)
57
+ with py_file.open("w", encoding="utf-8") as f:
58
+ f.write("\n".join(lines) + "\n")
59
+
60
+ print(f"Generated: {py_file}")
61
+ return 0
62
+
63
+ except Exception as e:
64
+ print(f"Error: {e}", file=sys.stderr)
65
+ return 1
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())
70
+