comparison env/lib/python3.9/site-packages/setuptools/config.py @ 0:4f3585e2f14b draft default tip

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