Skip to content

Commit 681e8ae

Browse files
committed
fix(provider): use encoding settings in config
1 parent fbafcaf commit 681e8ae

File tree

8 files changed

+248
-22
lines changed

8 files changed

+248
-22
lines changed

commitizen/providers/base_provider.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class FileProvider(VersionProvider):
4949
def file(self) -> Path:
5050
return Path() / self.filename
5151

52+
def _get_encoding(self) -> str:
53+
return self.config.settings["encoding"]
54+
5255

5356
class JsonProvider(FileProvider):
5457
"""
@@ -58,13 +61,16 @@ class JsonProvider(FileProvider):
5861
indent: ClassVar[int] = 2
5962

6063
def get_version(self) -> str:
61-
document = json.loads(self.file.read_text())
64+
document = json.loads(self.file.read_text(encoding=self._get_encoding()))
6265
return self.get(document)
6366

6467
def set_version(self, version: str) -> None:
65-
document = json.loads(self.file.read_text())
68+
document = json.loads(self.file.read_text(encoding=self._get_encoding()))
6669
self.set(document, version)
67-
self.file.write_text(json.dumps(document, indent=self.indent) + "\n")
70+
self.file.write_text(
71+
json.dumps(document, indent=self.indent) + "\n",
72+
encoding=self._get_encoding(),
73+
)
6874

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

8187
def get_version(self) -> str:
82-
document = tomlkit.parse(self.file.read_text())
88+
document = tomlkit.parse(self.file.read_text(encoding=self._get_encoding()))
8389
return self.get(document)
8490

8591
def set_version(self, version: str) -> None:
86-
document = tomlkit.parse(self.file.read_text())
92+
document = tomlkit.parse(self.file.read_text(encoding=self._get_encoding()))
8793
self.set(document, version)
88-
self.file.write_text(tomlkit.dumps(document))
94+
self.file.write_text(tomlkit.dumps(document), encoding=self._get_encoding())
8995

9096
def get(self, document: tomlkit.TOMLDocument) -> str:
9197
return document["project"]["version"] # type: ignore[index,return-value]

commitizen/providers/cargo_provider.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ def set_version(self, version: str) -> None:
4343
self.set_lock_version(version)
4444

4545
def set_lock_version(self, version: str) -> None:
46-
cargo_toml_content = parse(self.file.read_text())
47-
cargo_lock_content = parse(self.lock_file.read_text())
46+
cargo_toml_content = parse(self.file.read_text(encoding=self._get_encoding()))
47+
cargo_lock_content = parse(
48+
self.lock_file.read_text(encoding=self._get_encoding())
49+
)
4850
packages = cargo_lock_content["package"]
4951

5052
if TYPE_CHECKING:
@@ -75,7 +77,9 @@ def set_lock_version(self, version: str) -> None:
7577
continue
7678

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

95-
self.lock_file.write_text(dumps(cargo_lock_content))
99+
self.lock_file.write_text(
100+
dumps(cargo_lock_content), encoding=self._get_encoding()
101+
)
96102

97103

98104
def _try_get_workspace(document: TOMLDocument) -> dict:

commitizen/providers/npm_provider.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
from pathlib import Path
55
from typing import TYPE_CHECKING, Any, ClassVar
66

7-
from commitizen.providers.base_provider import VersionProvider
7+
from commitizen.providers.base_provider import JsonProvider
88

99
if TYPE_CHECKING:
1010
from collections.abc import Mapping
1111

1212

13-
class NpmProvider(VersionProvider):
13+
class NpmProvider(JsonProvider):
1414
"""
1515
npm package.json and package-lock.json version management
1616
"""
@@ -36,29 +36,39 @@ def get_version(self) -> str:
3636
"""
3737
Get the current version from package.json
3838
"""
39-
package_document = json.loads(self.package_file.read_text())
39+
package_document = json.loads(
40+
self.package_file.read_text(encoding=self._get_encoding())
41+
)
4042
return self.get_package_version(package_document)
4143

4244
def set_version(self, version: str) -> None:
4345
package_document = self.set_package_version(
44-
json.loads(self.package_file.read_text()), version
46+
json.loads(self.package_file.read_text(encoding=self._get_encoding())),
47+
version,
4548
)
4649
self.package_file.write_text(
47-
json.dumps(package_document, indent=self.indent) + "\n"
50+
json.dumps(package_document, indent=self.indent) + "\n",
51+
encoding=self._get_encoding(),
4852
)
4953
if self.lock_file.is_file():
5054
lock_document = self.set_lock_version(
51-
json.loads(self.lock_file.read_text()), version
55+
json.loads(self.lock_file.read_text(encoding=self._get_encoding())),
56+
version,
5257
)
5358
self.lock_file.write_text(
54-
json.dumps(lock_document, indent=self.indent) + "\n"
59+
json.dumps(lock_document, indent=self.indent) + "\n",
60+
encoding=self._get_encoding(),
5561
)
5662
if self.shrinkwrap_file.is_file():
5763
shrinkwrap_document = self.set_shrinkwrap_version(
58-
json.loads(self.shrinkwrap_file.read_text()), version
64+
json.loads(
65+
self.shrinkwrap_file.read_text(encoding=self._get_encoding())
66+
),
67+
version,
5968
)
6069
self.shrinkwrap_file.write_text(
61-
json.dumps(shrinkwrap_document, indent=self.indent) + "\n"
70+
json.dumps(shrinkwrap_document, indent=self.indent) + "\n",
71+
encoding=self._get_encoding(),
6272
)
6373

6474
def get_package_version(self, document: Mapping[str, str]) -> str:

commitizen/providers/uv_provider.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,21 @@ def set_version(self, version: str) -> None:
2626
self.set_lock_version(version)
2727

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

33-
document = tomlkit.parse(self.lock_file.read_text())
35+
document = tomlkit.parse(
36+
self.lock_file.read_text(encoding=self._get_encoding())
37+
)
3438

3539
packages: tomlkit.items.AoT = document["package"] # type: ignore[assignment]
3640
for i, package in enumerate(packages):
3741
if package["name"] == normalized_project_name:
3842
document["package"][i]["version"] = version # type: ignore[index]
3943
break
40-
self.lock_file.write_text(tomlkit.dumps(document))
44+
self.lock_file.write_text(
45+
tomlkit.dumps(document), encoding=self._get_encoding()
46+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "encoding-test-composer",
3+
"description": "Тест описания для проверки кодировки",
4+
"version": "0.1.0"
5+
}
6+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[project]
2+
name = "pythonproject-test"
3+
version = "0.4.1"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = []
8+
9+
10+
[tool.commitizen]
11+
name = "cz_customize"
12+
tag_format = "v$version"
13+
version_scheme = "pep440"
14+
version_provider = "uv"
15+
update_changelog_on_bump = true
16+
changelog_start_rev = "v1.1.0"
17+
18+
[tool.commitizen.customize]
19+
message_template = "{{ change_type }}{% if scope != 'none' %}({{ scope }}){% endif %}: {{ message }}"
20+
commit_parser = '^(?P<change_type>feat|fix|refactor|test|perf|misc):\s(?P<message>.*)'
21+
schema_pattern = '(feat|fix|refactor|test|perf|misc)(\((api|core)\))?:\s(.{3,})'
22+
bump_pattern = "^(feat|fix|refactor|test|perf|misc)"
23+
change_type_map = { "feat" = "Новое", "fix" = "Исправление" }
24+
25+
[[tool.commitizen.customize.questions]]
26+
name = "change_type"
27+
type = "list"
28+
message = "Выберите тип изменений"
29+
choices = [
30+
{ value = "feat", name = "feat: Новая функциональность" },
31+
{ value = "fix", name = "fix: Исправление" },
32+
{ value = "refactor", name = "refactor: Рефакторинг" },
33+
{ value = "test", name = "test: Изменение авто-тестов" },
34+
{ value = "perf", name = "perf: Оптимизации" },
35+
{ value = "misc", name = "misc: Другое" },
36+
]

tests/providers/test_base_provider.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
from commitizen.exceptions import VersionProviderUnknown
88
from commitizen.providers import get_provider
99
from commitizen.providers.commitizen_provider import CommitizenProvider
10+
from commitizen.providers.composer_provider import ComposerProvider
11+
from commitizen.providers.pep621_provider import Pep621Provider
12+
from commitizen.providers.uv_provider import UvProvider
1013

1114
if TYPE_CHECKING:
15+
from pathlib import Path
16+
1217
from commitizen.config.base_config import BaseConfig
1318

1419

@@ -22,3 +27,98 @@ def test_raise_for_unknown_provider(config: BaseConfig):
2227
config.settings["version_provider"] = "unknown"
2328
with pytest.raises(VersionProviderUnknown):
2429
get_provider(config)
30+
31+
32+
@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"])
33+
def test_file_provider_get_encoding(config: BaseConfig, encoding: str):
34+
"""_get_encoding should return the configured encoding."""
35+
config.settings["encoding"] = encoding
36+
provider = ComposerProvider(config)
37+
assert provider._get_encoding() == encoding
38+
39+
40+
def test_json_provider_uses_encoding_with_encoding_fixture(
41+
config: BaseConfig,
42+
chdir: Path,
43+
data_dir: Path,
44+
):
45+
"""JsonProvider should correctly read a JSON file with non-ASCII content."""
46+
source = data_dir / "encoding_test_composer.json"
47+
target = chdir / "composer.json"
48+
target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
49+
50+
config.settings["encoding"] = "utf-8"
51+
config.settings["version_provider"] = "composer"
52+
53+
provider = get_provider(config)
54+
assert isinstance(provider, ComposerProvider)
55+
assert provider.get_version() == "0.1.0"
56+
57+
58+
def test_toml_provider_uses_encoding_with_encoding_fixture(
59+
config: BaseConfig,
60+
chdir: Path,
61+
data_dir: Path,
62+
):
63+
"""TomlProvider should correctly read a TOML file with non-ASCII content."""
64+
source = data_dir / "encoding_test_pyproject.toml"
65+
target = chdir / "pyproject.toml"
66+
target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
67+
68+
config.settings["encoding"] = "utf-8"
69+
config.settings["version_provider"] = "uv"
70+
71+
provider = get_provider(config)
72+
assert isinstance(provider, UvProvider)
73+
assert provider.get_version() == "0.4.1"
74+
75+
76+
def test_json_provider_handles_various_unicode_characters(
77+
config: BaseConfig,
78+
chdir: Path,
79+
):
80+
"""JsonProvider should handle a wide range of Unicode characters."""
81+
config.settings["encoding"] = "utf-8"
82+
config.settings["version_provider"] = "composer"
83+
84+
filename = ComposerProvider.filename
85+
file = chdir / filename
86+
file.write_text(
87+
(
88+
"{\n"
89+
' "name": "多言語-имя-árbol",\n'
90+
' "description": "Emoji 😀 – 漢字 – العربية",\n'
91+
' "version": "0.1.0"\n'
92+
"}\n"
93+
),
94+
encoding="utf-8",
95+
)
96+
97+
provider = get_provider(config)
98+
assert isinstance(provider, ComposerProvider)
99+
assert provider.get_version() == "0.1.0"
100+
101+
102+
def test_toml_provider_handles_various_unicode_characters(
103+
config: BaseConfig,
104+
chdir: Path,
105+
):
106+
"""TomlProvider should handle a wide range of Unicode characters."""
107+
config.settings["encoding"] = "utf-8"
108+
config.settings["version_provider"] = "pep621"
109+
110+
filename = Pep621Provider.filename
111+
file = chdir / filename
112+
file.write_text(
113+
(
114+
"[project]\n"
115+
'name = "多言語-имя-árbol"\n'
116+
'description = "Emoji 😀 – 漢字 – العربية"\n'
117+
'version = "0.1.0"\n'
118+
),
119+
encoding="utf-8",
120+
)
121+
122+
provider = get_provider(config)
123+
assert isinstance(provider, Pep621Provider)
124+
assert provider.get_version() == "0.1.0"

tests/providers/test_npm_provider.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@
6363
}
6464
"""
6565

