From 591fe80131a66e4a39f6f4094233fd9f862ac95e Mon Sep 17 00:00:00 2001 From: Christian Ullrich Date: Mon, 24 Sep 2018 21:17:27 +0200 Subject: [PATCH] Support files with encoding other than UTF-8. --- README.rst | 12 ++++++++++++ bumpversion/__init__.py | 30 ++++++++++++++++++++---------- tests/test_cli.py | 9 +++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 3505557..d9b6d31 100644 --- a/README.rst +++ b/README.rst @@ -159,6 +159,12 @@ General configuration is grouped in a ``[bumpversion]`` section. Also available as ``--message`` (e.g.: ``bumpversion --message '[{now:%Y-%m-%d}] Jenkins Build {$BUILD_NUMBER}: {new_version}' patch``) +``encoding =`` + **default:** ``utf-8`` + + The encoding of the files to modify, unless overridden for a specific + file. This can be any encoding supported by Python's ``codecs`` module. + Part specific configuration --------------------------- @@ -304,6 +310,12 @@ File specific configuration Can be multiple lines, templated using `Python Format String Syntax `_. +``encoding =`` + **default:** ``utf-8`` + + The encoding of the file. This can be any encoding supported by + Python's ``codecs`` module. + Options ======= diff --git a/bumpversion/__init__.py b/bumpversion/__init__.py index 757e4f7..159a2a7 100644 --- a/bumpversion/__init__.py +++ b/bumpversion/__init__.py @@ -191,8 +191,9 @@ def prefixed_environ(): class ConfiguredFile(object): - def __init__(self, path, versionconfig): + def __init__(self, path, encoding, versionconfig): self.path = path + self.encoding = encoding self._versionconfig = versionconfig def should_contain_version(self, version, context): @@ -213,12 +214,12 @@ def should_contain_version(self, version, context): assert False, msg def contains(self, search): - with io.open(self.path, 'rb') as f: + with io.open(self.path, 'rt', encoding=self.encoding) as f: search_lines = search.splitlines() lookbehind = [] for lineno, line in enumerate(f.readlines()): - lookbehind.append(line.decode('utf-8').rstrip("\n")) + lookbehind.append(line.rstrip("\n")) if len(lookbehind) > len(search_lines): lookbehind = lookbehind[1:] @@ -227,14 +228,14 @@ def contains(self, search): search_lines[-1] in lookbehind[-1] and search_lines[1:-1] == lookbehind[1:-1]): logger.info("Found '{}' in {} at line {}: {}".format( - search, self.path, lineno - (len(lookbehind) - 1), line.decode('utf-8').rstrip())) + search, self.path, lineno - (len(lookbehind) - 1), line.rstrip())) return True return False def replace(self, current_version, new_version, context, dry_run): - with io.open(self.path, 'rb') as f: - file_content_before = f.read().decode('utf-8') + with io.open(self.path, 'rt', encoding=self.encoding) as f: + file_content_before = f.read() context['current_version'] = self._versionconfig.serialize(current_version, context) context['new_version'] = self._versionconfig.serialize(new_version, context) @@ -272,8 +273,8 @@ def replace(self, current_version, new_version, context, dry_run): )) if not dry_run: - with io.open(self.path, 'wb') as f: - f.write(file_content_after.encode('utf-8')) + with io.open(self.path, 'wt', encoding=self.encoding) as f: + f.write(file_content_after) def __str__(self): return self.path @@ -483,6 +484,7 @@ def serialize(self, version, context): OPTIONAL_ARGUMENTS_THAT_TAKE_VALUES = [ '--config-file', '--current-version', + '--encoding', '--message', '--new-version', '--parse', @@ -677,7 +679,12 @@ def main(original_args=None): if not 'replace' in section_config: section_config['replace'] = defaults.get("replace", '{new_version}') - files.append(ConfiguredFile(filename, VersionConfig(**section_config))) + if 'encoding' in section_config: + encoding = section_config.pop("encoding") + else: + encoding = defaults.get("encoding", 'utf-8') + + files.append(ConfiguredFile(filename, encoding, VersionConfig(**section_config))) else: message = "Could not read config file at {}".format(config_file) @@ -704,6 +711,9 @@ def main(original_args=None): parser2.add_argument('--replace', metavar='REPLACE', help='Template for complete string to replace', default=defaults.get("replace", '{new_version}')) + parser2.add_argument('--encoding', + help='File encoding', + default=defaults.get("encoding", 'utf-8')) known_args, remaining_argv = parser2.parse_known_args(args) @@ -808,7 +818,7 @@ def main(original_args=None): file_names = file_names or positionals[1:] for file_name in file_names: - files.append(ConfiguredFile(file_name, vc)) + files.append(ConfiguredFile(file_name, defaults.get("encoding", 'utf-8'), vc)) for vcs in VCS: if vcs.is_usable(): diff --git a/tests/test_cli.py b/tests/test_cli.py index 4d03806..18f5bce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -108,6 +108,7 @@ def _mock_calls_to_string(called_mock): {current_version}) --replace REPLACE Template for complete string to replace (default: {new_version}) + --encoding ENCODING File encoding (default: utf-8) --current-version VERSION Version that needs to be updated (default: None) --dry-run, -n Don't write any files, just pretend. (default: False) @@ -215,6 +216,14 @@ def test_simple_replacement_in_utf8_file(tmpdir): assert "'Kr\\xc3\\xb6t1.3.1'" in repr(out) +def test_simple_replacement_in_utf16le_file(tmpdir): + tmpdir.join("VERSION").write("Kröt1.3.0".encode('utf-16le'), 'wb') + tmpdir.chdir() + main(shlex_split("patch --encoding utf-16le --current-version 1.3.0 --new-version 1.3.1 VERSION")) + out = tmpdir.join("VERSION").read('rb') + assert "'K\\x00r\\x00\\xf6\\x00t\\x001\\x00.\\x003\\x00.\\x001\\x00'" in repr(out) + + def test_config_file(tmpdir): tmpdir.join("file1").write("0.9.34") tmpdir.join("mybumpconfig.cfg").write("""[bumpversion]