1
from __future__ import absolute_import, unicode_literals
5
from collections import defaultdict
6
from functools import partial
8
from distutils.errors import DistutilsOptionError, DistutilsFileError
9
from setuptools.py26compat import import_module
10
from six import string_types
13
def read_configuration(
14
filepath, find_others=False, ignore_option_errors=False):
15
"""Read given configuration file and returns options from it as a dict.
17
:param str|unicode filepath: Path to configuration file
20
:param bool find_others: Whether to search for other configuration files
21
which could be on in various places.
23
:param bool ignore_option_errors: Whether to silently ignore
24
options, values of which could not be resolved (e.g. due to exceptions
25
in directives such as file:, attr:, etc.).
26
If False exceptions are propagated as expected.
30
from setuptools.dist import Distribution, _Distribution
32
filepath = os.path.abspath(filepath)
34
if not os.path.isfile(filepath):
35
raise DistutilsFileError(
36
'Configuration file %s does not exist.' % filepath)
38
current_directory = os.getcwd()
39
os.chdir(os.path.dirname(filepath))
44
filenames = dist.find_config_files() if find_others else []
45
if filepath not in filenames:
46
filenames.append(filepath)
48
_Distribution.parse_config_files(dist, filenames=filenames)
50
handlers = parse_configuration(
51
dist, dist.command_options,
52
ignore_option_errors=ignore_option_errors)
55
os.chdir(current_directory)
57
return configuration_to_dict(handlers)
60
def configuration_to_dict(handlers):
61
"""Returns configuration data gathered by given handlers as a dict.
63
:param list[ConfigHandler] handlers: Handlers list,
64
usually from parse_configuration()
68
config_dict = defaultdict(dict)
70
for handler in handlers:
72
obj_alias = handler.section_prefix
73
target_obj = handler.target_obj
75
for option in handler.set_options:
76
getter = getattr(target_obj, 'get_%s' % option, None)
79
value = getattr(target_obj, option)
84
config_dict[obj_alias][option] = value
89
def parse_configuration(
90
distribution, command_options, ignore_option_errors=False):
91
"""Performs additional parsing of configuration options
94
Returns a list of used option handlers.
96
:param Distribution distribution:
97
:param dict command_options:
98
:param bool ignore_option_errors: Whether to silently ignore
99
options, values of which could not be resolved (e.g. due to exceptions
100
in directives such as file:, attr:, etc.).
101
If False exceptions are propagated as expected.
104
meta = ConfigMetadataHandler(
105
distribution.metadata, command_options, ignore_option_errors)
108
options = ConfigOptionsHandler(
109
distribution, command_options, ignore_option_errors)
112
return [meta, options]
115
class ConfigHandler(object):
116
"""Handles metadata supplied in configuration files."""
118
section_prefix = None
119
"""Prefix for config sections handled by this handler.
120
Must be provided by class heirs.
126
For compatibility with various packages. E.g.: d2to1 and pbr.
127
Note: `-` in keys is replaced with `_` by config parser.
131
def __init__(self, target_obj, options, ignore_option_errors=False):
134
section_prefix = self.section_prefix
135
for section_name, section_options in options.items():
136
if not section_name.startswith(section_prefix):
139
section_name = section_name.replace(section_prefix, '').strip('.')
140
sections[section_name] = section_options
142
self.ignore_option_errors = ignore_option_errors
143
self.target_obj = target_obj
144
self.sections = sections
145
self.set_options = []
149
"""Metadata item name to parser function mapping."""
150
raise NotImplementedError(
151
'%s must provide .parsers property' % self.__class__.__name__)
153
def __setitem__(self, option_name, value):
155
target_obj = self.target_obj
157
# Translate alias into real name.
158
option_name = self.aliases.get(option_name, option_name)
160
current_value = getattr(target_obj, option_name, unknown)
162
if current_value is unknown:
163
raise KeyError(option_name)
166
# Already inhabited. Skipping.
170
parser = self.parsers.get(option_name)
173
value = parser(value)
177
if not self.ignore_option_errors:
183
setter = getattr(target_obj, 'set_%s' % option_name, None)
185
setattr(target_obj, option_name, value)
189
self.set_options.append(option_name)
192
def _parse_list(cls, value, separator=','):
193
"""Represents value as a list.
195
Value is split either by separator (defaults to comma) or by lines.
198
:param separator: List items separator character.
201
if isinstance(value, list): # _get_parser_compound case
205
value = value.splitlines()
207
value = value.split(separator)
209
return [chunk.strip() for chunk in value if chunk.strip()]
212
def _parse_dict(cls, value):
213
"""Represents value as a dict.
220
for line in cls._parse_list(value):
221
key, sep, val = line.partition(separator)
223
raise DistutilsOptionError(
224
'Unable to parse option value to dict: %s' % value)
225
result[key.strip()] = val.strip()
230
def _parse_bool(cls, value):
231
"""Represents value as boolean.
236
value = value.lower()
237
return value in ('1', 'true', 'yes')
240
def _parse_file(cls, value):
241
"""Represents value as a string, allowing including text
242
from nearest files using `file:` directive.
244
Directive is sandboxed and won't reach anything outside
245
directory with setup.py.
249
include: src/file.txt
254
if not isinstance(value, string_types):
257
include_directive = 'file:'
258
if not value.startswith(include_directive):
261
current_directory = os.getcwd()
263
filepath = value.replace(include_directive, '').strip()
264
filepath = os.path.abspath(filepath)
266
if not filepath.startswith(current_directory):
267
raise DistutilsOptionError(
268
'`file:` directive can not access %s' % filepath)
270
if os.path.isfile(filepath):
271
with io.open(filepath, encoding='utf-8') as f:
277
def _parse_attr(cls, value):
278
"""Represents value as a module attribute.
282
attr: package.module.attr
287
attr_directive = 'attr:'
288
if not value.startswith(attr_directive):
291
attrs_path = value.replace(attr_directive, '').strip().split('.')
292
attr_name = attrs_path.pop()
294
module_name = '.'.join(attrs_path)
295
module_name = module_name or '__init__'
297
sys.path.insert(0, os.getcwd())
299
module = import_module(module_name)
300
value = getattr(module, attr_name)
303
sys.path = sys.path[1:]
308
def _get_parser_compound(cls, *parse_methods):
309
"""Returns parser function to represents value as a list.
311
Parses a value applying given methods one after another.
313
:param parse_methods:
319
for method in parse_methods:
320
parsed = method(parsed)
327
def _parse_section_to_dict(cls, section_options, values_parser=None):
328
"""Parses section options into a dictionary.
330
Optionally applies a given parser to values.
332
:param dict section_options:
333
:param callable values_parser:
337
values_parser = values_parser or (lambda val: val)
338
for key, (_, val) in section_options.items():
339
value[key] = values_parser(val)
342
def parse_section(self, section_options):
343
"""Parses configuration file section.
345
:param dict section_options:
347
for (name, (_, value)) in section_options.items():
352
pass # Keep silent for a new option may appear anytime.
355
"""Parses configuration file items from one
356
or more related sections.
359
for section_name, section_options in self.sections.items():
362
if section_name: # [section.option] variant
363
method_postfix = '_%s' % section_name
365
section_parser_method = getattr(
367
# Dots in section names are tranlsated into dunderscores.
368
('parse_section%s' % method_postfix).replace('.', '__'),
371
if section_parser_method is None:
372
raise DistutilsOptionError(
373
'Unsupported distribution option section: [%s.%s]' % (
374
self.section_prefix, section_name))
376
section_parser_method(section_options)
379
class ConfigMetadataHandler(ConfigHandler):
381
section_prefix = 'metadata'
385
'summary': 'description',
386
'classifier': 'classifiers',
387
'platform': 'platforms',
391
"""We need to keep it loose, to be partially compatible with
392
`pbr` and `d2to1` packages which also uses `metadata` section.
398
"""Metadata item name to parser function mapping."""
399
parse_list = self._parse_list
400
parse_file = self._parse_file
403
'platforms': parse_list,
404
'keywords': parse_list,
405
'provides': parse_list,
406
'requires': parse_list,
407
'obsoletes': parse_list,
408
'classifiers': self._get_parser_compound(parse_file, parse_list),
409
'license': parse_file,
410
'description': parse_file,
411
'long_description': parse_file,
412
'version': self._parse_version,
415
def parse_section_classifiers(self, section_options):
416
"""Parses configuration file section.
418
:param dict section_options:
421
for begin, (_, rest) in section_options.items():
422
classifiers.append('%s :%s' % (begin.title(), rest))
424
self['classifiers'] = classifiers
426
def _parse_version(self, value):
427
"""Parses `version` option value.
433
version = self._parse_attr(value)
435
if callable(version):
438
if not isinstance(version, string_types):
439
if hasattr(version, '__iter__'):
440
version = '.'.join(map(str, version))
442
version = '%s' % version
447
class ConfigOptionsHandler(ConfigHandler):
449
section_prefix = 'options'
453
"""Metadata item name to parser function mapping."""
454
parse_list = self._parse_list
455
parse_list_semicolon = partial(self._parse_list, separator=';')
456
parse_bool = self._parse_bool
457
parse_dict = self._parse_dict
460
'zip_safe': parse_bool,
461
'use_2to3': parse_bool,
462
'include_package_data': parse_bool,
463
'package_dir': parse_dict,
464
'use_2to3_fixers': parse_list,
465
'use_2to3_exclude_fixers': parse_list,
466
'convert_2to3_doctests': parse_list,
467
'scripts': parse_list,
468
'eager_resources': parse_list,
469
'dependency_links': parse_list,
470
'namespace_packages': parse_list,
471
'install_requires': parse_list_semicolon,
472
'setup_requires': parse_list_semicolon,
473
'tests_require': parse_list_semicolon,
474
'packages': self._parse_packages,
475
'entry_points': self._parse_file,
478
def _parse_packages(self, value):
479
"""Parses `packages` option value.
484
find_directive = 'find:'
486
if not value.startswith(find_directive):
487
return self._parse_list(value)
489
# Read function arguments from a dedicated section.
490
find_kwargs = self.parse_section_packages__find(
491
self.sections.get('packages.find', {}))
493
from setuptools import find_packages
495
return find_packages(**find_kwargs)
497
def parse_section_packages__find(self, section_options):
498
"""Parses `packages.find` configuration file section.
500
To be used in conjunction with _parse_packages().
502
:param dict section_options:
504
section_data = self._parse_section_to_dict(
505
section_options, self._parse_list)
507
valid_keys = ['where', 'include', 'exclude']
510
[(k, v) for k, v in section_data.items() if k in valid_keys and v])
512
where = find_kwargs.get('where')
513
if where is not None:
514
find_kwargs['where'] = where[0] # cast list to single val
518
def parse_section_entry_points(self, section_options):
519
"""Parses `entry_points` configuration file section.
521
:param dict section_options:
523
parsed = self._parse_section_to_dict(section_options, self._parse_list)
524
self['entry_points'] = parsed
526
def _parse_package_data(self, section_options):
527
parsed = self._parse_section_to_dict(section_options, self._parse_list)
529
root = parsed.get('*')
536
def parse_section_package_data(self, section_options):
537
"""Parses `package_data` configuration file section.
539
:param dict section_options:
541
self['package_data'] = self._parse_package_data(section_options)
543
def parse_section_exclude_package_data(self, section_options):
544
"""Parses `exclude_package_data` configuration file section.
546
:param dict section_options:
548
self['exclude_package_data'] = self._parse_package_data(
551
def parse_section_extras_require(self, section_options):
552
"""Parses `extras_require` configuration file section.
554
:param dict section_options:
556
parse_list = partial(self._parse_list, separator=';')
557
self['extras_require'] = self._parse_section_to_dict(
558
section_options, parse_list)