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