Commit 2c897b5b authored by Benoit Pierre's avatar Benoit Pierre

improve encoding handling for `setup.cfg`

Support the same mechanism as for Python sources for declaring
the encoding to be used when reading `setup.cfg` (see PEP 263),
and return the results of reading it as Unicode.

Fix #1062 and #1136.
parent 3686dedb
......@@ -4,9 +4,12 @@ import os
import functools
import distutils.core
import distutils.filelist
import re
from distutils.errors import DistutilsOptionError
from distutils.util import convert_path
from fnmatch import fnmatchcase
from setuptools.extern.six import string_types
from setuptools.extern.six.moves import filter, map
import setuptools.version
......@@ -127,6 +130,37 @@ class Command(_Command):
_Command.__init__(self, dist)
vars(self).update(kw)
def _ensure_stringlike(self, option, what, default=None):
val = getattr(self, option)
if val is None:
setattr(self, option, default)
return default
elif not isinstance(val, string_types):
raise DistutilsOptionError("'%s' must be a %s (got `%s`)"
% (option, what, val))
return val
def ensure_string_list(self, option):
r"""Ensure that 'option' is a list of strings. If 'option' is
currently a string, we split it either on /,\s*/ or /\s+/, so
"foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
["foo", "bar", "baz"].
"""
val = getattr(self, option)
if val is None:
return
elif isinstance(val, string_types):
setattr(self, option, re.split(r',\s*|\s+', val))
else:
if isinstance(val, list):
ok = all(isinstance(v, string_types) for v in val)
else:
ok = False
if not ok:
raise DistutilsOptionError(
"'%s' must be a list of strings (got %r)"
% (option, val))
def reinitialize_command(self, command, reinit_subcommands=0, **kw):
cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
vars(cmd).update(kw)
......
......@@ -432,7 +432,7 @@ class Distribution(Distribution_parse_config_files, _Distribution):
and loads configuration.
"""
_Distribution.parse_config_files(self, filenames=filenames)
Distribution_parse_config_files.parse_config_files(self, filenames=filenames)
parse_configuration(self, self.command_options)
self._finalize_requires()
......
import io
import re
import sys
from distutils.errors import DistutilsOptionError
from distutils.util import strtobool
from distutils.debug import DEBUG
from setuptools.extern import six
CODING_RE = re.compile(br'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)')
def detect_encoding(fp):
first_line = fp.readline()
fp.seek(0)
m = CODING_RE.match(first_line)
if m is None:
return None
return m.group(1).decode('ascii')
class Distribution_parse_config_files:
......@@ -13,10 +27,10 @@ class Distribution_parse_config_files:
as implemented in distutils.
"""
def parse_config_files(self, filenames=None):
from configparser import ConfigParser
from setuptools.extern.six.moves.configparser import ConfigParser
# Ignore install directory options if we have a venv
if sys.prefix != sys.base_prefix:
if six.PY3 and sys.prefix != sys.base_prefix:
ignore_options = [
'install-base', 'install-platbase', 'install-lib',
'install-platlib', 'install-purelib', 'install-headers',
......@@ -33,11 +47,16 @@ class Distribution_parse_config_files:
if DEBUG:
self.announce("Distribution.parse_config_files():")
parser = ConfigParser(interpolation=None)
parser = ConfigParser()
for filename in filenames:
if DEBUG:
self.announce(" reading %s" % filename)
parser.read(filename)
with io.open(filename, 'rb') as fp:
encoding = detect_encoding(fp)
if DEBUG:
self.announce(" reading %s [%s]" % (
filename, encoding or 'locale')
)
reader = io.TextIOWrapper(fp, encoding=encoding)
(parser.read_file if six.PY3 else parser.readfp)(reader)
for section in parser.sections():
options = parser.options(section)
opt_dict = self.get_option_dict(section)
......@@ -69,12 +88,6 @@ class Distribution_parse_config_files:
raise DistutilsOptionError(msg)
if sys.version_info < (3,):
# Python 2 behavior is sufficient
class Distribution_parse_config_files:
pass
if False:
# When updated behavior is available upstream,
# disable override here.
......
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import contextlib
import pytest
from distutils.errors import DistutilsOptionError, DistutilsFileError
from setuptools.dist import Distribution
from setuptools.config import ConfigHandler, read_configuration
from setuptools.extern.six.moves.configparser import InterpolationMissingOptionError
from setuptools.tests import is_ascii
class ErrConfigHandler(ConfigHandler):
......@@ -17,7 +21,7 @@ def make_package_dir(name, base_dir):
return dir_package, init_file
def fake_env(tmpdir, setup_cfg, setup_py=None):
def fake_env(tmpdir, setup_cfg, setup_py=None, encoding='ascii'):
if setup_py is None:
setup_py = (
......@@ -27,7 +31,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None):
tmpdir.join('setup.py').write(setup_py)
config = tmpdir.join('setup.cfg')
config.write(setup_cfg)
config.write(setup_cfg.encode(encoding), mode='wb')
package_dir, init_file = make_package_dir('fake_package', tmpdir)
......@@ -317,6 +321,63 @@ class TestMetadata:
with get_dist(tmpdir):
pass
skip_if_not_ascii = pytest.mark.skipif(not is_ascii, reason='Test not supported with this locale')
@skip_if_not_ascii
def test_non_ascii_1(self, tmpdir):
fake_env(
tmpdir,
'[metadata]\n'
'description = éàïôñ\n',
encoding='utf-8'
)
with pytest.raises(UnicodeDecodeError):
with get_dist(tmpdir):
pass
def test_non_ascii_2(self, tmpdir):
fake_env(
tmpdir,
'# -*- coding: invalid\n'
)
with pytest.raises(LookupError):
with get_dist(tmpdir):
pass
def test_non_ascii_3(self, tmpdir):
fake_env(
tmpdir,
'\n'
'# -*- coding: invalid\n'
)
with get_dist(tmpdir):
pass
@skip_if_not_ascii
def test_non_ascii_4(self, tmpdir):
fake_env(
tmpdir,
'# -*- coding: utf-8\n'
'[metadata]\n'
'description = éàïôñ\n',
encoding='utf-8'
)
with get_dist(tmpdir) as dist:
assert dist.metadata.description == 'éàïôñ'
@skip_if_not_ascii
def test_non_ascii_5(self, tmpdir):
fake_env(
tmpdir,
'# vim: set fileencoding=iso-8859-15 :\n'
'[metadata]\n'
'description = éàïôñ\n',
encoding='iso-8859-15'
)
with get_dist(tmpdir) as dist:
assert dist.metadata.description == 'éàïôñ'
class TestOptions:
def test_basic(self, tmpdir):
......
......@@ -497,3 +497,49 @@ class TestEggInfo(object):
# expect exactly one result
result, = results
return result
EGG_INFO_TESTS = (
# Check for issue #1136: invalid string type when
# reading declarative `setup.cfg` under Python 2.
{
'setup.py': DALS(
"""
from setuptools import setup
setup(
name="foo",
)
"""),
'setup.cfg': DALS(
"""
[options]
package_dir =
= src
"""),
'src': {},
},
# Check Unicode can be used in `setup.py` under Python 2.
{
'setup.py': DALS(
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from setuptools import setup, find_packages
setup(
name="foo",
package_dir={'': 'src'},
)
"""),
'src': {},
}
)
@pytest.mark.parametrize('package_files', EGG_INFO_TESTS)
def test_egg_info(self, tmpdir_cwd, env, package_files):
"""
"""
build_files(package_files)
code, data = environment.run_setup_py(
cmd=['egg_info'],
data_stream=1,
)
assert not code, data
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment