From 34a4d0eecbaac6820ff28768ca1b3bb872540a8b Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Thu, 14 Nov 2024 12:54:44 +0100 Subject: [PATCH 1/4] feat: add gh_release method to BaseVCS class --- bumpr.rc | 1 + bumpx/config.py | 3 ++- bumpx/forge.py | 37 +++++++++++++++++++++++++++++++++++++ bumpx/releaser.py | 44 ++++++++++++++++++++++++++++++++------------ 4 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 bumpx/forge.py diff --git a/bumpr.rc b/bumpr.rc index 388d42c..5f4cd58 100644 --- a/bumpr.rc +++ b/bumpr.rc @@ -2,6 +2,7 @@ file = pyproject.toml regex = version\s*=\s*"(?P.+?)" vcs = git +forge = github tag_annotation = version {version} push = true tests = diff --git a/bumpx/config.py b/bumpx/config.py index 461cb18..db7ae4a 100644 --- a/bumpx/config.py +++ b/bumpx/config.py @@ -15,6 +15,7 @@ "regex": r'(__version__|VERSION)\s*=\s*(\'|")(?P.+?)(\'|")', "encoding": "utf8", "vcs": None, + "forge": None, "commit": True, "tag": True, "tag_format": "{version}", @@ -155,7 +156,7 @@ def override_from_config(self, filename: str) -> None: self[hook.key] = False def override_from_args(self, parsed_args): - for arg in "file", "vcs", "files": + for arg in "file", "vcs", "forge", "files": if arg in parsed_args and getattr(parsed_args, arg) not in ( None, [], diff --git a/bumpx/forge.py b/bumpx/forge.py new file mode 100644 index 0000000..91c165e --- /dev/null +++ b/bumpx/forge.py @@ -0,0 +1,37 @@ +import logging +from typing import Optional + +from .helpers import execute + +log = logging.getLogger(__name__) + + +class BaseForge: + def __init__(self, verbose: bool = False) -> None: + self.verbose = verbose + + def execute(self, command: list[str]) -> None: + """Execute a command""" + execute(command, verbose=self.verbose) + + def release(self, version: str, notes: Optional[str] = None) -> None: + """Create a release on the forge""" + raise NotImplementedError + + +class GitHub(BaseForge): + def release(self, version: str, notes: Optional[str] = None) -> None: + if notes: + self.execute(["gh", "release", "create", version, "--title", version, "--notes", notes]) + else: + self.execute(["gh", "release", "create", version, "--title", version]) + + +class GitLab(BaseForge): + pass + + +FORGES = { + "github": GitHub, + "gitlab": GitLab, +} diff --git a/bumpx/releaser.py b/bumpx/releaser.py index 4336cc3..81cbc4c 100644 --- a/bumpx/releaser.py +++ b/bumpx/releaser.py @@ -2,7 +2,9 @@ import re from datetime import datetime from difflib import unified_diff +from typing import Optional +from .forge import FORGES from .helpers import BumprError, execute from .hooks import HOOKS from .vcs import VCS @@ -25,23 +27,23 @@ def __init__(self, config): version_string = match.group("version") self.prev_version = Version.parse(version_string) except Exception: - raise BumprError("Unable to extract version from {0}".format(config.file)) + raise BumprError(f"Unable to extract version from {config.file}") - logger.debug("Previous version: {0}".format(self.prev_version)) + logger.debug(f"Previous version: {self.prev_version}") self.version = self.prev_version.copy() self.version.bump(config.bump.part, config.bump.unsuffix, config.bump.suffix) - logger.debug("Bumped version: {0}".format(self.version)) + logger.debug(f"Bumped version: {self.version}") self.next_version = self.version.copy() self.next_version.bump(config.prepare.part, config.prepare.unsuffix, config.prepare.suffix) - logger.debug("Prepared version: {0}".format(self.next_version)) + logger.debug(f"Prepared version: {self.next_version}") self.tag_label = self.config.tag_format.format(version=self.version) - logger.debug("Tag: {0}".format(self.tag_label)) + logger.debug(f"Tag: {self.tag_label}") if self.config.tag_annotation: self.tag_annotation = self.config.tag_annotation.format(version=self.version) - logger.debug("Tag annotation: {0}".format(self.tag_annotation)) + logger.debug(f"Tag annotation: {self.tag_annotation}") self.timestamp = None @@ -49,6 +51,9 @@ def __init__(self, config): self.vcs = VCS[config.vcs](verbose=config.verbose) self.vcs.validate(dryrun=config.dryrun) + if config.forge: + self.forge = FORGES[config.forge](verbose=config.verbose) + if config.dryrun: self.modified = {} self.diffs = {} @@ -170,11 +175,14 @@ def bump_files(self, replacements): self.perform(filename, before, after) def publish(self): - """Publish the current release to PyPI""" + """Publish the current release to PyPI and to the Forge if defined""" if self.config.publish: logger.info("Publish") self.execute(self.config.publish) + if self.config.forge: + self.create_forge_release() # TODO: get the release notes from the changelog hook + def tag(self): if self.config.commit and self.config.tag: if self.config.tag_annotation: @@ -182,15 +190,27 @@ def tag(self): if not self.config.dryrun: self.vcs.tag(self.tag_label, self.tag_annotation) else: - logger.dryrun( - "tag: {0} annotation: {1}".format(self.tag_label, self.tag_annotation) - ) + logger.dryrun(f"tag: {self.tag_label} annotation: {self.tag_annotation}") else: logger.debug(f"Tag: {self.tag_label}") if not self.config.dryrun: self.vcs.tag(self.tag_label) else: - logger.dryrun("tag: {0}".format(self.tag_label)) + logger.dryrun(f"tag: {self.tag_label}") + + def create_forge_release(self, notes: Optional[str] = None) -> None: + if self.config.tag: + if notes: + logger.debug(f"Forge release: {self.tag_label} with notes") + else: + logger.debug(f"Forge release: {self.tag_label}") + if not self.config.dryrun: + self.forge.release(version=self.tag_label, notes=notes) + else: + if notes: + logger.dryrun(f"Forge release: {self.tag_label} with notes") # type: ignore + else: + logger.dryrun(f"Forge release: {self.tag_label}") # type: ignore def commit(self, message): if self.config.commit: @@ -198,7 +218,7 @@ def commit(self, message): if not self.config.dryrun: self.vcs.commit(message) else: - logger.dryrun("commit: {0}".format(message)) + logger.dryrun(f"commit: {message}") def push(self): if self.config.vcs and self.config.commit and self.config.push: From 05b0e3b42befbf0fd341e2c54346ea1966671867 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Thu, 14 Nov 2024 18:37:51 +0100 Subject: [PATCH 2/4] tests: add forges tests --- tests/test_forges.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_forges.py diff --git a/tests/test_forges.py b/tests/test_forges.py new file mode 100644 index 0000000..caf6f09 --- /dev/null +++ b/tests/test_forges.py @@ -0,0 +1,33 @@ +from bumpx.forge import BaseForge, GitHub + + +class BaseForgeTest: + def test_execute_verbose(self, mocker): + forge = BaseForge(verbose=True) + execute = mocker.patch("bumpx.forge.execute") + forge.execute("cmd arg") + execute.assert_called_with("cmd arg", verbose=True) + + def test_execute_quiet(self, mocker): + forge = BaseForge(verbose=False) + execute = mocker.patch("bumpx.forge.execute") + forge.execute("cmd arg") + execute.assert_called_with("cmd arg", verbose=False) + + +class GitHubTest: + def test_release(self, mocker): + github = GitHub() + + execute = mocker.patch.object(github, "execute") + github.release(version="fake") + execute.assert_called_with(["gh", "release", "create", "fake", "--title", "fake"]) + + def test_release_with_notes(self, mocker): + github = GitHub() + + execute = mocker.patch.object(github, "execute") + github.release(version="fake", notes="some notes") + execute.assert_called_with( + ["gh", "release", "create", "fake", "--title", "fake", "--notes", "some notes"] + ) From b4c74e7736e02f3025d958424736ac539433b101 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Fri, 15 Nov 2024 16:58:15 +0100 Subject: [PATCH 3/4] docs: update README --- README.md | 11 ++++++++++- bumpx/helpers.py | 4 +--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 46d697d..6dcb793 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,9 @@ Bump'X intend to be customizable with the following features: ## Compatibility -Bump'X requires Python `>=3.9` (and `<4.0`) +Bump'X requires Python `>=3.9` (and `<4.0`). + +Bump'X also requires that you have [git CLI](https://git-scm.com/) and [GitHub CLI](https://cli.github.com/) installed on your system. ## Installation @@ -53,6 +55,7 @@ Here's an exemple: [bumpx] file = fake/__init__.py vcs = git +forge = github tests = tox publish = python setup.py sdist register upload clean = @@ -85,6 +88,12 @@ bumpx -M # Bump the major bumpx # Bump the default part aka. patch ``` +If you use GitHub as a forge and publish release on it, you might have to specify which origin should be used to publish the releases, with: + +```bash +gh repo set-default +``` + ## Documentation The documentation for the upstream project [Bump'X](https://github.com/datagouv/bumpx) is hosted on Read the Docs: diff --git a/bumpx/helpers.py b/bumpx/helpers.py index b4daae8..3c77361 100644 --- a/bumpx/helpers.py +++ b/bumpx/helpers.py @@ -45,9 +45,7 @@ def execute( if hasattr(exception, "output") and exception.output: print(exception.output) cmd = " ".join(cmd) if isinstance(cmd, (list, tuple)) else cmd - raise BumprError( - 'Command "{0}" failed with exit code {1}'.format(cmd, exception.returncode) - ) + raise BumprError(f'Command "{cmd}" failed with exit code {exception.returncode}') return output From 11b9657de67a329df06168df07d27d817a9df002 Mon Sep 17 00:00:00 2001 From: Adrien Carpentier Date: Thu, 28 Nov 2024 15:30:21 +0100 Subject: [PATCH 4/4] fix: fix update github release --- bumpx/forge.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bumpx/forge.py b/bumpx/forge.py index 91c165e..3a0a388 100644 --- a/bumpx/forge.py +++ b/bumpx/forge.py @@ -20,11 +20,8 @@ def release(self, version: str, notes: Optional[str] = None) -> None: class GitHub(BaseForge): - def release(self, version: str, notes: Optional[str] = None) -> None: - if notes: - self.execute(["gh", "release", "create", version, "--title", version, "--notes", notes]) - else: - self.execute(["gh", "release", "create", version, "--title", version]) + def release(self, version: str, notes: Optional[str] = "") -> None: + self.execute(["gh", "release", "create", version, "--title", version, "--notes", notes]) class GitLab(BaseForge):