From 2de2ea4b4f4ef892fe773fdc5e3343aba31e7921 Mon Sep 17 00:00:00 2001 From: Axel H Date: Mon, 12 Dec 2022 00:09:25 +0100 Subject: [PATCH 1/6] feat(providers): add a `commitizen.provider` endpoint for alternative versions providers --- commitizen/commands/bump.py | 11 +++-- commitizen/commands/version.py | 5 +- commitizen/defaults.py | 2 + commitizen/exceptions.py | 5 ++ commitizen/providers.py | 66 ++++++++++++++++++++++++++ docs/config.md | 49 +++++++++++++++++++ docs/exit_codes.md | 5 +- pyproject.toml | 3 ++ tests/commands/test_bump_command.py | 20 ++++++++ tests/commands/test_version_command.py | 31 ++++++++++++ tests/test_conf.py | 2 + tests/test_version_providers.py | 35 ++++++++++++++ 12 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 commitizen/providers.py create mode 100644 tests/test_version_providers.py diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 808141c32f..00d90b48c8 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -21,6 +21,7 @@ NotAllowed, NoVersionSpecifiedError, ) +from commitizen.providers import get_provider logger = getLogger("commitizen") @@ -94,14 +95,14 @@ def find_increment(self, commits: List[git.GitCommit]) -> Optional[str]: def __call__(self): # noqa: C901 """Steps executed to bump.""" + provider = get_provider(self.config) + current_version: str = provider.get_version() + try: - current_version_instance: Version = Version(self.bump_settings["version"]) + current_version_instance: Version = Version(current_version) except TypeError: raise NoVersionSpecifiedError() - # Initialize values from sources (conf) - current_version: str = self.config.settings["version"] - tag_format: str = self.bump_settings["tag_format"] bump_commit_message: str = self.bump_settings["bump_message"] version_files: List[str] = self.bump_settings["version_files"] @@ -280,7 +281,7 @@ def __call__(self): # noqa: C901 check_consistency=self.check_consistency, ) - self.config.set_key("version", str(new_version)) + provider.set_version(str(new_version)) if self.pre_bump_hooks: hooks.run( diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py index dc47e7aa0c..45d553c710 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -4,6 +4,7 @@ from commitizen import out from commitizen.__version__ import __version__ from commitizen.config import BaseConfig +from commitizen.providers import get_provider class Version: @@ -21,14 +22,14 @@ def __call__(self): out.write(f"Python Version: {self.python_version}") out.write(f"Operating System: {self.operating_system}") elif self.parameter.get("project"): - version = self.config.settings["version"] + version = get_provider(self.config).get_version() if version: out.write(f"{version}") else: out.error("No project information in this project.") elif self.parameter.get("verbose"): out.write(f"Installed Commitizen Version: {__version__}") - version = self.config.settings["version"] + version = get_provider(self.config).get_version() if version: out.write(f"Project Version: {version}") else: diff --git a/commitizen/defaults.py b/commitizen/defaults.py index f2447483e9..296193534f 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -29,6 +29,7 @@ class Settings(TypedDict, total=False): name: str version: Optional[str] version_files: List[str] + version_provider: Optional[str] tag_format: Optional[str] bump_message: Optional[str] allow_abort: bool @@ -59,6 +60,7 @@ class Settings(TypedDict, total=False): "name": "cz_conventional_commits", "version": None, "version_files": [], + "version_provider": "commitizen", "tag_format": None, # example v$version "bump_message": None, # bumped v$current_version to $new_version "allow_abort": False, diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index c7d0b50e69..ba4aca1397 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -31,6 +31,7 @@ class ExitCode(enum.IntEnum): INVALID_MANUAL_VERSION = 24 INIT_FAILED = 25 RUN_HOOK_FAILED = 26 + VERSION_PROVIDER_UNKNOWN = 27 class CommitizenException(Exception): @@ -173,3 +174,7 @@ class InitFailedError(CommitizenException): class RunHookError(CommitizenException): exit_code = ExitCode.RUN_HOOK_FAILED + + +class VersionProviderUnknown(CommitizenException): + exit_code = ExitCode.VERSION_PROVIDER_UNKNOWN diff --git a/commitizen/providers.py b/commitizen/providers.py new file mode 100644 index 0000000000..0269f59fb3 --- /dev/null +++ b/commitizen/providers.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import cast + +import importlib_metadata as metadata + +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import VersionProviderUnknown + +PROVIDER_ENTRYPOINT = "commitizen.provider" +DEFAULT_PROVIDER = "commitizen" + + +class VersionProvider(ABC): + """ + Abstract base class for version providers. + + Each version provider should inherit and implement this class. + """ + + config: BaseConfig + + def __init__(self, config: BaseConfig): + self.config = config + + @abstractmethod + def get_version(self) -> str: + """ + Get the current version + """ + ... + + @abstractmethod + def set_version(self, version: str): + """ + Set the new current version + """ + ... + + +class CommitizenProvider(VersionProvider): + """ + Default version provider: Fetch and set version in commitizen config. + """ + + def get_version(self) -> str: + return self.config.settings["version"] # type: ignore + + def set_version(self, version: str): + self.config.set_key("version", version) + + +def get_provider(config: BaseConfig) -> VersionProvider: + """ + Get the version provider as defined in the configuration + + :raises VersionProviderUnknown: if the provider named by `version_provider` is not found. + """ + provider_name = config.settings["version_provider"] or DEFAULT_PROVIDER + try: + (ep,) = metadata.entry_points(name=provider_name, group=PROVIDER_ENTRYPOINT) + except ValueError: + raise VersionProviderUnknown(f'Version Provider "{provider_name}" unknown.') + provider_cls = ep.load() + return cast(VersionProvider, provider_cls(config)) diff --git a/docs/config.md b/docs/config.md index c6cf12b02f..d49df8461b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -7,6 +7,7 @@ | `name` | `str` | `"cz_conventional_commits"` | Name of the committing rules to use | | `version` | `str` | `None` | Current version. Example: "0.1.2" | | `version_files` | `list` | `[ ]` | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more][version_files] | +| `version_provider` | `str` | `commitizen` | Version provider used to read and write version [See more](#version-providers) | | `tag_format` | `str` | `None` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more][tag_format] | | `update_changelog_on_bump` | `bool` | `false` | Create changelog when running `cz bump` | | `gpg_sign` | `bool` | `false` | Use gpg signed tags instead of lightweight tags. | @@ -112,6 +113,54 @@ commitizen: - fg:#858585 italic ``` +## Version providers + +Commitizen can read and write version from different sources. +By default, it use the `commitizen` one which is using the `version` field from the commitizen settings. +But you can use any `commitizen.provider` entrypoint as value for `version_provider`. + +### Custom version provider + +You can add you own version provider by extending `VersionProvider` and exposing it on the `commitizen.provider` entrypoint. + +Here a quick example of a `my-provider` provider reading and writing version in a `VERSION` file. + +`my_provider.py` + +```python +from pathlib import Path +from commitizen.providers import VersionProvider + + +class MyProvider(VersionProvider): + file = Path() / "VERSION" + + def get_version(self) -> str: + return self.file.read_text() + + def set_version(self, version: str): + self.file.write_text(version) + +``` + +`setup.py` + +```python +from setuptools import setup + +setup( + name='my-commitizen-provider', + version='0.1.0', + py_modules=['my_provider'], + install_requires=['commitizen'], + entry_points = { + 'commitizen.provider': [ + 'my-provider = my_provider:MyProvider', + ] + } +) +``` + [version_files]: bump.md#version_files [tag_format]: bump.md#tag_format [bump_message]: bump.md#bump_message diff --git a/docs/exit_codes.md b/docs/exit_codes.md index 54be954ff4..e7c7454478 100644 --- a/docs/exit_codes.md +++ b/docs/exit_codes.md @@ -30,4 +30,7 @@ These exit codes can be found in `commitizen/exceptions.py::ExitCode`. | NotAllowed | 20 | `--incremental` cannot be combined with a `rev_range` | | NoneIncrementExit | 21 | The commits found are not eligible to be bumped | | CharacterSetDecodeError | 22 | The character encoding of the command output could not be determined | -| GitCommandError | 23 | Unexpected failure while calling a git command | +| GitCommandError | 23 | Unexpected failure while calling a git command | +| InvalidManualVersion | 24 | Manually provided version is invalid | +| InitFailedError | 25 | Failed to initialize pre-commit | +| VersionProviderUnknown | 26 | `version_provider` setting is set to an unknown version provider indentifier | diff --git a/pyproject.toml b/pyproject.toml index 458eef8a05..7434613fb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,9 @@ cz_conventional_commits = "commitizen.cz.conventional_commits:ConventionalCommit cz_jira = "commitizen.cz.jira:JiraSmartCz" cz_customize = "commitizen.cz.customize:CustomizeCommitsCz" +[tool.poetry.plugins."commitizen.provider"] +commitizen = "commitizen.providers:CommitizenProvider" + [tool.isort] profile = "black" known_first_party = ["commitizen", "tests"] diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 21a5230c61..2a117aa5a1 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -833,3 +833,23 @@ def test_bump_manual_version_disallows_prerelease_offset(mocker): "--prerelease-offset cannot be combined with MANUAL_VERSION" ) assert expected_error_message in str(excinfo.value) + + +@pytest.mark.usefixtures("tmp_git_project") +def test_bump_use_version_provider(mocker: MockFixture): + mock = mocker.MagicMock(name="provider") + mock.get_version.return_value = "0.0.0" + get_provider = mocker.patch( + "commitizen.commands.bump.get_provider", return_value=mock + ) + + create_file_and_commit("fix: fake commit") + testargs = ["cz", "bump", "--yes", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + + cli.main() + + assert git.tag_exist("0.0.1") + get_provider.assert_called_once() + mock.get_version.assert_called_once() + mock.set_version.assert_called_once_with("0.0.1") diff --git a/tests/commands/test_version_command.py b/tests/commands/test_version_command.py index 7e6ec3c851..d6bc83b8b8 100644 --- a/tests/commands/test_version_command.py +++ b/tests/commands/test_version_command.py @@ -1,8 +1,12 @@ import platform import sys +import pytest +from pytest_mock import MockerFixture + from commitizen import commands from commitizen.__version__ import __version__ +from commitizen.config.base_config import BaseConfig def test_version_for_showing_project_version(config, capsys): @@ -70,3 +74,30 @@ def test_version_for_showing_commitizen_system_info(config, capsys): assert f"Commitizen Version: {__version__}" in captured.out assert f"Python Version: {sys.version}" in captured.out assert f"Operating System: {platform.system()}" in captured.out + + +@pytest.mark.parametrize("project", (True, False)) +@pytest.mark.usefixtures("tmp_git_project") +def test_version_use_version_provider( + mocker: MockerFixture, + config: BaseConfig, + capsys: pytest.CaptureFixture, + project: bool, +): + version = "0.0.0" + mock = mocker.MagicMock(name="provider") + mock.get_version.return_value = version + get_provider = mocker.patch( + "commitizen.commands.version.get_provider", return_value=mock + ) + + commands.Version( + config, + {"report": False, "project": project, "commitizen": False, "verbose": True}, + )() + captured = capsys.readouterr() + + assert version in captured.out + get_provider.assert_called_once() + mock.get_version.assert_called_once() + mock.set_version.assert_not_called() diff --git a/tests/test_conf.py b/tests/test_conf.py index d39de8a048..ff28f71144 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -44,6 +44,7 @@ _settings = { "name": "cz_jira", "version": "1.0.0", + "version_provider": "commitizen", "tag_format": None, "bump_message": None, "allow_abort": False, @@ -63,6 +64,7 @@ _new_settings = { "name": "cz_jira", "version": "2.0.0", + "version_provider": "commitizen", "tag_format": None, "bump_message": None, "allow_abort": False, diff --git a/tests/test_version_providers.py b/tests/test_version_providers.py new file mode 100644 index 0000000000..6887cdfcdb --- /dev/null +++ b/tests/test_version_providers.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import VersionProviderUnknown +from commitizen.providers import CommitizenProvider, get_provider + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + +def test_default_version_provider_is_commitizen_config(config: BaseConfig): + provider = get_provider(config) + + assert isinstance(provider, CommitizenProvider) + + +def test_raise_for_unknown_provider(config: BaseConfig): + config.settings["version_provider"] = "unknown" + with pytest.raises(VersionProviderUnknown): + get_provider(config) + + +def test_commitizen_provider(config: BaseConfig, mocker: MockerFixture): + config.settings["version"] = "42" + mock = mocker.patch.object(config, "set_key") + + provider = CommitizenProvider(config) + assert provider.get_version() == "42" + + provider.set_version("43.1") + mock.assert_called_once_with("version", "43.1") From 3bf2021af1384832c771bd5dbfca08ef9a3409f2 Mon Sep 17 00:00:00 2001 From: Axel H Date: Tue, 27 Dec 2022 01:22:33 +0100 Subject: [PATCH 2/6] feat(providers): add support for some TOML-based versions (PEP621, Poetry, Cargo) --- commitizen/providers.py | 73 ++++++++++++++++++++++++- docs/config.md | 9 ++++ docs/faq.md | 23 +++----- pyproject.toml | 3 ++ tests/test_version_providers.py | 96 ++++++++++++++++++++++++++++++++- 5 files changed, 186 insertions(+), 18 deletions(-) diff --git a/commitizen/providers.py b/commitizen/providers.py index 0269f59fb3..03b3e31226 100644 --- a/commitizen/providers.py +++ b/commitizen/providers.py @@ -1,9 +1,11 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import cast +from pathlib import Path +from typing import ClassVar, cast import importlib_metadata as metadata +import tomlkit from commitizen.config.base_config import BaseConfig from commitizen.exceptions import VersionProviderUnknown @@ -51,6 +53,75 @@ def set_version(self, version: str): self.config.set_key("version", version) +class FileProvider(VersionProvider): + """ + Base class for file-based version providers + """ + + filename: ClassVar[str] + + @property + def file(self) -> Path: + return Path() / self.filename + + +class TomlProvider(FileProvider): + """ + Base class for TOML-based version providers + """ + + def get_version(self) -> str: + document = tomlkit.parse(self.file.read_text()) + return self.get(document) + + def set_version(self, version: str): + document = tomlkit.parse(self.file.read_text()) + self.set(document, version) + self.file.write_text(tomlkit.dumps(document)) + + def get(self, document: tomlkit.TOMLDocument) -> str: + return document["project"]["version"] # type: ignore + + def set(self, document: tomlkit.TOMLDocument, version: str): + document["project"]["version"] = version # type: ignore + + +class Pep621Provider(TomlProvider): + """ + PEP-621 version management + """ + + filename = "pyproject.toml" + + +class PoetryProvider(TomlProvider): + """ + Poetry version management + """ + + filename = "pyproject.toml" + + def get(self, pyproject: tomlkit.TOMLDocument) -> str: + return pyproject["tool"]["poetry"]["version"] # type: ignore + + def set(self, pyproject: tomlkit.TOMLDocument, version: str): + pyproject["tool"]["poetry"]["version"] = version # type: ignore + + +class CargoProvider(TomlProvider): + """ + Cargo version management + """ + + filename = "Cargo.toml" + + def get(self, document: tomlkit.TOMLDocument) -> str: + return document["package"]["version"] # type: ignore + + def set(self, document: tomlkit.TOMLDocument, version: str): + document["package"]["version"] = version # type: ignore + + def get_provider(config: BaseConfig) -> VersionProvider: """ Get the version provider as defined in the configuration diff --git a/docs/config.md b/docs/config.md index d49df8461b..c22abbff41 100644 --- a/docs/config.md +++ b/docs/config.md @@ -119,6 +119,15 @@ Commitizen can read and write version from different sources. By default, it use the `commitizen` one which is using the `version` field from the commitizen settings. But you can use any `commitizen.provider` entrypoint as value for `version_provider`. +Commitizen provides some version providers for some well known formats: + +| name | description | +| ---- | ----------- | +| `commitizen` | Default version provider: Fetch and set version in commitizen config. | +| `pep621` | Get and set version from `pyproject.toml` `project.version` field | +| `poetry` | Get and set version from `pyproject.toml` `tool.poetry.version` field | +| `cargo` | Get and set version from `Cargo.toml` `project.version` field | + ### Custom version provider You can add you own version provider by extending `VersionProvider` and exposing it on the `commitizen.provider` entrypoint. diff --git a/docs/faq.md b/docs/faq.md index 7c076f0a62..060de78c30 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -5,26 +5,19 @@ PEP621 establishes a `[project]` definition inside `pyproject.toml` ```toml [project] name = "spam" -version = "2020.0.0" +version = "2.5.1" ``` -Commitizen **won't** use the `project.version` as a source of truth because it's a -tool aimed for any kind of project. - -If we were to use it, it would increase the complexity of the tool. Also why -wouldn't we support other project files like `cargo.toml` or `package.json`? - -Instead of supporting all the different project files, you can use `version_files` -inside `[tool.commitizen]`, and it will cheaply keep any of these project files in sync +Commitizen provides a [`pep621` version provider](config.md#version-providers) to get and set version from this field. +You just need to set the proper `version_provider` setting: ```toml -[tool.commitizen] +[project] +name = "spam" version = "2.5.1" -version_files = [ - "pyproject.toml:^version", - "cargo.toml:^version", - "package.json:\"version\":" -] + +[tool.commitizen] +version_provider = "pep621" ``` ## Why are `revert` and `chore` valid types in the check pattern of cz conventional_commits but not types we can select? diff --git a/pyproject.toml b/pyproject.toml index 7434613fb6..2392515e8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,10 @@ cz_jira = "commitizen.cz.jira:JiraSmartCz" cz_customize = "commitizen.cz.customize:CustomizeCommitsCz" [tool.poetry.plugins."commitizen.provider"] +cargo = "commitizen.providers:CargoProvider" commitizen = "commitizen.providers:CommitizenProvider" +pep621 = "commitizen.providers:Pep621Provider" +poetry = "commitizen.providers:PoetryProvider" [tool.isort] profile = "black" diff --git a/tests/test_version_providers.py b/tests/test_version_providers.py index 6887cdfcdb..5efb463d9f 100644 --- a/tests/test_version_providers.py +++ b/tests/test_version_providers.py @@ -1,17 +1,34 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import os +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING, Iterator import pytest from commitizen.config.base_config import BaseConfig from commitizen.exceptions import VersionProviderUnknown -from commitizen.providers import CommitizenProvider, get_provider +from commitizen.providers import ( + CargoProvider, + CommitizenProvider, + Pep621Provider, + PoetryProvider, + get_provider, +) if TYPE_CHECKING: from pytest_mock import MockerFixture +@pytest.fixture +def chdir(tmp_path: Path) -> Iterator[Path]: + cwd = Path() + os.chdir(tmp_path) + yield tmp_path + os.chdir(cwd) + + def test_default_version_provider_is_commitizen_config(config: BaseConfig): provider = get_provider(config) @@ -33,3 +50,78 @@ def test_commitizen_provider(config: BaseConfig, mocker: MockerFixture): provider.set_version("43.1") mock.assert_called_once_with("version", "43.1") + + +def test_pep621_provider(config: BaseConfig, chdir: Path): + pyproject_toml = chdir / "pyproject.toml" + pyproject_toml.write_text( + dedent( + """\ + [project] + version = "0.1.0" + """ + ) + ) + + provider = Pep621Provider(config) + + assert provider.get_version() == "0.1.0" + + provider.set_version("43.1") + + assert pyproject_toml.read_text() == dedent( + """\ + [project] + version = "43.1" + """ + ) + + +def test_poetry_provider(config: BaseConfig, chdir: Path): + pyproject_toml = chdir / "pyproject.toml" + pyproject_toml.write_text( + dedent( + """\ + [tool.poetry] + version = "0.1.0" + """ + ) + ) + config.settings["version_provider"] = "poetry" + + provider = get_provider(config) + assert isinstance(provider, PoetryProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("43.1") + assert pyproject_toml.read_text() == dedent( + """\ + [tool.poetry] + version = "43.1" + """ + ) + + +def test_cargo_provider(config: BaseConfig, chdir: Path): + cargo_toml = chdir / "Cargo.toml" + cargo_toml.write_text( + dedent( + """\ + [package] + version = "0.1.0" + """ + ) + ) + config.settings["version_provider"] = "cargo" + + provider = get_provider(config) + assert isinstance(provider, CargoProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("43.1") + assert cargo_toml.read_text() == dedent( + """\ + [package] + version = "43.1" + """ + ) From 4a5fda3c5a874aa8865b420419a7d2a426e79282 Mon Sep 17 00:00:00 2001 From: Axel H Date: Tue, 27 Dec 2022 01:50:56 +0100 Subject: [PATCH 3/6] feat(providers): add support for some JSON-based version providers (NPM, Composer) --- commitizen/providers.py | 43 ++++++++++++++++++++++- docs/config.md | 2 ++ pyproject.toml | 2 ++ tests/test_version_providers.py | 60 +++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/commitizen/providers.py b/commitizen/providers.py index 03b3e31226..1022d89d16 100644 --- a/commitizen/providers.py +++ b/commitizen/providers.py @@ -1,8 +1,9 @@ from __future__ import annotations +import json from abc import ABC, abstractmethod from pathlib import Path -from typing import ClassVar, cast +from typing import Any, ClassVar, cast import importlib_metadata as metadata import tomlkit @@ -122,6 +123,46 @@ def set(self, document: tomlkit.TOMLDocument, version: str): document["package"]["version"] = version # type: ignore +class JsonProvider(FileProvider): + """ + Base class for JSON-based version providers + """ + + indent: ClassVar[int] = 2 + + def get_version(self) -> str: + document = json.loads(self.file.read_text()) + return self.get(document) + + def set_version(self, version: str): + document = json.loads(self.file.read_text()) + self.set(document, version) + self.file.write_text(json.dumps(document, indent=self.indent) + "\n") + + def get(self, document: dict[str, Any]) -> str: + return document["version"] # type: ignore + + def set(self, document: dict[str, Any], version: str): + document["version"] = version + + +class NpmProvider(JsonProvider): + """ + npm package.json version management + """ + + filename = "package.json" + + +class ComposerProvider(JsonProvider): + """ + Composer version management + """ + + filename = "composer.json" + indent = 4 + + def get_provider(config: BaseConfig) -> VersionProvider: """ Get the version provider as defined in the configuration diff --git a/docs/config.md b/docs/config.md index c22abbff41..74ff22b465 100644 --- a/docs/config.md +++ b/docs/config.md @@ -127,6 +127,8 @@ Commitizen provides some version providers for some well known formats: | `pep621` | Get and set version from `pyproject.toml` `project.version` field | | `poetry` | Get and set version from `pyproject.toml` `tool.poetry.version` field | | `cargo` | Get and set version from `Cargo.toml` `project.version` field | +| `npm` | Get and set version from `package.json` `project.version` field | +| `composer` | Get and set version from `composer.json` `project.version` field | ### Custom version provider diff --git a/pyproject.toml b/pyproject.toml index 2392515e8a..6e09a16a0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,8 @@ cz_customize = "commitizen.cz.customize:CustomizeCommitsCz" [tool.poetry.plugins."commitizen.provider"] cargo = "commitizen.providers:CargoProvider" commitizen = "commitizen.providers:CommitizenProvider" +composer = "commitizen.providers:ComposerProvider" +npm = "commitizen.providers:NpmProvider" pep621 = "commitizen.providers:Pep621Provider" poetry = "commitizen.providers:PoetryProvider" diff --git a/tests/test_version_providers.py b/tests/test_version_providers.py index 5efb463d9f..fc29f3a76e 100644 --- a/tests/test_version_providers.py +++ b/tests/test_version_providers.py @@ -12,6 +12,8 @@ from commitizen.providers import ( CargoProvider, CommitizenProvider, + ComposerProvider, + NpmProvider, Pep621Provider, PoetryProvider, get_provider, @@ -125,3 +127,61 @@ def test_cargo_provider(config: BaseConfig, chdir: Path): version = "43.1" """ ) + + +def test_npm_provider(config: BaseConfig, chdir: Path): + package_json = chdir / "package.json" + package_json.write_text( + dedent( + """\ + { + "name": "whatever", + "version": "0.1.0" + } + """ + ) + ) + config.settings["version_provider"] = "npm" + + provider = get_provider(config) + assert isinstance(provider, NpmProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("43.1") + assert package_json.read_text() == dedent( + """\ + { + "name": "whatever", + "version": "43.1" + } + """ + ) + + +def test_composer_provider(config: BaseConfig, chdir: Path): + composer_json = chdir / "composer.json" + composer_json.write_text( + dedent( + """\ + { + "name": "whatever", + "version": "0.1.0" + } + """ + ) + ) + config.settings["version_provider"] = "composer" + + provider = get_provider(config) + assert isinstance(provider, ComposerProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("43.1") + assert composer_json.read_text() == dedent( + """\ + { + "name": "whatever", + "version": "43.1" + } + """ + ) From ec5caf05eda18536e44b75db673a00f5146231f6 Mon Sep 17 00:00:00 2001 From: Axel H Date: Tue, 27 Dec 2022 19:10:04 +0100 Subject: [PATCH 4/6] docs(deps): upgrade mkdocs-material and tidy some snippets --- docs/config.md | 8 ++--- mkdocs.yml | 2 ++ poetry.lock | 83 ++++++++++++++++++++++++++------------------------ pyproject.toml | 2 +- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/docs/config.md b/docs/config.md index 74ff22b465..9804fc0bdc 100644 --- a/docs/config.md +++ b/docs/config.md @@ -136,9 +136,7 @@ You can add you own version provider by extending `VersionProvider` and exposing Here a quick example of a `my-provider` provider reading and writing version in a `VERSION` file. -`my_provider.py` - -```python +```python title="my_provider.py" from pathlib import Path from commitizen.providers import VersionProvider @@ -154,9 +152,7 @@ class MyProvider(VersionProvider): ``` -`setup.py` - -```python +```python title="setup.py" from setuptools import setup setup( diff --git a/mkdocs.yml b/mkdocs.yml index 7d8dc430b0..e9206b18c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,3 +35,5 @@ markdown_extensions: - admonition - codehilite - extra + - pymdownx.highlight + - pymdownx.superfences diff --git a/poetry.lock b/poetry.lock index 7e7904280e..f7d7e797b3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -536,14 +536,14 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markdown" -version = "3.4.1" +version = "3.3.7" description = "Python implementation of Markdown." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" files = [ - {file = "Markdown-3.4.1-py3-none-any.whl", hash = "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186"}, - {file = "Markdown-3.4.1.tar.gz", hash = "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff"}, + {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, + {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] [package.dependencies] @@ -643,48 +643,66 @@ files = [ [[package]] name = "mkdocs" -version = "1.3.0" +version = "1.4.2" description = "Project documentation with Markdown." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "mkdocs-1.3.0-py3-none-any.whl", hash = "sha256:26bd2b03d739ac57a3e6eed0b7bcc86168703b719c27b99ad6ca91dc439aacde"}, - {file = "mkdocs-1.3.0.tar.gz", hash = "sha256:b504405b04da38795fec9b2e5e28f6aa3a73bb0960cb6d5d27ead28952bd35ea"}, + {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, + {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, ] [package.dependencies] -click = ">=3.3" +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = ">=4.3" -Jinja2 = ">=2.10.2" -Markdown = ">=3.2.1" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.2.1,<3.4" mergedeep = ">=1.3.4" packaging = ">=20.5" -PyYAML = ">=3.10" +pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] [[package]] name = "mkdocs-material" -version = "4.6.3" -description = "A Material Design theme for MkDocs" +version = "8.5.11" +description = "Documentation that simply works" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"}, - {file = "mkdocs_material-4.6.3-py2.py3-none-any.whl", hash = "sha256:7f3afa0a09c07d0b89a6a9755fdb00513aee8f0cec3538bb903325c80f66f444"}, + {file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"}, + {file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"}, ] [package.dependencies] +jinja2 = ">=3.0.2" markdown = ">=3.2" -mkdocs = ">=1.0" -Pygments = ">=2.4" -pymdown-extensions = ">=6.3" +mkdocs = ">=1.4.0" +mkdocs-material-extensions = ">=1.1" +pygments = ">=2.12" +pymdown-extensions = ">=9.4" +requests = ">=2.26" + +[[package]] +name = "mkdocs-material-extensions" +version = "1.1.1" +description = "Extension pack for Python Markdown and MkDocs Material." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, + {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, +] [[package]] name = "mypy" @@ -857,14 +875,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.20.0" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, - {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, ] [package.dependencies] @@ -873,8 +891,7 @@ identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" +virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" @@ -1285,18 +1302,6 @@ files = [ [package.extras] tests = ["pytest", "pytest-cov"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -1518,4 +1523,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "bbe0d066f84d0f48ea255f0015ca216f455ac49d471e96162a14456dd7a6a12a" +content-hash = "12181f1e683c5555c8be62a7de363a6c89b179ca070fbd4f0c68519d9509e47d" diff --git a/pyproject.toml b/pyproject.toml index 6e09a16a0d..4f63e821a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ types-PyYAML = "^5.4.3" types-termcolor = "^0.1.1" # documentation mkdocs = "^1.0" -mkdocs-material = "^4.1" +mkdocs-material = "^8.5.11" pydocstyle = "^5.0.2" pytest-xdist = "^3.1.0" From 2db5c75d8c7440e2247a743b5d774357bf1dfd54 Mon Sep 17 00:00:00 2001 From: Axel H Date: Sun, 1 Jan 2023 19:54:15 +0100 Subject: [PATCH 5/6] feat(providers): add a `scm` version provider Reads version from the repository last tag matching `tag_format` Fixes #641 --- commitizen/providers.py | 64 ++++++++++++++++++++++++++++++++- docs/config.md | 4 +++ pyproject.toml | 1 + tests/test_version_providers.py | 63 +++++++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 2 deletions(-) diff --git a/commitizen/providers.py b/commitizen/providers.py index 1022d89d16..17b99e7b23 100644 --- a/commitizen/providers.py +++ b/commitizen/providers.py @@ -1,15 +1,18 @@ from __future__ import annotations import json +import re from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, ClassVar, cast +from typing import Any, Callable, ClassVar, Optional, cast import importlib_metadata as metadata import tomlkit +from packaging.version import VERSION_PATTERN, Version from commitizen.config.base_config import BaseConfig from commitizen.exceptions import VersionProviderUnknown +from commitizen.git import get_tags PROVIDER_ENTRYPOINT = "commitizen.provider" DEFAULT_PROVIDER = "commitizen" @@ -163,6 +166,65 @@ class ComposerProvider(JsonProvider): indent = 4 +class ScmProvider(VersionProvider): + """ + A provider fetching the current/last version from the repository history + + The version is fetched using `git describe` and is never set. + + It is meant for `setuptools-scm` or any package manager `*-scm` provider. + """ + + TAG_FORMAT_REGEXS = { + "$version": r"(?P.+)", + "$major": r"(?P\d+)", + "$minor": r"(?P\d+)", + "$patch": r"(?P\d+)", + "$prerelease": r"(?P\w+\d+)?", + "$devrelease": r"(?P\.dev\d+)?", + } + + def _tag_format_matcher(self) -> Callable[[str], Optional[str]]: + pattern = self.config.settings.get("tag_format") or VERSION_PATTERN + for var, tag_pattern in self.TAG_FORMAT_REGEXS.items(): + pattern = pattern.replace(var, tag_pattern) + + regex = re.compile(f"^{pattern}$", re.VERBOSE) + + def matcher(tag: str) -> Optional[str]: + match = regex.match(tag) + if not match: + return None + groups = match.groupdict() + if "version" in groups: + return groups["version"] + elif "major" in groups: + return "".join( + ( + groups["major"], + f".{groups['minor']}" if groups.get("minor") else "", + f".{groups['patch']}" if groups.get("patch") else "", + groups["prerelease"] if groups.get("prerelease") else "", + groups["devrelease"] if groups.get("devrelease") else "", + ) + ) + elif pattern == VERSION_PATTERN: + return str(Version(tag)) + return None + + return matcher + + def get_version(self) -> str: + matcher = self._tag_format_matcher() + return next( + (cast(str, matcher(t.name)) for t in get_tags() if matcher(t.name)), "0.0.0" + ) + + def set_version(self, version: str): + # Not necessary + pass + + def get_provider(config: BaseConfig) -> VersionProvider: """ Get the version provider as defined in the configuration diff --git a/docs/config.md b/docs/config.md index 9804fc0bdc..cf38c5c37f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -124,12 +124,16 @@ Commitizen provides some version providers for some well known formats: | name | description | | ---- | ----------- | | `commitizen` | Default version provider: Fetch and set version in commitizen config. | +| `scm` | Fetch the version from git and does not need to set it back | | `pep621` | Get and set version from `pyproject.toml` `project.version` field | | `poetry` | Get and set version from `pyproject.toml` `tool.poetry.version` field | | `cargo` | Get and set version from `Cargo.toml` `project.version` field | | `npm` | Get and set version from `package.json` `project.version` field | | `composer` | Get and set version from `composer.json` `project.version` field | +!!! note + The `scm` provider is meant to be used with `setuptools-scm` or any packager `*-scm` plugin. + ### Custom version provider You can add you own version provider by extending `VersionProvider` and exposing it on the `commitizen.provider` entrypoint. diff --git a/pyproject.toml b/pyproject.toml index 4f63e821a2..2884715b29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,7 @@ composer = "commitizen.providers:ComposerProvider" npm = "commitizen.providers:NpmProvider" pep621 = "commitizen.providers:Pep621Provider" poetry = "commitizen.providers:PoetryProvider" +scm = "commitizen.providers:ScmProvider" [tool.isort] profile = "black" diff --git a/tests/test_version_providers.py b/tests/test_version_providers.py index fc29f3a76e..696b34269a 100644 --- a/tests/test_version_providers.py +++ b/tests/test_version_providers.py @@ -3,7 +3,7 @@ import os from pathlib import Path from textwrap import dedent -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING, Iterator, Optional import pytest @@ -16,8 +16,10 @@ NpmProvider, Pep621Provider, PoetryProvider, + ScmProvider, get_provider, ) +from tests.utils import create_file_and_commit, create_tag if TYPE_CHECKING: from pytest_mock import MockerFixture @@ -185,3 +187,62 @@ def test_composer_provider(config: BaseConfig, chdir: Path): } """ ) + + +@pytest.mark.parametrize( + "tag_format,tag,version", + ( + (None, "0.1.0", "0.1.0"), + (None, "v0.1.0", "0.1.0"), + ("v$version", "v0.1.0", "0.1.0"), + ("version-$version", "version-0.1.0", "0.1.0"), + ("version-$version", "version-0.1", "0.1"), + ("version-$version", "version-0.1.0rc1", "0.1.0rc1"), + ("v$minor.$major.$patch", "v1.0.0", "0.1.0"), + ("version-$major.$minor.$patch", "version-0.1.0", "0.1.0"), + ("v$major.$minor$prerelease$devrelease", "v1.0rc1", "1.0rc1"), + ("v$major.$minor.$patch$prerelease$devrelease", "v0.1.0", "0.1.0"), + ("v$major.$minor.$patch$prerelease$devrelease", "v0.1.0rc1", "0.1.0rc1"), + ("v$major.$minor.$patch$prerelease$devrelease", "v1.0.0.dev0", "1.0.0.dev0"), + ), +) +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider( + config: BaseConfig, tag_format: Optional[str], tag: str, version: str +): + create_file_and_commit("test: fake commit") + create_tag(tag) + create_file_and_commit("test: fake commit") + create_tag("should-not-match") + + config.settings["version_provider"] = "scm" + config.settings["tag_format"] = tag_format + + provider = get_provider(config) + assert isinstance(provider, ScmProvider) + assert provider.get_version() == version + + # Should not fail on set_version() + provider.set_version("43.1") + + +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider_default_without_matching_tag(config: BaseConfig): + create_file_and_commit("test: fake commit") + create_tag("should-not-match") + create_file_and_commit("test: fake commit") + + config.settings["version_provider"] = "scm" + + provider = get_provider(config) + assert isinstance(provider, ScmProvider) + assert provider.get_version() == "0.0.0" + + +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider_default_without_commits_and_tags(config: BaseConfig): + config.settings["version_provider"] = "scm" + + provider = get_provider(config) + assert isinstance(provider, ScmProvider) + assert provider.get_version() == "0.0.0" From 5672eaf91e23e1dbb455c932e2cfebdd7f921990 Mon Sep 17 00:00:00 2001 From: Axel H Date: Fri, 27 Jan 2023 01:26:37 +0100 Subject: [PATCH 6/6] test(providers): factorize some version providers tests --- tests/commands/test_version_command.py | 7 +- tests/test_version_providers.py | 201 ++++++++++--------------- 2 files changed, 89 insertions(+), 119 deletions(-) diff --git a/tests/commands/test_version_command.py b/tests/commands/test_version_command.py index d6bc83b8b8..3f9de50d00 100644 --- a/tests/commands/test_version_command.py +++ b/tests/commands/test_version_command.py @@ -93,7 +93,12 @@ def test_version_use_version_provider( commands.Version( config, - {"report": False, "project": project, "commitizen": False, "verbose": True}, + { + "report": False, + "project": project, + "commitizen": False, + "verbose": not project, + }, )() captured = capsys.readouterr() diff --git a/tests/test_version_providers.py b/tests/test_version_providers.py index 696b34269a..1c48fc3603 100644 --- a/tests/test_version_providers.py +++ b/tests/test_version_providers.py @@ -3,7 +3,7 @@ import os from pathlib import Path from textwrap import dedent -from typing import TYPE_CHECKING, Iterator, Optional +from typing import TYPE_CHECKING, Iterator, Optional, Type import pytest @@ -17,6 +17,7 @@ Pep621Provider, PoetryProvider, ScmProvider, + VersionProvider, get_provider, ) from tests.utils import create_file_and_commit, create_tag @@ -56,137 +57,101 @@ def test_commitizen_provider(config: BaseConfig, mocker: MockerFixture): mock.assert_called_once_with("version", "43.1") -def test_pep621_provider(config: BaseConfig, chdir: Path): - pyproject_toml = chdir / "pyproject.toml" - pyproject_toml.write_text( - dedent( - """\ - [project] - version = "0.1.0" - """ - ) - ) - - provider = Pep621Provider(config) - - assert provider.get_version() == "0.1.0" - - provider.set_version("43.1") - - assert pyproject_toml.read_text() == dedent( +FILE_PROVIDERS = dict( + pep621=( + "pyproject.toml", + Pep621Provider, """\ [project] - version = "43.1" - """ - ) - - -def test_poetry_provider(config: BaseConfig, chdir: Path): - pyproject_toml = chdir / "pyproject.toml" - pyproject_toml.write_text( - dedent( - """\ - [tool.poetry] - version = "0.1.0" - """ - ) - ) - config.settings["version_provider"] = "poetry" - - provider = get_provider(config) - assert isinstance(provider, PoetryProvider) - assert provider.get_version() == "0.1.0" - - provider.set_version("43.1") - assert pyproject_toml.read_text() == dedent( + version = "0.1.0" + """, + """\ + [project] + version = "42.1" + """, + ), + poetry=( + "pyproject.toml", + PoetryProvider, """\ [tool.poetry] - version = "43.1" - """ - ) - - -def test_cargo_provider(config: BaseConfig, chdir: Path): - cargo_toml = chdir / "Cargo.toml" - cargo_toml.write_text( - dedent( - """\ - [package] - version = "0.1.0" - """ - ) - ) - config.settings["version_provider"] = "cargo" - - provider = get_provider(config) - assert isinstance(provider, CargoProvider) - assert provider.get_version() == "0.1.0" - - provider.set_version("43.1") - assert cargo_toml.read_text() == dedent( + version = "0.1.0" + """, + """\ + [tool.poetry] + version = "42.1" + """, + ), + cargo=( + "Cargo.toml", + CargoProvider, """\ [package] - version = "43.1" - """ - ) - - -def test_npm_provider(config: BaseConfig, chdir: Path): - package_json = chdir / "package.json" - package_json.write_text( - dedent( - """\ - { - "name": "whatever", - "version": "0.1.0" - } - """ - ) - ) - config.settings["version_provider"] = "npm" - - provider = get_provider(config) - assert isinstance(provider, NpmProvider) - assert provider.get_version() == "0.1.0" - - provider.set_version("43.1") - assert package_json.read_text() == dedent( + version = "0.1.0" + """, + """\ + [package] + version = "42.1" + """, + ), + npm=( + "package.json", + NpmProvider, """\ { "name": "whatever", - "version": "43.1" + "version": "0.1.0" } - """ - ) - - -def test_composer_provider(config: BaseConfig, chdir: Path): - composer_json = chdir / "composer.json" - composer_json.write_text( - dedent( - """\ - { - "name": "whatever", - "version": "0.1.0" - } - """ - ) - ) - config.settings["version_provider"] = "composer" - - provider = get_provider(config) - assert isinstance(provider, ComposerProvider) - assert provider.get_version() == "0.1.0" - - provider.set_version("43.1") - assert composer_json.read_text() == dedent( + """, + """\ + { + "name": "whatever", + "version": "42.1" + } + """, + ), + composer=( + "composer.json", + ComposerProvider, """\ { "name": "whatever", - "version": "43.1" + "version": "0.1.0" } - """ - ) + """, + """\ + { + "name": "whatever", + "version": "42.1" + } + """, + ), +) + + +@pytest.mark.parametrize( + "id,filename,cls,content,expected", + (pytest.param(id, *FILE_PROVIDERS[id], id=id) for id in FILE_PROVIDERS), +) +def test_file_providers( + config: BaseConfig, + chdir: Path, + id: str, + filename: str, + cls: Type[VersionProvider], + content: str, + expected: str, +): + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = id + + provider = get_provider(config) + assert isinstance(provider, cls) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) @pytest.mark.parametrize(