setupcfg.py 25 KB

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