| | """ |
| | Load setuptools configuration from ``setup.cfg`` files. |
| | |
| | **API will be made private in the future** |
| | """ |
| | import os |
| |
|
| | import contextlib |
| | import functools |
| | import warnings |
| | from collections import defaultdict |
| | from functools import partial |
| | from functools import wraps |
| | from typing import (TYPE_CHECKING, Callable, Any, Dict, Generic, Iterable, List, |
| | Optional, Set, Tuple, TypeVar, Union) |
| |
|
| | from distutils.errors import DistutilsOptionError, DistutilsFileError |
| | from setuptools.extern.packaging.requirements import Requirement, InvalidRequirement |
| | from setuptools.extern.packaging.version import Version, InvalidVersion |
| | from setuptools.extern.packaging.specifiers import SpecifierSet |
| | from setuptools._deprecation_warning import SetuptoolsDeprecationWarning |
| |
|
| | from . import expand |
| |
|
| | if TYPE_CHECKING: |
| | from setuptools.dist import Distribution |
| | from distutils.dist import DistributionMetadata |
| |
|
| | _Path = Union[str, os.PathLike] |
| | SingleCommandOptions = Dict["str", Tuple["str", Any]] |
| | """Dict that associate the name of the options of a particular command to a |
| | tuple. The first element of the tuple indicates the origin of the option value |
| | (e.g. the name of the configuration file where it was read from), |
| | while the second element of the tuple is the option value itself |
| | """ |
| | AllCommandOptions = Dict["str", SingleCommandOptions] |
| | Target = TypeVar("Target", bound=Union["Distribution", "DistributionMetadata"]) |
| |
|
| |
|
| | def read_configuration( |
| | filepath: _Path, |
| | find_others=False, |
| | ignore_option_errors=False |
| | ) -> dict: |
| | """Read given configuration file and returns options from it as a dict. |
| | |
| | :param str|unicode filepath: Path to configuration file |
| | to get options from. |
| | |
| | :param bool find_others: Whether to search for other configuration files |
| | which could be on in various places. |
| | |
| | :param bool ignore_option_errors: Whether to silently ignore |
| | options, values of which could not be resolved (e.g. due to exceptions |
| | in directives such as file:, attr:, etc.). |
| | If False exceptions are propagated as expected. |
| | |
| | :rtype: dict |
| | """ |
| | from setuptools.dist import Distribution |
| |
|
| | dist = Distribution() |
| | filenames = dist.find_config_files() if find_others else [] |
| | handlers = _apply(dist, filepath, filenames, ignore_option_errors) |
| | return configuration_to_dict(handlers) |
| |
|
| |
|
| | def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution": |
| | """Apply the configuration from a ``setup.cfg`` file into an existing |
| | distribution object. |
| | """ |
| | _apply(dist, filepath) |
| | dist._finalize_requires() |
| | return dist |
| |
|
| |
|
| | def _apply( |
| | dist: "Distribution", filepath: _Path, |
| | other_files: Iterable[_Path] = (), |
| | ignore_option_errors: bool = False, |
| | ) -> Tuple["ConfigHandler", ...]: |
| | """Read configuration from ``filepath`` and applies to the ``dist`` object.""" |
| | from setuptools.dist import _Distribution |
| |
|
| | filepath = os.path.abspath(filepath) |
| |
|
| | if not os.path.isfile(filepath): |
| | raise DistutilsFileError('Configuration file %s does not exist.' % filepath) |
| |
|
| | current_directory = os.getcwd() |
| | os.chdir(os.path.dirname(filepath)) |
| | filenames = [*other_files, filepath] |
| |
|
| | try: |
| | _Distribution.parse_config_files(dist, filenames=filenames) |
| | handlers = parse_configuration( |
| | dist, dist.command_options, ignore_option_errors=ignore_option_errors |
| | ) |
| | dist._finalize_license_files() |
| | finally: |
| | os.chdir(current_directory) |
| |
|
| | return handlers |
| |
|
| |
|
| | def _get_option(target_obj: Target, key: str): |
| | """ |
| | Given a target object and option key, get that option from |
| | the target object, either through a get_{key} method or |
| | from an attribute directly. |
| | """ |
| | getter_name = 'get_{key}'.format(**locals()) |
| | by_attribute = functools.partial(getattr, target_obj, key) |
| | getter = getattr(target_obj, getter_name, by_attribute) |
| | return getter() |
| |
|
| |
|
| | def configuration_to_dict(handlers: Tuple["ConfigHandler", ...]) -> dict: |
| | """Returns configuration data gathered by given handlers as a dict. |
| | |
| | :param list[ConfigHandler] handlers: Handlers list, |
| | usually from parse_configuration() |
| | |
| | :rtype: dict |
| | """ |
| | config_dict: dict = defaultdict(dict) |
| |
|
| | for handler in handlers: |
| | for option in handler.set_options: |
| | value = _get_option(handler.target_obj, option) |
| | config_dict[handler.section_prefix][option] = value |
| |
|
| | return config_dict |
| |
|
| |
|
| | def parse_configuration( |
| | distribution: "Distribution", |
| | command_options: AllCommandOptions, |
| | ignore_option_errors=False |
| | ) -> Tuple["ConfigMetadataHandler", "ConfigOptionsHandler"]: |
| | """Performs additional parsing of configuration options |
| | for a distribution. |
| | |
| | Returns a list of used option handlers. |
| | |
| | :param Distribution distribution: |
| | :param dict command_options: |
| | :param bool ignore_option_errors: Whether to silently ignore |
| | options, values of which could not be resolved (e.g. due to exceptions |
| | in directives such as file:, attr:, etc.). |
| | If False exceptions are propagated as expected. |
| | :rtype: list |
| | """ |
| | with expand.EnsurePackagesDiscovered(distribution) as ensure_discovered: |
| | options = ConfigOptionsHandler( |
| | distribution, |
| | command_options, |
| | ignore_option_errors, |
| | ensure_discovered, |
| | ) |
| |
|
| | options.parse() |
| | if not distribution.package_dir: |
| | distribution.package_dir = options.package_dir |
| |
|
| | meta = ConfigMetadataHandler( |
| | distribution.metadata, |
| | command_options, |
| | ignore_option_errors, |
| | ensure_discovered, |
| | distribution.package_dir, |
| | distribution.src_root, |
| | ) |
| | meta.parse() |
| | distribution._referenced_files.update( |
| | options._referenced_files, meta._referenced_files |
| | ) |
| |
|
| | return meta, options |
| |
|
| |
|
| | def _warn_accidental_env_marker_misconfig(label: str, orig_value: str, parsed: list): |
| | """Because users sometimes misinterpret this configuration: |
| | |
| | [options.extras_require] |
| | foo = bar;python_version<"4" |
| | |
| | It looks like one requirement with an environment marker |
| | but because there is no newline, it's parsed as two requirements |
| | with a semicolon as separator. |
| | |
| | Therefore, if: |
| | * input string does not contain a newline AND |
| | * parsed result contains two requirements AND |
| | * parsing of the two parts from the result ("<first>;<second>") |
| | leads in a valid Requirement with a valid marker |
| | a UserWarning is shown to inform the user about the possible problem. |
| | """ |
| | if "\n" in orig_value or len(parsed) != 2: |
| | return |
| |
|
| | with contextlib.suppress(InvalidRequirement): |
| | original_requirements_str = ";".join(parsed) |
| | req = Requirement(original_requirements_str) |
| | if req.marker is not None: |
| | msg = ( |
| | f"One of the parsed requirements in `{label}` " |
| | f"looks like a valid environment marker: '{parsed[1]}'\n" |
| | "Make sure that the config is correct and check " |
| | "https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#opt-2" |
| | ) |
| | warnings.warn(msg, UserWarning) |
| |
|
| |
|
| | class ConfigHandler(Generic[Target]): |
| | """Handles metadata supplied in configuration files.""" |
| |
|
| | section_prefix: str |
| | """Prefix for config sections handled by this handler. |
| | Must be provided by class heirs. |
| | |
| | """ |
| |
|
| | aliases: Dict[str, str] = {} |
| | """Options aliases. |
| | For compatibility with various packages. E.g.: d2to1 and pbr. |
| | Note: `-` in keys is replaced with `_` by config parser. |
| | |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | target_obj: Target, |
| | options: AllCommandOptions, |
| | ignore_option_errors, |
| | ensure_discovered: expand.EnsurePackagesDiscovered, |
| | ): |
| | sections: AllCommandOptions = {} |
| |
|
| | section_prefix = self.section_prefix |
| | for section_name, section_options in options.items(): |
| | if not section_name.startswith(section_prefix): |
| | continue |
| |
|
| | section_name = section_name.replace(section_prefix, '').strip('.') |
| | sections[section_name] = section_options |
| |
|
| | self.ignore_option_errors = ignore_option_errors |
| | self.target_obj = target_obj |
| | self.sections = sections |
| | self.set_options: List[str] = [] |
| | self.ensure_discovered = ensure_discovered |
| | self._referenced_files: Set[str] = set() |
| | """After parsing configurations, this property will enumerate |
| | all files referenced by the "file:" directive. Private API for setuptools only. |
| | """ |
| |
|
| | @property |
| | def parsers(self): |
| | """Metadata item name to parser function mapping.""" |
| | raise NotImplementedError( |
| | '%s must provide .parsers property' % self.__class__.__name__ |
| | ) |
| |
|
| | def __setitem__(self, option_name, value): |
| | unknown = tuple() |
| | target_obj = self.target_obj |
| |
|
| | |
| | option_name = self.aliases.get(option_name, option_name) |
| |
|
| | current_value = getattr(target_obj, option_name, unknown) |
| |
|
| | if current_value is unknown: |
| | raise KeyError(option_name) |
| |
|
| | if current_value: |
| | |
| | return |
| |
|
| | skip_option = False |
| | parser = self.parsers.get(option_name) |
| | if parser: |
| | try: |
| | value = parser(value) |
| |
|
| | except Exception: |
| | skip_option = True |
| | if not self.ignore_option_errors: |
| | raise |
| |
|
| | if skip_option: |
| | return |
| |
|
| | setter = getattr(target_obj, 'set_%s' % option_name, None) |
| | if setter is None: |
| | setattr(target_obj, option_name, value) |
| | else: |
| | setter(value) |
| |
|
| | self.set_options.append(option_name) |
| |
|
| | @classmethod |
| | def _parse_list(cls, value, separator=','): |
| | """Represents value as a list. |
| | |
| | Value is split either by separator (defaults to comma) or by lines. |
| | |
| | :param value: |
| | :param separator: List items separator character. |
| | :rtype: list |
| | """ |
| | if isinstance(value, list): |
| | return value |
| |
|
| | if '\n' in value: |
| | value = value.splitlines() |
| | else: |
| | value = value.split(separator) |
| |
|
| | return [chunk.strip() for chunk in value if chunk.strip()] |
| |
|
| | @classmethod |
| | def _parse_dict(cls, value): |
| | """Represents value as a dict. |
| | |
| | :param value: |
| | :rtype: dict |
| | """ |
| | separator = '=' |
| | result = {} |
| | for line in cls._parse_list(value): |
| | key, sep, val = line.partition(separator) |
| | if sep != separator: |
| | raise DistutilsOptionError( |
| | 'Unable to parse option value to dict: %s' % value |
| | ) |
| | result[key.strip()] = val.strip() |
| |
|
| | return result |
| |
|
| | @classmethod |
| | def _parse_bool(cls, value): |
| | """Represents value as boolean. |
| | |
| | :param value: |
| | :rtype: bool |
| | """ |
| | value = value.lower() |
| | return value in ('1', 'true', 'yes') |
| |
|
| | @classmethod |
| | def _exclude_files_parser(cls, key): |
| | """Returns a parser function to make sure field inputs |
| | are not files. |
| | |
| | Parses a value after getting the key so error messages are |
| | more informative. |
| | |
| | :param key: |
| | :rtype: callable |
| | """ |
| |
|
| | def parser(value): |
| | exclude_directive = 'file:' |
| | if value.startswith(exclude_directive): |
| | raise ValueError( |
| | 'Only strings are accepted for the {0} field, ' |
| | 'files are not accepted'.format(key) |
| | ) |
| | return value |
| |
|
| | return parser |
| |
|
| | def _parse_file(self, value, root_dir: _Path): |
| | """Represents value as a string, allowing including text |
| | from nearest files using `file:` directive. |
| | |
| | Directive is sandboxed and won't reach anything outside |
| | directory with setup.py. |
| | |
| | Examples: |
| | file: README.rst, CHANGELOG.md, src/file.txt |
| | |
| | :param str value: |
| | :rtype: str |
| | """ |
| | include_directive = 'file:' |
| |
|
| | if not isinstance(value, str): |
| | return value |
| |
|
| | if not value.startswith(include_directive): |
| | return value |
| |
|
| | spec = value[len(include_directive) :] |
| | filepaths = [path.strip() for path in spec.split(',')] |
| | self._referenced_files.update(filepaths) |
| | return expand.read_files(filepaths, root_dir) |
| |
|
| | def _parse_attr(self, value, package_dir, root_dir: _Path): |
| | """Represents value as a module attribute. |
| | |
| | Examples: |
| | attr: package.attr |
| | attr: package.module.attr |
| | |
| | :param str value: |
| | :rtype: str |
| | """ |
| | attr_directive = 'attr:' |
| | if not value.startswith(attr_directive): |
| | return value |
| |
|
| | attr_desc = value.replace(attr_directive, '') |
| |
|
| | |
| | package_dir.update(self.ensure_discovered.package_dir) |
| | return expand.read_attr(attr_desc, package_dir, root_dir) |
| |
|
| | @classmethod |
| | def _get_parser_compound(cls, *parse_methods): |
| | """Returns parser function to represents value as a list. |
| | |
| | Parses a value applying given methods one after another. |
| | |
| | :param parse_methods: |
| | :rtype: callable |
| | """ |
| |
|
| | def parse(value): |
| | parsed = value |
| |
|
| | for method in parse_methods: |
| | parsed = method(parsed) |
| |
|
| | return parsed |
| |
|
| | return parse |
| |
|
| | @classmethod |
| | def _parse_section_to_dict_with_key(cls, section_options, values_parser): |
| | """Parses section options into a dictionary. |
| | |
| | Applies a given parser to each option in a section. |
| | |
| | :param dict section_options: |
| | :param callable values_parser: function with 2 args corresponding to key, value |
| | :rtype: dict |
| | """ |
| | value = {} |
| | for key, (_, val) in section_options.items(): |
| | value[key] = values_parser(key, val) |
| | return value |
| |
|
| | @classmethod |
| | def _parse_section_to_dict(cls, section_options, values_parser=None): |
| | """Parses section options into a dictionary. |
| | |
| | Optionally applies a given parser to each value. |
| | |
| | :param dict section_options: |
| | :param callable values_parser: function with 1 arg corresponding to option value |
| | :rtype: dict |
| | """ |
| | parser = (lambda _, v: values_parser(v)) if values_parser else (lambda _, v: v) |
| | return cls._parse_section_to_dict_with_key(section_options, parser) |
| |
|
| | def parse_section(self, section_options): |
| | """Parses configuration file section. |
| | |
| | :param dict section_options: |
| | """ |
| | for (name, (_, value)) in section_options.items(): |
| | with contextlib.suppress(KeyError): |
| | |
| | self[name] = value |
| |
|
| | def parse(self): |
| | """Parses configuration file items from one |
| | or more related sections. |
| | |
| | """ |
| | for section_name, section_options in self.sections.items(): |
| |
|
| | method_postfix = '' |
| | if section_name: |
| | method_postfix = '_%s' % section_name |
| |
|
| | section_parser_method: Optional[Callable] = getattr( |
| | self, |
| | |
| | ('parse_section%s' % method_postfix).replace('.', '__'), |
| | None, |
| | ) |
| |
|
| | if section_parser_method is None: |
| | raise DistutilsOptionError( |
| | 'Unsupported distribution option section: [%s.%s]' |
| | % (self.section_prefix, section_name) |
| | ) |
| |
|
| | section_parser_method(section_options) |
| |
|
| | def _deprecated_config_handler(self, func, msg, warning_class): |
| | """this function will wrap around parameters that are deprecated |
| | |
| | :param msg: deprecation message |
| | :param warning_class: class of warning exception to be raised |
| | :param func: function to be wrapped around |
| | """ |
| |
|
| | @wraps(func) |
| | def config_handler(*args, **kwargs): |
| | warnings.warn(msg, warning_class) |
| | return func(*args, **kwargs) |
| |
|
| | return config_handler |
| |
|
| |
|
| | class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]): |
| |
|
| | section_prefix = 'metadata' |
| |
|
| | aliases = { |
| | 'home_page': 'url', |
| | 'summary': 'description', |
| | 'classifier': 'classifiers', |
| | 'platform': 'platforms', |
| | } |
| |
|
| | strict_mode = False |
| | """We need to keep it loose, to be partially compatible with |
| | `pbr` and `d2to1` packages which also uses `metadata` section. |
| | |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | target_obj: "DistributionMetadata", |
| | options: AllCommandOptions, |
| | ignore_option_errors: bool, |
| | ensure_discovered: expand.EnsurePackagesDiscovered, |
| | package_dir: Optional[dict] = None, |
| | root_dir: _Path = os.curdir |
| | ): |
| | super().__init__(target_obj, options, ignore_option_errors, ensure_discovered) |
| | self.package_dir = package_dir |
| | self.root_dir = root_dir |
| |
|
| | @property |
| | def parsers(self): |
| | """Metadata item name to parser function mapping.""" |
| | parse_list = self._parse_list |
| | parse_file = partial(self._parse_file, root_dir=self.root_dir) |
| | parse_dict = self._parse_dict |
| | exclude_files_parser = self._exclude_files_parser |
| |
|
| | return { |
| | 'platforms': parse_list, |
| | 'keywords': parse_list, |
| | 'provides': parse_list, |
| | 'requires': self._deprecated_config_handler( |
| | parse_list, |
| | "The requires parameter is deprecated, please use " |
| | "install_requires for runtime dependencies.", |
| | SetuptoolsDeprecationWarning, |
| | ), |
| | 'obsoletes': parse_list, |
| | 'classifiers': self._get_parser_compound(parse_file, parse_list), |
| | 'license': exclude_files_parser('license'), |
| | 'license_file': self._deprecated_config_handler( |
| | exclude_files_parser('license_file'), |
| | "The license_file parameter is deprecated, " |
| | "use license_files instead.", |
| | SetuptoolsDeprecationWarning, |
| | ), |
| | 'license_files': parse_list, |
| | 'description': parse_file, |
| | 'long_description': parse_file, |
| | 'version': self._parse_version, |
| | 'project_urls': parse_dict, |
| | } |
| |
|
| | def _parse_version(self, value): |
| | """Parses `version` option value. |
| | |
| | :param value: |
| | :rtype: str |
| | |
| | """ |
| | version = self._parse_file(value, self.root_dir) |
| |
|
| | if version != value: |
| | version = version.strip() |
| | |
| | |
| | try: |
| | Version(version) |
| | except InvalidVersion: |
| | tmpl = ( |
| | 'Version loaded from {value} does not ' |
| | 'comply with PEP 440: {version}' |
| | ) |
| | raise DistutilsOptionError(tmpl.format(**locals())) |
| |
|
| | return version |
| |
|
| | return expand.version(self._parse_attr(value, self.package_dir, self.root_dir)) |
| |
|
| |
|
| | class ConfigOptionsHandler(ConfigHandler["Distribution"]): |
| |
|
| | section_prefix = 'options' |
| |
|
| | def __init__( |
| | self, |
| | target_obj: "Distribution", |
| | options: AllCommandOptions, |
| | ignore_option_errors: bool, |
| | ensure_discovered: expand.EnsurePackagesDiscovered, |
| | ): |
| | super().__init__(target_obj, options, ignore_option_errors, ensure_discovered) |
| | self.root_dir = target_obj.src_root |
| | self.package_dir: Dict[str, str] = {} |
| |
|
| | @classmethod |
| | def _parse_list_semicolon(cls, value): |
| | return cls._parse_list(value, separator=';') |
| |
|
| | def _parse_file_in_root(self, value): |
| | return self._parse_file(value, root_dir=self.root_dir) |
| |
|
| | def _parse_requirements_list(self, label: str, value: str): |
| | |
| | parsed = self._parse_list_semicolon(self._parse_file_in_root(value)) |
| | _warn_accidental_env_marker_misconfig(label, value, parsed) |
| | |
| | |
| | return [line for line in parsed if not line.startswith("#")] |
| |
|
| | @property |
| | def parsers(self): |
| | """Metadata item name to parser function mapping.""" |
| | parse_list = self._parse_list |
| | parse_bool = self._parse_bool |
| | parse_dict = self._parse_dict |
| | parse_cmdclass = self._parse_cmdclass |
| |
|
| | return { |
| | 'zip_safe': parse_bool, |
| | 'include_package_data': parse_bool, |
| | 'package_dir': parse_dict, |
| | 'scripts': parse_list, |
| | 'eager_resources': parse_list, |
| | 'dependency_links': parse_list, |
| | 'namespace_packages': self._deprecated_config_handler( |
| | parse_list, |
| | "The namespace_packages parameter is deprecated, " |
| | "consider using implicit namespaces instead (PEP 420).", |
| | SetuptoolsDeprecationWarning, |
| | ), |
| | 'install_requires': partial( |
| | self._parse_requirements_list, "install_requires" |
| | ), |
| | 'setup_requires': self._parse_list_semicolon, |
| | 'tests_require': self._parse_list_semicolon, |
| | 'packages': self._parse_packages, |
| | 'entry_points': self._parse_file_in_root, |
| | 'py_modules': parse_list, |
| | 'python_requires': SpecifierSet, |
| | 'cmdclass': parse_cmdclass, |
| | } |
| |
|
| | def _parse_cmdclass(self, value): |
| | package_dir = self.ensure_discovered.package_dir |
| | return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir) |
| |
|
| | def _parse_packages(self, value): |
| | """Parses `packages` option value. |
| | |
| | :param value: |
| | :rtype: list |
| | """ |
| | find_directives = ['find:', 'find_namespace:'] |
| | trimmed_value = value.strip() |
| |
|
| | if trimmed_value not in find_directives: |
| | return self._parse_list(value) |
| |
|
| | |
| | find_kwargs = self.parse_section_packages__find( |
| | self.sections.get('packages.find', {}) |
| | ) |
| |
|
| | find_kwargs.update( |
| | namespaces=(trimmed_value == find_directives[1]), |
| | root_dir=self.root_dir, |
| | fill_package_dir=self.package_dir, |
| | ) |
| |
|
| | return expand.find_packages(**find_kwargs) |
| |
|
| | def parse_section_packages__find(self, section_options): |
| | """Parses `packages.find` configuration file section. |
| | |
| | To be used in conjunction with _parse_packages(). |
| | |
| | :param dict section_options: |
| | """ |
| | section_data = self._parse_section_to_dict(section_options, self._parse_list) |
| |
|
| | valid_keys = ['where', 'include', 'exclude'] |
| |
|
| | find_kwargs = dict( |
| | [(k, v) for k, v in section_data.items() if k in valid_keys and v] |
| | ) |
| |
|
| | where = find_kwargs.get('where') |
| | if where is not None: |
| | find_kwargs['where'] = where[0] |
| |
|
| | return find_kwargs |
| |
|
| | def parse_section_entry_points(self, section_options): |
| | """Parses `entry_points` configuration file section. |
| | |
| | :param dict section_options: |
| | """ |
| | parsed = self._parse_section_to_dict(section_options, self._parse_list) |
| | self['entry_points'] = parsed |
| |
|
| | def _parse_package_data(self, section_options): |
| | package_data = self._parse_section_to_dict(section_options, self._parse_list) |
| | return expand.canonic_package_data(package_data) |
| |
|
| | def parse_section_package_data(self, section_options): |
| | """Parses `package_data` configuration file section. |
| | |
| | :param dict section_options: |
| | """ |
| | self['package_data'] = self._parse_package_data(section_options) |
| |
|
| | def parse_section_exclude_package_data(self, section_options): |
| | """Parses `exclude_package_data` configuration file section. |
| | |
| | :param dict section_options: |
| | """ |
| | self['exclude_package_data'] = self._parse_package_data(section_options) |
| |
|
| | def parse_section_extras_require(self, section_options): |
| | """Parses `extras_require` configuration file section. |
| | |
| | :param dict section_options: |
| | """ |
| | parsed = self._parse_section_to_dict_with_key( |
| | section_options, |
| | lambda k, v: self._parse_requirements_list(f"extras_require[{k}]", v) |
| | ) |
| |
|
| | self['extras_require'] = parsed |
| |
|
| | def parse_section_data_files(self, section_options): |
| | """Parses `data_files` configuration file section. |
| | |
| | :param dict section_options: |
| | """ |
| | parsed = self._parse_section_to_dict(section_options, self._parse_list) |
| | self['data_files'] = expand.canonic_data_files(parsed, self.root_dir) |
| |
|