diff --git a/commitizen/providers.py b/commitizen/providers.py deleted file mode 100644 index df353bb365..0000000000 --- a/commitizen/providers.py +++ /dev/null @@ -1,319 +0,0 @@ -from __future__ import annotations - -import json -import re -from abc import ABC, abstractmethod -from pathlib import Path -from typing import Any, Callable, ClassVar, cast - -import importlib_metadata as metadata -import tomlkit - -from commitizen.config.base_config import BaseConfig -from commitizen.exceptions import VersionProviderUnknown -from commitizen.git import get_tags -from commitizen.version_schemes import get_version_scheme - -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) - - -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 - - With support for `workspaces` - """ - - filename = "Cargo.toml" - - def get(self, document: tomlkit.TOMLDocument) -> str: - try: - return document["package"]["version"] # type: ignore - except tomlkit.exceptions.NonExistentKey: - ... - return document["workspace"]["package"]["version"] # type: ignore - - def set(self, document: tomlkit.TOMLDocument, version: str): - try: - document["workspace"]["package"]["version"] = version # type: ignore - return - except tomlkit.exceptions.NonExistentKey: - ... - 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 and package-lock.json version management - """ - - indent: ClassVar[int] = 2 - package_filename = "package.json" - lock_filename = "package-lock.json" - shrinkwrap_filename = "npm-shrinkwrap.json" - - @property - def package_file(self) -> Path: - return Path() / self.package_filename - - @property - def lock_file(self) -> Path: - return Path() / self.lock_filename - - @property - def shrinkwrap_file(self) -> Path: - return Path() / self.shrinkwrap_filename - - def get_version(self) -> str: - """ - Get the current version from package.json - """ - package_document = json.loads(self.package_file.read_text()) - return self.get_package_version(package_document) - - def set_version(self, version: str) -> None: - package_document = self.set_package_version( - json.loads(self.package_file.read_text()), version - ) - self.package_file.write_text( - json.dumps(package_document, indent=self.indent) + "\n" - ) - if self.lock_file.exists(): - lock_document = self.set_lock_version( - json.loads(self.lock_file.read_text()), version - ) - self.lock_file.write_text( - json.dumps(lock_document, indent=self.indent) + "\n" - ) - if self.shrinkwrap_file.exists(): - shrinkwrap_document = self.set_shrinkwrap_version( - json.loads(self.shrinkwrap_file.read_text()), version - ) - self.shrinkwrap_file.write_text( - json.dumps(shrinkwrap_document, indent=self.indent) + "\n" - ) - - def get_package_version(self, document: dict[str, Any]) -> str: - return document["version"] # type: ignore - - def set_package_version( - self, document: dict[str, Any], version: str - ) -> dict[str, Any]: - document["version"] = version - return document - - def set_lock_version( - self, document: dict[str, Any], version: str - ) -> dict[str, Any]: - document["version"] = version - document["packages"][""]["version"] = version - return document - - def set_shrinkwrap_version( - self, document: dict[str, Any], version: str - ) -> dict[str, Any]: - document["version"] = version - document["packages"][""]["version"] = version - return document - - -class ComposerProvider(JsonProvider): - """ - Composer version management - """ - - filename = "composer.json" - 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], str | None]: - version_scheme = get_version_scheme(self.config) - pattern = self.config.settings["tag_format"] - if pattern == "$version": - pattern = version_scheme.parser.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) -> str | None: - 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_scheme.parser.pattern: - return str(version_scheme(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 - - :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/commitizen/providers/__init__.py b/commitizen/providers/__init__.py new file mode 100644 index 0000000000..51302d2b37 --- /dev/null +++ b/commitizen/providers/__init__.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import cast + +import importlib_metadata as metadata + +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import VersionProviderUnknown + +from commitizen.providers.base_provider import VersionProvider +from commitizen.providers.cargo_provider import CargoProvider +from commitizen.providers.commitizen_provider import CommitizenProvider +from commitizen.providers.composer_provider import ComposerProvider +from commitizen.providers.npm_provider import NpmProvider +from commitizen.providers.pep621_provider import Pep621Provider +from commitizen.providers.poetry_provider import PoetryProvider +from commitizen.providers.scm_provider import ScmProvider + +__all__ = [ + "get_provider", + "CargoProvider", + "CommitizenProvider", + "ComposerProvider", + "NpmProvider", + "Pep621Provider", + "PoetryProvider", + "ScmProvider", +] + +PROVIDER_ENTRYPOINT = "commitizen.provider" +DEFAULT_PROVIDER = "commitizen" + + +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/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py new file mode 100644 index 0000000000..34048318e2 --- /dev/null +++ b/commitizen/providers/base_provider.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import json +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any, ClassVar + +import tomlkit + +from commitizen.config.base_config import BaseConfig + + +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 FileProvider(VersionProvider): + """ + Base class for file-based version providers + """ + + filename: ClassVar[str] + + @property + def file(self) -> Path: + return Path() / self.filename + + +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 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 diff --git a/commitizen/providers/cargo_provider.py b/commitizen/providers/cargo_provider.py new file mode 100644 index 0000000000..f64c003edd --- /dev/null +++ b/commitizen/providers/cargo_provider.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import tomlkit + + +from commitizen.providers.base_provider import TomlProvider + + +class CargoProvider(TomlProvider): + """ + Cargo version management + + With support for `workspaces` + """ + + filename = "Cargo.toml" + + def get(self, document: tomlkit.TOMLDocument) -> str: + try: + return document["package"]["version"] # type: ignore + except tomlkit.exceptions.NonExistentKey: + ... + return document["workspace"]["package"]["version"] # type: ignore + + def set(self, document: tomlkit.TOMLDocument, version: str): + try: + document["workspace"]["package"]["version"] = version # type: ignore + return + except tomlkit.exceptions.NonExistentKey: + ... + document["package"]["version"] = version # type: ignore diff --git a/commitizen/providers/commitizen_provider.py b/commitizen/providers/commitizen_provider.py new file mode 100644 index 0000000000..fd0cdb3ebf --- /dev/null +++ b/commitizen/providers/commitizen_provider.py @@ -0,0 +1,16 @@ +from __future__ import annotations + + +from commitizen.providers.base_provider import VersionProvider + + +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) diff --git a/commitizen/providers/composer_provider.py b/commitizen/providers/composer_provider.py new file mode 100644 index 0000000000..ef36af5a72 --- /dev/null +++ b/commitizen/providers/composer_provider.py @@ -0,0 +1,13 @@ +from __future__ import annotations + + +from commitizen.providers.base_provider import JsonProvider + + +class ComposerProvider(JsonProvider): + """ + Composer version management + """ + + filename = "composer.json" + indent = 4 diff --git a/commitizen/providers/npm_provider.py b/commitizen/providers/npm_provider.py new file mode 100644 index 0000000000..f625c3c6c3 --- /dev/null +++ b/commitizen/providers/npm_provider.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, ClassVar + + +from commitizen.providers.base_provider import VersionProvider + + +class NpmProvider(VersionProvider): + """ + npm package.json and package-lock.json version management + """ + + indent: ClassVar[int] = 2 + package_filename = "package.json" + lock_filename = "package-lock.json" + shrinkwrap_filename = "npm-shrinkwrap.json" + + @property + def package_file(self) -> Path: + return Path() / self.package_filename + + @property + def lock_file(self) -> Path: + return Path() / self.lock_filename + + @property + def shrinkwrap_file(self) -> Path: + return Path() / self.shrinkwrap_filename + + def get_version(self) -> str: + """ + Get the current version from package.json + """ + package_document = json.loads(self.package_file.read_text()) + return self.get_package_version(package_document) + + def set_version(self, version: str) -> None: + package_document = self.set_package_version( + json.loads(self.package_file.read_text()), version + ) + self.package_file.write_text( + json.dumps(package_document, indent=self.indent) + "\n" + ) + if self.lock_file.exists(): + lock_document = self.set_lock_version( + json.loads(self.lock_file.read_text()), version + ) + self.lock_file.write_text( + json.dumps(lock_document, indent=self.indent) + "\n" + ) + if self.shrinkwrap_file.exists(): + shrinkwrap_document = self.set_shrinkwrap_version( + json.loads(self.shrinkwrap_file.read_text()), version + ) + self.shrinkwrap_file.write_text( + json.dumps(shrinkwrap_document, indent=self.indent) + "\n" + ) + + def get_package_version(self, document: dict[str, Any]) -> str: + return document["version"] # type: ignore + + def set_package_version( + self, document: dict[str, Any], version: str + ) -> dict[str, Any]: + document["version"] = version + return document + + def set_lock_version( + self, document: dict[str, Any], version: str + ) -> dict[str, Any]: + document["version"] = version + document["packages"][""]["version"] = version + return document + + def set_shrinkwrap_version( + self, document: dict[str, Any], version: str + ) -> dict[str, Any]: + document["version"] = version + document["packages"][""]["version"] = version + return document diff --git a/commitizen/providers/pep621_provider.py b/commitizen/providers/pep621_provider.py new file mode 100644 index 0000000000..b6d32f1a63 --- /dev/null +++ b/commitizen/providers/pep621_provider.py @@ -0,0 +1,12 @@ +from __future__ import annotations + + +from commitizen.providers.base_provider import TomlProvider + + +class Pep621Provider(TomlProvider): + """ + PEP-621 version management + """ + + filename = "pyproject.toml" diff --git a/commitizen/providers/poetry_provider.py b/commitizen/providers/poetry_provider.py new file mode 100644 index 0000000000..d301131115 --- /dev/null +++ b/commitizen/providers/poetry_provider.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import tomlkit + + +from commitizen.providers.base_provider import TomlProvider + + +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 diff --git a/commitizen/providers/scm_provider.py b/commitizen/providers/scm_provider.py new file mode 100644 index 0000000000..bc9dda4b8a --- /dev/null +++ b/commitizen/providers/scm_provider.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import re +from typing import Callable, cast + + +from commitizen.git import get_tags +from commitizen.version_schemes import get_version_scheme + +from commitizen.providers.base_provider import VersionProvider + + +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], str | None]: + version_scheme = get_version_scheme(self.config) + pattern = self.config.settings["tag_format"] + if pattern == "$version": + pattern = version_scheme.parser.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) -> str | None: + 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_scheme.parser.pattern: + return str(version_scheme(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 diff --git a/pyproject.toml b/pyproject.toml index 3ed3bf13e7..83cbe088fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ tag_format = "v$version" version_files = [ "pyproject.toml:version", "commitizen/__version__.py", - ".pre-commit-config.yaml:rev:.+Commitizen" + ".pre-commit-config.yaml:rev:.+Commitizen", ] [tool.poetry] @@ -122,7 +122,7 @@ semver = "commitizen.version_schemes:SemVer" '.venv/*', '*/virtualenv/*', '*/virtualenvs/*', - '*/tests/*' + '*/tests/*', ] [build-system] @@ -137,7 +137,7 @@ line-length = 88 ignore = [ "E501", "D1", - "D415", + "D415" ] [tool.ruff.isort] diff --git a/tests/providers/conftest.py b/tests/providers/conftest.py new file mode 100644 index 0000000000..b4432ca524 --- /dev/null +++ b/tests/providers/conftest.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Iterator + +import pytest + + +@pytest.fixture +def chdir(tmp_path: Path) -> Iterator[Path]: + cwd = Path() + os.chdir(tmp_path) + yield tmp_path + os.chdir(cwd) diff --git a/tests/providers/test_base_provider.py b/tests/providers/test_base_provider.py new file mode 100644 index 0000000000..09a2bd71cd --- /dev/null +++ b/tests/providers/test_base_provider.py @@ -0,0 +1,21 @@ +from __future__ import annotations + + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import VersionProviderUnknown +from commitizen.providers import get_provider +from commitizen.providers.commitizen_provider import CommitizenProvider + + +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) diff --git a/tests/providers/test_cargo_provider.py b/tests/providers/test_cargo_provider.py new file mode 100644 index 0000000000..cde868deb5 --- /dev/null +++ b/tests/providers/test_cargo_provider.py @@ -0,0 +1,61 @@ +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.cargo_provider import CargoProvider + + +CARGO_TOML = """\ +[package] +name = "whatever" +version = "0.1.0" +""" + +CARGO_EXPECTED = """\ +[package] +name = "whatever" +version = "42.1" +""" + +CARGO_WORKSPACE_TOML = """\ +[workspace.package] +name = "whatever" +version = "0.1.0" +""" + +CARGO_WORKSPACE_EXPECTED = """\ +[workspace.package] +name = "whatever" +version = "42.1" +""" + + +@pytest.mark.parametrize( + "content, expected", + ( + (CARGO_TOML, CARGO_EXPECTED), + (CARGO_WORKSPACE_TOML, CARGO_WORKSPACE_EXPECTED), + ), +) +def test_cargo_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = CargoProvider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "cargo" + + provider = get_provider(config) + assert isinstance(provider, CargoProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_commitizen_provider.py b/tests/providers/test_commitizen_provider.py new file mode 100644 index 0000000000..887adf3d12 --- /dev/null +++ b/tests/providers/test_commitizen_provider.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + + +from commitizen.config.base_config import BaseConfig +from commitizen.providers.commitizen_provider import CommitizenProvider + + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + +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") diff --git a/tests/providers/test_composer_provider.py b/tests/providers/test_composer_provider.py new file mode 100644 index 0000000000..ce72ae4703 --- /dev/null +++ b/tests/providers/test_composer_provider.py @@ -0,0 +1,48 @@ +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.composer_provider import ComposerProvider + + +COMPOSER_JSON = """\ +{ + "name": "whatever", + "version": "0.1.0" +} +""" + +COMPOSER_EXPECTED = """\ +{ + "name": "whatever", + "version": "42.1" +} +""" + + +@pytest.mark.parametrize( + "content, expected", + ((COMPOSER_JSON, COMPOSER_EXPECTED),), +) +def test_composer_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = ComposerProvider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "composer" + + provider = get_provider(config) + assert isinstance(provider, ComposerProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_npm_provider.py b/tests/providers/test_npm_provider.py new file mode 100644 index 0000000000..2e5ceb42d5 --- /dev/null +++ b/tests/providers/test_npm_provider.py @@ -0,0 +1,99 @@ +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.npm_provider import NpmProvider + + +NPM_PACKAGE_JSON = """\ +{ + "name": "whatever", + "version": "0.1.0" +} +""" + +NPM_PACKAGE_EXPECTED = """\ +{ + "name": "whatever", + "version": "42.1" +} +""" + +NPM_LOCKFILE_JSON = """\ +{ + "name": "whatever", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "whatever", + "version": "0.1.0" + }, + "someotherpackage": { + "version": "0.1.0" + } + } +} +""" + +NPM_LOCKFILE_EXPECTED = """\ +{ + "name": "whatever", + "version": "42.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "whatever", + "version": "42.1" + }, + "someotherpackage": { + "version": "0.1.0" + } + } +} +""" + + +@pytest.mark.parametrize( + "pkg_shrinkwrap_content, pkg_shrinkwrap_expected", + ((NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)), +) +@pytest.mark.parametrize( + "pkg_lock_content, pkg_lock_expected", + ((NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)), +) +def test_npm_provider( + config: BaseConfig, + chdir: Path, + pkg_lock_content: str, + pkg_lock_expected: str, + pkg_shrinkwrap_content: str, + pkg_shrinkwrap_expected: str, +): + pkg = chdir / NpmProvider.package_filename + pkg.write_text(dedent(NPM_PACKAGE_JSON)) + if pkg_lock_content: + pkg_lock = chdir / NpmProvider.lock_filename + pkg_lock.write_text(dedent(pkg_lock_content)) + if pkg_shrinkwrap_content: + pkg_shrinkwrap = chdir / NpmProvider.shrinkwrap_filename + pkg_shrinkwrap.write_text(dedent(pkg_shrinkwrap_content)) + config.settings["version_provider"] = "npm" + + provider = get_provider(config) + assert isinstance(provider, NpmProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert pkg.read_text() == dedent(NPM_PACKAGE_EXPECTED) + if pkg_lock_content: + assert pkg_lock.read_text() == dedent(pkg_lock_expected) + if pkg_shrinkwrap_content: + assert pkg_shrinkwrap.read_text() == dedent(pkg_shrinkwrap_expected) diff --git a/tests/providers/test_pep621_provider.py b/tests/providers/test_pep621_provider.py new file mode 100644 index 0000000000..9e82213294 --- /dev/null +++ b/tests/providers/test_pep621_provider.py @@ -0,0 +1,44 @@ +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.pep621_provider import Pep621Provider + + +PEP621_TOML = """\ +[project] +version = "0.1.0" +""" + +PEP621_EXPECTED = """\ +[project] +version = "42.1" +""" + + +@pytest.mark.parametrize( + "content, expected", + ((PEP621_TOML, PEP621_EXPECTED),), +) +def test_cargo_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = Pep621Provider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "pep621" + + provider = get_provider(config) + assert isinstance(provider, Pep621Provider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_poetry_provider.py b/tests/providers/test_poetry_provider.py new file mode 100644 index 0000000000..9e327db6ad --- /dev/null +++ b/tests/providers/test_poetry_provider.py @@ -0,0 +1,44 @@ +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.poetry_provider import PoetryProvider + + +POETRY_TOML = """\ +[tool.poetry] +version = "0.1.0" +""" + +POETRY_EXPECTED = """\ +[tool.poetry] +version = "42.1" +""" + + +@pytest.mark.parametrize( + "content, expected", + ((POETRY_TOML, POETRY_EXPECTED),), +) +def test_cargo_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = PoetryProvider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "poetry" + + provider = get_provider(config) + assert isinstance(provider, PoetryProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_scm_provider.py b/tests/providers/test_scm_provider.py new file mode 100644 index 0000000000..a0bfc46474 --- /dev/null +++ b/tests/providers/test_scm_provider.py @@ -0,0 +1,64 @@ +from __future__ import annotations + + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.scm_provider import ScmProvider +from tests.utils import create_file_and_commit, create_tag + + +@pytest.mark.parametrize( + "tag_format,tag,expected_version", + ( + # If tag_format is $version (the default), version_scheme.parser is used. + # Its DEFAULT_VERSION_PARSER allows a v prefix, but matches PEP440 otherwise. + ("$version", "no-match-because-version-scheme-is-strict", "0.0.0"), + ("$version", "0.1.0", "0.1.0"), + ("$version", "v0.1.0", "0.1.0"), + ("$version", "v-0.1.0", "0.0.0"), + # If tag_format is not None or $version, TAG_FORMAT_REGEXS are used, which are + # much more lenient but require a v prefix. + ("v$version", "v0.1.0", "0.1.0"), + ("v$version", "no-match-because-no-v-prefix", "0.0.0"), + ("v$version", "v-match-TAG_FORMAT_REGEXS", "-match-TAG_FORMAT_REGEXS"), + ("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: str, tag: str, expected_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) + actual_version = provider.get_version() + assert actual_version == expected_version + + # Should not fail on set_version() + provider.set_version("43.1") + + +@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" diff --git a/tests/test_version_providers.py b/tests/test_version_providers.py deleted file mode 100644 index 1cf0e736d8..0000000000 --- a/tests/test_version_providers.py +++ /dev/null @@ -1,316 +0,0 @@ -from __future__ import annotations - -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 ( - CargoProvider, - CommitizenProvider, - ComposerProvider, - NpmProvider, - Pep621Provider, - PoetryProvider, - ScmProvider, - VersionProvider, - get_provider, -) -from tests.utils import create_file_and_commit, create_tag - -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) - - 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") - - -FILE_PROVIDERS = [ - ( - "pep621", - "pyproject.toml", - Pep621Provider, - """\ - [project] - version = "0.1.0" - """, - """\ - [project] - version = "42.1" - """, - ), - ( - "poetry", - "pyproject.toml", - PoetryProvider, - """\ - [tool.poetry] - version = "0.1.0" - """, - """\ - [tool.poetry] - version = "42.1" - """, - ), - ( - "cargo", - "Cargo.toml", - CargoProvider, - """\ - [workspace.package] - version = "0.1.0" - """, - """\ - [workspace.package] - version = "42.1" - """, - ), - ( - "cargo", - "Cargo.toml", - CargoProvider, - """\ - [package] - version = "0.1.0" - """, - """\ - [package] - version = "42.1" - """, - ), - ( - "npm", - "package.json", - NpmProvider, - """\ - { - "name": "whatever", - "version": "0.1.0" - } - """, - """\ - { - "name": "whatever", - "version": "42.1" - } - """, - ), - ( - "composer", - "composer.json", - ComposerProvider, - """\ - { - "name": "whatever", - "version": "0.1.0" - } - """, - """\ - { - "name": "whatever", - "version": "42.1" - } - """, - ), -] - - -@pytest.mark.parametrize( - "id,filename,cls,content,expected", - 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( - "tag_format,tag,expected_version", - ( - # If tag_format is $version (the default), version_scheme.parser is used. - # Its DEFAULT_VERSION_PARSER allows a v prefix, but matches PEP440 otherwise. - ("$version", "no-match-because-version-scheme-is-strict", "0.0.0"), - ("$version", "0.1.0", "0.1.0"), - ("$version", "v0.1.0", "0.1.0"), - ("$version", "v-0.1.0", "0.0.0"), - # If tag_format is not None or $version, TAG_FORMAT_REGEXS are used, which are - # much more lenient but require a v prefix. - ("v$version", "v0.1.0", "0.1.0"), - ("v$version", "no-match-because-no-v-prefix", "0.0.0"), - ("v$version", "v-match-TAG_FORMAT_REGEXS", "-match-TAG_FORMAT_REGEXS"), - ("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: str, tag: str, expected_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) - actual_version = provider.get_version() - assert actual_version == expected_version - - # Should not fail on set_version() - provider.set_version("43.1") - - -@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" - - -NPM_PACKAGE_JSON = """\ -{ - "name": "whatever", - "version": "0.1.0" -} -""" - -NPM_PACKAGE_EXPECTED = """\ -{ - "name": "whatever", - "version": "42.1" -} -""" - -NPM_LOCKFILE_JSON = """\ -{ - "name": "whatever", - "version": "0.1.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "whatever", - "version": "0.1.0" - }, - "someotherpackage": { - "version": "0.1.0" - } - } -} -""" - -NPM_LOCKFILE_EXPECTED = """\ -{ - "name": "whatever", - "version": "42.1", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "whatever", - "version": "42.1" - }, - "someotherpackage": { - "version": "0.1.0" - } - } -} -""" - - -@pytest.mark.parametrize( - "pkg_shrinkwrap_content, pkg_shrinkwrap_expected", - ((NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)), -) -@pytest.mark.parametrize( - "pkg_lock_content, pkg_lock_expected", - ((NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)), -) -def test_npm_provider( - config: BaseConfig, - chdir: Path, - pkg_lock_content: str, - pkg_lock_expected: str, - pkg_shrinkwrap_content: str, - pkg_shrinkwrap_expected: str, -): - pkg = chdir / "package.json" - pkg.write_text(dedent(NPM_PACKAGE_JSON)) - if pkg_lock_content: - pkg_lock = chdir / "package-lock.json" - pkg_lock.write_text(dedent(pkg_lock_content)) - if pkg_shrinkwrap_content: - pkg_shrinkwrap = chdir / "npm-shrinkwrap.json" - pkg_shrinkwrap.write_text(dedent(pkg_shrinkwrap_content)) - config.settings["version_provider"] = "npm" - - provider = get_provider(config) - assert isinstance(provider, NpmProvider) - assert provider.get_version() == "0.1.0" - - provider.set_version("42.1") - assert pkg.read_text() == dedent(NPM_PACKAGE_EXPECTED) - if pkg_lock_content: - assert pkg_lock.read_text() == dedent(pkg_lock_expected) - if pkg_shrinkwrap_content: - assert pkg_shrinkwrap.read_text() == dedent(pkg_shrinkwrap_expected)