bumpversion-slim 0.1.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.
File without changes
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from bumpversion_slim import errors
9
+
10
+ if t.TYPE_CHECKING:
11
+ from bumpversion_slim.config import Config
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ T = t.TypeVar("T", bound="BumpVersion")
16
+
17
+
18
+ @dataclass
19
+ class ModifyFile:
20
+ """Configured file for modification."""
21
+
22
+ filename: str
23
+ path: Path
24
+ patterns: list[str]
25
+
26
+ def update(self, current: str, new: str, *, dry_run: bool) -> tuple[Path, Path]:
27
+ """Update file with configured patterns."""
28
+ try:
29
+ with self.path.open("r") as f:
30
+ contents = f.read()
31
+ except FileNotFoundError as e:
32
+ msg = f"Configured file not found '{self.filename}'."
33
+ raise errors.VersionError(msg) from e
34
+
35
+ for pattern in self.patterns:
36
+ try:
37
+ search, replace = pattern.format(version=current), pattern.format(version=new)
38
+ except KeyError as e:
39
+ msg = f"Incorrect pattern '{pattern}' for '{self.filename}'."
40
+ raise errors.VersionError(msg) from e
41
+
42
+ if search == replace:
43
+ msg = f"Pattern '{pattern}' generated no change for '{self.filename}'."
44
+ raise errors.VersionError(msg)
45
+
46
+ new_contents = contents.replace(search, replace)
47
+ if new_contents == contents:
48
+ msg = f"No change for '{self.filename}', ensure pattern '{pattern}' is correct."
49
+ raise errors.VersionError(msg)
50
+ contents = new_contents
51
+
52
+ backup = Path(f"{self.path}.bak")
53
+ if not dry_run:
54
+ with backup.open("w") as f:
55
+ f.write(contents)
56
+
57
+ return (self.path, backup)
58
+
59
+
60
+ class BumpVersion: # noqa: D101
61
+ def __init__(self, cfg: Config, new: str = "", *, allow_dirty: bool = False, dry_run: bool = False) -> None:
62
+ self.allow_dirty = allow_dirty
63
+ self.dry_run = dry_run
64
+ self.config = cfg
65
+ self.new = new
66
+
67
+ def replace(self, version: str) -> list[str]: # noqa: D102
68
+ cwd = Path.cwd()
69
+ files_to_modify = {
70
+ "pyproject.toml": ModifyFile("pyproject.toml", cwd / "pyproject.toml", ['current_version = "{version}"']),
71
+ }
72
+ for file in self.config.files:
73
+ mf = files_to_modify.get(file["filename"], ModifyFile(file["filename"], cwd / file["filename"], []))
74
+ mf.patterns.append(file.get("pattern", "{version}"))
75
+ files_to_modify[file["filename"]] = mf
76
+
77
+ modified_files = []
78
+ for file in files_to_modify.values():
79
+ try:
80
+ modified_files.append(file.update(self.config.current_version, version, dry_run=self.dry_run))
81
+ except Exception: # noqa: PERF203
82
+ if not self.dry_run:
83
+ for _original, backup in modified_files:
84
+ backup.unlink()
85
+ raise
86
+
87
+ if not self.dry_run:
88
+ for original, backup in modified_files:
89
+ original.write_text(backup.read_text())
90
+ backup.unlink()
91
+
92
+ return sorted({mf.filename for mf in files_to_modify.values()})
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib
5
+ import importlib.metadata
6
+ import sys
7
+ import time
8
+
9
+ from bumpversion_slim import (
10
+ config,
11
+ errors,
12
+ )
13
+ from bumpversion_slim.bump import BumpVersion
14
+ from bumpversion_slim.context import Context
15
+ from bumpversion_slim.git import Git
16
+
17
+ version = importlib.metadata.version("bumpversion_slim")
18
+ parser = argparse.ArgumentParser(prog="bumpversion")
19
+ parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
20
+ parser.add_argument("version_number", help="The desired version number to bump to.")
21
+ parser.add_argument("--dry-run", action=argparse.BooleanOptionalAction, help="Don't commit changes, check for errors.")
22
+ parser.add_argument(
23
+ "--allow-dirty",
24
+ action=argparse.BooleanOptionalAction,
25
+ help="Don't abort if branch contains uncommitted changes.",
26
+ )
27
+ parser.add_argument(
28
+ "--allow-missing",
29
+ action=argparse.BooleanOptionalAction,
30
+ help="Don't abort if missing commits on origin.",
31
+ )
32
+ parser.add_argument(
33
+ "--commit",
34
+ action=argparse.BooleanOptionalAction,
35
+ help="Commit changes made to package, and configured files, after writing.",
36
+ )
37
+ parser.add_argument("--tag", action=argparse.BooleanOptionalAction, help="Tag changes made after commit.")
38
+ parser.add_argument("--verbose", "-v", action="count", default=0, help="Set output verbosity.")
39
+
40
+
41
+ def process_info(info: dict, context: Context, cfg: config.Config, *, dry_run: bool) -> None:
42
+ """Process git info and raise on invalid state."""
43
+ if dry_run:
44
+ return
45
+
46
+ if info["dirty"] and not cfg.allow_dirty:
47
+ context.error("Working directory is not clean. Use `allow_dirty` configuration to ignore.")
48
+ sys.exit(1)
49
+
50
+ if info["missing_local"] and not cfg.allow_missing:
51
+ context.error(
52
+ "Current local branch is missing commits from remote %s.\nUse `allow_missing` configuration to ignore.",
53
+ info["branch"],
54
+ )
55
+ sys.exit(1)
56
+
57
+ if info["missing_remote"] and not cfg.allow_missing:
58
+ context.error(
59
+ "Current remote branch is missing commits from local %s.\nUse `allow_missing` configuration to ignore.",
60
+ info["branch"],
61
+ )
62
+ sys.exit(1)
63
+
64
+ allowed_branches = cfg.allowed_branches
65
+ if allowed_branches and info["branch"] not in allowed_branches:
66
+ context.error("Current branch not in allowed generation branches.")
67
+ sys.exit(1)
68
+
69
+
70
+ def main() -> None:
71
+ """Bump package version."""
72
+ parsed = parser.parse_args()
73
+ start = time.time()
74
+ cfg = config.read(
75
+ allow_dirty=parsed.allow_dirty,
76
+ allow_missing=parsed.allow_missing,
77
+ commit=parsed.commit,
78
+ tag=parsed.tag,
79
+ verbose=parsed.verbose,
80
+ )
81
+ context = Context(cfg, parsed.verbose)
82
+
83
+ try:
84
+ _bump(
85
+ context,
86
+ cfg,
87
+ parsed.version_number,
88
+ dry_run=parsed.dry_run,
89
+ )
90
+ except errors.BumpException as ex:
91
+ context.stacktrace()
92
+ context.debug("Run time (error) %fms", (time.time() - start) * 1000)
93
+ context.error(str(ex))
94
+ sys.exit(1)
95
+ context.debug("Run time %fms", (time.time() - start) * 1000)
96
+ sys.exit(0)
97
+
98
+
99
+ def _bump(
100
+ context: Context,
101
+ cfg: config.Config,
102
+ new_version: str,
103
+ *,
104
+ dry_run: bool = False,
105
+ ) -> None:
106
+ bv = BumpVersion(cfg, new_version, dry_run=dry_run, allow_dirty=cfg.allow_dirty)
107
+ git = Git(context=context, dry_run=dry_run, commit=cfg.commit, tag=cfg.tag)
108
+
109
+ process_info(git.get_current_info(), context, cfg, dry_run=dry_run)
110
+
111
+ version_tag = cfg.version_string.format(new_version=str(new_version))
112
+
113
+ def release_hook(_context: Context, new_version: str) -> list[str]:
114
+ return bv.replace(new_version)
115
+
116
+ hooks = [release_hook]
117
+ for hook in cfg.hooks:
118
+ try:
119
+ import_path, hook_func = hook.split(":")
120
+ except ValueError:
121
+ context.error("Invalid hook format, expected `path.to.module:hook_func`.")
122
+ sys.exit(1)
123
+
124
+ try:
125
+ mod = importlib.import_module(import_path)
126
+ except ModuleNotFoundError:
127
+ context.error("Invalid hook module `%s`, not found.", import_path)
128
+ sys.exit(1)
129
+
130
+ try:
131
+ hooks.append(getattr(mod, hook_func))
132
+ except AttributeError:
133
+ context.error("Invalid hook func `%s`, not found in hook module.", hook_func)
134
+ sys.exit(1)
135
+
136
+ paths = cfg.commit_extra
137
+ for hook in hooks:
138
+ hook_paths = hook(context, new_version)
139
+ paths.extend(hook_paths)
140
+
141
+ git.commit(cfg.current_version, new_version, version_tag, paths)
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import typing
5
+ from pathlib import Path
6
+
7
+ import rtoml
8
+
9
+ from bumpversion_slim import errors
10
+
11
+ P = typing.ParamSpec("P")
12
+
13
+
14
+ @dataclasses.dataclass
15
+ class Config:
16
+ """Changelog configuration options."""
17
+
18
+ current_version: str
19
+ allowed_branches: list[str] = dataclasses.field(default_factory=list)
20
+
21
+ # CLI overrides
22
+ verbose: int = 0
23
+ commit: bool = True
24
+ tag: bool = True
25
+ allow_dirty: bool = False
26
+ allow_missing: bool = False
27
+ commit_extra: list[str] = dataclasses.field(default_factory=list)
28
+
29
+ # Version bumping
30
+ files: dict = dataclasses.field(default_factory=dict)
31
+ version_string: str = "v{new_version}"
32
+
33
+ # Hooks
34
+ hooks: list[str] = dataclasses.field(default_factory=list)
35
+ custom: dict = dataclasses.field(default_factory=dict)
36
+
37
+
38
+ def _process_overrides(overrides: dict) -> dict:
39
+ """Process provided overrides.
40
+
41
+ Remove any unsupplied values (None).
42
+ """
43
+ return {k: v for k, v in overrides.items() if v is not None}
44
+
45
+
46
+ def _process_pyproject(pyproject: Path) -> dict:
47
+ cfg = {}
48
+ with pyproject.open() as f:
49
+ data = rtoml.load(f)
50
+
51
+ if "tool" not in data or "bumpversion" not in data["tool"]:
52
+ return cfg
53
+
54
+ return data["tool"]["bumpversion"]
55
+
56
+
57
+ def check_deprecations(cfg: dict) -> None: # noqa: ARG001
58
+ """Check parsed configuration dict for deprecated features."""
59
+ # No current deprecations
60
+ return
61
+
62
+
63
+ def read(path: str = "pyproject.toml", *args: P.args, **kwargs: P.kwargs) -> Config: # noqa: ARG001
64
+ """Read configuration from local environment.
65
+
66
+ Supported configuration locations (checked in order):
67
+ * pyproject.toml
68
+ """
69
+ overrides = _process_overrides(kwargs)
70
+ cfg = {}
71
+
72
+ pyproject = Path(path)
73
+
74
+ if not pyproject.exists():
75
+ msg = "pyproject.toml configuration missing."
76
+ raise errors.BumpException(msg)
77
+
78
+ # parse pyproject
79
+ cfg = _process_pyproject(pyproject)
80
+
81
+ cfg.update(overrides)
82
+
83
+ check_deprecations(cfg) # pragma: no mutate
84
+
85
+ try:
86
+ return Config(**cfg)
87
+ except TypeError as e:
88
+ msg = "Invalid configuration."
89
+ raise errors.BumpException(msg) from e
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import sys
5
+ import traceback
6
+ import typing
7
+ from enum import IntEnum
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from bumpversion_slim.config import Config
11
+
12
+
13
+ class Verbosity(IntEnum):
14
+ """Verbosity levels."""
15
+
16
+ quiet = 0
17
+ verbose1 = 1
18
+ verbose2 = 2
19
+ verbose3 = 3
20
+
21
+
22
+ P = typing.ParamSpec("P")
23
+
24
+
25
+ class Context:
26
+ """Global context class."""
27
+
28
+ def __init__(self, config: Config, verbose: int = 0) -> None:
29
+ self.config = config
30
+ self._verbose = verbose
31
+ self._indent = 0
32
+
33
+ def reset(self) -> None:
34
+ """Reset context messaging indentation."""
35
+ self._indent = 0
36
+
37
+ def indent(self) -> None:
38
+ """Indent context messaging."""
39
+ self._indent += 1
40
+
41
+ def dedent(self) -> None:
42
+ """Dedent context messaging."""
43
+ self._indent = max(0, self._indent - 1)
44
+
45
+ def _echo(self, message: str, *args: P.args, **kwargs: P.kwargs) -> None: # noqa: ARG002
46
+ """Echo to the console."""
47
+ message = message % args
48
+ print(f"{' ' * self._indent}{message}")
49
+
50
+ def error(self, message: str, *args: P.args, **kwargs: P.kwargs) -> None:
51
+ """Echo to the console."""
52
+ self._echo(message, *args, **kwargs)
53
+
54
+ def warning(self, message: str, *args: P.args, **kwargs: P.kwargs) -> None:
55
+ """Echo to the console for -v."""
56
+ if self._verbose > Verbosity.quiet:
57
+ self._echo(message, *args, **kwargs)
58
+
59
+ def info(self, message: str, *args: P.args, **kwargs: P.kwargs) -> None:
60
+ """Echo to the console for -vv."""
61
+ if self._verbose > Verbosity.verbose1:
62
+ self._echo(message, *args, **kwargs)
63
+
64
+ def debug(self, message: str, *args: P.args, **kwargs: P.kwargs) -> None:
65
+ """Echo to the console for -vvv."""
66
+ if self._verbose > Verbosity.verbose2:
67
+ self._echo(message, *args, **kwargs)
68
+
69
+ def stacktrace(self) -> None:
70
+ """Echo exceptions to console for -vvv."""
71
+ if self._verbose > Verbosity.verbose2:
72
+ t, v, tb = sys.exc_info()
73
+ sio = io.StringIO()
74
+ traceback.print_exception(t, v, tb, None, sio)
75
+ s = sio.getvalue()
76
+ # Clean up odd python 3.11, 3.12 formatting on mac
77
+ s = s.replace("\n ^^^^^^^^^^^^^^^^^^^^^^^^^^", "")
78
+ sio.close()
79
+ self._echo(s)
@@ -0,0 +1,10 @@
1
+ class BumpException(Exception): # noqa: N818
2
+ """Base exception class."""
3
+
4
+
5
+ class GitError(BumpException):
6
+ """Version control error."""
7
+
8
+
9
+ class VersionError(BumpException):
10
+ """Version change error."""
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ import git
6
+
7
+ from bumpversion_slim import errors
8
+
9
+ if t.TYPE_CHECKING:
10
+ from bumpversion_slim.context import Context
11
+
12
+ T = t.TypeVar("T", bound="Git")
13
+
14
+
15
+ class Git:
16
+ """VCS implementation for git repositories."""
17
+
18
+ def __init__(
19
+ self,
20
+ context: Context,
21
+ *,
22
+ commit: bool = True,
23
+ tag: bool = True,
24
+ dry_run: bool = False,
25
+ ) -> None:
26
+ self.context = context
27
+ self._commit = commit
28
+ self._tag = tag
29
+ self.dry_run = dry_run
30
+ try:
31
+ self.repo = git.Repo()
32
+ except git.exc.InvalidGitRepositoryError as e:
33
+ msg = "No git repository found, please run git init."
34
+ raise errors.GitError(msg) from e
35
+
36
+ def get_current_info(self) -> dict[str, str | bool]:
37
+ """Get current state info from git."""
38
+ branch = self.repo.active_branch.name
39
+ try:
40
+ missing_local = list(self.repo.iter_commits(f"HEAD..origin/{branch}"))
41
+ except git.GitCommandError as e:
42
+ if f"bad revision 'HEAD..origin/{branch}'" in str(e):
43
+ missing_local = []
44
+ else:
45
+ msg = f"Unable to determine missing commit status: {e}"
46
+ raise errors.GitError(msg) from e
47
+
48
+ try:
49
+ missing_remote = list(self.repo.iter_commits(f"origin/{branch}..HEAD"))
50
+ except git.GitCommandError as e:
51
+ if f"bad revision 'origin/{branch}..HEAD'" in str(e):
52
+ missing_remote = ["missing"]
53
+ else:
54
+ msg = f"Unable to determine missing commit status: {e}"
55
+ raise errors.GitError(msg) from e
56
+
57
+ return {
58
+ "missing_local": missing_local != [],
59
+ "missing_remote": missing_remote != [],
60
+ "dirty": self.repo.is_dirty(),
61
+ "branch": branch,
62
+ }
63
+
64
+ def add_paths(self, paths: list[str]) -> None:
65
+ """Add path to git repository."""
66
+ if self.dry_run:
67
+ self.context.warning(" Would add paths '%s' to Git", "', '".join(paths))
68
+ return
69
+ self.repo.git.add(*paths)
70
+
71
+ def commit(self, current: str, new: str, tag: str, paths: list[str] | None = None) -> None:
72
+ """Commit changes to git repository."""
73
+ self.context.warning("Would prepare Git commit")
74
+ paths = paths or []
75
+
76
+ if paths:
77
+ self.add_paths(paths)
78
+
79
+ msg = [
80
+ f"Update CHANGELOG for {new}",
81
+ f"Bump version: {current} → {new}",
82
+ ]
83
+
84
+ message = "\n".join(msg).strip()
85
+ if self.dry_run or not self._commit:
86
+ self.context.warning(" Would commit to Git with message '%s", message)
87
+ return
88
+
89
+ try:
90
+ self.repo.git.commit(message=message)
91
+ except git.GitCommandError as e:
92
+ msg = f"Unable to commit: {e}"
93
+ raise errors.GitError(msg) from e
94
+
95
+ if not self._tag:
96
+ self.context.warning(" Would tag with version '%s", tag)
97
+ return
98
+
99
+ try:
100
+ self.repo.git.tag(tag)
101
+ except git.GitCommandError as e:
102
+ self.revert()
103
+ msg = f"Unable to tag: {e}"
104
+ raise errors.GitError(msg) from e
105
+
106
+ def revert(self) -> None:
107
+ """Revert a commit."""
108
+ if self.dry_run:
109
+ self.context.warning("Would revert commit in Git")
110
+ return
111
+ self.repo.git.reset("HEAD~1", hard=True)
File without changes
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: bumpversion-slim
3
+ Version: 0.1.0
4
+ Summary: A lightweight tool to bump package versions
5
+ Author-email: Daniel Edgecombe <daniel@nrwl.co>
6
+ License-Expression: Apache-2.0
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Build Tools
21
+ Classifier: Topic :: Software Development :: Version Control
22
+ Classifier: Topic :: System :: Software Distribution
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: gitpython>=3.1.46
25
+ Requires-Dist: rtoml>=0.13.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: git-cliff>=2.12.0; extra == 'dev'
28
+ Requires-Dist: prek>=0.3.4; extra == 'dev'
29
+ Requires-Dist: ruff>=0.14.10; extra == 'dev'
30
+ Requires-Dist: ty>=0.0.17; extra == 'dev'
31
+ Provides-Extra: test
32
+ Requires-Dist: freezegun>=1.2.1; extra == 'test'
33
+ Requires-Dist: path<17,>=16; extra == 'test'
34
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
35
+ Requires-Dist: pytest-git<1.8,>=1.7.0; extra == 'test'
36
+ Requires-Dist: pytest-httpx>=0.36.0; extra == 'test'
37
+ Requires-Dist: pytest-random-order>=1.2.0; extra == 'test'
38
+ Requires-Dist: pytest>=9.0.0; extra == 'test'
@@ -0,0 +1,12 @@
1
+ bumpversion_slim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ bumpversion_slim/bump.py,sha256=kHJMsWAvUf3MkqlHSM13fCrvBz4_ZcthSSZqR7hAV1I,3213
3
+ bumpversion_slim/cli.py,sha256=tysVOK80yCtUaCUDagtAuApiKYIlB-HThtePLbQ0fxY,4636
4
+ bumpversion_slim/config.py,sha256=OeZLnBKzkm8JNveSOJwKioGwbhjK1JlzvPjpwZtT1ow,2206
5
+ bumpversion_slim/context.py,sha256=nXbsP4iTrXexh500NggIqIRizNAWVIg3nmFK2IKS1i0,2355
6
+ bumpversion_slim/errors.py,sha256=e4oUs0DoqZOeV7Uq5DmjbVMgKfDnXVrJ10pASQLPJXY,213
7
+ bumpversion_slim/git.py,sha256=l4g7B-2lSuzdNf6bvfK_q6Q3mdFHuStQATlM9plyfY8,3476
8
+ bumpversion_slim/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ bumpversion_slim-0.1.0.dist-info/METADATA,sha256=QieAreL1C2ljr9fDNZc0OC0m0oLS6zgrfId0P0ToZc0,1660
10
+ bumpversion_slim-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ bumpversion_slim-0.1.0.dist-info/entry_points.txt,sha256=Slc323FHt9s9JUJQ_dRTbGYk_1Jg6hCYR3lhei7Ax5I,58
12
+ bumpversion_slim-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bumpversion = bumpversion_slim.cli:main