diff --git a/docs/cli.md b/docs/cli.md index 4a950a9b2a5..d5d02a74c63 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1360,8 +1360,21 @@ The option `--next-phase` allows the increment of prerelease phase versions. | prerelease --next-phase | 1.0.3b0 | 1.0.3rc0 | | prerelease --next-phase | 1.0.3rc0 | 1.0.3 | +The option `--dev` creates or bumps development release versions for bump rules. + +| rule | before | after | +|--------------------------------|---------------|---------------| +| major --dev | 1.1.0 | 2.0.0.dev0 | +| major --dev | 2.0.0.dev0 | 2.0.0.dev1 | +| major | 2.0.0.dev1 | 2.0.0 | +| prerelease --dev | 1.1.0 | 1.1.1a0.dev0 | +| prerelease --dev | 1.1.1a0.dev0 | 1.1.1a0.dev1 | +| prerelease | 1.1.1a0.dev1 | 1.1.1a0 | +| prerelease --next-phase --dev | 1.1.0a2.dev1 | 1.1.0b0.dev0 | + #### Options +* `--dev`: Create or bump a development release for a bump rule. * `--next-phase`: Increment the phase of the current version. * `--short (-s)`: Output the version number only. * `--dry-run`: Do not update pyproject.toml file. diff --git a/src/poetry/console/commands/version.py b/src/poetry/console/commands/version.py index f3b1575927c..f80f6c428b0 100644 --- a/src/poetry/console/commands/version.py +++ b/src/poetry/console/commands/version.py @@ -22,6 +22,17 @@ class VersionCommand(Command): "Shows the version of the project or bumps it when a valid " "bump rule is provided." ) + BUMP_RULES: ClassVar[frozenset[str]] = frozenset( + { + "major", + "minor", + "patch", + "premajor", + "preminor", + "prepatch", + "prerelease", + } + ) arguments: ClassVar[list[Argument]] = [ argument( @@ -38,6 +49,7 @@ class VersionCommand(Command): "Do not update pyproject.toml file", ), option("next-phase", None, "Increment the phase of the current version"), + option("dev", None, "Create or bump a development release for a bump rule"), ] help = """\ @@ -54,7 +66,10 @@ def handle(self) -> int: if version: version = self.increment_version( - self.poetry.package.pretty_version, version, self.option("next-phase") + self.poetry.package.pretty_version, + version, + self.option("next-phase"), + self.option("dev"), ) if self.option("short"): @@ -87,7 +102,7 @@ def handle(self) -> int: return 0 def increment_version( - self, version: str, rule: str, next_phase: bool = False + self, version: str, rule: str, next_phase: bool = False, dev: bool = False ) -> Version: from poetry.core.constraints.version import Version @@ -96,6 +111,12 @@ def increment_version( except InvalidVersionError: raise ValueError("The project's version doesn't seem to follow semver") + if dev and rule not in self.BUMP_RULES: + raise ValueError( + "The --dev option can only be used with a bump rule, " + "not an explicit version." + ) + if rule in {"major", "premajor"}: new = parsed.next_major() if rule == "premajor": @@ -109,7 +130,9 @@ def increment_version( if rule == "prepatch": new = new.first_prerelease() elif rule == "prerelease": - if parsed.is_unstable(): + if parsed.dev is not None and (not next_phase or parsed.pre is None): + new = parsed.without_devrelease() + elif parsed.is_unstable(): pre = parsed.pre assert pre is not None pre = pre.next_phase() if next_phase else pre.next() @@ -119,4 +142,10 @@ def increment_version( else: new = Version.parse(rule) + if dev: + if parsed.dev is not None and new == parsed.without_devrelease(): + new = parsed.next_devrelease() + else: + new = new.first_devrelease() + return new diff --git a/tests/console/commands/test_version.py b/tests/console/commands/test_version.py index 5741cf3d9f5..4e471568232 100644 --- a/tests/console/commands/test_version.py +++ b/tests/console/commands/test_version.py @@ -89,6 +89,80 @@ def test_next_phase_version( assert command.increment_version(version, rule, True).text == expected +@pytest.mark.parametrize( + "version, rule, next_phase, expected", + [ + ("1.1.0", "major", False, "2.0.0.dev0"), + ("1.1.0", "minor", False, "1.2.0.dev0"), + ("1.1.0", "patch", False, "1.1.1.dev0"), + ("1.1.0", "premajor", False, "2.0.0a0.dev0"), + ("1.1.0", "preminor", False, "1.2.0a0.dev0"), + ("1.1.0", "prepatch", False, "1.1.1a0.dev0"), + ("1.1.0", "prerelease", False, "1.1.1a0.dev0"), + ("2.0.0.dev0", "major", False, "2.0.0.dev1"), + ("1.1.0.dev0", "minor", False, "1.1.0.dev1"), + ("1.1.1a0.dev0", "prerelease", False, "1.1.1a0.dev1"), + ], +) +def test_dev_version_creates_or_increments_dev_release( + version: str, + rule: str, + next_phase: bool, + expected: str, + command: VersionCommand, +) -> None: + assert ( + command.increment_version(version, rule, next_phase, dev=True).text == expected + ) + + +@pytest.mark.parametrize( + "version, rule, next_phase, expected", + [ + ("1.1.1.dev0", "minor", False, "1.2.0.dev0"), + ("1.1.0a2.dev1", "prerelease", True, "1.1.0b0.dev0"), + ], +) +def test_dev_version_resets_dev_release_when_base_changes( + version: str, + rule: str, + next_phase: bool, + expected: str, + command: VersionCommand, +) -> None: + assert ( + command.increment_version(version, rule, next_phase, dev=True).text == expected + ) + + +@pytest.mark.parametrize( + "version, rule, next_phase, expected", + [ + ("2.0.0.dev1", "major", False, "2.0.0"), + ("1.1.1a0.dev1", "prerelease", False, "1.1.1a0"), + ("1.1.0a2.dev1", "prerelease", True, "1.1.0b0"), + ], +) +def test_version_without_dev_flag_removes_dev_release( + version: str, + rule: str, + next_phase: bool, + expected: str, + command: VersionCommand, +) -> None: + assert command.increment_version(version, rule, next_phase).text == expected + + +def test_dev_option_requires_bump_rule(command: VersionCommand) -> None: + with pytest.raises(ValueError) as e: + command.increment_version("1.0.0", "2.0.0", dev=True) + + assert ( + str(e.value) == "The --dev option can only be used with a bump rule, " + "not an explicit version." + ) + + def test_version_show(tester: CommandTester) -> None: tester.execute() assert tester.io.fetch_output() == "simple-project 1.2.3\n" @@ -124,6 +198,13 @@ def test_phase_version_update(tester: CommandTester) -> None: assert tester.io.fetch_output() == "Bumping version from 1.2.4a0 to 1.2.4b0\n" +def test_dev_version_update(tester: CommandTester) -> None: + assert isinstance(tester.command, VersionCommand) + tester.command.poetry.package._set_version("1.1.0") + tester.execute("major --dev") + assert tester.io.fetch_output() == "Bumping version from 1.1.0 to 2.0.0.dev0\n" + + def test_dry_run(tester: CommandTester) -> None: assert isinstance(tester.command, VersionCommand) old_pyproject = tester.command.poetry.file.path.read_text(encoding="utf-8")