Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions commitizen/providers/base_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ class FileProvider(VersionProvider):
def file(self) -> Path:
return Path() / self.filename

def _get_encoding(self) -> str:
return self.config.settings["encoding"]


class JsonProvider(FileProvider):
"""
Expand All @@ -58,13 +61,16 @@ class JsonProvider(FileProvider):
indent: ClassVar[int] = 2

def get_version(self) -> str:
document = json.loads(self.file.read_text())
document = json.loads(self.file.read_text(encoding=self._get_encoding()))
return self.get(document)

def set_version(self, version: str) -> None:
document = json.loads(self.file.read_text())
document = json.loads(self.file.read_text(encoding=self._get_encoding()))
self.set(document, version)
self.file.write_text(json.dumps(document, indent=self.indent) + "\n")
self.file.write_text(
json.dumps(document, indent=self.indent) + "\n",
encoding=self._get_encoding(),
)

def get(self, document: Mapping[str, str]) -> str:
return document["version"]
Expand All @@ -79,13 +85,13 @@ class TomlProvider(FileProvider):
"""

def get_version(self) -> str:
document = tomlkit.parse(self.file.read_text())
document = tomlkit.parse(self.file.read_text(encoding=self._get_encoding()))
return self.get(document)

def set_version(self, version: str) -> None:
document = tomlkit.parse(self.file.read_text())
document = tomlkit.parse(self.file.read_text(encoding=self._get_encoding()))
self.set(document, version)
self.file.write_text(tomlkit.dumps(document))
self.file.write_text(tomlkit.dumps(document), encoding=self._get_encoding())

def get(self, document: tomlkit.TOMLDocument) -> str:
return document["project"]["version"] # type: ignore[index,return-value]
Expand Down
14 changes: 10 additions & 4 deletions commitizen/providers/cargo_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ def set_version(self, version: str) -> None:
self.set_lock_version(version)

def set_lock_version(self, version: str) -> None:
cargo_toml_content = parse(self.file.read_text())
cargo_lock_content = parse(self.lock_file.read_text())
cargo_toml_content = parse(self.file.read_text(encoding=self._get_encoding()))
cargo_lock_content = parse(
self.lock_file.read_text(encoding=self._get_encoding())
)
packages = cargo_lock_content["package"]

if TYPE_CHECKING:
Expand Down Expand Up @@ -75,7 +77,9 @@ def set_lock_version(self, version: str) -> None:
continue

cargo_file = Path(path) / "Cargo.toml"
package_content = parse(cargo_file.read_text()).get("package", {})
package_content = parse(
cargo_file.read_text(encoding=self._get_encoding())
).get("package", {})
if TYPE_CHECKING:
assert isinstance(package_content, dict)
try:
Expand All @@ -92,7 +96,9 @@ def set_lock_version(self, version: str) -> None:
if package["name"] in members_inheriting:
cargo_lock_content["package"][i]["version"] = version # type: ignore[index]

self.lock_file.write_text(dumps(cargo_lock_content))
self.lock_file.write_text(
dumps(cargo_lock_content), encoding=self._get_encoding()
)


def _try_get_workspace(document: TOMLDocument) -> dict:
Expand Down
28 changes: 19 additions & 9 deletions commitizen/providers/npm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar

from commitizen.providers.base_provider import VersionProvider
from commitizen.providers.base_provider import JsonProvider

if TYPE_CHECKING:
from collections.abc import Mapping


class NpmProvider(VersionProvider):
class NpmProvider(JsonProvider):
"""
npm package.json and package-lock.json version management
"""
Expand All @@ -36,29 +36,39 @@ def get_version(self) -> str:
"""
Get the current version from package.json
"""
package_document = json.loads(self.package_file.read_text())
package_document = json.loads(
self.package_file.read_text(encoding=self._get_encoding())
)
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
json.loads(self.package_file.read_text(encoding=self._get_encoding())),
version,
)
self.package_file.write_text(
json.dumps(package_document, indent=self.indent) + "\n"
json.dumps(package_document, indent=self.indent) + "\n",
encoding=self._get_encoding(),
)
if self.lock_file.is_file():
lock_document = self.set_lock_version(
json.loads(self.lock_file.read_text()), version
json.loads(self.lock_file.read_text(encoding=self._get_encoding())),
version,
)
self.lock_file.write_text(
json.dumps(lock_document, indent=self.indent) + "\n"
json.dumps(lock_document, indent=self.indent) + "\n",
encoding=self._get_encoding(),
)
if self.shrinkwrap_file.is_file():
shrinkwrap_document = self.set_shrinkwrap_version(
json.loads(self.shrinkwrap_file.read_text()), version
json.loads(
self.shrinkwrap_file.read_text(encoding=self._get_encoding())
),
version,
)
self.shrinkwrap_file.write_text(
json.dumps(shrinkwrap_document, indent=self.indent) + "\n"
json.dumps(shrinkwrap_document, indent=self.indent) + "\n",
encoding=self._get_encoding(),
)

