zendev-commit 0.2.0__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.
@@ -0,0 +1,182 @@
1
+ """Offline gitmoji catalog and official-header parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import tomllib
8
+ from dataclasses import dataclass
9
+ from functools import cache
10
+ from importlib.resources import files
11
+
12
+ from zendev.conventional import ParseIssue
13
+
14
+ __all__ = [
15
+ "EmojiConvention",
16
+ "Gitmoji",
17
+ "GitmojiCommit",
18
+ "GitmojiMatch",
19
+ "load_emoji_conventions",
20
+ "load_gitmojis",
21
+ "match_gitmoji",
22
+ "parse_gitmoji_commit",
23
+ ]
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class Gitmoji:
28
+ emoji: str
29
+ code: str
30
+ description: str
31
+ name: str
32
+ semver: str | None
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class EmojiConvention:
37
+ """A Gitmoji intention paired with its canonical zendev commit type."""
38
+
39
+ type: str
40
+ gitmoji: Gitmoji
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class GitmojiMatch:
45
+ gitmoji: Gitmoji
46
+ token: str
47
+ remainder: str
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class GitmojiCommit:
52
+ intention: Gitmoji
53
+ token: str
54
+ scope: str | None
55
+ message: str
56
+ body: str | None
57
+
58
+
59
+ @cache
60
+ def load_gitmojis() -> tuple[Gitmoji, ...]:
61
+ catalog_path = files("zendev").joinpath("data/gitmojis.json")
62
+ payload = json.loads(catalog_path.read_text(encoding="utf-8"))
63
+ return tuple(
64
+ Gitmoji(
65
+ emoji=item["emoji"],
66
+ code=item["code"],
67
+ description=item["description"],
68
+ name=item["name"],
69
+ semver=item["semver"],
70
+ )
71
+ for item in payload["gitmojis"]
72
+ )
73
+
74
+
75
+ @cache
76
+ def load_emoji_conventions() -> tuple[EmojiConvention, ...]:
77
+ """Load and validate the complete, strict emoji-to-type convention."""
78
+
79
+ mapping_path = files("zendev").joinpath("data/emoji-conventions.toml")
80
+ payload = tomllib.loads(mapping_path.read_text(encoding="utf-8"))
81
+ types = payload.get("types")
82
+ if not isinstance(types, dict):
83
+ raise ValueError("emoji-conventions.toml must contain a string-to-string [types] table.")
84
+ type_mapping = {name: value for name, value in types.items() if isinstance(name, str) and isinstance(value, str)}
85
+ if len(type_mapping) != len(types):
86
+ raise ValueError("emoji-conventions.toml must contain a string-to-string [types] table.")
87
+
88
+ catalog = load_gitmojis()
89
+ catalog_names = {item.name for item in catalog}
90
+ mapped_names = set(type_mapping)
91
+ if mapped_names != catalog_names:
92
+ missing = ", ".join(sorted(catalog_names - mapped_names)) or "none"
93
+ extra = ", ".join(sorted(mapped_names - catalog_names)) or "none"
94
+ raise ValueError(
95
+ f"Emoji convention must cover the Gitmoji catalog exactly (missing: {missing}; extra: {extra})."
96
+ )
97
+
98
+ type_names = tuple(type_mapping[item.name] for item in catalog)
99
+ if any(re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", type_name) is None for type_name in type_names):
100
+ raise ValueError("Emoji convention types must be lowercase words separated by single hyphens.")
101
+ if len(set(type_names)) != len(type_names):
102
+ raise ValueError("Emoji convention types must be unique.")
103
+
104
+ return tuple(EmojiConvention(type=type_mapping[item.name], gitmoji=item) for item in catalog)
105
+
106
+
107
+ @cache
108
+ def _token_index() -> tuple[tuple[str, Gitmoji], ...]:
109
+ tokens: dict[str, Gitmoji] = {}
110
+ for gitmoji in load_gitmojis():
111
+ tokens[gitmoji.emoji] = gitmoji
112
+ tokens[gitmoji.code] = gitmoji
113
+ without_variation_selector = gitmoji.emoji.replace("\ufe0f", "")
114
+ tokens.setdefault(without_variation_selector, gitmoji)
115
+ return tuple(sorted(tokens.items(), key=lambda item: len(item[0]), reverse=True))
116
+
117
+
118
+ def match_gitmoji(text: str) -> GitmojiMatch | None:
119
+ for token, gitmoji in _token_index():
120
+ if text.startswith(token) and len(text) > len(token) and text[len(token)].isspace():
121
+ return GitmojiMatch(gitmoji=gitmoji, token=token, remainder=text[len(token) :].lstrip())
122
+ return None
123
+
124
+
125
+ def parse_gitmoji_commit(text: str) -> tuple[GitmojiCommit | None, ParseIssue | None]:
126
+ """Parse the official gitmoji title form and an optional Git body."""
127
+
128
+ lines = text.splitlines()
129
+ if not lines or not lines[0]:
130
+ return None, ParseIssue("empty-message", "The commit message is empty.")
131
+ match = match_gitmoji(lines[0])
132
+ if match is None:
133
+ return None, ParseIssue(
134
+ "invalid-gitmoji",
135
+ "Expected an official gitmoji Unicode token or shortcode.",
136
+ )
137
+
138
+ remainder = match.remainder
139
+ scope: str | None = None
140
+ if remainder.startswith("("):
141
+ closing = remainder.find(")")
142
+ if closing <= 1:
143
+ return None, ParseIssue("invalid-gitmoji-scope", "The gitmoji scope must be non-empty.")
144
+ scope = remainder[1:closing]
145
+ remainder = remainder[closing + 1 :]
146
+ if remainder.startswith(":"):
147
+ if len(remainder) == 1 or not remainder[1].isspace():
148
+ return None, ParseIssue(
149
+ "invalid-gitmoji-separator",
150
+ "A colon after the gitmoji scope must be followed by a space.",
151
+ )
152
+ remainder = remainder[1:]
153
+ elif not remainder or not remainder[0].isspace():
154
+ return None, ParseIssue(
155
+ "invalid-gitmoji-separator",
156
+ "The gitmoji scope and message must be separated by whitespace or ': '.",
157
+ )
158
+ elif remainder.startswith(":"):
159
+ if len(remainder) == 1 or not remainder[1].isspace():
160
+ return None, ParseIssue(
161
+ "invalid-gitmoji-separator",
162
+ "A gitmoji colon must be followed by a space.",
163
+ )
164
+ remainder = remainder[1:]
165
+ message = remainder.lstrip()
166
+ if not message:
167
+ return None, ParseIssue("missing-gitmoji-message", "The gitmoji message is required.")
168
+
169
+ if len(lines) > 1 and lines[1] != "":
170
+ return None, ParseIssue(
171
+ "missing-header-separator",
172
+ "The body must begin one blank line after the gitmoji message.",
173
+ line=2,
174
+ )
175
+ body = "\n".join(lines[2:]).strip() if len(lines) > 2 else ""
176
+ return GitmojiCommit(
177
+ intention=match.gitmoji,
178
+ token=match.token,
179
+ scope=scope,
180
+ message=message,
181
+ body=body or None,
182
+ ), None
File without changes
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.5
2
+ Name: zendev-commit
3
+ Version: 0.2.0
4
+ Summary: Commit-message conventions and interactive commit workflows
5
+ Project-URL: Homepage, https://github.com/zendev-lab/zendev
6
+ Project-URL: Issues, https://github.com/zendev-lab/zendev/issues
7
+ Project-URL: Repository, https://github.com/zendev-lab/zendev
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: questionary>=2.1
11
+ Requires-Dist: typer>=0.27
12
+ Description-Content-Type: text/markdown
13
+
14
+ # zendev-commit
15
+
16
+ `zendev-commit` owns commit profiles, Conventional Commits and Gitmoji
17
+ validation, the interactive commit flow, and its vendored data. It can be
18
+ installed and used without the complete `zendev` toolkit.
19
+
20
+ ```console
21
+ $ uv add --dev zendev-commit
22
+ $ uvx --from zendev-commit zendev-commit --help
23
+ $ uvx --from zendev-commit zendev-commit-msg --help
24
+ ```
25
+
26
+ `zendev-commit-msg` validates complete commit messages. `zendev-commit`
27
+ interactively builds a message and invokes `git commit`.
28
+
29
+ ## Python API
30
+
31
+ ```python
32
+ from zendev.commit import CommitProfile, validate_commit_message
33
+
34
+ result = validate_commit_message(
35
+ "feat(parser): accept empty input",
36
+ profile=CommitProfile.CONVENTIONAL,
37
+ )
38
+ assert result.valid
39
+ ```
40
+
41
+ ## Profiles
42
+
43
+ Configure the default profile in the consuming repository:
44
+
45
+ ```toml
46
+ [tool.zendev.commit]
47
+ profile = "conventional"
48
+ ```
49
+
50
+ The `--profile` option overrides repository configuration. `auto` reads the
51
+ nearest `pyproject.toml` and falls back to `zendev`.
52
+
53
+ | Profile | Contract |
54
+ | --- | --- |
55
+ | `zendev` | A Gitmoji emoji or shortcode paired with its canonical commit type. |
56
+ | `conventional` | Conventional Commits 1.0.0, including scopes, breaking changes, bodies, and footers. |
57
+ | `gitmoji` | The Gitmoji title shape with Unicode or shortcode intentions and optional scope/body. |
58
+
59
+ Examples accepted by the default profile:
60
+
61
+ ```text
62
+ 🎉 init: begin a project
63
+ ✨ feat: add export
64
+ 🐛 fix(parser): handle null token
65
+ :memo: docs: update README
66
+ 🚀 deploy: publish the package
67
+ ```
68
+
69
+ Git-generated `Merge`, `Revert`, `fixup!`, `squash!`, `amend!`, and `reword!`
70
+ messages are accepted.
71
+
72
+ ## Commit hook
73
+
74
+ With `.pre-commit-config.yaml`:
75
+
76
+ ```yaml
77
+ repos:
78
+ - repo: https://github.com/zendev-lab/zendev
79
+ rev: v0.1.0
80
+ hooks:
81
+ - id: zendev-commit-msg
82
+ ```
83
+
84
+ With `prek.toml`:
85
+
86
+ ```toml
87
+ [[repos]]
88
+ repo = "https://github.com/zendev-lab/zendev"
89
+ rev = "v0.1.0"
90
+ hooks = [
91
+ { id = "zendev-commit-msg" },
92
+ ]
93
+ ```
94
+
95
+ Install the Git hook:
96
+
97
+ ```console
98
+ $ uvx prek install --hook-type commit-msg
99
+ ```
100
+
101
+ An explicit command is also available:
102
+
103
+ ```console
104
+ $ uvx --from zendev-commit zendev-commit-msg --profile conventional .git/COMMIT_EDITMSG
105
+ ```
106
+
107
+ ## Gitmoji data
108
+
109
+ The default profile uses the vendored catalog in
110
+ [`src/zendev/data/gitmojis.json`](./src/zendev/data/gitmojis.json) and the
111
+ reviewable pairing table in
112
+ [`src/zendev/data/emoji-conventions.toml`](./src/zendev/data/emoji-conventions.toml).
113
+ Validation is deterministic and does not access the network.
114
+
115
+ The retained upstream license is in
116
+ [`src/zendev/data/LICENSE.gitmoji`](./src/zendev/data/LICENSE.gitmoji).
117
+ Maintainers should follow the
118
+ [vendored-data procedure](../../CONTRIBUTING.md#vendored-gitmoji-data) when
119
+ refreshing the catalog.
@@ -0,0 +1,13 @@
1
+ zendev/commit/__init__.py,sha256=B46lCIchjlCFtZEu7wP8LQ7vIbzxYnmw6lAAmswmrzw,18458
2
+ zendev/commit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ zendev/conventional/__init__.py,sha256=2FrhUa9r8pLctJy_3E0XF9u0x3ztt19854Q49HBeWlI,4130
4
+ zendev/conventional/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ zendev/data/LICENSE.gitmoji,sha256=Rj862ib3jsO1Zs4S-gmYLmDl-MBubo3sHzK1WcT01aY,1075
6
+ zendev/data/emoji-conventions.toml,sha256=yqdFA84-QxJtmfvjhaadLwqbozcyD9m2Z7YeyugirL4,1873
7
+ zendev/data/gitmojis.json,sha256=t-8tSHnRPufn-Bd3hP_bAACC0fIvQmF_R_u7fK3yMWc,14924
8
+ zendev/gitmoji/__init__.py,sha256=QCzrKJLOUBqUrmgQ7nlzrGnMMnWX9dXg3cdWZH_c33w,6239
9
+ zendev/gitmoji/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ zendev_commit-0.2.0.dist-info/METADATA,sha256=KPbCHZNTMQhp3FJSaTxSo6B7gSj4hwMD8DNcAQLgAtg,3182
11
+ zendev_commit-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
12
+ zendev_commit-0.2.0.dist-info/entry_points.txt,sha256=BHU_xD0A8mJquP3qenSFeZiKG4horu78PzFES9kM0c4,97
13
+ zendev_commit-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ zendev-commit = zendev.commit:main
3
+ zendev-commit-msg = zendev.commit:hook_main