python (3.11.7)
       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, 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  
     176      return meta, options
     177  
     178  
     179  def _warn_accidental_env_marker_misconfig(label: str, orig_value: str, parsed: list):
     180      """Because users sometimes misinterpret this configuration:
     181  
     182      [options.extras_require]
     183      foo = bar;python_version<"4"
     184  
     185      It looks like one requirement with an environment marker
     186      but because there is no newline, it's parsed as two requirements
     187      with a semicolon as separator.
     188  
     189      Therefore, if:
     190          * input string does not contain a newline AND
     191          * parsed result contains two requirements AND
     192          * parsing of the two parts from the result ("<first>;<second>")
     193          leads in a valid Requirement with a valid marker
     194      a UserWarning is shown to inform the user about the possible problem.
     195      """
     196      if "\n" in orig_value or len(parsed) != 2:
     197          return
     198  
     199      with contextlib.suppress(InvalidRequirement):
     200          original_requirements_str = ";".join(parsed)
     201          req = Requirement(original_requirements_str)
     202          if req.marker is not None:
     203              msg = (
     204                  f"One of the parsed requirements in `{label}` "
     205                  f"looks like a valid environment marker: '{parsed[1]}'\n"
     206                  "Make sure that the config is correct and check "
     207                  "https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#opt-2"  # noqa: E501
     208              )
     209              warnings.warn(msg, UserWarning)
     210  
     211  
     212  class ESC[4;38;5;81mConfigHandler(ESC[4;38;5;149mGeneric[Target]):
     213      """Handles metadata supplied in configuration files."""
     214  
     215      section_prefix: str
     216      """Prefix for config sections handled by this handler.
     217      Must be provided by class heirs.
     218  
     219      """
     220  
     221      aliases: Dict[str, str] = {}
     222      """Options aliases.
     223      For compatibility with various packages. E.g.: d2to1 and pbr.
     224      Note: `-` in keys is replaced with `_` by config parser.
     225  
     226      """
     227  
     228      def __init__(
     229          self,
     230          target_obj: Target,
     231          options: AllCommandOptions,
     232          ignore_option_errors,
     233          ensure_discovered: expand.EnsurePackagesDiscovered,
     234      ):
     235          sections: AllCommandOptions = {}
     236  
     237          section_prefix = self.section_prefix
     238          for section_name, section_options in options.items():
     239              if not section_name.startswith(section_prefix):
     240                  continue
     241  
     242              section_name = section_name.replace(section_prefix, '').strip('.')
     243              sections[section_name] = section_options
     244  
     245          self.ignore_option_errors = ignore_option_errors
     246          self.target_obj = target_obj
     247          self.sections = sections
     248          self.set_options: List[str] = []
     249          self.ensure_discovered = ensure_discovered
     250  
     251      @property
     252      def parsers(self):
     253          """Metadata item name to parser function mapping."""
     254          raise NotImplementedError(
     255              '%s must provide .parsers property' % self.__class__.__name__
     256          )
     257  
     258      def __setitem__(self, option_name, value):
     259          unknown = tuple()
     260          target_obj = self.target_obj
     261  
     262          # Translate alias into real name.
     263          option_name = self.aliases.get(option_name, option_name)
     264  
     265          current_value = getattr(target_obj, option_name, unknown)
     266  
     267          if current_value is unknown:
     268              raise KeyError(option_name)
     269  
     270          if current_value:
     271              # Already inhabited. Skipping.
     272              return
     273  
     274          skip_option = False
     275          parser = self.parsers.get(option_name)
     276          if parser:
     277              try:
     278                  value = parser(value)
     279  
     280              except Exception:
     281                  skip_option = True
     282                  if not self.ignore_option_errors:
     283                      raise
     284  
     285          if skip_option:
     286              return
     287  
     288          setter = getattr(target_obj, 'set_%s' % option_name, None)
     289          if setter is None:
     290              setattr(target_obj, option_name, value)
     291          else:
     292              setter(value)
     293  
     294          self.set_options.append(option_name)
     295  
     296      @classmethod
     297      def _parse_list(cls, value, separator=','):
     298          """Represents value as a list.
     299  
     300          Value is split either by separator (defaults to comma) or by lines.
     301  
     302          :param value:
     303          :param separator: List items separator character.
     304          :rtype: list
     305          """
     306          if isinstance(value, list):  # _get_parser_compound case
     307              return value
     308  
     309          if '\n' in value:
     310              value = value.splitlines()
     311          else:
     312              value = value.split(separator)
     313  
     314          return [chunk.strip() for chunk in value if chunk.strip()]
     315  
     316      @classmethod
     317      def _parse_dict(cls, value):
     318          """Represents value as a dict.
     319  
     320          :param value:
     321          :rtype: dict
     322          """
     323          separator = '='
     324          result = {}
     325          for line in cls._parse_list(value):
     326              key, sep, val = line.partition(separator)
     327              if sep != separator:
     328                  raise DistutilsOptionError(
     329                      'Unable to parse option value to dict: %s' % value
     330                  )
     331              result[key.strip()] = val.strip()
     332  
     333          return result
     334  
     335      @classmethod
     336      def _parse_bool(cls, value):
     337          """Represents value as boolean.
     338  
     339          :param value:
     340          :rtype: bool
     341          """
     342          value = value.lower()
     343          return value in ('1', 'true', 'yes')
     344  
     345      @classmethod
     346      def _exclude_files_parser(cls, key):
     347          """Returns a parser function to make sure field inputs
     348          are not files.
     349  
     350          Parses a value after getting the key so error messages are
     351          more informative.
     352  
     353          :param key:
     354          :rtype: callable
     355          """
     356  
     357          def parser(value):
     358              exclude_directive = 'file:'
     359              if value.startswith(exclude_directive):
     360                  raise ValueError(
     361                      'Only strings are accepted for the {0} field, '
     362                      'files are not accepted'.format(key)
     363                  )
     364              return value
     365  
     366          return parser
     367  
     368      @classmethod
     369      def _parse_file(cls, value, root_dir: _Path):
     370          """Represents value as a string, allowing including text
     371          from nearest files using `file:` directive.
     372  
     373          Directive is sandboxed and won't reach anything outside
     374          directory with setup.py.
     375  
     376          Examples:
     377              file: README.rst, CHANGELOG.md, src/file.txt
     378  
     379          :param str value:
     380          :rtype: str
     381          """
     382          include_directive = 'file:'
     383  
     384          if not isinstance(value, str):
     385              return value
     386  
     387          if not value.startswith(include_directive):
     388              return value
     389  
     390          spec = value[len(include_directive) :]
     391          filepaths = (path.strip() for path in spec.split(','))
     392          return expand.read_files(filepaths, root_dir)
     393  
     394      def _parse_attr(self, value, package_dir, root_dir: _Path):
     395          """Represents value as a module attribute.
     396  
     397          Examples:
     398              attr: package.attr
     399              attr: package.module.attr
     400  
     401          :param str value:
     402          :rtype: str
     403          """
     404          attr_directive = 'attr:'
     405          if not value.startswith(attr_directive):
     406              return value
     407  
     408          attr_desc = value.replace(attr_directive, '')
     409  
     410          # Make sure package_dir is populated correctly, so `attr:` directives can work
     411          package_dir.update(self.ensure_discovered.package_dir)
     412          return expand.read_attr(attr_desc, package_dir, root_dir)
     413  
     414      @classmethod
     415      def _get_parser_compound(cls, *parse_methods):
     416          """Returns parser function to represents value as a list.
     417  
     418          Parses a value applying given methods one after another.
     419  
     420          :param parse_methods:
     421          :rtype: callable
     422          """
     423  
     424          def parse(value):
     425              parsed = value
     426  
     427              for method in parse_methods:
     428                  parsed = method(parsed)
     429  
     430              return parsed
     431  
     432          return parse
     433  
     434      @classmethod
     435      def _parse_section_to_dict_with_key(cls, section_options, values_parser):
     436          """Parses section options into a dictionary.
     437  
     438          Applies a given parser to each option in a section.
     439  
     440          :param dict section_options:
     441          :param callable values_parser: function with 2 args corresponding to key, value
     442          :rtype: dict
     443          """
     444          value = {}
     445          for key, (_, val) in section_options.items():
     446              value[key] = values_parser(key, val)
     447          return value
     448  
     449      @classmethod
     450      def _parse_section_to_dict(cls, section_options, values_parser=None):
     451          """Parses section options into a dictionary.
     452  
     453          Optionally applies a given parser to each value.
     454  
     455          :param dict section_options:
     456          :param callable values_parser: function with 1 arg corresponding to option value
     457          :rtype: dict
     458          """
     459          parser = (lambda _, v: values_parser(v)) if values_parser else (lambda _, v: v)
     460          return cls._parse_section_to_dict_with_key(section_options, parser)
     461  
     462      def parse_section(self, section_options):
     463          """Parses configuration file section.
     464  
     465          :param dict section_options:
     466          """
     467          for (name, (_, value)) in section_options.items():
     468              with contextlib.suppress(KeyError):
     469                  # Keep silent for a new option may appear anytime.
     470                  self[name] = value
     471  
     472      def parse(self):
     473          """Parses configuration file items from one
     474          or more related sections.
     475  
     476          """
     477          for section_name, section_options in self.sections.items():
     478  
     479              method_postfix = ''
     480              if section_name:  # [section.option] variant
     481                  method_postfix = '_%s' % section_name
     482  
     483              section_parser_method: Optional[Callable] = getattr(
     484                  self,
     485                  # Dots in section names are translated into dunderscores.
     486                  ('parse_section%s' % method_postfix).replace('.', '__'),
     487                  None,
     488              )
     489  
     490              if section_parser_method is None:
     491                  raise DistutilsOptionError(
     492                      'Unsupported distribution option section: [%s.%s]'
     493                      % (self.section_prefix, section_name)
     494                  )
     495  
     496              section_parser_method(section_options)
     497  
     498      def _deprecated_config_handler(self, func, msg, warning_class):
     499          """this function will wrap around parameters that are deprecated
     500  
     501          :param msg: deprecation message
     502          :param warning_class: class of warning exception to be raised
     503          :param func: function to be wrapped around
     504          """
     505  
     506          @wraps(func)
     507          def config_handler(*args, **kwargs):
     508              warnings.warn(msg, warning_class)
     509              return func(*args, **kwargs)
     510  
     511          return config_handler
     512  
     513  
     514  class ESC[4;38;5;81mConfigMetadataHandler(ESC[4;38;5;149mConfigHandler["DistributionMetadata"]):
     515  
     516      section_prefix = 'metadata'
     517  
     518      aliases = {
     519          'home_page': 'url',
     520          'summary': 'description',
     521          'classifier': 'classifiers',
     522          'platform': 'platforms',
     523      }
     524  
     525      strict_mode = False
     526      """We need to keep it loose, to be partially compatible with
     527      `pbr` and `d2to1` packages which also uses `metadata` section.
     528  
     529      """
     530  
     531      def __init__(
     532          self,
     533          target_obj: "DistributionMetadata",
     534          options: AllCommandOptions,
     535          ignore_option_errors: bool,
     536          ensure_discovered: expand.EnsurePackagesDiscovered,
     537          package_dir: Optional[dict] = None,
     538          root_dir: _Path = os.curdir
     539      ):
     540          super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
     541          self.package_dir = package_dir
     542          self.root_dir = root_dir
     543  
     544      @property
     545      def parsers(self):
     546          """Metadata item name to parser function mapping."""
     547          parse_list = self._parse_list
     548          parse_file = partial(self._parse_file, root_dir=self.root_dir)
     549          parse_dict = self._parse_dict
     550          exclude_files_parser = self._exclude_files_parser
     551  
     552          return {
     553              'platforms': parse_list,
     554              'keywords': parse_list,
     555              'provides': parse_list,
     556              'requires': self._deprecated_config_handler(
     557                  parse_list,
     558                  "The requires parameter is deprecated, please use "
     559                  "install_requires for runtime dependencies.",
     560                  SetuptoolsDeprecationWarning,
     561              ),
     562              'obsoletes': parse_list,
     563              'classifiers': self._get_parser_compound(parse_file, parse_list),
     564              'license': exclude_files_parser('license'),
     565              'license_file': self._deprecated_config_handler(
     566                  exclude_files_parser('license_file'),
     567                  "The license_file parameter is deprecated, "
     568                  "use license_files instead.",
     569                  SetuptoolsDeprecationWarning,
     570              ),
     571              'license_files': parse_list,
     572              'description': parse_file,
     573              'long_description': parse_file,
     574              'version': self._parse_version,
     575              'project_urls': parse_dict,
     576          }
     577  
     578      def _parse_version(self, value):
     579          """Parses `version` option value.
     580  
     581          :param value:
     582          :rtype: str
     583  
     584          """
     585          version = self._parse_file(value, self.root_dir)
     586  
     587          if version != value:
     588              version = version.strip()
     589              # Be strict about versions loaded from file because it's easy to
     590              # accidentally include newlines and other unintended content
     591              try:
     592                  Version(version)
     593              except InvalidVersion:
     594                  tmpl = (
     595                      'Version loaded from {value} does not '
     596                      'comply with PEP 440: {version}'
     597                  )
     598                  raise DistutilsOptionError(tmpl.format(**locals()))
     599  
     600              return version
     601  
     602          return expand.version(self._parse_attr(value, self.package_dir, self.root_dir))
     603  
     604  
     605  class ESC[4;38;5;81mConfigOptionsHandler(ESC[4;38;5;149mConfigHandler["Distribution"]):
     606  
     607      section_prefix = 'options'
     608  
     609      def __init__(
     610          self,
     611          target_obj: "Distribution",
     612          options: AllCommandOptions,
     613          ignore_option_errors: bool,
     614          ensure_discovered: expand.EnsurePackagesDiscovered,
     615      ):
     616          super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
     617          self.root_dir = target_obj.src_root
     618          self.package_dir: Dict[str, str] = {}  # To be filled by `find_packages`
     619  
     620      @classmethod
     621      def _parse_list_semicolon(cls, value):
     622          return cls._parse_list(value, separator=';')
     623  
     624      def _parse_file_in_root(self, value):
     625          return self._parse_file(value, root_dir=self.root_dir)
     626  
     627      def _parse_requirements_list(self, label: str, value: str):
     628          # Parse a requirements list, either by reading in a `file:`, or a list.
     629          parsed = self._parse_list_semicolon(self._parse_file_in_root(value))
     630          _warn_accidental_env_marker_misconfig(label, value, parsed)
     631          # Filter it to only include lines that are not comments. `parse_list`
     632          # will have stripped each line and filtered out empties.
     633          return [line for line in parsed if not line.startswith("#")]
     634  
     635      @property
     636      def parsers(self):
     637          """Metadata item name to parser function mapping."""
     638          parse_list = self._parse_list
     639          parse_bool = self._parse_bool
     640          parse_dict = self._parse_dict
     641          parse_cmdclass = self._parse_cmdclass
     642  
     643          return {
     644              'zip_safe': parse_bool,
     645              'include_package_data': parse_bool,
     646              'package_dir': parse_dict,
     647              'scripts': parse_list,
     648              'eager_resources': parse_list,
     649              'dependency_links': parse_list,
     650              'namespace_packages': self._deprecated_config_handler(
     651                  parse_list,
     652                  "The namespace_packages parameter is deprecated, "
     653                  "consider using implicit namespaces instead (PEP 420).",
     654                  SetuptoolsDeprecationWarning,
     655              ),
     656              'install_requires': partial(
     657                  self._parse_requirements_list, "install_requires"
     658              ),
     659              'setup_requires': self._parse_list_semicolon,
     660              'tests_require': self._parse_list_semicolon,
     661              'packages': self._parse_packages,
     662              'entry_points': self._parse_file_in_root,
     663              'py_modules': parse_list,
     664              'python_requires': SpecifierSet,
     665              'cmdclass': parse_cmdclass,
     666          }
     667  
     668      def _parse_cmdclass(self, value):
     669          package_dir = self.ensure_discovered.package_dir
     670          return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir)
     671  
     672      def _parse_packages(self, value):
     673          """Parses `packages` option value.
     674  
     675          :param value:
     676          :rtype: list
     677          """
     678          find_directives = ['find:', 'find_namespace:']
     679          trimmed_value = value.strip()
     680  
     681          if trimmed_value not in find_directives:
     682              return self._parse_list(value)
     683  
     684          # Read function arguments from a dedicated section.
     685          find_kwargs = self.parse_section_packages__find(
     686              self.sections.get('packages.find', {})
     687          )
     688  
     689          find_kwargs.update(
     690              namespaces=(trimmed_value == find_directives[1]),
     691              root_dir=self.root_dir,
     692              fill_package_dir=self.package_dir,
     693          )
     694  
     695          return expand.find_packages(**find_kwargs)
     696  
     697      def parse_section_packages__find(self, section_options):
     698          """Parses `packages.find` configuration file section.
     699  
     700          To be used in conjunction with _parse_packages().
     701  
     702          :param dict section_options:
     703          """
     704          section_data = self._parse_section_to_dict(section_options, self._parse_list)
     705  
     706          valid_keys = ['where', 'include', 'exclude']
     707  
     708          find_kwargs = dict(
     709              [(k, v) for k, v in section_data.items() if k in valid_keys and v]
     710          )
     711  
     712          where = find_kwargs.get('where')
     713          if where is not None:
     714              find_kwargs['where'] = where[0]  # cast list to single val
     715  
     716          return find_kwargs
     717  
     718      def parse_section_entry_points(self, section_options):
     719          """Parses `entry_points` configuration file section.
     720  
     721          :param dict section_options:
     722          """
     723          parsed = self._parse_section_to_dict(section_options, self._parse_list)
     724          self['entry_points'] = parsed
     725  
     726      def _parse_package_data(self, section_options):
     727          package_data = self._parse_section_to_dict(section_options, self._parse_list)
     728          return expand.canonic_package_data(package_data)
     729  
     730      def parse_section_package_data(self, section_options):
     731          """Parses `package_data` configuration file section.
     732  
     733          :param dict section_options:
     734          """
     735          self['package_data'] = self._parse_package_data(section_options)
     736  
     737      def parse_section_exclude_package_data(self, section_options):
     738          """Parses `exclude_package_data` configuration file section.
     739  
     740          :param dict section_options:
     741          """
     742          self['exclude_package_data'] = self._parse_package_data(section_options)
     743  
     744      def parse_section_extras_require(self, section_options):
     745          """Parses `extras_require` configuration file section.
     746  
     747          :param dict section_options:
     748          """
     749          parsed = self._parse_section_to_dict_with_key(
     750              section_options,
     751              lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v)
     752          )
     753  
     754          self['extras_require'] = parsed
     755  
     756      def parse_section_data_files(self, section_options):
     757          """Parses `data_files` configuration file section.
     758  
     759          :param dict section_options:
     760          """
     761          parsed = self._parse_section_to_dict(section_options, self._parse_list)
     762          self['data_files'] = expand.canonic_data_files(parsed, self.root_dir)