66+
NPM_PACKAGE_JSON_LATIN1 = """\
67+
{
68+
"name": "calf\u00e9-n\u00famero",
69+
"version": "0.1.0"
70+
}
71+
"""
72+
73+
NPM_LOCKFILE_JSON_LATIN1 = """\
74+
{
75+
"name": "calf\u00e9-n\u00famero",
76+
"version": "0.1.0",
77+
"lockfileVersion": 2,
78+
"requires": true,
79+
"packages": {
80+
"": {
81+
"name": "calf\u00e9-n\u00famero",
82+
"version": "0.1.0"
83+
}
84+
}
85+
}
86+
"""
87+
6688

6789
@pytest.mark.parametrize(
6890
("pkg_shrinkwrap_content", "pkg_shrinkwrap_expected"),
@@ -100,3 +122,37 @@ def test_npm_provider(
100122
assert pkg_lock.read_text() == dedent(pkg_lock_expected)
101123
if pkg_shrinkwrap_content:
102124
assert pkg_shrinkwrap.read_text() == dedent(pkg_shrinkwrap_expected)
125+
126+
127+
def test_npm_provider_respects_configured_encoding_for_all_files(
128+
config: BaseConfig,
129+
chdir: Path,
130+
):
131+
"""NpmProvider should use the configured encoding for all files it touches."""
132+
config.settings["encoding"] = "latin-1"
133+
config.settings["version_provider"] = "npm"
134+
135+
pkg = chdir / NpmProvider.package_filename
136+
pkg_lock = chdir / NpmProvider.lock_filename
137+
pkg_shrinkwrap = chdir / NpmProvider.shrinkwrap_filename
138+
139+
# Write initial contents using latin-1 encoding
140+
pkg.write_text(dedent(NPM_PACKAGE_JSON_LATIN1), encoding="latin-1")
141+
pkg_lock.write_text(dedent(NPM_LOCKFILE_JSON_LATIN1), encoding="latin-1")
142+
pkg_shrinkwrap.write_text(dedent(NPM_LOCKFILE_JSON_LATIN1), encoding="latin-1")
143+
144+
provider = get_provider(config)
145+
assert isinstance(provider, NpmProvider)
146+
assert provider.get_version() == "0.1.0"
147+
148+
provider.set_version("42.1")
149+
150+
# Verify that the files can be read back using the configured encoding
151+
pkg_text = pkg.read_text(encoding="latin-1")
152+
pkg_lock_text = pkg_lock.read_text(encoding="latin-1")
153+
pkg_shrinkwrap_text = pkg_shrinkwrap.read_text(encoding="latin-1")
154+
155+
# Version was updated everywhere
156+
assert '"version": "42.1"' in pkg_text
157+
assert '"version": "42.1"' in pkg_lock_text
158+
assert '"version": "42.1"' in pkg_shrinkwrap_text

0 commit comments

Comments
 (0)