Skip to content

Commit 5def1d0

Browse files
committed
feat(version-schemes): expose version_schemes as a commitizen.scheme endpoint.
Refactors `version_types` (which is now deprecated) into `version_schemes`, exposes them through a `commitizen.scheme` entrypoint and ensure it encapsulates all the versionning logic for parsing and bumping.
1 parent cbab456 commit 5def1d0

26 files changed

+573
-399
lines changed

commitizen/bump.py

+11-133
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
1+
from __future__ import annotations
2+
13
import os
24
import re
3-
import sys
4-
import typing
55
from collections import OrderedDict
6-
from itertools import zip_longest
76
from string import Template
8-
from typing import List, Optional, Tuple, Type, Union
9-
10-
from packaging.version import Version
7+
from typing import List, Optional, Tuple, Union
118

12-
from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message
9+
from commitizen.defaults import bump_message
1310
from commitizen.exceptions import CurrentVersionNotFoundError
1411
from commitizen.git import GitCommit, smart_open
15-
16-
if sys.version_info >= (3, 8):
17-
from commitizen.version_types import VersionProtocol
18-
else:
19-
# workaround mypy issue for 3.7 python
20-
VersionProtocol = typing.Any
12+
from commitizen.version_schemes import DEFAULT_SCHEME, VersionScheme, Version
2113

2214