def get_package_version(self, document: Mapping[str, str]) -> str:
Expand Down
12 changes: 9 additions & 3 deletions commitizen/providers/uv_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@ def set_version(self, version: str) -> None:
self.set_lock_version(version)

def set_lock_version(self, version: str) -> None:
pyproject_toml_content = tomlkit.parse(self.file.read_text())
pyproject_toml_content = tomlkit.parse(
self.file.read_text(encoding=self._get_encoding())
)
project_name = pyproject_toml_content["project"]["name"] # type: ignore[index]
normalized_project_name = canonicalize_name(str(project_name))

document = tomlkit.parse(self.lock_file.read_text())
document = tomlkit.parse(
self.lock_file.read_text(encoding=self._get_encoding())
)

packages: tomlkit.items.AoT = document["package"] # type: ignore[assignment]
for i, package in enumerate(packages):
if package["name"] == normalized_project_name:
document["package"][i]["version"] = version # type: ignore[index]
break
self.lock_file.write_text(tomlkit.dumps(document))
self.lock_file.write_text(
tomlkit.dumps(document), encoding=self._get_encoding()
)
6 changes: 6 additions & 0 deletions tests/data/encoding_test_composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "encoding-test-composer",
"description": "Тест описания для проверки кодировки",
"version": "0.1.0"
}

36 changes: 36 additions & 0 deletions tests/data/encoding_test_pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[project]
name = "pythonproject-test"
version = "0.4.1"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []


[tool.commitizen]
name = "cz_customize"
tag_format = "v$version"
version_scheme = "pep440"
version_provider = "uv"
update_changelog_on_bump = true
changelog_start_rev = "v1.1.0"

[tool.commitizen.customize]
message_template = "{{ change_type }}{% if scope != 'none' %}({{ scope }}){% endif %}: {{ message }}"
commit_parser = '^(?P<change_type>feat|fix|refactor|test|perf|misc):\s(?P<message>.*)'
schema_pattern = '(feat|fix|refactor|test|perf|misc)(\((api|core)\))?:\s(.{3,})'
bump_pattern = "^(feat|fix|refactor|test|perf|misc)"
change_type_map = { "feat" = "Новое", "fix" = "Исправление" }

[[tool.commitizen.customize.questions]]
name = "change_type"
type = "list"
message = "Выберите тип изменений"
choices = [
{ value = "feat", name = "feat: Новая функциональность" },
{ value = "fix", name = "fix: Исправление" },
{ value = "refactor", name = "refactor: Рефакторинг" },
{ value = "test", name = "test: Изменение авто-тестов" },
{ value = "perf", name = "perf: Оптимизации" },
{ value = "misc", name = "misc: Другое" },
]
100 changes: 100 additions & 0 deletions tests/providers/test_base_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
from commitizen.exceptions import VersionProviderUnknown
from commitizen.providers import get_provider
from commitizen.providers.commitizen_provider import CommitizenProvider
from commitizen.providers.composer_provider import ComposerProvider
from commitizen.providers.pep621_provider import Pep621Provider
from commitizen.providers.uv_provider import UvProvider

if TYPE_CHECKING:
from pathlib import Path

from commitizen.config.base_config import BaseConfig


Expand All @@ -22,3 +27,98 @@ def test_raise_for_unknown_provider(config: BaseConfig):
config.settings["version_provider"] = "unknown"
with pytest.raises(VersionProviderUnknown):
get_provider(config)


@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"])
def test_file_provider_get_encoding(config: BaseConfig, encoding: str):
"""_get_encoding should return the configured encoding."""
config.settings["encoding"] = encoding
provider = ComposerProvider(config)
assert provider._get_encoding() == encoding


def test_json_provider_uses_encoding_with_encoding_fixture(
config: BaseConfig,
chdir: Path,
data_dir: Path,
):
"""JsonProvider should correctly read a JSON file with non-ASCII content."""
source = data_dir / "encoding_test_composer.json"
target = chdir / "composer.json"
target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")

