comparison lib/python3.8/site-packages/setuptools/config.py @ 1:64071f2a4cf0 draft default tip

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