2315
def find_increment(
@@ -53,115 +45,6 @@ def find_increment(
5345
return increment
5446

5547

56-
def prerelease_generator(
57-
current_version: str, prerelease: Optional[str] = None, offset: int = 0
58-
) -> str:
59-
"""Generate prerelease
60-
61-
X.YaN # Alpha release
62-
X.YbN # Beta release
63-
X.YrcN # Release Candidate
64-
X.Y # Final
65-
66-
This function might return something like 'alpha1'
67-
but it will be handled by Version.
68-
"""
69-
if not prerelease:
70-
return ""
71-
72-
version = Version(current_version)
73-
# version.pre is needed for mypy check
74-
if version.is_prerelease and version.pre and prerelease.startswith(version.pre[0]):
75-
prev_prerelease: int = version.pre[1]
76-
new_prerelease_number = prev_prerelease + 1
77-
else:
78-
new_prerelease_number = offset
79-
pre_version = f"{prerelease}{new_prerelease_number}"
80-
return pre_version
81-
82-
83-
def devrelease_generator(devrelease: int = None) -> str:
84-
"""Generate devrelease
85-
86-
The devrelease version should be passed directly and is not
87-
inferred based on the previous version.
88-
"""
89-
if devrelease is None:
90-
return ""
91-
92-
return f"dev{devrelease}"
93-
94-
95-
def semver_generator(current_version: str, increment: str = None) -> str:
96-
version = Version(current_version)
97-
prev_release = list(version.release)
98-
increments = [MAJOR, MINOR, PATCH]
99-
increments_version = dict(zip_longest(increments, prev_release, fillvalue=0))
100-
101-
# This flag means that current version
102-
# must remove its prerelease tag,
103-
# so it doesn't matter the increment.
104-
# Example: 1.0.0a0 with PATCH/MINOR -> 1.0.0
105-
if not version.is_prerelease:
106-
if increment == MAJOR:
107-
increments_version[MAJOR] += 1
108-
increments_version[MINOR] = 0
109-
increments_version[PATCH] = 0
110-
elif increment == MINOR:
111-
increments_version[MINOR] += 1
112-
increments_version[PATCH] = 0
113-
elif increment == PATCH:
114-
increments_version[PATCH] += 1
115-
116-
return str(
117-
f"{increments_version['MAJOR']}."
118-
f"{increments_version['MINOR']}."
119-
f"{increments_version['PATCH']}"
120-
)
121-
122-
123-
def generate_version(
124-
current_version: str,
125-
increment: str,
126-
prerelease: Optional[str] = None,
127-
prerelease_offset: int = 0,
128-
devrelease: Optional[int] = None,
129-
is_local_version: bool = False,
130-
version_type_cls: Optional[Type[VersionProtocol]] = None,
131-
) -> VersionProtocol:
132-
"""Based on the given increment a proper semver will be generated.
133-
134-
For now the rules and versioning scheme is based on
135-
python's PEP 0440.
136-
More info: https://www.python.org/dev/peps/pep-0440/
137-
138-
Example:
139-
PATCH 1.0.0 -> 1.0.1
140-
MINOR 1.0.0 -> 1.1.0
141-
MAJOR 1.0.0 -> 2.0.0
142-
"""
143-
if version_type_cls is None:
144-
version_type_cls = Version
145-
if is_local_version:
146-
version = version_type_cls(current_version)
147-
dev_version = devrelease_generator(devrelease=devrelease)
148-
pre_version = prerelease_generator(
149-
str(version.local), prerelease=prerelease, offset=prerelease_offset
150-
)
151-
semver = semver_generator(str(version.local), increment=increment)
152-
153-
return version_type_cls(f"{version.public}+{semver}{pre_version}{dev_version}")
154-
else:
155-
dev_version = devrelease_generator(devrelease=devrelease)
156-
pre_version = prerelease_generator(
157-
current_version, prerelease=prerelease, offset=prerelease_offset
158-
)
159-
semver = semver_generator(current_version, increment=increment)
160-
161-
# TODO: post version
162-
return version_type_cls(f"{semver}{pre_version}{dev_version}")
163-
164-
16548
def update_version_in_files(
16649
current_version: str, new_version: str, files: List[str], *, check_consistency=False
16750
) -> None:
@@ -218,9 +101,9 @@ def _version_to_regex(version: str) -> str:
218101

219102

220103
def normalize_tag(
221-
version: Union[VersionProtocol, str],
104+
version: Union[Version, str],
222105
tag_format: Optional[str] = None,
223-
version_type_cls: Optional[Type[VersionProtocol]] = None,
106+
scheme: Optional[VersionScheme] = None,
224107
) -> str:
225108
"""The tag and the software version might be different.
226109
@@ -233,19 +116,14 @@ def normalize_tag(
233116
| ver1.0.0 | 1.0.0 |
234117
| ver1.0.0.a0 | 1.0.0a0 |
235118
"""
236-
if version_type_cls is None:
237-
version_type_cls = Version
238-
if isinstance(version, str):
239-
version = version_type_cls(version)
119+
scheme = scheme or DEFAULT_SCHEME
120+
version = scheme(version) if isinstance(version, str) else version
240121

241122
if not tag_format:
242123
return str(version)
243124

244125
major, minor, patch = version.release
245-
prerelease = ""
246-
# version.pre is needed for mypy check
247-
if version.is_prerelease and version.pre:
248-
prerelease = f"{version.pre[0]}{version.pre[1]}"
126+
prerelease = version.prerelease or ""
249127

250128
t = Template(tag_format)
251129
return t.safe_substitute(
@@ -256,7 +134,7 @@ def normalize_tag(
256134
def create_commit_message(
257135
current_version: Union[Version, str],
258136
new_version: Union[Version, str],
259-
message_template: str = None,
137+
message_template: Optional[str] = None,
260138
) -> str:
261139
if message_template is None:
262140
message_template = bump_message

commitizen/changelog.py

+24-35
Original file line numberDiff line numberDiff line change
@@ -24,51 +24,41 @@
2424
- [x] hook after changelog is generated (api calls)
2525
- [x] add support for change_type maps
2626
"""
27+
from __future__ import annotations
2728

2829
import os
2930
import re
30-
import sys
31-
import typing
3231
from collections import OrderedDict, defaultdict
3332
from datetime import date
34-
from typing import Callable, Dict, Iterable, List, Optional, Tuple, Type
33+
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Tuple, cast
3534

3635
from jinja2 import Environment, PackageLoader
37-
from packaging.version import InvalidVersion, Version
3836

39-
from commitizen import defaults
4037
from commitizen.bump import normalize_tag
4138
from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError
4239
from commitizen.git import GitCommit, GitTag
40+
from commitizen.version_schemes import DEFAULT_SCHEME, Pep440, InvalidVersion
4341

44-
if sys.version_info >= (3, 8):
45-
from commitizen.version_types import VersionProtocol
46-
else:
47-
# workaround mypy issue for 3.7 python
48-
VersionProtocol = typing.Any
42+
if TYPE_CHECKING:
43+
from commitizen.version_schemes import VersionScheme
4944

5045

5146
def get_commit_tag(commit: GitCommit, tags: List[GitTag]) -> Optional[GitTag]:
5247
return next((tag for tag in tags if tag.rev == commit.rev), None)
5348

5449

55-
def get_version(tag: GitTag) -> Optional[Version]:
56-
version = None
57-
try:
58-
version = Version(tag.name)
59-
except InvalidVersion:
60-
pass
61-
return version
62-
63-
6450
def tag_included_in_changelog(
65-
tag: GitTag, used_tags: List, merge_prerelease: bool
51+
tag: GitTag,
52+
used_tags: List,
53+
merge_prerelease: bool,
54+
scheme: VersionScheme = DEFAULT_SCHEME,
6655
) -> bool:
6756
if tag in used_tags:
6857
return False
6958

70-
version = get_version(tag)
71-
if version is None:
59+
try:
60+
version = scheme(tag.name)
61+
except InvalidVersion:
7262
return False
7363

7464
if merge_prerelease and version.is_prerelease:
@@ -86,6 +76,7 @@ def generate_tree_from_commits(
8676
change_type_map: Optional[Dict[str, str]] = None,
8777
changelog_message_builder_hook: Optional[Callable] = None,
8878
merge_prerelease: bool = False,
79+
scheme: VersionScheme = DEFAULT_SCHEME,
8980
) -> Iterable[Dict]:
9081
pat = re.compile(changelog_pattern)
9182
map_pat = re.compile(commit_parser, re.MULTILINE)
@@ -111,7 +102,7 @@ def generate_tree_from_commits(
111102
commit_tag = get_commit_tag(commit, tags)
112103

113104
if commit_tag is not None and tag_included_in_changelog(
114-
commit_tag, used_tags, merge_prerelease
105+
commit_tag, used_tags, merge_prerelease, scheme=scheme
115106
):
116107
used_tags.append(commit_tag)
117108
yield {
@@ -184,13 +175,15 @@ def render_changelog(tree: Iterable) -> str:
184175
return changelog
185176

186177

187-
def parse_version_from_markdown(value: str) -> Optional[str]:
178+
def parse_version_from_markdown(
179+
value: str, scheme: VersionScheme = Pep440
180+
) -> Optional[str]:
188181
if not value.startswith("#"):
189182
return None
190-
m = re.search(defaults.version_parser, value)
183+
m = scheme.parser.search(value)
191184
if not m:
192185
return None
193-
return m.groupdict().get("version")
186+
return cast(str, m.groupdict().get("version"))
194187

195188

196189
def parse_title_type_of_line(value: str) -> Optional[str]:
@@ -201,7 +194,7 @@ def parse_title_type_of_line(value: str) -> Optional[str]:
201194
return m.groupdict().get("title")
202195

203196

204-
def get_metadata(filepath: str) -> Dict:
197+
def get_metadata(filepath: str, scheme: VersionScheme = Pep440) -> Dict:
205198
unreleased_start: Optional[int] = None
206199
unreleased_end: Optional[int] = None
207200
unreleased_title: Optional[str] = None
@@ -234,7 +227,7 @@ def get_metadata(filepath: str) -> Dict:
234227
unreleased_end = index
235228

236229
# Try to find the latest release done
237-
version = parse_version_from_markdown(line)
230+
version = parse_version_from_markdown(line, scheme)
238231
if version:
239232
latest_version = version
240233
latest_version_position = index
@@ -326,7 +319,7 @@ def get_oldest_and_newest_rev(
326319
tags: List[GitTag],
327320
version: str,
328321
tag_format: str,
329-
version_type_cls: Optional[Type[VersionProtocol]] = None,
322+
scheme: Optional[VersionScheme] = None,
330323
) -> Tuple[Optional[str], Optional[str]]:
331324
"""Find the tags for the given version.
332325
@@ -341,15 +334,11 @@ def get_oldest_and_newest_rev(
341334
except ValueError:
342335
newest = version
343336

344-
newest_tag = normalize_tag(
345-
newest, tag_format=tag_format, version_type_cls=version_type_cls
346-
)
337+
newest_tag = normalize_tag(newest, tag_format=tag_format, scheme=scheme)
347338

348339
oldest_tag = None
349340
if oldest:
350-
oldest_tag = normalize_tag(
351-
oldest, tag_format=tag_format, version_type_cls=version_type_cls
352-
)
341+
oldest_tag = normalize_tag(oldest, tag_format=tag_format, scheme=scheme)
353342

354343
tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag)
355344
if not tags_range:

0 commit comments

Comments
 (0)