diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a09747f3b..65aaef318c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: exclude: "poetry.lock" - repo: https://github.com/commitizen-tools/commitizen - rev: v3.8.1 # automatically updated by Commitizen + rev: v3.8.2 # automatically updated by Commitizen hooks: - id: commitizen - id: commitizen-branch diff --git a/CHANGELOG.md b/CHANGELOG.md index 93cd19a514..d5e652ce47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ +## v3.8.2 (2023-09-09) + +### Refactor + +- **provider**: split provider code and related tests into individual files for maintainability (#830) + ## v3.8.1 (2023-09-08) ### Fix diff --git a/commitizen/__version__.py b/commitizen/__version__.py index e4e78c0b9d..2ae7a96321 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "3.8.1" +__version__ = "3.8.2" 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/poetry.lock b/poetry.lock index 4bbb977395..a3bd4cddd0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -593,6 +593,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -975,13 +985,13 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pytest" -version = "7.4.1" +version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.1-py3-none-any.whl", hash = "sha256:460c9a59b14e27c602eb5ece2e47bec99dc5fc5f6513cf924a7d03a578991b1f"}, - {file = "pytest-7.4.1.tar.gz", hash = "sha256:2f2301e797521b23e4d2585a0a3d7b5e50fdddaaf7e7d6773ea26ddb17c213ab"}, + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 3ed3bf13e7..17b10d2bee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [tool.commitizen] -version = "3.8.1" +version = "3.8.2" 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] name = "commitizen" -version = "3.8.1" +version = "3.8.2" description = "Python commitizen client tool" authors = ["Santiago Fraire "] license = "MIT" @@ -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)