Add config option to set default character encoding

This commit adds a new config option 'encoding' which when set will take
a codec string from the stdlib codec library and use it as the encoding
kwarg for all open() calls that are part of the reno commands. This will
let a project assert a specific encoding for all release note files,
which can be important if the project is using special characters and
have contributors using multiple platforms or locales.

Change-Id: If90422fa95cb4eabc4c1722104cef3b28b7fca2d
Story: #2007757
Task: #39959
This commit is contained in:
Matthew Treinish 2020-06-04 11:23:53 -04:00
parent 1d503fa8ca
commit 984bcba17e
No known key found for this signature in database
GPG Key ID: FD12A0F214C9E177
8 changed files with 35 additions and 16 deletions

View File

@ -210,6 +210,7 @@ the most convenient way to manage the values consistently.
template: |
<template-used-to-create-new-notes>
...
encoding: utf8
Many of the settings in the configuration file can be overridden by
using command-line switches. For example:

View File

@ -79,17 +79,18 @@ def write_cache_db(conf, versions_to_include,
Return the name of the file created, if any.
"""
encoding = conf.options['encoding']
if outfilename == '-':
stream = sys.stdout
close_stream = False
elif outfilename:
stream = open(outfilename, 'w')
stream = open(outfilename, 'w', encoding=encoding)
close_stream = True
else:
outfilename = loader.get_cache_filename(conf)
if not os.path.exists(os.path.dirname(outfilename)):
os.makedirs(os.path.dirname(outfilename))
stream = open(outfilename, 'w')
stream = open(outfilename, 'w', encoding=encoding)
close_stream = True
try:
cache = build_cache_db(

View File

@ -180,6 +180,14 @@ _OPTIONS = [
released version. If this option is unset, the development
version number is used (for example, ``3.0.0-3``).
""")),
Opt('encoding', None,
textwrap.dedent("""\
The character encoding to use when opening note files. If not
specified it will be dependent on the system running reno (whatever
'locale.getpreferredencoding()' returns. This takes in a string
name that will be passed to the encoding kwarg for open(), so any
codec or alias from stdlib's codec module is valid.
""")),
]

View File

@ -30,11 +30,11 @@ def _pick_note_file_name(notesdir, slug):
)
def _make_note_file(filename, template):
def _make_note_file(filename, template, encoding=None):
notesdir = os.path.dirname(filename)
if not os.path.exists(notesdir):
os.makedirs(notesdir)
with open(filename, 'w') as f:
with open(filename, 'w', encoding=encoding) as f:
f.write(template)
@ -45,13 +45,13 @@ def _edit_file(filename):
return True
def _get_user_template(template_file):
def _get_user_template(template_file, encoding=None):
if not os.path.exists(template_file):
raise ValueError(
'The provided template file %s doesn\'t '
'exist' % template_file,
)
with open(template_file, 'r') as f:
with open(template_file, 'r', encoding=encoding) as f:
return f.read()
@ -65,11 +65,12 @@ def create_cmd(args, conf):
# concern.
slug = args.slug.replace(' ', '-')
filename = _pick_note_file_name(conf.notespath, slug)
encoding = conf.options['encoding']
if args.from_template:
template = _get_user_template(args.from_template)
template = _get_user_template(args.from_template, encoding=encoding)
else:
template = conf.template
_make_note_file(filename, template)
_make_note_file(filename, template, encoding=encoding)
if args.edit and not _edit_file(filename):
print('Was unable to edit the new note. EDITOR environment variable '
'is missing!')

View File

@ -58,6 +58,7 @@ class Loader(object):
self._scanner_output = None
self._tags_to_dates = None
self._cache_filename = get_cache_filename(conf)
self._encoding = conf.options['encoding']
self._load_data()
@ -69,7 +70,7 @@ class Loader(object):
if (not self._ignore_cache) and cache_file_exists:
LOG.debug('loading cache file %s', self._cache_filename)
with open(self._cache_filename, 'r') as f:
with open(self._cache_filename, 'r', encoding=self._encoding) as f:
self._cache = yaml.safe_load(f.read())
# Save the cached scanner output to the same attribute
# it would be in if we had loaded it "live". This

View File

@ -17,6 +17,7 @@ from reno import loader
def report_cmd(args, conf):
"Generates a release notes report"
ldr = loader.Loader(conf)
encoding = conf.options['encoding']
if args.version:
versions = args.version
else:
@ -30,7 +31,7 @@ def report_cmd(args, conf):
branch=args.branch,
)
if args.output:
with open(args.output, 'w') as f:
with open(args.output, 'w', encoding=encoding) as f:
f.write(text)
else:
print(text)

View File

@ -460,7 +460,7 @@ class RenoRepo(repo.Repo):
tree = self[tree_sha]
return tree
def get_file_at_commit(self, filename, sha):
def get_file_at_commit(self, filename, sha, encoding=None):
"""Return the contents of the file.
If sha is None, return the working copy of the file. If the
@ -474,7 +474,8 @@ class RenoRepo(repo.Repo):
if sha is None:
# Get the copy from the working directory.
try:
with open(os.path.join(self.path, filename), 'r') as f:
with open(os.path.join(self.path, filename), 'r',
encoding=encoding) as f:
return f.read()
except IOError:
return None
@ -530,6 +531,7 @@ class Scanner(object):
_get_unique_id(fn)
for fn in self.conf.ignore_notes
)
self._encoding = conf.options['encoding']
def _get_ref(self, name):
if name:
@ -812,11 +814,13 @@ class Scanner(object):
def get_file_at_commit(self, filename, sha):
"Return the contents of the file if it exists at the commit, or None."
return self._repo.get_file_at_commit(filename, sha)
return self._repo.get_file_at_commit(filename, sha,
encoding=self._encoding)
def _file_exists_at_commit(self, filename, sha):
"Return true if the file exists at the given commit."
return bool(self.get_file_at_commit(filename, sha))
return bool(self.get_file_at_commit(filename, sha,
encoding=self._encoding))
def get_series_branches(self):
"Get branches matching the branch_name_re config option."

View File

@ -17,6 +17,7 @@ from unittest import mock
import fixtures
import io
from reno import config
from reno import create
from reno.tests import base
@ -69,8 +70,9 @@ class TestCreate(base.TestCase):
args.from_template = self._create_user_template('i-am-a-user-template')
args.slug = 'theslug'
args.edit = False
conf = mock.Mock()
conf = mock.create_autospec(config.Config)
conf.notespath = self.tmpdir
conf.options = {'encoding': None}
with mock.patch('sys.stdout', new=io.StringIO()) as fake_out:
create.create_cmd(args, conf)
filename = self._get_file_path_from_output(fake_out.getvalue())
@ -83,7 +85,7 @@ class TestCreate(base.TestCase):
args.from_template = 'some-unexistent-file.yaml'
args.slug = 'theslug'
args.edit = False
conf = mock.Mock()
conf = mock.create_autospec(config.Config)
conf.notespath = self.tmpdir
self.assertRaises(ValueError, create.create_cmd, args, conf)