comver 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.
comver/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
2
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """comver internal API reference.
7
+
8
+ This section of documentation should be mainly considered by people who:
9
+
10
+ - are curious how the project works under the hood
11
+ - want to provide integration of `comver` with third part tooling.
12
+
13
+ Important:
14
+ Check out guidelines and tutorials for information about CLI/plugin
15
+ as this is a more common starting point.
16
+
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from comver import error, plugin, type_definitions
22
+ from comver._version import Version, _version
23
+
24
+ __version__ = _version
25
+ """Current comver version."""
26
+
27
+ __all__: list[str] = [
28
+ "Version",
29
+ "__version__",
30
+ "error",
31
+ "plugin",
32
+ "type_definitions",
33
+ ]
comver/__main__.py ADDED
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
2
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """The `python -m comver` entrypoint."""
7
+
8
+ from __future__ import annotations # pragma: no cover
9
+
10
+ if __name__ == "__main__": # pragma: no cover
11
+ from comver._cli import main
12
+
13
+ main()
comver/_cli.py ADDED
@@ -0,0 +1,38 @@
1
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
2
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """CLI entrypoint of comver."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+
12
+ from comver import _parser, _subcommand
13
+
14
+
15
+ def main(args: list[str] | None = None) -> None:
16
+ """Command-line entry point of the `comver`.
17
+
18
+ Parses arguments and dispatches execution to the appropriate subcommand
19
+ based on user input. If no arguments are provided explicitly, the arguments
20
+ from `sys.argv[1:]` are used instead.
21
+
22
+ Args:
23
+ args:
24
+ CLI arguments passed, if any (used mainly during testing).
25
+
26
+ """
27
+ parsed_args = _parser.root().parse_args(args)
28
+ subcommand = getattr(_subcommand, parsed_args.subcommand, None)
29
+
30
+ # Cannot be `None`, but left to make pyright feel at peace
31
+ if subcommand is None: # pragma: no cover
32
+ print( # noqa: T201
33
+ "Unknown command chosen",
34
+ file=sys.stderr,
35
+ )
36
+ sys.exit(1)
37
+
38
+ subcommand(parsed_args)
comver/_parser.py ADDED
@@ -0,0 +1,132 @@
1
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
2
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Parser of the `comver` CLI."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import textwrap
12
+
13
+ from comver._version import _version
14
+
15
+
16
+ def root() -> argparse.ArgumentParser:
17
+ """Create and return the top-level CLI parser.
18
+
19
+ This parser defines all available subcommands and any global flags.
20
+ Each subcommand is parsed with its corresponding arguments and stored
21
+ in the `subcommand` attribute for later dispatching.
22
+
23
+ Returns:
24
+ The argument parser configured with all CLI subcommands.
25
+
26
+ """
27
+ parser = argparse.ArgumentParser(
28
+ description="Tool CLI with support for subcommands.",
29
+ formatter_class=argparse.RawDescriptionHelpFormatter,
30
+ )
31
+
32
+ _ = parser.add_argument(
33
+ "--version",
34
+ action="version",
35
+ version=_version,
36
+ help="Show the tool version and exit.",
37
+ )
38
+
39
+ subparsers = parser.add_subparsers(
40
+ dest="subcommand",
41
+ required=True,
42
+ )
43
+ _calculate(subparsers)
44
+ _verify(subparsers)
45
+
46
+ return parser
47
+
48
+
49
+ def _calculate(subparsers) -> None: # noqa: ANN001 # pyright: ignore [reportUnknownParameterType, reportMissingParameterType]
50
+ """Create `calculate` subcommand subparser.
51
+
52
+ Args:
53
+ subparsers:
54
+ Object where this subparser is registered.
55
+
56
+ """
57
+ parser = subparsers.add_parser(
58
+ "calculate",
59
+ description=textwrap.dedent("""\
60
+ Calculate semantic version based on commits.
61
+
62
+ NOTE:
63
+
64
+ - This command runs on the git-tree found in current
65
+ working directory.
66
+ - Each value (e.g. version and sha) is space separated
67
+ """),
68
+ formatter_class=argparse.RawDescriptionHelpFormatter,
69
+ )
70
+
71
+ parser.add_argument(
72
+ "--format",
73
+ choices=["line", "json"],
74
+ default="line",
75
+ help="Format of the output (default: line, each output space separated)",
76
+ )
77
+
78
+ parser.add_argument(
79
+ "--sha",
80
+ action="store_true",
81
+ required=False,
82
+ help=(
83
+ "Return sha of the commit related to the version (usable for verification)"
84
+ ),
85
+ )
86
+
87
+ parser.add_argument(
88
+ "--checksum",
89
+ action="store_true",
90
+ required=False,
91
+ help="Return checksum of the configuration (usable for verification)",
92
+ )
93
+
94
+
95
+ def _verify(subparsers) -> None: # noqa: ANN001 # pyright: ignore [reportUnknownParameterType, reportMissingParameterType]
96
+ """Create `verify` subcommand subparser.
97
+
98
+ Args:
99
+ subparsers:
100
+ Object where this subparser is registered.
101
+
102
+ """
103
+ parser = subparsers.add_parser(
104
+ "verify",
105
+ description=textwrap.dedent("""
106
+ Verify version consistency.
107
+
108
+ NOTE:
109
+
110
+ - This command runs on the git-tree found in current
111
+ working directory.
112
+ - You can feed the output of the `calculate` command here
113
+ """),
114
+ formatter_class=argparse.RawDescriptionHelpFormatter,
115
+ )
116
+
117
+ parser.add_argument(
118
+ "version",
119
+ help="Version to check (e.g. `1.37.21`).",
120
+ )
121
+
122
+ parser.add_argument(
123
+ "sha",
124
+ help="Sha of the commit to compare against.",
125
+ )
126
+
127
+ parser.add_argument(
128
+ "checksum",
129
+ help="Checksum of the configuration to check against.",
130
+ )
131
+
132
+ return parser
@@ -0,0 +1,17 @@
1
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
2
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Regex related internal functionalities."""
7
+
8
+ from __future__ import annotations
9
+
10
+ from comver._regex import match, semantic
11
+ from comver._regex._process import process
12
+
13
+ __all__ = [
14
+ "match",
15
+ "process",
16
+ "semantic",
17
+ ]
@@ -0,0 +1,41 @@
1
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
2
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Compilation/performance related regex functionalities."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import typing
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from comver.type_definitions import OptionalStringsOrPatterns
15
+
16
+
17
+ def process(
18
+ regexes: OptionalStringsOrPatterns, default: str | None = None
19
+ ) -> re.Pattern[str] | None:
20
+ """Cached compilation of multiple regexes.
21
+
22
+ > [!IMPORTANT]
23
+ > `compile` should use internal cache, see
24
+ > here: https://github.com/python/cpython/blob/v3.12.0/Lib/re/__init__.py#L271-L329
25
+
26
+ `None` is used, as the regex matching might be slow
27
+ and is avoided whenever possible throughout
28
+ the whole `comver`.
29
+
30
+ Arguments:
31
+ regexes:
32
+ Regexes which should be compiled together
33
+ (if provided).
34
+ default:
35
+ Default regex, if provided.
36
+ """
37
+ if not regexes:
38
+ if default is None:
39
+ return None
40
+ return re.compile(default)
41
+ return re.compile(r"(" + r"|".join(rf"({r})" for r in regexes) + r")")
comver/_regex/match.py ADDED
@@ -0,0 +1,83 @@
1
+ # SPDX-FileCopyrightText: © 2025 nosludge <https://github.com/nosludge>
2
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
3
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ """Regex matching functions."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import typing
12
+
13
+ if typing.TYPE_CHECKING:
14
+ import re
15
+
16
+ import git
17
+
18
+
19
+ def item(
20
+ what: str, include: re.Pattern[str] | None, exclude: re.Pattern[str] | None
21
+ ) -> bool:
22
+ """Check if the `what` is included based on the `include` and `exclude`.
23
+
24
+ Note:
25
+ Empty `include` are treated as include-everything.
26
+
27
+ Note:
28
+ Empty `exclude` are treated as include-everything.
29
+
30
+ Warning:
31
+ Exclude regexes take precedence over include regexes.
32
+
33
+ Args:
34
+ what:
35
+ The string to check.
36
+ include:
37
+ The regex to include the string.
38
+ exclude:
39
+ The regex to exclude.
40
+
41
+ Returns:
42
+ True if the `what` is included, False otherwise.
43
+
44
+ """
45
+ return (include is None or include.search(what) is not None) and (
46
+ exclude is None or exclude.search(what) is None
47
+ )
48
+
49
+
50
+ def path(
51
+ commit: git.Commit,
52
+ include: re.Pattern[str] | None,
53
+ exclude: re.Pattern[str] | None,
54
+ ) -> bool:
55
+ """Check if the commit touched the file based regexes.
56
+
57
+ Note:
58
+ Empty `include` are treated as include-everything.
59
+
60
+ Note:
61
+ Empty `exclude` are treated as include-everything.
62
+
63
+ Warning:
64
+ Exclude regexes take precedence over include regexes.
65
+
66
+ Args:
67
+ commit:
68
+ The commit to check.
69
+ include:
70
+ The regex to include the file.
71
+ exclude:
72
+ The regex to exclude the file.
73
+
74
+ Returns:
75
+ True if the commit touched the file, False otherwise.
76
+ """
77
+ if include is None and exclude is None:
78
+ return True
79
+
80
+ return not commit.diff() or any(
81
+ item(diff.a_path, include, exclude) if diff.a_path else True
82
+ for diff in commit.diff()
83
+ )
@@ -0,0 +1,97 @@
1
+ # SPDX-FileCopyrightText: © 2025 nosludge <https://github.com/nosludge>
2
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
3
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
4
+ #
5
+ # SPDX-License-Identifier: Apache-2.0
6
+
7
+ """Semantic versioning related regexes."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import typing
13
+
14
+ from comver._regex._process import process
15
+
16
+ if typing.TYPE_CHECKING:
17
+ from comver.type_definitions import OptionalStringsOrPatterns
18
+
19
+ Key = typing.TypeVar("Key")
20
+
21
+ Semantic = tuple[Key, re.Pattern[str] | None]
22
+ Major = Semantic[typing.Literal["major"]]
23
+ Minor = Semantic[typing.Literal["minor"]]
24
+ Patch = Semantic[typing.Literal["patch"]]
25
+
26
+ StringOrPattern = str | re.Pattern[str]
27
+
28
+
29
+ def components(
30
+ major_regexes: OptionalStringsOrPatterns,
31
+ minor_regexes: OptionalStringsOrPatterns,
32
+ patch_regexes: OptionalStringsOrPatterns,
33
+ ) -> tuple[Major, Minor, Patch]:
34
+ """Grouped semantic versioning regexes.
35
+
36
+ This allows iteration over all necessary components
37
+ with one function.
38
+
39
+ Args:
40
+ major_regexes:
41
+ The regexes finding the `major` version.
42
+ minor_regexes:
43
+ The regexes finding the `minor` version.
44
+ patch_regexes:
45
+ The regexes finding the `patch` version.
46
+
47
+ Returns:
48
+ Dictionary mapping element of semantic versioning to the regex.
49
+
50
+ """
51
+ return (
52
+ ("major", major(major_regexes)),
53
+ ("minor", minor(minor_regexes)),
54
+ ("patch", patch(patch_regexes)),
55
+ )
56
+
57
+
58
+ def major(regexes: OptionalStringsOrPatterns) -> re.Pattern[str] | None:
59
+ """Return compiled `major` regex.
60
+
61
+ Args:
62
+ regexes:
63
+ Specified `major` version element regexes.
64
+
65
+ Returns:
66
+ Compiled regular expression (either default or provided).
67
+
68
+ """
69
+ return process(regexes, r".*BREAKING CHANGE.*|^(feat|fix)(\(.*?\))?!: .*")
70
+
71
+
72
+ def minor(regexes: OptionalStringsOrPatterns) -> re.Pattern[str] | None:
73
+ """Return compiled `mior` regex.
74
+
75
+ Args:
76
+ regexes:
77
+ Specified `minor` version element regexes.
78
+
79
+ Returns:
80
+ Compiled regular expression (either default or provided).
81
+
82
+ """
83
+ return process(regexes, "^feat(\\(.*?\\))?: .*")
84
+
85
+
86
+ def patch(regexes: OptionalStringsOrPatterns) -> re.Pattern[str] | None:
87
+ """Return compiled `patch` regex.
88
+
89
+ Args:
90
+ regexes:
91
+ Specified `patch` version element regexes.
92
+
93
+ Returns:
94
+ Compiled regular expression (either default or provided).
95
+
96
+ """
97
+ return process(regexes, "^fix(\\(.*?\\))?: .*")
comver/_subcommand.py ADDED
@@ -0,0 +1,174 @@
1
+ # SPDX-FileCopyrightText: © 2025 open-nudge <https://github.com/open-nudge>
2
+ # SPDX-FileContributor: szymonmaszke <github@maszke.co>
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+
6
+ """Subcommands of the CLI `comver`."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import collections
11
+ import hashlib
12
+ import json
13
+ import sys
14
+ import typing
15
+
16
+ import loadfig
17
+
18
+ from comver._version import Version, VersionCommit
19
+
20
+ if typing.TYPE_CHECKING:
21
+ import argparse
22
+
23
+
24
+ def calculate(args: argparse.Namespace) -> typing.NoReturn:
25
+ """Calculate semantic versioning based on commit messages.
26
+
27
+ Outputs version and (optionally) sha of a commit
28
+ related to this version (the last one in commit chain).
29
+
30
+ This output allows to later compare git trees and inferred
31
+ versions if necessary.
32
+
33
+ Args:
34
+ args:
35
+ Arguments from the CLI.
36
+
37
+ """
38
+ print(_calculate(args)) # noqa: T201
39
+ sys.exit(0)
40
+
41
+
42
+ def verify(args: argparse.Namespace) -> typing.NoReturn:
43
+ """Verify commit sha and inferred version match.
44
+
45
+ Given `version` (as string, e.g. `1.2.3`) and
46
+ commit sha verify whether this version was created
47
+ from this commit chain.
48
+
49
+ Args:
50
+ args:
51
+ Arguments from the CLI.
52
+
53
+ """
54
+ sys.exit(_verify(args))
55
+
56
+
57
+ def _calculate(args: argparse.Namespace) -> str: # noqa: C901
58
+ """Implementation of calculate cli command.
59
+
60
+ Args:
61
+ args:
62
+ Arguments from the CLI.
63
+
64
+ Returns:
65
+ Either formatted dictionary (if `args.json`) string
66
+ or space separated "version sha". `sha` component
67
+ is optional based on `args.sha` flag.
68
+
69
+ """
70
+ version = VersionCommit()
71
+ for version in Version.from_git_configured(): # noqa: B007
72
+ pass
73
+
74
+ sha = version.commit.hexsha if version.commit is not None else None
75
+ checksum = _checksum_config()
76
+
77
+ version = str(version.version)
78
+
79
+ if args.format == "line":
80
+ if args.sha:
81
+ version = f"{version} {sha}"
82
+ if args.checksum:
83
+ version = f"{version} {checksum}"
84
+ return version
85
+
86
+ output = {"version": str(version)}
87
+ if args.sha and isinstance(sha, str):
88
+ output["sha"] = sha
89
+ if args.checksum:
90
+ output["checksum"] = checksum
91
+
92
+ return json.dumps(output, indent=4)
93
+
94
+
95
+ def _verify(args: argparse.Namespace) -> bool:
96
+ """Verify commit sha and inferred version match.
97
+
98
+ Warning:
99
+ This subcommand also outputs messages for the end user
100
+
101
+ Args:
102
+ args:
103
+ Arguments from the CLI.
104
+
105
+ Returns:
106
+ Code status of the command (`True` for error, `False`
107
+ on successful verification).
108
+
109
+ """
110
+ checksum = _checksum_config()
111
+
112
+ if args.checksum != checksum:
113
+ print( # noqa: T201
114
+ "Provided checksum and the checksum of configuration do not match.",
115
+ file=sys.stderr,
116
+ )
117
+ return True
118
+
119
+ for output in Version.from_git_configured():
120
+ version, commit = output.version, output.commit
121
+
122
+ sha = commit.hexsha if commit is not None else None
123
+
124
+ if args.version == version:
125
+ if sha is not None and sha == args.sha:
126
+ return False
127
+
128
+ print( # noqa: T201
129
+ f"Specified version: `{args.version}` has sha: `{sha}`, while expected sha is: `{args.sha}`",
130
+ file=sys.stderr,
131
+ ) # pragma: no cover
132
+ return True # pragma: no cover
133
+
134
+ if args.sha == sha:
135
+ print( # noqa: T201
136
+ f"Specified sha: `{args.sha}` corresponds to version: `{version}`, while expected version is: `{args.version}`", # noqa: E501
137
+ file=sys.stderr,
138
+ )
139
+ return True
140
+
141
+ print( # noqa: T201
142
+ f"Neither specified sha: `{args.sha}` nor its corresponding version: `{args.version}` was found in the git tree", # noqa: E501
143
+ file=sys.stderr,
144
+ )
145
+ return True
146
+
147
+
148
+ def _checksum_config() -> str:
149
+ """Get checksum of config.
150
+
151
+ Returns:
152
+ Checksum of subconfig.
153
+
154
+ """
155
+ config = collections.defaultdict(lambda: None, loadfig.config("comver"))
156
+ keys = [
157
+ "message_includes",
158
+ "message_excludes",
159
+ "path_includes",
160
+ "path_excludes",
161
+ "author_name_includes",
162
+ "author_name_excludes",
163
+ "author_email_includes",
164
+ "author_email_excludes",
165
+ "major_regexes",
166
+ "minor_regexes",
167
+ "patch_regexes",
168
+ "unrecognized_message",
169
+ ]
170
+
171
+ # Get only relevant sections of the dict
172
+ subconfig = {k: config[k] for k in keys}
173
+ stringified = json.dumps(subconfig, sort_keys=True)
174
+ return hashlib.sha256(stringified.encode()).hexdigest()