comparison env/lib/python3.7/site-packages/setuptools/config.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:26e78fe6e8c4
1 from __future__ import absolute_import, unicode_literals
2 import io
3 import os
4 import sys
5
6 import warnings
7 import functools
8 from collections import defaultdict
9 from functools import partial
10 from functools import wraps
11 from importlib import import_module
12
13 from distutils.errors import DistutilsOptionError, DistutilsFileError
14 from setuptools.extern.packaging.version import LegacyVersion, parse
15 from setuptools.extern.six import string_types, PY3
16
17
18 __metaclass__ = type
19
20
21 def read_configuration(
22 filepath, find_others=False, ignore_option_errors=False):
23 """Read given configuration file and returns options from it as a dict.
24
25 :param str|unicode filepath: Path to configuration file
26 to get options from.
27
28 :param bool find_others: Whether to search for other configuration files
29 which could be on in various places.
30
31 :param bool ignore_option_errors: Whether to silently ignore
32 options, values of which could not be resolved (e.g. due to exceptions
33 in directives such as file:, attr:, etc.).
34 If False exceptions are propagated as expected.
35
36 :rtype: dict
37 """
38 from setuptools.dist import Distribution, _Distribution
39
40 filepath = os.path.abspath(filepath)
41
42 if not os.path.isfile(filepath):
43 raise DistutilsFileError(
44 'Configuration file %s does not exist.' % filepath)
45
46 current_directory = os.getcwd()
47 os.chdir(os.path.dirname(filepath))
48
49 try:
50 dist = Distribution()
51
52 filenames = dist.find_config_files() if find_others else []
53 if filepath not in filenames:
54 filenames.append(filepath)
55
56 _Distribution.parse_config_files(dist, filenames=filenames)
57
58 handlers = parse_configuration(
59 dist, dist.command_options,
60 ignore_option_errors=ignore_option_errors)
61
62 finally:
63 os.chdir(current_directory)
64
65 return configuration_to_dict(handlers)
66
67
68 def _get_option(target_obj, key):
69 """
70 Given a target object and option key, get that option from
71 the target object, either through a get_{key} method or
72 from an attribute directly.
73 """
74 getter_name = 'get_{key}'.format(**locals())
75 by_attribute = functools.partial(getattr, target_obj, key)
76 getter = getattr(target_obj, getter_name, by_attribute)
77 return getter()
78
79
80 def configuration_to_dict(handlers):
81 """Returns configuration data gathered by given handlers as a dict.
82
83 :param list[ConfigHandler] handlers: Handlers list,
84 usually from parse_configuration()
85
86 :rtype: dict
87 """
88 config_dict = defaultdict(dict)
89
90 for handler in handlers:
91 for option in handler.set_options:
92 value = _get_option(handler.target_obj, option)
93 config_dict[handler.section_prefix][option] = value
94
95 return config_dict
96
97
98 def parse_configuration(
99 distribution, command_options, ignore_option_errors=False):
100 """Performs additional parsing of configuration options
101 for a distribution.
102
103 Returns a list of used option handlers.
104
105 :param Distribution distribution:
106 :param dict command_options:
107 :param bool ignore_option_errors: Whether to silently ignore
108 options, values of which could not be resolved (e.g. due to exceptions
109 in directives such as file:, attr:, etc.).
110 If False exceptions are propagated as expected.
111 :rtype: list
112 """
113 options = ConfigOptionsHandler(
114 distribution, command_options, ignore_option_errors)
115 options.parse()
116
117 meta = ConfigMetadataHandler(
118 distribution.metadata, command_options, ignore_option_errors,
119 distribution.package_dir)
120 meta.parse()
121
122 return meta, options
123
124
125 class ConfigHandler:
126 """Handles metadata supplied in configuration files."""
127
128 section_prefix = None
129 """Prefix for config sections handled by this handler.
130 Must be provided by class heirs.
131
132 """
133
134 aliases = {}
135 """Options aliases.
136 For compatibility with various packages. E.g.: d2to1 and pbr.
137 Note: `-` in keys is replaced with `_` by config parser.
138
139 """
140
141 def __init__(self, target_obj, options, ignore_option_errors=False):
142 sections = {}
143
144 section_prefix = self.section_prefix
145 for section_name, section_options in options.items():
146 if not section_name.startswith(section_prefix):
147 continue
148
149 section_name = section_name.replace(section_prefix, '').strip('.')
150 sections[section_name] = section_options
151
152 self.ignore_option_errors = ignore_option_errors
153 self.target_obj = target_obj
154 self.sections = sections
155 self.set_options = []
156
157 @property
158 def parsers(self):
159 """Metadata item name to parser function mapping."""
160 raise NotImplementedError(
161 '%s must provide .parsers property' % self.__class__.__name__)
162
163 def __setitem__(self, option_name, value):
164 unknown = tuple()
165 target_obj = self.target_obj
166
167 # Translate alias into real name.
168 option_name = self.aliases.get(option_name, option_name)
169
170 current_value = getattr(target_obj, option_name, unknown)
171
172 if current_value is unknown:
173 raise KeyError(option_name)
174
175 if current_value:
176 # Already inhabited. Skipping.
177 return
178
179 skip_option = False
180 parser = self.parsers.get(option_name)
181 if parser:
182 try:
183 value = parser(value)
184
185 except Exception:
186 skip_option = True
187 if not self.ignore_option_errors:
188 raise
189
190 if skip_option:
191 return
192
193 setter = getattr(target_obj, 'set_%s' % option_name, None)
194 if setter is None:
195 setattr(target_obj, option_name, value)
196 else:
197 setter(value)
198
199 self.set_options.append(option_name)
200
201 @classmethod
202 def _parse_list(cls, value, separator=','):
203 """Represents value as a list.
204
205 Value is split either by separator (defaults to comma) or by lines.
206
207 :param value:
208 :param separator: List items separator character.
209 :rtype: list
210 """
211 if isinstance(value, list): # _get_parser_compound case
212 return value
213
214 if '\n' in value:
215 value = value.splitlines()
216 else:
217 value = value.split(separator)
218
219 return [chunk.strip() for chunk in value if chunk.strip()]
220
221 @classmethod
222 def _parse_dict(cls, value):
223 """Represents value as a dict.
224
225 :param value:
226 :rtype: dict
227 """
228 separator = '='
229 result = {}
230 for line in cls._parse_list(value):
231 key, sep, val = line.partition(separator)
232 if sep != separator:
233 raise DistutilsOptionError(
234 'Unable to parse option value to dict: %s' % value)
235 result[key.strip()] = val.strip()
236
237 return result
238
239 @classmethod
240 def _parse_bool(cls, value):
241 """Represents value as boolean.
242
243 :param value:
244 :rtype: bool
245 """
246 value = value.lower()
247 return value in ('1', 'true', 'yes')
248
249 @classmethod
250 def _exclude_files_parser(cls, key):
251 """Returns a parser function to make sure field inputs
252 are not files.
253
254 Parses a value after getting the key so error messages are
255 more informative.
256
257 :param key:
258 :rtype: callable
259 """
260 def parser(value):
261 exclude_directive = 'file:'
262 if value.startswith(exclude_directive):
263 raise ValueError(
264 'Only strings are accepted for the {0} field, '
265 'files are not accepted'.format(key))
266 return value
267 return parser
268
269 @classmethod
270 def _parse_file(cls, value):
271 """Represents value as a string, allowing including text
272 from nearest files using `file:` directive.
273
274 Directive is sandboxed and won't reach anything outside
275 directory with setup.py.
276
277 Examples:
278 file: README.rst, CHANGELOG.md, src/file.txt
279
280 :param str value:
281 :rtype: str
282 """
283 include_directive = 'file:'
284
285 if not isinstance(value, string_types):
286 return value
287
288 if not value.startswith(include_directive):
289 return value
290
291 spec = value[len(include_directive):]
292 filepaths = (os.path.abspath(path.strip()) for path in spec.split(','))
293 return '\n'.join(
294 cls._read_file(path)
295 for path in filepaths
296 if (cls._assert_local(path) or True)
297 and os.path.isfile(path)
298 )
299
300 @staticmethod
301 def _assert_local(filepath):
302 if not filepath.startswith(os.getcwd()):
303 raise DistutilsOptionError(
304 '`file:` directive can not access %s' % filepath)
305
306 @staticmethod
307 def _read_file(filepath):
308 with io.open(filepath, encoding='utf-8') as f:
309 return f.read()
310
311 @classmethod
312 def _parse_attr(cls, value, package_dir=None):
313 """Represents value as a module attribute.
314
315 Examples:
316 attr: package.attr
317 attr: package.module.attr
318
319 :param str value:
320 :rtype: str
321 """
322 attr_directive = 'attr:'
323 if not value.startswith(attr_directive):
324 return value
325
326 attrs_path = value.replace(attr_directive, '').strip().split('.')
327 attr_name = attrs_path.pop()
328
329 module_name = '.'.join(attrs_path)
330 module_name = module_name or '__init__'
331
332 parent_path = os.getcwd()
333 if package_dir:
334 if attrs_path[0] in package_dir:
335 # A custom path was specified for the module we want to import
336 custom_path = package_dir[attrs_path[0]]
337 parts = custom_path.rsplit('/', 1)
338 if len(parts) > 1:
339 parent_path = os.path.join(os.getcwd(), parts[0])
340 module_name = parts[1]
341 else:
342 module_name = custom_path
343 elif '' in package_dir:
344 # A custom parent directory was specified for all root modules
345 parent_path = os.path.join(os.getcwd(), package_dir[''])
346 sys.path.insert(0, parent_path)
347 try:
348 module = import_module(module_name)
349 value = getattr(module, attr_name)
350
351 finally:
352 sys.path = sys.path[1:]
353
354 return value
355
356 @classmethod
357 def _get_parser_compound(cls, *parse_methods):
358 """Returns parser function to represents value as a list.
359
360 Parses a value applying given methods one after another.
361
362 :param parse_methods:
363 :rtype: callable
364 """
365 def parse(value):
366 parsed = value
367
368 for method in parse_methods:
369 parsed = method(parsed)
370
371 return parsed
372
373 return parse
374
375 @classmethod
376 def _parse_section_to_dict(cls, section_options, values_parser=None):
377 """Parses section options into a dictionary.
378
379 Optionally applies a given parser to values.
380
381 :param dict section_options:
382 :param callable values_parser:
383 :rtype: dict
384 """
385 value = {}
386 values_parser = values_parser or (lambda val: val)
387 for key, (_, val) in section_options.items():
388 value[key] = values_parser(val)
389 return value
390
391 def parse_section(self, section_options):
392 """Parses configuration file section.
393
394 :param dict section_options:
395 """
396 for (name, (_, value)) in section_options.items():
397 try:
398 self[name] = value
399
400 except KeyError:
401 pass # Keep silent for a new option may appear anytime.
402
403 def parse(self):
404 """Parses configuration file items from one
405 or more related sections.
406
407 """
408 for section_name, section_options in self.sections.items():
409
410 method_postfix = ''
411 if section_name: # [section.option] variant
412 method_postfix = '_%s' % section_name
413
414 section_parser_method = getattr(
415 self,
416 # Dots in section names are translated into dunderscores.
417 ('parse_section%s' % method_postfix).replace('.', '__'),
418 None)
419
420 if section_parser_method is None:
421 raise DistutilsOptionError(
422 'Unsupported distribution option section: [%s.%s]' % (
423 self.section_prefix, section_name))
424
425 section_parser_method(section_options)
426
427 def _deprecated_config_handler(self, func, msg, warning_class):
428 """ this function will wrap around parameters that are deprecated
429
430 :param msg: deprecation message
431 :param warning_class: class of warning exception to be raised
432 :param func: function to be wrapped around
433 """
434 @wraps(func)
435 def config_handler(*args, **kwargs):
436 warnings.warn(msg, warning_class)
437 return func(*args, **kwargs)
438
439 return config_handler
440
441
442 class ConfigMetadataHandler(ConfigHandler):
443
444 section_prefix = 'metadata'
445
446 aliases = {
447 'home_page': 'url',
448 'summary': 'description',
449 'classifier': 'classifiers',
450 'platform': 'platforms',
451 }
452
453 strict_mode = False
454 """We need to keep it loose, to be partially compatible with
455 `pbr` and `d2to1` packages which also uses `metadata` section.
456
457 """
458
459 def __init__(self, target_obj, options, ignore_option_errors=False,
460 package_dir=None):
461 super(ConfigMetadataHandler, self).__init__(target_obj, options,
462 ignore_option_errors)
463 self.package_dir = package_dir
464
465 @property
466 def parsers(self):
467 """Metadata item name to parser function mapping."""
468 parse_list = self._parse_list
469 parse_file = self._parse_file
470 parse_dict = self._parse_dict
471 exclude_files_parser = self._exclude_files_parser
472
473 return {
474 'platforms': parse_list,
475 'keywords': parse_list,
476 'provides': parse_list,
477 'requires': self._deprecated_config_handler(
478 parse_list,
479 "The requires parameter is deprecated, please use "
480 "install_requires for runtime dependencies.",
481 DeprecationWarning),
482 'obsoletes': parse_list,
483 'classifiers': self._get_parser_compound(parse_file, parse_list),
484 'license': exclude_files_parser('license'),
485 'description': parse_file,
486 'long_description': parse_file,
487 'version': self._parse_version,
488 'project_urls': parse_dict,
489 }
490
491 def _parse_version(self, value):
492 """Parses `version` option value.
493
494 :param value:
495 :rtype: str
496
497 """
498 version = self._parse_file(value)
499
500 if version != value:
501 version = version.strip()
502 # Be strict about versions loaded from file because it's easy to
503 # accidentally include newlines and other unintended content
504 if isinstance(parse(version), LegacyVersion):
505 tmpl = (
506 'Version loaded from {value} does not '
507 'comply with PEP 440: {version}'
508 )
509 raise DistutilsOptionError(tmpl.format(**locals()))
510
511 return version
512
513 version = self._parse_attr(value, self.package_dir)
514
515 if callable(version):
516 version = version()
517
518 if not isinstance(version, string_types):
519 if hasattr(version, '__iter__'):
520 version = '.'.join(map(str, version))
521 else:
522 version = '%s' % version
523
524 return version
525
526
527 class ConfigOptionsHandler(ConfigHandler):
528
529 section_prefix = 'options'
530
531 @property
532 def parsers(self):
533 """Metadata item name to parser function mapping."""
534 parse_list = self._parse_list
535 parse_list_semicolon = partial(self._parse_list, separator=';')
536 parse_bool = self._parse_bool
537 parse_dict = self._parse_dict
538
539 return {
540 'zip_safe': parse_bool,
541 'use_2to3': parse_bool,
542 'include_package_data': parse_bool,
543 'package_dir': parse_dict,
544 'use_2to3_fixers': parse_list,
545 'use_2to3_exclude_fixers': parse_list,
546 'convert_2to3_doctests': parse_list,
547 'scripts': parse_list,
548 'eager_resources': parse_list,
549 'dependency_links': parse_list,
550 'namespace_packages': parse_list,
551 'install_requires': parse_list_semicolon,
552 'setup_requires': parse_list_semicolon,
553 'tests_require': parse_list_semicolon,
554 'packages': self._parse_packages,
555 'entry_points': self._parse_file,
556 'py_modules': parse_list,
557 }
558
559 def _parse_packages(self, value):
560 """Parses `packages` option value.
561
562 :param value:
563 :rtype: list
564 """
565 find_directives = ['find:', 'find_namespace:']
566 trimmed_value = value.strip()
567
568 if trimmed_value not in find_directives:
569 return self._parse_list(value)
570
571 findns = trimmed_value == find_directives[1]
572 if findns and not PY3:
573 raise DistutilsOptionError(
574 'find_namespace: directive is unsupported on Python < 3.3')
575
576 # Read function arguments from a dedicated section.
577 find_kwargs = self.parse_section_packages__find(
578 self.sections.get('packages.find', {}))
579
580 if findns:
581 from setuptools import find_namespace_packages as find_packages
582 else:
583 from setuptools import find_packages
584
585 return find_packages(**find_kwargs)
586
587 def parse_section_packages__find(self, section_options):
588 """Parses `packages.find` configuration file section.
589
590 To be used in conjunction with _parse_packages().
591
592 :param dict section_options:
593 """
594 section_data = self._parse_section_to_dict(
595 section_options, self._parse_list)
596
597 valid_keys = ['where', 'include', 'exclude']
598
599 find_kwargs = dict(
600 [(k, v) for k, v in section_data.items() if k in valid_keys and v])
601
602 where = find_kwargs.get('where')
603 if where is not None:
604 find_kwargs['where'] = where[0] # cast list to single val
605
606 return find_kwargs
607
608 def parse_section_entry_points(self, section_options):
609 """Parses `entry_points` configuration file section.
610
611 :param dict section_options:
612 """
613 parsed = self._parse_section_to_dict(section_options, self._parse_list)
614 self['entry_points'] = parsed
615
616 def _parse_package_data(self, section_options):
617 parsed = self._parse_section_to_dict(section_options, self._parse_list)
618
619 root = parsed.get('*')
620 if root:
621 parsed[''] = root
622 del parsed['*']
623
624 return parsed
625
626 def parse_section_package_data(self, section_options):
627 """Parses `package_data` configuration file section.
628
629 :param dict section_options:
630 """
631 self['package_data'] = self._parse_package_data(section_options)
632
633 def parse_section_exclude_package_data(self, section_options):
634 """Parses `exclude_package_data` configuration file section.
635
636 :param dict section_options:
637 """
638 self['exclude_package_data'] = self._parse_package_data(
639 section_options)
640
641 def parse_section_extras_require(self, section_options):
642 """Parses `extras_require` configuration file section.
643
644 :param dict section_options:
645 """
646 parse_list = partial(self._parse_list, separator=';')
647 self['extras_require'] = self._parse_section_to_dict(
648 section_options, parse_list)
649
650 def parse_section_data_files(self, section_options):
651 """Parses `data_files` configuration file section.
652
653 :param dict section_options:
654 """
655 parsed = self._parse_section_to_dict(section_options, self._parse_list)
656 self['data_files'] = [(k, v) for k, v in parsed.items()]