setupcfg.py 25 KB

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