Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefer native strings on Python 2 when reading config files. #1660

Merged
merged 3 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1660.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On Python 2, when reading config files, downcast options from text to bytes to satisfy distutils expectations.
22 changes: 21 additions & 1 deletion setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ def _parse_config_files(self, filenames=None):

for opt in options:
if opt != '__name__' and opt not in ignore_options:
val = parser.get(section, opt)
val = self._try_str(parser.get(section, opt))
opt = opt.replace('-', '_')
opt_dict[opt] = (filename, val)

Expand All @@ -627,6 +627,26 @@ def _parse_config_files(self, filenames=None):
except ValueError as msg:
raise DistutilsOptionError(msg)

@staticmethod
def _try_str(val):
"""
On Python 2, much of distutils relies on string values being of
type 'str' (bytes) and not unicode text. If the value can be safely
encoded to bytes using the default encoding, prefer that.

Why the default encoding? Because that value can be implicitly
decoded back to text if needed.

Ref #1653
"""
if six.PY3:
return val
try:
return val.encode()
except UnicodeEncodeError:
pass
return val

def _set_command_options(self, command_obj, option_dict=None):
"""
Set the options for 'command_obj' from 'option_dict'. Basically
Expand Down
2 changes: 2 additions & 0 deletions setuptools/tests/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def run_setup_py(cmd, pypath=None, path=None,
cmd, stdout=_PIPE, stderr=_PIPE, shell=shell, env=env,
)

if isinstance(data_stream, tuple):
data_stream = slice(*data_stream)
data = proc.communicate()[data_stream]
except OSError:
return 1, ''
Expand Down
70 changes: 70 additions & 0 deletions setuptools/tests/test_build_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
from setuptools.dist import Distribution
from setuptools.extension import Extension

from . import environment
from .files import build_files
from .textwrap import DALS


class TestBuildExt:
def test_get_ext_filename(self):
Expand Down Expand Up @@ -43,3 +47,69 @@ def test_abi3_filename(self):
assert res.endswith('eggs.pyd')
else:
assert 'abi3' in res


def test_build_ext_config_handling(tmpdir_cwd):
files = {
'setup.py': DALS(
"""
from setuptools import Extension, setup
setup(
name='foo',
version='0.0.0',
ext_modules=[Extension('foo', ['foo.c'])],
)
"""),
'foo.c': DALS(
"""
#include "Python.h"

#if PY_MAJOR_VERSION >= 3

static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"foo",
NULL,
0,
NULL,
NULL,
NULL,
NULL,
NULL
};

#define INITERROR return NULL

PyMODINIT_FUNC PyInit_foo(void)

#else

#define INITERROR return

void initfoo(void)

#endif
{
#if PY_MAJOR_VERSION >= 3
PyObject *module = PyModule_Create(&moduledef);
#else
PyObject *module = Py_InitModule("extension", NULL);
#endif
if (module == NULL)
INITERROR;
#if PY_MAJOR_VERSION >= 3
return module;
#endif
}
"""),
'setup.cfg': DALS(
"""
[build]
build-base = foo_build
"""),
}
build_files(files)
code, output = environment.run_setup_py(
cmd=['build'], data_stream=(0, 2),
)
assert code == 0, '\nSTDOUT:\n%s\nSTDERR:\n%s' % output