Skip to content

Commit b31997d

Browse files
authored
Merge pull request #1180 from benoit-pierre/fix_889_and_non-ascii_in_setup.cfg_take_2
improve encoding handling for `setup.cfg`
2 parents 5cd8698 + 249f24a commit b31997d

File tree

7 files changed

+289
-89
lines changed

7 files changed

+289
-89
lines changed

changelog.d/1180.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for non-ASCII in setup.cfg (#1062). Add support for native strings on some parameters (#1136).

setuptools/__init__.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import functools
66
import distutils.core
77
import distutils.filelist
8+
import re
9+
from distutils.errors import DistutilsOptionError
810
from distutils.util import convert_path
911
from fnmatch import fnmatchcase
1012

1113
from ._deprecation_warning import SetuptoolsDeprecationWarning
1214

13-
from setuptools.extern.six import PY3
15+
from setuptools.extern.six import PY3, string_types
1416
from setuptools.extern.six.moves import filter, map
1517

1618
import setuptools.version
@@ -161,6 +163,37 @@ def __init__(self, dist, **kw):
161163
_Command.__init__(self, dist)
162164
vars(self).update(kw)
163165

166+
def _ensure_stringlike(self, option, what, default=None):
167+
val = getattr(self, option)
168+
if val is None:
169+
setattr(self, option, default)
170+
return default
171+
elif not isinstance(val, string_types):
172+
raise DistutilsOptionError("'%s' must be a %s (got `%s`)"
173+
% (option, what, val))
174+
return val
175+
176+
def ensure_string_list(self, option):
177+
r"""Ensure that 'option' is a list of strings. If 'option' is
178+
currently a string, we split it either on /,\s*/ or /\s+/, so
179+
"foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
180+
["foo", "bar", "baz"].
181+
"""
182+
val = getattr(self, option)
183+
if val is None:
184+
return
185+
elif isinstance(val, string_types):
186+
setattr(self, option, re.split(r',\s*|\s+', val))
187+
else:
188+
if isinstance(val, list):
189+
ok = all(isinstance(v, string_types) for v in val)
190+
else:
191+
ok = False
192+
if not ok:
193+
raise DistutilsOptionError(
194+
"'%s' must be a list of strings (got %r)"
195+
% (option, val))
196+
164197
def reinitialize_command(self, command, reinit_subcommands=0, **kw):
165198
cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
166199
vars(cmd).update(kw)

setuptools/dist.py

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# -*- coding: utf-8 -*-
22
__all__ = ['Distribution']
33

4+
import io
5+
import sys
46
import re
57
import os
68
import warnings
@@ -9,9 +11,12 @@
911
import distutils.core
1012
import distutils.cmd
1113
import distutils.dist
14+
from distutils.errors import DistutilsOptionError
15+
from distutils.util import strtobool
16+
from distutils.debug import DEBUG
17+
from distutils.fancy_getopt import translate_longopt
1218
import itertools
1319

14-
1520
from collections import defaultdict
1621
from email import message_from_file
1722

@@ -31,8 +36,8 @@
3136
from setuptools import windows_support
3237
from setuptools.monkey import get_unpatched
3338
from setuptools.config import parse_configuration
39+
from .unicode_utils import detect_encoding
3440
import pkg_resources
35-
from .py36compat import Distribution_parse_config_files
3641

3742
__import__('setuptools.extern.packaging.specifiers')
3843
__import__('setuptools.extern.packaging.version')
@@ -332,7 +337,7 @@ def check_packages(dist, attr, value):
332337
_Distribution = get_unpatched(distutils.core.Distribution)
333338

334339

335-
class Distribution(Distribution_parse_config_files, _Distribution):
340+
class Distribution(_Distribution):
336341
"""Distribution with support for features, tests, and package data
337342
338343
This is an enhanced version of 'distutils.dist.Distribution' that
@@ -556,12 +561,125 @@ def _clean_req(self, req):
556561
req.marker = None
557562
return req
558563

564+
def _parse_config_files(self, filenames=None):
565+
"""
566+
Adapted from distutils.dist.Distribution.parse_config_files,
567+
this method provides the same functionality in subtly-improved
568+
ways.
569+
"""
570+
from setuptools.extern.six.moves.configparser import ConfigParser
571+
572+
# Ignore install directory options if we have a venv
573+
if six.PY3 and sys.prefix != sys.base_prefix:
574+
ignore_options = [
575+
'install-base', 'install-platbase', 'install-lib',
576+
'install-platlib', 'install-purelib', 'install-headers',
577+
'install-scripts', 'install-data', 'prefix', 'exec-prefix',
578+
'home', 'user', 'root']
579+
else:
580+
ignore_options = []
581+
582+
ignore_options = frozenset(ignore_options)
583+
584+
if filenames is None:
585+
filenames = self.find_config_files()
586+
587+
if DEBUG:
588+
self.announce("Distribution.parse_config_files():")
589+
590+
parser = ConfigParser()
591+
for filename in filenames:
592+
with io.open(filename, 'rb') as fp:
593+
encoding = detect_encoding(fp)
594+
if DEBUG:
595+
self.announce(" reading %s [%s]" % (
596+
filename, encoding or 'locale')
597+
)
598+
reader = io.TextIOWrapper(fp, encoding=encoding)
599+
(parser.read_file if six.PY3 else parser.readfp)(reader)
600+
for section in parser.sections():
601+
options = parser.options(section)
602+
opt_dict = self.get_option_dict(section)
603+
604+
for opt in options:
605+
if opt != '__name__' and opt not in ignore_options:
606+
val = parser.get(section, opt)
607+
opt = opt.replace('-', '_')
608+
opt_dict[opt] = (filename, val)
609+
610+
# Make the ConfigParser forget everything (so we retain
611+
# the original filenames that options come from)
612+
parser.__init__()
613+
614+
# If there was a "global" section in the config file, use it
615+
# to set Distribution options.
616+
617+
if 'global' in self.command_options:
618+
for (opt, (src, val)) in self.command_options['global'].items():
619+
alias = self.negative_opt.get(opt)
620+
try:
621+
if alias:
622+
setattr(self, alias, not strtobool(val))
623+
elif opt in ('verbose', 'dry_run'): # ugh!
624+
setattr(self, opt, strtobool(val))
625+
else:
626+
setattr(self, opt, val)
627+
except ValueError as msg:
628+
raise DistutilsOptionError(msg)
629+
630+
def _set_command_options(self, command_obj, option_dict=None):
631+
"""
632+
Set the options for 'command_obj' from 'option_dict'. Basically
633+
this means copying elements of a dictionary ('option_dict') to
634+
attributes of an instance ('command').
635+
636+
'command_obj' must be a Command instance. If 'option_dict' is not
637+
supplied, uses the standard option dictionary for this command
638+
(from 'self.command_options').
639+
640+
(Adopted from distutils.dist.Distribution._set_command_options)
641+
"""
642+
command_name = command_obj.get_command_name()
643+
if option_dict is None:
644+
option_dict = self.get_option_dict(command_name)
645+
646+
if DEBUG:
647+
self.announce(" setting options for '%s' command:" % command_name)
648+
for (option, (source, value)) in option_dict.items():
649+
if DEBUG:
650+
self.announce(" %s = %s (from %s)" % (option, value,
651+
source))
652+
try:
653+
bool_opts = [translate_longopt(o)
654+
for o in command_obj.boolean_options]
655+
except AttributeError:
656+
bool_opts = []
657+
try:
658+
neg_opt = command_obj.negative_opt
659+
except AttributeError:
660+
neg_opt = {}
661+
662+
try:
663+
is_string = isinstance(value, six.string_types)
664+
if option in neg_opt and is_string:
665+
setattr(command_obj, neg_opt[option], not strtobool(value))
666+
elif option in bool_opts and is_string:
667+
setattr(command_obj, option, strtobool(value))
668+
elif hasattr(command_obj, option):
669+
setattr(command_obj, option, value)
670+
else:
671+
raise DistutilsOptionError(
672+
"error in %s: command '%s' has no such option '%s'"
673+
% (source, command_name, option))
674+
except ValueError as msg:
675+
raise DistutilsOptionError(msg)
676+
559677
def parse_config_files(self, filenames=None, ignore_option_errors=False):
560678
"""Parses configuration files from various levels
561679
and loads configuration.
562680
563681
"""
564-
_Distribution.parse_config_files(self, filenames=filenames)
682+
self._parse_config_files(filenames=filenames)
565683

566684
parse_configuration(self, self.command_options,
567685
ignore_option_errors=ignore_option_errors)

setuptools/py36compat.py

Lines changed: 0 additions & 82 deletions
This file was deleted.

0 commit comments

Comments
 (0)