diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index 1207cd02ee..4ae4327209 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -59,6 +59,10 @@ def is_rust_cargo(self) -> bool: def is_npm_package(self) -> bool: return os.path.isfile("package.json") + @property + def is_dart_package(self) -> bool: + return os.path.isfile("pubspec.yaml") + @property def is_php_composer(self) -> bool: return os.path.isfile("composer.json") @@ -234,6 +238,7 @@ def _ask_version_provider(self) -> str: "cargo": "cargo: Get and set version from Cargo.toml:project.version field", "composer": "composer: Get and set version from composer.json:project.version field", "npm": "npm: Get and set version from package.json:project.version field", + "dart": "dart: Get and set version from pubspec.yaml:version field", "pep621": "pep621: Get and set version from pyproject.toml:project.version field", "poetry": "poetry: Get and set version from pyproject.toml:tool.poetry.version field", "uv": "uv: Get and Get and set version from pyproject.toml and uv.lock", @@ -252,6 +257,8 @@ def _ask_version_provider(self) -> str: default_val = "cargo" elif self.project_info.is_npm_package: default_val = "npm" + elif self.project_info.is_dart_package: + default_val = "dart" elif self.project_info.is_php_composer: default_val = "composer" diff --git a/commitizen/providers/__init__.py b/commitizen/providers/__init__.py index 3e01fe22f8..de7bd414a7 100644 --- a/commitizen/providers/__init__.py +++ b/commitizen/providers/__init__.py @@ -14,6 +14,7 @@ from commitizen.providers.cargo_provider import CargoProvider from commitizen.providers.commitizen_provider import CommitizenProvider from commitizen.providers.composer_provider import ComposerProvider +from commitizen.providers.dart_provider import DartProvider from commitizen.providers.npm_provider import NpmProvider from commitizen.providers.pep621_provider import Pep621Provider from commitizen.providers.poetry_provider import PoetryProvider @@ -24,6 +25,7 @@ "CargoProvider", "CommitizenProvider", "ComposerProvider", + "DartProvider", "NpmProvider", "Pep621Provider", "PoetryProvider", diff --git a/commitizen/providers/dart_provider.py b/commitizen/providers/dart_provider.py new file mode 100644 index 0000000000..6da1265535 --- /dev/null +++ b/commitizen/providers/dart_provider.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +from ruamel.yaml import YAML + +from commitizen.providers.base_provider import VersionProvider + + +class DartProvider(VersionProvider): + """ + dart pubspec.yaml version management + """ + + package_filename = "pubspec.yaml" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._yaml = YAML() + self._yaml.preserve_quotes = True + self._yaml.indent(mapping=2, sequence=4, offset=2) + + @property + def package_file(self) -> Path: + return Path() / self.package_filename + + def get_version(self) -> str: + print(self.package_file.read_text()) + document = self._yaml.load(self.package_file.read_text()) + return self.get(document) + + def set_version(self, version: str) -> None: + document = self._yaml.load(self.package_file.read_text()) + self.set(document, version) + self._yaml.dump(document, self.package_file) + + def get(self, document: Mapping[str, str]) -> str: + # Extract the version without the build number + version = document["version"] + return version.split("+")[0] if "+" in version else version + + def set(self, document: dict[str, Any], version: str) -> None: + # To enforce to save the version in pubspec without quotes + # even if the version could be interpreted as float by yaml + # dump + version_value: Any + try: + version_value = float(version) + except ValueError: + version_value = version + document["version"] = version_value diff --git a/docs/commands/init.md b/docs/commands/init.md index 4d92112d34..cc013430b7 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -33,6 +33,7 @@ During the initialization process, you'll be prompted to configure the following 2. **Version Provider**: Choose how to manage versioning in your project. Commitizen supports multiple version management systems: - `commitizen`: Uses Commitizen's built-in version management system - `npm`: Manages version in `package.json` for Node.js projects + - `dart`: Manages version in `pubspec.yaml` for Dart projects - `cargo`: Manages version in `Cargo.toml` for Rust projects - `composer`: Manages version in `composer.json` for PHP projects - `pep621`: Uses `pyproject.toml` with PEP 621 standard diff --git a/docs/config.md b/docs/config.md index 649881daec..b77b5a3049 100644 --- a/docs/config.md +++ b/docs/config.md @@ -355,6 +355,7 @@ Commitizen provides some version providers for some well known formats: | `uv` | Get and set version from `pyproject.toml` `project.version` field and `uv.lock` `package.version` field whose `package.name` field is the same as `pyproject.toml` `project.name` field | | `cargo` | Get and set version from `Cargo.toml` `package.version` field and `Cargo.lock` `package.version` field whose `package.name` field is the same as `Cargo.toml` `package.name` field | | `npm` | Get and set version from `package.json` `version` field, `package-lock.json` `version,packages.''.version` fields if the file exists, and `npm-shrinkwrap.json` `version,packages.''.version` fields if the file exists | +| `dart` | Get and set version from `pubspec.yaml` `version` field | | `composer` | Get and set version from `composer.json` `project.version` field | !!! note diff --git a/poetry.lock b/poetry.lock index e7f0e96e51..43d85218b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1489,6 +1489,82 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.1 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruamel-yaml" +version = "0.18.14" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2"}, + {file = "ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.12" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\"" +files = [ + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, + {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, +] + [[package]] name = "ruff" version = "0.11.13" @@ -1943,4 +2019,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "c98af83d4ce726bbfba04eea3de90e642fb7bdb08bedf0bf1b00b92a1b140e76" +content-hash = "4cb25b503aea4ec4e55bf27aeb17482af891f28e3a8b9785b725c22bc2e3d5a6" diff --git a/pyproject.toml b/pyproject.toml index ab3ba39ee1..20e4162dff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "charset-normalizer (>=2.1.0,<4)", # Use the Python 3.11 and 3.12 compatible API: https://github.com/python/importlib_metadata#compatibility "importlib-metadata >=8.0.0,<8.7.0 ; python_version < '3.10'", + "ruamel-yaml (>=0.18.14,<0.19.0)", ] keywords = ["commitizen", "conventional", "commits", "git"] # See also: https://pypi.org/classifiers/ @@ -71,6 +72,7 @@ cargo = "commitizen.providers:CargoProvider" commitizen = "commitizen.providers:CommitizenProvider" composer = "commitizen.providers:ComposerProvider" npm = "commitizen.providers:NpmProvider" +dart = "commitizen.providers:DartProvider" pep621 = "commitizen.providers:Pep621Provider" poetry = "commitizen.providers:PoetryProvider" scm = "commitizen.providers:ScmProvider" diff --git a/tests/providers/test_dart_provider.py b/tests/providers/test_dart_provider.py new file mode 100644 index 0000000000..bd687f2263 --- /dev/null +++ b/tests/providers/test_dart_provider.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.dart_provider import DartProvider + +DART_YAML = """\ +name: my_super_package +description: A new Flutter project. + +# Bla, bla, bla +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +version: 0.1.0+1 + +environment: + sdk: ^3.7.2 + # note that this only enforces that the Flutter version is at least the value specified. It does not + # force it to this specific version + flutter: 3.29.2 + +# Bla, bla, bla +dependencies: + flutter: + sdk: flutter + + # Some package dependencies + # network + dio: ^5.8.0+1 + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ +""" + +DART_YAML_FLOAT_PARSEABLE_EXPECTED = """\ +name: my_super_package +description: A new Flutter project. + +# Bla, bla, bla +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +version: 42.1 + +environment: + sdk: ^3.7.2 + # note that this only enforces that the Flutter version is at least the value specified. It does not + # force it to this specific version + flutter: 3.29.2 + +# Bla, bla, bla +dependencies: + flutter: + sdk: flutter + + # Some package dependencies + # network + dio: ^5.8.0+1 + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ +""" + +DART_YAML_SEMVER_EXPECTED = """\ +name: my_super_package +description: A new Flutter project. + +# Bla, bla, bla +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +version: 2.3.4 + +environment: + sdk: ^3.7.2 + # note that this only enforces that the Flutter version is at least the value specified. It does not + # force it to this specific version + flutter: 3.29.2 + +# Bla, bla, bla +dependencies: + flutter: + sdk: flutter + + # Some package dependencies + # network + dio: ^5.8.0+1 + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ +""" + + +@pytest.mark.parametrize( + "content, expected", + ((DART_YAML, DART_YAML_FLOAT_PARSEABLE_EXPECTED),), +) +def test_dart_provider_with_float_parseable_new_version( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = DartProvider.package_filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "dart" + + provider = get_provider(config) + assert isinstance(provider, DartProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) + + +@pytest.mark.parametrize( + "content, expected", + ((DART_YAML, DART_YAML_SEMVER_EXPECTED),), +) +def test_dart_provider_with_semver_new_version( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = DartProvider.package_filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "dart" + + provider = get_provider(config) + assert isinstance(provider, DartProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("2.3.4") + assert file.read_text() == dedent(expected)