From 681e8aeb1d4b4ac048d7a2b0230b9b722490731b Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Mon, 9 Feb 2026 15:41:46 +0800 Subject: [PATCH] fix(provider): use encoding settings in config --- commitizen/providers/base_provider.py | 18 +++-- commitizen/providers/cargo_provider.py | 14 +++- commitizen/providers/npm_provider.py | 28 ++++--- commitizen/providers/uv_provider.py | 12 ++- tests/data/encoding_test_composer.json | 6 ++ tests/data/encoding_test_pyproject.toml | 36 +++++++++ tests/providers/test_base_provider.py | 100 ++++++++++++++++++++++++ tests/providers/test_npm_provider.py | 56 +++++++++++++ 8 files changed, 248 insertions(+), 22 deletions(-) create mode 100644 tests/data/encoding_test_composer.json create mode 100644 tests/data/encoding_test_pyproject.toml diff --git a/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py index 84b745e326..b77d86394c 100644 --- a/commitizen/providers/base_provider.py +++ b/commitizen/providers/base_provider.py @@ -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): """ @@ -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"] @@ -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] diff --git a/commitizen/providers/cargo_provider.py b/commitizen/providers/cargo_provider.py index e7b127c4bd..235d4a110c 100644 --- a/commitizen/providers/cargo_provider.py +++ b/commitizen/providers/cargo_provider.py @@ -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: @@ -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: @@ -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: diff --git a/commitizen/providers/npm_provider.py b/commitizen/providers/npm_provider.py index 597fce4b6e..6794f6a714 100644 --- a/commitizen/providers/npm_provider.py +++ b/commitizen/providers/npm_provider.py @@ -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 """ @@ -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: diff --git a/commitizen/providers/uv_provider.py b/commitizen/providers/uv_provider.py index 4f49a29528..0eb19e51a6 100644 --- a/commitizen/providers/uv_provider.py +++ b/commitizen/providers/uv_provider.py @@ -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() + ) diff --git a/tests/data/encoding_test_composer.json b/tests/data/encoding_test_composer.json new file mode 100644 index 0000000000..2cbf2e70cc --- /dev/null +++ b/tests/data/encoding_test_composer.json @@ -0,0 +1,6 @@ +{ + "name": "encoding-test-composer", + "description": "Тест описания для проверки кодировки", + "version": "0.1.0" +} + diff --git a/tests/data/encoding_test_pyproject.toml b/tests/data/encoding_test_pyproject.toml new file mode 100644 index 0000000000..6e47e88ecd --- /dev/null +++ b/tests/data/encoding_test_pyproject.toml @@ -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 = '^(?Pfeat|fix|refactor|test|perf|misc):\s(?P.*)' +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: Другое" }, +] \ No newline at end of file diff --git a/tests/providers/test_base_provider.py b/tests/providers/test_base_provider.py index 782a8ba89e..4129fa8c22 100644 --- a/tests/providers/test_base_provider.py +++ b/tests/providers/test_base_provider.py @@ -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 @@ -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" diff --git a/tests/providers/test_npm_provider.py b/tests/providers/test_npm_provider.py index 4de171a927..429b46fac9 100644 --- a/tests/providers/test_npm_provider.py +++ b/tests/providers/test_npm_provider.py @@ -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"), @@ -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