config.settings["encoding"] = "utf-8"
config.settings["version_provider"] = "composer"

provider = get_provider(config)
assert isinstance(provider, ComposerProvider)
assert provider.get_version() == "0.1.0"


def test_toml_provider_uses_encoding_with_encoding_fixture(
config: BaseConfig,
chdir: Path,
data_dir: Path,
):
"""TomlProvider should correctly read a TOML file with non-ASCII content."""
source = data_dir / "encoding_test_pyproject.toml"
target = chdir / "pyproject.toml"
target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")

config.settings["encoding"] = "utf-8"
config.settings["version_provider"] = "uv"

provider = get_provider(config)
assert isinstance(provider, UvProvider)
assert provider.get_version() == "0.4.1"


def test_json_provider_handles_various_unicode_characters(
config: BaseConfig,
chdir: Path,
):
"""JsonProvider should handle a wide range of Unicode characters."""
config.settings["encoding"] = "utf-8"
config.settings["version_provider"] = "composer"

filename = ComposerProvider.filename
file = chdir / filename
file.write_text(
(
"{\n"
' "name": "多言語-имя-árbol",\n'
' "description": "Emoji 😀 – 漢字 – العربية",\n'
' "version": "0.1.0"\n'
"}\n"
),
encoding="utf-8",
)

provider = get_provider(config)
assert isinstance(provider, ComposerProvider)
assert provider.get_version() == "0.1.0"


def test_toml_provider_handles_various_unicode_characters(
config: BaseConfig,
chdir: Path,
):
"""TomlProvider should handle a wide range of Unicode characters."""
config.settings["encoding"] = "utf-8"
config.settings["version_provider"] = "pep621"

filename = Pep621Provider.filename
file = chdir / filename
file.write_text(
(
"[project]\n"
'name = "多言語-имя-árbol"\n'
'description = "Emoji 😀 – 漢字 – العربية"\n'
'version = "0.1.0"\n'
),
encoding="utf-8",
)

provider = get_provider(config)
assert isinstance(provider, Pep621Provider)
assert provider.get_version() == "0.1.0"
56 changes: 56 additions & 0 deletions tests/providers/test_npm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@
}
"""

NPM_PACKAGE_JSON_LATIN1 = """\
{
"name": "calf\u00e9-n\u00famero",
"version": "0.1.0"
}
"""

NPM_LOCKFILE_JSON_LATIN1 = """\
{
"name": "calf\u00e9-n\u00famero",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "calf\u00e9-n\u00famero",
"version": "0.1.0"
}
}
}
"""


@pytest.mark.parametrize(
("pkg_shrinkwrap_content", "pkg_shrinkwrap_expected"),
Expand Down Expand Up @@ -100,3 +122,37 @@ def test_npm_provider(
assert pkg_lock.read_text() == dedent(pkg_lock_expected)
if pkg_shrinkwrap_content:
assert pkg_shrinkwrap.read_text() == dedent(pkg_shrinkwrap_expected)


def test_npm_provider_respects_configured_encoding_for_all_files(
config: BaseConfig,
chdir: Path,
):
"""NpmProvider should use the configured encoding for all files it touches."""
config.settings["encoding"] = "latin-1"
config.settings["version_provider"] = "npm"

pkg = chdir / NpmProvider.package_filename
pkg_lock = chdir / NpmProvider.lock_filename
pkg_shrinkwrap = chdir / NpmProvider.shrinkwrap_filename

# Write initial contents using latin-1 encoding
pkg.write_text(dedent(NPM_PACKAGE_JSON_LATIN1), encoding="latin-1")
pkg_lock.write_text(dedent(NPM_LOCKFILE_JSON_LATIN1), encoding="latin-1")
pkg_shrinkwrap.write_text(dedent(NPM_LOCKFILE_JSON_LATIN1), encoding="latin-1")

provider = get_provider(config)
assert isinstance(provider, NpmProvider)
assert provider.get_version() == "0.1.0"

provider.set_version("42.1")

# Verify that the files can be read back using the configured encoding
pkg_text = pkg.read_text(encoding="latin-1")
pkg_lock_text = pkg_lock.read_text(encoding="latin-1")
pkg_shrinkwrap_text = pkg_shrinkwrap.read_text(encoding="latin-1")

# Version was updated everywhere
assert '"version": "42.1"' in pkg_text
assert '"version": "42.1"' in pkg_lock_text
assert '"version": "42.1"' in pkg_shrinkwrap_text