]> jfr.im git - dlqueue.git/blob - venv/lib/python3.11/site-packages/setuptools/config/setupcfg.py
init: venv aand flask
[dlqueue.git] / venv / lib / python3.11 / site-packages / setuptools / config / setupcfg.py
1 """
2 Load setuptools configuration from ``setup.cfg`` files.
3
4 **API will be made private in the future**
5
6 To read project metadata, consider using
7 ``build.util.project_wheel_metadata`` (https://pypi.org/project/build/).
8 For simple scenarios, you can also try parsing the file directly
9 with the help of ``configparser``.
10 """
11 import contextlib
12 import functools
13 import os
14 from collections import defaultdict
15 from functools import partial
16 from functools import wraps
17 from typing import (
18 TYPE_CHECKING,
19 Callable,
20 Any,
21 Dict,
22 Generic,
23 Iterable,
24 List,
25 Optional,
26 Set,
27 Tuple,
28 TypeVar,
29 Union,
30 )
31
32 from ..errors import FileError, OptionError
33 from ..extern.packaging.markers import default_environment as marker_env
34 from ..extern.packaging.requirements import InvalidRequirement, Requirement
35 from ..extern.packaging.specifiers import SpecifierSet
36 from ..extern.packaging.version import InvalidVersion, Version
37 from ..warnings import SetuptoolsDeprecationWarning
38 from . import expand
39
40 if TYPE_CHECKING:
41 from distutils.dist import DistributionMetadata # noqa
42
43 from setuptools.dist import Distribution # noqa
44
45 _Path = Union[str, os.PathLike]
46 SingleCommandOptions = Dict["str", Tuple["str", Any]]
47 """Dict that associate the name of the options of a particular command to a
48 tuple. The first element of the tuple indicates the origin of the option value
49 (e.g. the name of the configuration file where it was read from),
50 while the second element of the tuple is the option value itself
51 """
52 AllCommandOptions = Dict["str", SingleCommandOptions] # cmd name => its options
53 Target = TypeVar("Target", bound=Union["Distribution", "DistributionMetadata"])
54
55
56 def read_configuration(
57 filepath: _Path, find_others=False, ignore_option_errors=False
58 ) -> dict:
59 """Read given configuration file and returns options from it as a dict.
60
61 :param str|unicode filepath: Path to configuration file
62 to get options from.
63
64 :param bool find_others: Whether to search for other configuration files
65 which could be on in various places.
66
67 :param bool ignore_option_errors: Whether to silently ignore
68 options, values of which could not be resolved (e.g. due to exceptions
69 in directives such as file:, attr:, etc.).
70 If False exceptions are propagated as expected.
71
72 :rtype: dict
73 """
74 from setuptools.dist import Distribution
75
76 dist = Distribution()
77 filenames = dist.find_config_files() if find_others else []
78 handlers = _apply(dist, filepath, filenames, ignore_option_errors)
79 return configuration_to_dict(handlers)
80
81
82 def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":
83 """Apply the configuration from a ``setup.cfg`` file into an existing
84 distribution object.
85 """
86 _apply(dist, filepath)
87 dist._finalize_requires()
88 return dist
89
90
91 def _apply(
92 dist: "Distribution",
93 filepath: _Path,
94 other_files: Iterable[_Path] = (),
95 ignore_option_errors: bool = False,
96 ) -> Tuple["ConfigHandler", ...]:
97 """Read configuration from ``filepath`` and applies to the ``dist`` object."""
98 from setuptools.dist import _Distribution
99
100 filepath = os.path.abspath(filepath)
101
102 if not os.path.isfile(filepath):
103 raise FileError(f'Configuration file {filepath} does not exist.')
104
105 current_directory = os.getcwd()
106 os.chdir(os.path.dirname(filepath))
107 filenames = [*other_files, filepath]
108
109 try:
110 _Distribution.parse_config_files(dist, filenames=filenames)
111 handlers = parse_configuration(
112 dist, dist.command_options, ignore_option_errors=ignore_option_errors
113 )
114 dist._finalize_license_files()
115 finally:
116 os.chdir(current_directory)
117
118 return handlers
119
120
121 def _get_option(target_obj: Target, key: str):
122 """
123 Given a target object and option key, get that option from
124 the target object, either through a get_{key} method or
125 from an attribute directly.
126 """
127 getter_name = f'get_{key}'
128 by_attribute = functools.partial(getattr, target_obj, key)
129 getter = getattr(target_obj, getter_name, by_attribute)
130 return getter()
131
132
133 def configuration_to_dict(handlers: Tuple["ConfigHandler", ...]) -> dict:
134 """Returns configuration data gathered by given handlers as a dict.
135
136 :param list[ConfigHandler] handlers: Handlers list,
137 usually from parse_configuration()
138
139 :rtype: dict
140 """
141 config_dict: dict = defaultdict(dict)
142
143 for handler in handlers:
144 for option in handler.set_options:
145 value = _get_option(handler.target_obj, option)
146 config_dict[handler.section_prefix][option] = value
147
148 return config_dict
149
150
151 def parse_configuration(
152 distribution: "Distribution",
153 command_options: AllCommandOptions,
154 ignore_option_errors=False,
155 ) -> Tuple["ConfigMetadataHandler", "ConfigOptionsHandler"]:
156 """Performs additional parsing of configuration options
157 for a distribution.
158
159 Returns a list of used option handlers.
160
161 :param Distribution distribution:
162 :param dict command_options:
163 :param bool ignore_option_errors: Whether to silently ignore
164 options, values of which could not be resolved (e.g. due to exceptions
165 in directives such as file:, attr:, etc.).
166 If False exceptions are propagated as expected.
167 :rtype: list
168 """
169 with expand.EnsurePackagesDiscovered(distribution) as ensure_discovered:
170 options = ConfigOptionsHandler(
171 distribution,
172 command_options,
173 ignore_option_errors,
174 ensure_discovered,
175 )
176
177 options.parse()
178 if not distribution.package_dir:
179 distribution.package_dir = options.package_dir # Filled by `find_packages`
180
181 meta = ConfigMetadataHandler(
182 distribution.metadata,
183 command_options,
184 ignore_option_errors,
185 ensure_discovered,
186 distribution.package_dir,
187 distribution.src_root,
188 )
189 meta.parse()
190 distribution._referenced_files.update(
191 options._referenced_files, meta._referenced_files
192 )
193
194 return meta, options
195
196
197 def _warn_accidental_env_marker_misconfig(label: str, orig_value: str, parsed: list):
198 """Because users sometimes misinterpret this configuration:
199
200 [options.extras_require]
201 foo = bar;python_version<"4"
202
203 It looks like one requirement with an environment marker
204 but because there is no newline, it's parsed as two requirements
205 with a semicolon as separator.
206
207 Therefore, if:
208 * input string does not contain a newline AND
209 * parsed result contains two requirements AND
210 * parsing of the two parts from the result ("<first>;<second>")
211 leads in a valid Requirement with a valid marker
212 a UserWarning is shown to inform the user about the possible problem.
213 """
214 if "\n" in orig_value or len(parsed) != 2:
215 return
216
217 markers = marker_env().keys()
218
219 try:
220 req = Requirement(parsed[1])
221 if req.name in markers:
222 _AmbiguousMarker.emit(field=label, req=parsed[1])
223 except InvalidRequirement as ex:
224 if any(parsed[1].startswith(marker) for marker in markers):
225 msg = _AmbiguousMarker.message(field=label, req=parsed[1])
226 raise InvalidRequirement(msg) from ex
227
228
229 class ConfigHandler(Generic[Target]):
230 """Handles metadata supplied in configuration files."""
231
232 section_prefix: str
233 """Prefix for config sections handled by this handler.
234 Must be provided by class heirs.
235
236 """
237
238 aliases: Dict[str, str] = {}
239 """Options aliases.
240 For compatibility with various packages. E.g.: d2to1 and pbr.
241 Note: `-` in keys is replaced with `_` by config parser.
242
243 """
244
245 def __init__(
246 self,
247 target_obj: Target,
248 options: AllCommandOptions,
249 ignore_option_errors,
250 ensure_discovered: expand.EnsurePackagesDiscovered,
251 ):
252 self.ignore_option_errors = ignore_option_errors
253 self.target_obj = target_obj
254 self.sections = dict(self._section_options(options))
255 self.set_options: List[str] = []
256 self.ensure_discovered = ensure_discovered
257 self._referenced_files: Set[str] = set()
258 """After parsing configurations, this property will enumerate
259 all files referenced by the "file:" directive. Private API for setuptools only.
260 """
261
262 @classmethod
263 def _section_options(cls, options: AllCommandOptions):
264 for full_name, value in options.items():
265 pre, sep, name = full_name.partition(cls.section_prefix)
266 if pre:
267 continue
268 yield name.lstrip('.'), value
269
270 @property
271 def parsers(self):
272 """Metadata item name to parser function mapping."""
273 raise NotImplementedError(
274 '%s must provide .parsers property' % self.__class__.__name__
275 )
276
277 def __setitem__(self, option_name, value):
278 target_obj = self.target_obj
279
280 # Translate alias into real name.
281 option_name = self.aliases.get(option_name, option_name)
282
283 try:
284 current_value = getattr(target_obj, option_name)
285 except AttributeError:
286 raise KeyError(option_name)
287
288 if current_value:
289 # Already inhabited. Skipping.
290 return
291
292 try:
293 parsed = self.parsers.get(option_name, lambda x: x)(value)
294 except (Exception,) * self.ignore_option_errors:
295 return
296
297 simple_setter = functools.partial(target_obj.__setattr__, option_name)
298 setter = getattr(target_obj, 'set_%s' % option_name, simple_setter)
299 setter(parsed)
300
301 self.set_options.append(option_name)
302
303 @classmethod
304 def _parse_list(cls, value, separator=','):
305 """Represents value as a list.
306
307 Value is split either by separator (defaults to comma) or by lines.
308
309 :param value:
310 :param separator: List items separator character.
311 :rtype: list
312 """
313 if isinstance(value, list): # _get_parser_compound case
314 return value
315
316 if '\n' in value:
317 value = value.splitlines()
318 else:
319 value = value.split(separator)
320
321 return [chunk.strip() for chunk in value if chunk.strip()]
322
323 @classmethod
324 def _parse_dict(cls, value):
325 """Represents value as a dict.
326
327 :param value:
328 :rtype: dict
329 """
330 separator = '='
331 result = {}
332 for line in cls._parse_list(value):
333 key, sep, val = line.partition(separator)
334 if sep != separator:
335 raise OptionError(f"Unable to parse option value to dict: {value}")
336 result[key.strip()] = val.strip()
337
338 return result
339
340 @classmethod
341 def _parse_bool(cls, value):
342 """Represents value as boolean.
343
344 :param value:
345 :rtype: bool
346 """
347 value = value.lower()
348 return value in ('1', 'true', 'yes')
349
350 @classmethod
351 def _exclude_files_parser(cls, key):
352 """Returns a parser function to make sure field inputs
353 are not files.
354
355 Parses a value after getting the key so error messages are
356 more informative.
357
358 :param key:
359 :rtype: callable
360 """
361
362 def parser(value):
363 exclude_directive = 'file:'
364 if value.startswith(exclude_directive):
365 raise ValueError(
366 'Only strings are accepted for the {0} field, '
367 'files are not accepted'.format(key)
368 )
369 return value
370
371 return parser
372
373 def _parse_file(self, value, root_dir: _Path):
374 """Represents value as a string, allowing including text
375 from nearest files using `file:` directive.
376
377 Directive is sandboxed and won't reach anything outside
378 directory with setup.py.
379
380 Examples:
381 file: README.rst, CHANGELOG.md, src/file.txt
382
383 :param str value:
384 :rtype: str
385 """
386 include_directive = 'file:'
387
388 if not isinstance(value, str):
389 return value
390
391 if not value.startswith(include_directive):
392 return value
393
394 spec = value[len(include_directive) :]
395 filepaths = [path.strip() for path in spec.split(',')]
396 self._referenced_files.update(filepaths)
397 return expand.read_files(filepaths, root_dir)
398
399 def _parse_attr(self, value, package_dir, root_dir: _Path):
400 """Represents value as a module attribute.
401
402 Examples:
403 attr: package.attr
404 attr: package.module.attr
405
406 :param str value:
407 :rtype: str
408 """
409 attr_directive = 'attr:'
410 if not value.startswith(attr_directive):
411 return value
412
413 attr_desc = value.replace(attr_directive, '')
414
415 # Make sure package_dir is populated correctly, so `attr:` directives can work
416 package_dir.update(self.ensure_discovered.package_dir)
417 return expand.read_attr(attr_desc, package_dir, root_dir)
418
419 @classmethod
420 def _get_parser_compound(cls, *parse_methods):
421 """Returns parser function to represents value as a list.
422
423 Parses a value applying given methods one after another.
424
425 :param parse_methods:
426 :rtype: callable
427 """
428
429 def parse(value):
430 parsed = value
431
432 for method in parse_methods:
433 parsed = method(parsed)
434
435 return parsed
436
437 return parse
438
439 @classmethod
440 def _parse_section_to_dict_with_key(cls, section_options, values_parser):
441 """Parses section options into a dictionary.
442
443 Applies a given parser to each option in a section.
444
445 :param dict section_options:
446 :param callable values_parser: function with 2 args corresponding to key, value
447 :rtype: dict
448 """
449 value = {}
450 for key, (_, val) in section_options.items():
451 value[key] = values_parser(key, val)
452 return value
453
454 @classmethod
455 def _parse_section_to_dict(cls, section_options, values_parser=None):
456 """Parses section options into a dictionary.
457
458 Optionally applies a given parser to each value.
459
460 :param dict section_options:
461 :param callable values_parser: function with 1 arg corresponding to option value
462 :rtype: dict
463 """
464 parser = (lambda _, v: values_parser(v)) if values_parser else (lambda _, v: v)
465 return cls._parse_section_to_dict_with_key(section_options, parser)
466
467 def parse_section(self, section_options):
468 """Parses configuration file section.
469
470 :param dict section_options:
471 """
472 for name, (_, value) in section_options.items():
473 with contextlib.suppress(KeyError):
474 # Keep silent for a new option may appear anytime.
475 self[name] = value
476
477 def parse(self):
478 """Parses configuration file items from one
479 or more related sections.
480
481 """
482 for section_name, section_options in self.sections.items():
483 method_postfix = ''
484 if section_name: # [section.option] variant
485 method_postfix = '_%s' % section_name
486
487 section_parser_method: Optional[Callable] = getattr(
488 self,
489 # Dots in section names are translated into dunderscores.
490 ('parse_section%s' % method_postfix).replace('.', '__'),
491 None,
492 )
493
494 if section_parser_method is None:
495 raise OptionError(
496 "Unsupported distribution option section: "
497 f"[{self.section_prefix}.{section_name}]"
498 )
499
500 section_parser_method(section_options)
501
502 def _deprecated_config_handler(self, func, msg, **kw):
503 """this function will wrap around parameters that are deprecated
504
505 :param msg: deprecation message
506 :param func: function to be wrapped around
507 """
508
509 @wraps(func)
510 def config_handler(*args, **kwargs):
511 kw.setdefault("stacklevel", 2)
512 _DeprecatedConfig.emit("Deprecated config in `setup.cfg`", msg, **kw)
513 return func(*args, **kwargs)
514
515 return config_handler
516
517
518 class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
519 section_prefix = 'metadata'
520
521 aliases = {
522 'home_page': 'url',
523 'summary': 'description',
524 'classifier': 'classifiers',
525 'platform': 'platforms',
526 }
527
528 strict_mode = False
529 """We need to keep it loose, to be partially compatible with
530 `pbr` and `d2to1` packages which also uses `metadata` section.
531
532 """
533
534 def __init__(
535 self,
536 target_obj: "DistributionMetadata",
537 options: AllCommandOptions,
538 ignore_option_errors: bool,
539 ensure_discovered: expand.EnsurePackagesDiscovered,
540 package_dir: Optional[dict] = None,
541 root_dir: _Path = os.curdir,
542 ):
543 super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
544 self.package_dir = package_dir
545 self.root_dir = root_dir
546
547 @property
548 def parsers(self):
549 """Metadata item name to parser function mapping."""
550 parse_list = self._parse_list
551 parse_file = partial(self._parse_file, root_dir=self.root_dir)
552 parse_dict = self._parse_dict
553 exclude_files_parser = self._exclude_files_parser
554
555 return {
556 'platforms': parse_list,
557 'keywords': parse_list,
558 'provides': parse_list,
559 'requires': self._deprecated_config_handler(
560 parse_list,
561 "The requires parameter is deprecated, please use "
562 "install_requires for runtime dependencies.",
563 due_date=(2023, 10, 30),
564 # Warning introduced in 27 Oct 2018
565 ),
566 'obsoletes': parse_list,
567 'classifiers': self._get_parser_compound(parse_file, parse_list),
568 'license': exclude_files_parser('license'),
569 'license_file': self._deprecated_config_handler(
570 exclude_files_parser('license_file'),
571 "The license_file parameter is deprecated, "
572 "use license_files instead.",
573 due_date=(2023, 10, 30),
574 # Warning introduced in 23 May 2021
575 ),
576 'license_files': parse_list,
577 'description': parse_file,
578 'long_description': parse_file,
579 'version': self._parse_version,
580 'project_urls': parse_dict,
581 }
582
583 def _parse_version(self, value):
584 """Parses `version` option value.
585
586 :param value:
587 :rtype: str
588
589 """
590 version = self._parse_file(value, self.root_dir)
591
592 if version != value:
593 version = version.strip()
594 # Be strict about versions loaded from file because it's easy to
595 # accidentally include newlines and other unintended content
596 try:
597 Version(version)
598 except InvalidVersion:
599 raise OptionError(
600 f'Version loaded from {value} does not '
601 f'comply with PEP 440: {version}'
602 )
603
604 return version
605
606 return expand.version(self._parse_attr(value, self.package_dir, self.root_dir))
607
608
609 class ConfigOptionsHandler(ConfigHandler["Distribution"]):
610 section_prefix = 'options'
611
612 def __init__(
613 self,
614 target_obj: "Distribution",
615 options: AllCommandOptions,
616 ignore_option_errors: bool,
617 ensure_discovered: expand.EnsurePackagesDiscovered,
618 ):
619 super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
620 self.root_dir = target_obj.src_root
621 self.package_dir: Dict[str, str] = {} # To be filled by `find_packages`
622
623 @classmethod
624 def _parse_list_semicolon(cls, value):
625 return cls._parse_list(value, separator=';')
626
627 def _parse_file_in_root(self, value):
628 return self._parse_file(value, root_dir=self.root_dir)
629
630 def _parse_requirements_list(self, label: str, value: str):
631 # Parse a requirements list, either by reading in a `file:`, or a list.
632 parsed = self._parse_list_semicolon(self._parse_file_in_root(value))
633 _warn_accidental_env_marker_misconfig(label, value, parsed)
634 # Filter it to only include lines that are not comments. `parse_list`
635 # will have stripped each line and filtered out empties.
636 return [line for line in parsed if not line.startswith("#")]
637
638 @property
639 def parsers(self):
640 """Metadata item name to parser function mapping."""
641 parse_list = self._parse_list
642 parse_bool = self._parse_bool
643 parse_dict = self._parse_dict
644 parse_cmdclass = self._parse_cmdclass
645
646 return {
647 'zip_safe': parse_bool,
648 'include_package_data': parse_bool,
649 'package_dir': parse_dict,
650 'scripts': parse_list,
651 'eager_resources': parse_list,
652 'dependency_links': parse_list,
653 'namespace_packages': self._deprecated_config_handler(
654 parse_list,
655 "The namespace_packages parameter is deprecated, "
656 "consider using implicit namespaces instead (PEP 420).",
657 # TODO: define due date, see setuptools.dist:check_nsp.
658 ),
659 'install_requires': partial(
660 self._parse_requirements_list, "install_requires"
661 ),
662 'setup_requires': self._parse_list_semicolon,
663 'tests_require': self._parse_list_semicolon,
664 'packages': self._parse_packages,
665 'entry_points': self._parse_file_in_root,
666 'py_modules': parse_list,
667 'python_requires': SpecifierSet,
668 'cmdclass': parse_cmdclass,
669 }
670
671 def _parse_cmdclass(self, value):
672 package_dir = self.ensure_discovered.package_dir
673 return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir)
674
675 def _parse_packages(self, value):
676 """Parses `packages` option value.
677
678 :param value:
679 :rtype: list
680 """
681 find_directives = ['find:', 'find_namespace:']
682 trimmed_value = value.strip()
683
684 if trimmed_value not in find_directives:
685 return self._parse_list(value)
686
687 # Read function arguments from a dedicated section.
688 find_kwargs = self.parse_section_packages__find(
689 self.sections.get('packages.find', {})
690 )
691
692 find_kwargs.update(
693 namespaces=(trimmed_value == find_directives[1]),
694 root_dir=self.root_dir,
695 fill_package_dir=self.package_dir,
696 )
697
698 return expand.find_packages(**find_kwargs)
699
700 def parse_section_packages__find(self, section_options):
701 """Parses `packages.find` configuration file section.
702
703 To be used in conjunction with _parse_packages().
704
705 :param dict section_options:
706 """
707 section_data = self._parse_section_to_dict(section_options, self._parse_list)
708
709 valid_keys = ['where', 'include', 'exclude']
710
711 find_kwargs = dict(
712 [(k, v) for k, v in section_data.items() if k in valid_keys and v]
713 )
714
715 where = find_kwargs.get('where')
716 if where is not None:
717 find_kwargs['where'] = where[0] # cast list to single val
718
719 return find_kwargs
720
721 def parse_section_entry_points(self, section_options):
722 """Parses `entry_points` configuration file section.
723
724 :param dict section_options:
725 """
726 parsed = self._parse_section_to_dict(section_options, self._parse_list)
727 self['entry_points'] = parsed
728
729 def _parse_package_data(self, section_options):
730 package_data = self._parse_section_to_dict(section_options, self._parse_list)
731 return expand.canonic_package_data(package_data)
732
733 def parse_section_package_data(self, section_options):
734 """Parses `package_data` configuration file section.
735
736 :param dict section_options:
737 """
738 self['package_data'] = self._parse_package_data(section_options)
739
740 def parse_section_exclude_package_data(self, section_options):
741 """Parses `exclude_package_data` configuration file section.
742
743 :param dict section_options:
744 """
745 self['exclude_package_data'] = self._parse_package_data(section_options)
746
747 def parse_section_extras_require(self, section_options):
748 """Parses `extras_require` configuration file section.
749
750 :param dict section_options:
751 """
752 parsed = self._parse_section_to_dict_with_key(
753 section_options,
754 lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v),
755 )
756
757 self['extras_require'] = parsed
758
759 def parse_section_data_files(self, section_options):
760 """Parses `data_files` configuration file section.
761
762 :param dict section_options:
763 """
764 parsed = self._parse_section_to_dict(section_options, self._parse_list)
765 self['data_files'] = expand.canonic_data_files(parsed, self.root_dir)
766
767
768 class _AmbiguousMarker(SetuptoolsDeprecationWarning):
769 _SUMMARY = "Ambiguous requirement marker."
770 _DETAILS = """
771 One of the parsed requirements in `{field}` looks like a valid environment marker:
772
773 {req!r}
774
775 Please make sure that the configuration file is correct.
776 You can use dangling lines to avoid this problem.
777 """
778 _SEE_DOCS = "userguide/declarative_config.html#opt-2"
779 # TODO: should we include due_date here? Initially introduced in 6 Aug 2022.
780 # Does this make sense with latest version of packaging?
781
782 @classmethod
783 def message(cls, **kw):
784 docs = f"https://setuptools.pypa.io/en/latest/{cls._SEE_DOCS}"
785 return cls._format(cls._SUMMARY, cls._DETAILS, see_url=docs, format_args=kw)
786
787
788 class _DeprecatedConfig(SetuptoolsDeprecationWarning):
789 _SEE_DOCS = "userguide/declarative_config.html"